feat: Add guest mode support and permission controls

This commit is contained in:
hstyi
2026-02-24 17:43:55 +08:00
committed by GitHub
parent c0f3ea8556
commit 5375a2e42e
23 changed files with 234 additions and 110 deletions

View File

@@ -1,8 +1,10 @@
package app.termora
import app.termora.account.AccountOwner
import app.termora.account.TeamRole
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.database.OwnerType
import app.termora.protocol.*
import app.termora.transfer.ScaleIcon
import com.formdev.flatlaf.extras.components.FlatToolBar
@@ -13,9 +15,11 @@ import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.exception.ExceptionUtils
import org.jdesktop.swingx.SwingXUtilities
import java.awt.BorderLayout
import java.awt.CardLayout
import java.awt.Dimension
import java.awt.Graphics
import java.awt.Window
import javax.swing.*
@@ -144,10 +148,33 @@ class NewHostDialogV2(
val provider = extension.getProtocolProvider()
testConnectionBtn.isVisible = provider is ProtocolTester
preventImportantData()
}
private fun preventImportantData() {
if (visitorMode()) {
for (component in SwingUtils.getDescendantsOfType(JComponent::class.java, cardPanel)) {
if (component is OutlinePasswordField) {
component.styleMap = component.styleMap.toMutableMap().apply {
put("showRevealButton", false)
}
}
}
}
}
private fun visitorMode(): Boolean {
return accountOwner.isVisitorMode()
}
override fun createActions(): List<AbstractAction> {
return listOf(createOkAction(), testConnectionAction, CancelAction())
val actions = mutableListOf<AbstractAction>()
if (visitorMode().not()) {
actions.add(createOkAction())
actions.add(testConnectionAction)
}
actions.add(CancelAction())
return actions
}
override fun createJButtonForAction(action: Action): JButton {
@@ -238,5 +265,4 @@ class NewHostDialogV2(
super.doOKAction()
}
}

View File

@@ -40,8 +40,8 @@ object AccountHttp {
throw ResponseException(response.code, response)
}
val text = response.use { response.body.use { it?.string() } }
if (text.isNullOrBlank()) {
val text = response.use { response.body.use { it.string() } }
if (text.isBlank()) {
throw ResponseException(response.code, "response body is empty", response)
}

View File

@@ -54,24 +54,24 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
fun getOwnerIds() = account.teams.map { it.id }.toMutableList().apply { add(getAccountId()) }.toSet()
fun getOwners(): Set<AccountOwner> {
val owners = mutableSetOf<AccountOwner>()
owners.add(AccountOwner(getAccountId(), getEmail(), OwnerType.User))
owners.add(AccountOwner(getAccountId(), getEmail(), OwnerType.User, StringUtils.EMPTY))
for (team in getTeams()) {
owners.add(AccountOwner(team.id, team.name, OwnerType.Team))
owners.add(AccountOwner(team.id, team.name, OwnerType.Team, team.role))
}
return owners
}
fun isFreePlan(): Boolean {
return isLocally() || getSubscription().plan == SubscriptionPlan.Free
return isLocally() || getSubscription().plan == SubscriptionPlan.Free.name
}
fun getSubscription(): Subscription {
if (isLocally().not()) {
val subscriptions = getSubscriptions()
val enterprises = getSubscriptions().filter { it.plan == SubscriptionPlan.Enterprise }
val teams = subscriptions.filter { it.plan == SubscriptionPlan.Team }
val pros = subscriptions.filter { it.plan == SubscriptionPlan.Pro }
val enterprises = getSubscriptions().filter { it.plan == SubscriptionPlan.Enterprise.name }
val teams = subscriptions.filter { it.plan == SubscriptionPlan.Team.name }
val pros = subscriptions.filter { it.plan == SubscriptionPlan.Pro.name }
val now = System.currentTimeMillis()
if (enterprises.any { it.endAt > now }) {
@@ -83,16 +83,9 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
}
}
return Subscription(id = "0", plan = SubscriptionPlan.Free, startAt = 0, endAt = 0)
return Subscription(id = "0", plan = SubscriptionPlan.Free.name, startAt = 0, endAt = 0)
}
fun hasTeamFeature(): Boolean {
if (accountProperties.signed.not()) return false
val plan = getSubscription().plan
return SubscriptionPlan.Team == plan || SubscriptionPlan.Enterprise == plan
}
/**
* 刷新 Token
*/

View File

@@ -123,7 +123,7 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
}
val planBox = Box.createHorizontalBox()
planBox.add(JLabel(if (isLocally) "-" else subscription.plan.name))
planBox.add(JLabel(if (isLocally) "-" else subscription.plan))
if (isFreePlan && isLocally.not()) {
planBox.add(Box.createHorizontalStrut(16))
val upgrade = JXHyperlink(object : AnAction(I18n.getString("termora.settings.account.upgrade")) {

View File

@@ -1,5 +1,12 @@
package app.termora.account
import app.termora.database.OwnerType
import org.apache.commons.lang3.StringUtils
data class AccountOwner(val id: String, val name: String, val type: OwnerType)
data class AccountOwner(val id: String, val name: String, val type: OwnerType, val role: String) {
constructor(id: String, name: String, type: OwnerType) : this(id, name, type, StringUtils.EMPTY)
fun isVisitorMode(): Boolean {
return type == OwnerType.Team && role == TeamRole.Visitor.name
}
}

View File

@@ -159,5 +159,5 @@ class ServerManager private constructor() {
@Serializable
data class MeTeam(val id: String, val name: String, val role: TeamRole, val secretKey: String)
data class MeTeam(val id: String, val name: String, val role: String, val secretKey: String)
}

View File

@@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class Subscription(
val id: String,
val plan: SubscriptionPlan,
val plan: String,
val startAt: Long,
val endAt: Long,
)

View File

@@ -25,7 +25,7 @@ class Team(
/**
* 所属角色
*/
val role: TeamRole,
val role: String,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true

View File

@@ -3,4 +3,5 @@ package app.termora.account
enum class TeamRole {
Member,
Owner,
Visitor,
}

View File

@@ -409,7 +409,8 @@ class DatabaseManager private constructor() : Disposable {
val accountOwner = AccountOwner(
id = account.id,
name = account.email,
type = OwnerType.User
type = OwnerType.User,
role = StringUtils.EMPTY,
)
for (host in hostManager.hosts()) {

View File

@@ -5,6 +5,7 @@ import app.termora.account.AccountManager
import app.termora.account.AccountOwner
import app.termora.database.OwnerType
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import org.apache.commons.lang3.StringUtils
import java.awt.Dimension
import java.awt.Window
import javax.swing.BorderFactory
@@ -45,12 +46,13 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
AccountOwner(
accountManager.getAccountId(),
accountManager.getEmail(),
OwnerType.User
OwnerType.User,
StringUtils.EMPTY,
)
).apply { Disposer.register(disposable, this) }
)
if (accountManager.hasTeamFeature()) {
// if (accountManager.hasTeamFeature()) {
for (team in accountManager.getTeams()) {
tabbed.addTab(
team.name,
@@ -59,11 +61,12 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
AccountOwner(
team.id,
team.name,
OwnerType.Team
OwnerType.Team,
team.role,
)
).apply { Disposer.register(disposable, this) })
}
}
// }
return tabbed

View File

@@ -248,6 +248,9 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
table.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
if (accountOwner.isVisitorMode()) {
return
}
if (SwingUtilities.isLeftMouseButton(e)) {
val row = table.rowAtPoint(e.point)
val column = table.columnAtPoint(e.point)
@@ -299,7 +302,8 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
if (keywordHighlight.backgroundColor in 0..16) {
if (keywordHighlight.backgroundColor == 0) {
dialog.backgroundColor.background = Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND))
dialog.backgroundColor.background =
Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND))
dialog.backgroundColor.colorIndex = -1
} else {
dialog.backgroundColor.color =
@@ -402,7 +406,7 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
val panel = JPanel(BorderLayout())
panel.add(JScrollPane(table).apply {
border = BorderFactory.createCompoundBorder(
BorderFactory.createEmptyBorder(8, 8, 8, 0),
BorderFactory.createEmptyBorder(8, 8, 8, if (accountOwner.isVisitorMode()) 8 else 0),
BorderFactory.createMatteBorder(1, 1, 1, 1, DynamicColor.BorderColor)
)
}, BorderLayout.CENTER)
@@ -414,6 +418,7 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
"default:grow",
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
)
if (accountOwner.isVisitorMode().not()) {
panel.add(
FormBuilder.create().layout(layout)
.border(BorderFactory.createEmptyBorder(8, 8, 8, 8))
@@ -424,7 +429,7 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
.add(exportBtn).xy(1, rows).apply { rows += step }
.build(),
BorderLayout.EAST)
}
return panel
}
}

View File

@@ -8,6 +8,7 @@ import app.termora.account.AccountManager
import app.termora.account.AccountOwner
import app.termora.database.OwnerType
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import org.apache.commons.lang3.StringUtils
import java.awt.Dimension
import java.awt.Window
import javax.swing.BorderFactory
@@ -65,13 +66,14 @@ class KeyManagerDialog(
AccountOwner(
accountManager.getAccountId(),
accountManager.getEmail(),
OwnerType.User
OwnerType.User,
StringUtils.EMPTY,
)
)
)
}
if (accountOwner != null && accountManager.hasTeamFeature()) {
if (accountOwner != null) {
for (team in accountManager.getTeams()) {
if (team.id == accountOwner.id) {
tabbed.addTab(
@@ -81,7 +83,8 @@ class KeyManagerDialog(
AccountOwner(
team.id,
team.name,
OwnerType.Team
OwnerType.Team,
team.role,
)
)
)
@@ -91,8 +94,7 @@ class KeyManagerDialog(
}
if (accountManager.hasTeamFeature()) {
// if (accountManager.hasTeamFeature()) {
for (team in accountManager.getTeams()) {
tabbed.addTab(
team.name,
@@ -101,12 +103,13 @@ class KeyManagerDialog(
AccountOwner(
team.id,
team.name,
OwnerType.Team
OwnerType.Team,
team.role,
)
)
)
}
}
// }
return tabbed

View File

@@ -2,8 +2,10 @@ package app.termora.keymgr
import app.termora.*
import app.termora.account.AccountOwner
import app.termora.account.TeamRole
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.database.OwnerType
import app.termora.plugin.internal.ssh.SSHProtocolProvider
import app.termora.tree.Filter
import app.termora.tree.HostTreeNode
@@ -56,6 +58,7 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
init {
initView()
initEvents()
preventImportantData()
}
@@ -89,6 +92,8 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
add(JScrollPane(keyPairTable).apply {
border = BorderFactory.createMatteBorder(1, 1, 1, 1, DynamicColor.BorderColor)
}, BorderLayout.CENTER)
if (accountOwner.type == OwnerType.User || (accountOwner.type == OwnerType.Team && accountOwner.role != TeamRole.Visitor.name)) {
add(
FormBuilder.create().layout(layout).padding(EmptyBorder(0, 12, 0, 0))
.add(generateBtn).xy(1, rows).apply { rows += step }
@@ -98,6 +103,8 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
.add(deleteBtn).xy(1, rows).apply { rows += step }
.add(sshCopyIdBtn).xy(1, rows).apply { rows += step }
.build(), BorderLayout.EAST)
}
border = BorderFactory.createEmptyBorder(12, 12, 12, 12)
}
@@ -199,6 +206,17 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
}
}
private fun preventImportantData() {
if (accountOwner.isVisitorMode()) {
generateBtn.isVisible = false
editBtn.isVisible = false
deleteBtn.isVisible = false
importBtn.isVisible = false
exportBtn.isVisible = false
sshCopyIdBtn.isVisible = false
}
}
private fun sshCopyId(evt: AnActionEvent) {
val keyPairs = keyPairTable.selectedRows.map { keyPairTableModel.getOhKeyPair(it) }
val publicKeys = mutableListOf<Pair<String, String>>()

View File

@@ -1,6 +1,7 @@
package app.termora.plugin.internal.rdp
import app.termora.*
import app.termora.account.AccountOwner
import app.termora.plugin.internal.BasicProxyOption
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatComboBox
@@ -17,7 +18,7 @@ import java.awt.event.ComponentEvent
import java.awt.event.ItemEvent
import javax.swing.*
internal open class RDPHostOptionsPane : OptionsPane() {
internal open class RDPHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() {
protected val generalOption = GeneralOption()
protected val proxyOption = BasicProxyOption()
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)

View File

@@ -2,11 +2,12 @@ package app.termora.plugin.internal.rdp
import app.termora.Disposer
import app.termora.Host
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import java.awt.BorderLayout
class RDPProtocolHostPanel : ProtocolHostPanel() {
private val pane = RDPHostOptionsPane()
class RDPProtocolHostPanel(private val accountOwner: AccountOwner) : ProtocolHostPanel() {
private val pane = RDPHostOptionsPane(accountOwner)
init {
initView()

View File

@@ -16,7 +16,7 @@ internal class RDPProtocolHostPanelExtension private constructor() : ProtocolHos
}
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return RDPProtocolHostPanel()
return RDPProtocolHostPanel(accountOwner)
}
override fun ordered(): Long {

View File

@@ -46,12 +46,12 @@ class TagDialog(owner: Window, private val accountOwnerId: String = StringUtils.
AccountOwner(
accountManager.getAccountId(),
accountManager.getEmail(),
OwnerType.User
OwnerType.User,
StringUtils.EMPTY,
)
).apply { Disposer.register(disposable, this) }
)
if (accountManager.hasTeamFeature()) {
for (team in accountManager.getTeams()) {
tabbed.addTab(
team.name,
@@ -60,7 +60,8 @@ class TagDialog(owner: Window, private val accountOwnerId: String = StringUtils.
AccountOwner(
team.id,
team.name,
OwnerType.Team
OwnerType.Team,
team.role,
)
).apply { Disposer.register(disposable, this) })
@@ -68,7 +69,6 @@ class TagDialog(owner: Window, private val accountOwnerId: String = StringUtils.
tabbed.selectedIndex = tabbed.tabCount - 1
}
}
}
return tabbed
}

View File

@@ -2,6 +2,8 @@ package app.termora.tag
import app.termora.*
import app.termora.account.AccountOwner
import app.termora.account.TeamRole
import app.termora.database.OwnerType
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import java.awt.BorderLayout
@@ -9,7 +11,7 @@ import java.awt.Component
import javax.swing.*
import javax.swing.border.EmptyBorder
class TagPanel(accountOwner: AccountOwner) : JPanel(BorderLayout()), Disposable {
class TagPanel(private val accountOwner: AccountOwner) : JPanel(BorderLayout()), Disposable {
private val owner get() = SwingUtilities.getWindowAncestor(this)
@@ -23,6 +25,7 @@ class TagPanel(accountOwner: AccountOwner) : JPanel(BorderLayout()), Disposable
init {
initView()
initEvents()
preventImportantData()
}
private fun initView() {
@@ -108,6 +111,14 @@ class TagPanel(accountOwner: AccountOwner) : JPanel(BorderLayout()), Disposable
}
private fun preventImportantData() {
if (accountOwner.isVisitorMode()) {
addBtn.isVisible = false
editBtn.isVisible = false
deleteBtn.isVisible = false
}
}
private fun createCenterPanel(): JComponent {
val panel = JPanel(BorderLayout())

View File

@@ -3,6 +3,8 @@ package app.termora.tree
import app.termora.*
import app.termora.Application.ohMyJson
import app.termora.account.AccountManager
import app.termora.account.Team
import app.termora.account.TeamRole
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.actions.OpenHostAction
@@ -455,6 +457,33 @@ class NewHostTree : SimpleTree(), Disposable {
})
popupMenu.addPopupMenuListener(object : PopupMenuListener {
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent?) {
for (node in nodes) {
val team = TeamTreeNode.parentTeam(node) ?: continue
if (team.role == TeamRole.Visitor.name) {
copy.isEnabled = false
remove.isEnabled = false
rename.isEnabled = false
importMenu.isEnabled = false
newMenu.isEnabled = false
tagsMenu.isEnabled = false
break
}
}
}
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent?) {
}
override fun popupMenuCanceled(e: PopupMenuEvent?) {
}
})
val mnemonics = mapOf(
refresh to KeyEvent.VK_R,
newMenu to KeyEvent.VK_W,

View File

@@ -61,11 +61,11 @@ class NewHostTreeModel private constructor() : SimpleTreeModel<Host>(
// 如果是根,需要引入团队功能
if (parent == getRoot()) {
if (accountManager.hasTeamFeature()) {
// if (accountManager.hasTeamFeature()) {
for (team in accountManager.getTeams()) {
nodes[team.id] = TeamTreeNode(team)
}
}
// }
nodes[accountManager.getAccountId()] = HostTreeNode(
Host(
@@ -93,11 +93,11 @@ class NewHostTreeModel private constructor() : SimpleTreeModel<Host>(
}
if (parent == getRoot()) {
if (accountManager.hasTeamFeature()) {
// if (accountManager.hasTeamFeature()) {
for (team in accountManager.getTeams()) {
parent.add(nodes.getValue(team.id))
}
}
// }
parent.add(nodes.getValue(accountManager.getAccountId()))
} else {
for (node in nodes.values) {

View File

@@ -1,6 +1,7 @@
package app.termora.tree
import app.termora.OutlineTextField
import app.termora.account.TeamRole
import com.formdev.flatlaf.ui.FlatTreeUI
import org.jdesktop.swingx.JXTree
import java.awt.Dimension
@@ -134,6 +135,13 @@ open class SimpleTree : JXTree() {
}
}
for (node in nodes) {
val team = TeamTreeNode.parentTeam(node) ?: continue
if (team.role == TeamRole.Visitor.name) {
return null
}
}
return MoveNodeTransferable(nodes)
}
@@ -153,9 +161,14 @@ open class SimpleTree : JXTree() {
if (nodes.isEmpty()) return false
if (!node.isFolder) return false
val team = TeamTreeNode.parentTeam(node)
if (team?.role == TeamRole.Visitor.name) {
return false
}
for (e in nodes) {
// 禁止拖拽到自己的子下面
if (path.equals(TreePath(e.path)) || TreePath(e.path).isDescendant(path)) {
if (path == TreePath(e.path) || TreePath(e.path).isDescendant(path)) {
return false
}
@@ -333,7 +346,7 @@ open class SimpleTree : JXTree() {
private inner class MyTreeUI : FlatTreeUI() {
override fun createNodeDimensions(): AbstractLayoutCache.NodeDimensions? {
override fun createNodeDimensions(): AbstractLayoutCache.NodeDimensions {
return object : NodeDimensionsHandler() {
override fun getNodeDimensions(
value: Any?, row: Int, depth: Int, expanded: Boolean,

View File

@@ -26,4 +26,16 @@ class TeamTreeNode(val team: Team) : HostTreeNode(
override fun toString(): String {
return team.name
}
companion object {
fun parentTeam(node: SimpleTreeNode<*>?): Team? {
if (node is TeamTreeNode) {
return node.team
} else if (node == null) {
return null
}
return parentTeam(node.parent)
}
}
}