chore: improve team sync

This commit is contained in:
hstyi
2025-07-02 16:49:50 +08:00
committed by hstyi
parent 9916edbd13
commit 168c4c5c64
34 changed files with 304 additions and 178 deletions

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.cos package app.termora.plugins.cos
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class COSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
return COSProtocolProvider.instance return COSProtocolProvider.instance
} }
override fun createProtocolHostPanel(): ProtocolHostPanel { override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return COSProtocolHostPanel() return COSProtocolHostPanel()
} }
} }

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.obs package app.termora.plugins.obs
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class OBSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
return OBSProtocolProvider.instance return OBSProtocolProvider.instance
} }
override fun createProtocolHostPanel(): ProtocolHostPanel { override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return OBSProtocolHostPanel() return OBSProtocolHostPanel()
} }
} }

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.oss package app.termora.plugins.oss
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class OSSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
return OSSProtocolProvider.instance return OSSProtocolProvider.instance
} }
override fun createProtocolHostPanel(): ProtocolHostPanel { override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return OSSProtocolHostPanel() return OSSProtocolHostPanel()
} }
} }

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.s3 package app.termora.plugins.s3
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class S3ProtocolHostPanelExtension private constructor() : ProtocolHostPanelExte
return S3ProtocolProvider.instance return S3ProtocolProvider.instance
} }
override fun createProtocolHostPanel(): ProtocolHostPanel { override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return S3ProtocolHostPanel() return S3ProtocolHostPanel()
} }
} }

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.smb package app.termora.plugins.smb
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class SMBProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
return SMBProtocolProvider.instance return SMBProtocolProvider.instance
} }
override fun createProtocolHostPanel(): ProtocolHostPanel { override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return SMBProtocolHostPanel() return SMBProtocolHostPanel()
} }
} }

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.webdav package app.termora.plugins.webdav
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class WebDAVProtocolHostPanelExtension private constructor() : ProtocolHostPanel
return WebDAVProtocolProvider.instance return WebDAVProtocolProvider.instance
} }
override fun createProtocolHostPanel(): ProtocolHostPanel { override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return WebDAVProtocolHostPanel() return WebDAVProtocolHostPanel()
} }
} }

View File

@@ -1,5 +1,6 @@
package app.termora package app.termora
import app.termora.account.AccountOwner
import app.termora.actions.AnAction import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
import app.termora.protocol.* import app.termora.protocol.*
@@ -18,7 +19,11 @@ import java.awt.Dimension
import java.awt.Window import java.awt.Window
import javax.swing.* import javax.swing.*
class NewHostDialogV2(owner: Window, private val editHost: Host? = null) : DialogWrapper(owner) { class NewHostDialogV2(
owner: Window,
private val editHost: Host? = null,
private val accountOwner: AccountOwner,
) : DialogWrapper(owner) {
private object Current { private object Current {
var card: ProtocolHostPanel? = null var card: ProtocolHostPanel? = null
@@ -65,11 +70,11 @@ class NewHostDialogV2(owner: Window, private val editHost: Host? = null) : Dialo
toolbar.add(Box.createHorizontalGlue()) toolbar.add(Box.createHorizontalGlue())
val extensions = ProtocolHostPanelExtension.extensions val extensions = ProtocolHostPanelExtension.extensions
.filter { it.canCreateProtocolHostPanel() } .filter { it.canCreateProtocolHostPanel(accountOwner) }
for ((index, extension) in extensions.withIndex()) { for ((index, extension) in extensions.withIndex()) {
val protocol = extension.getProtocolProvider().getProtocol() val protocol = extension.getProtocolProvider().getProtocol()
val icon = ScaleIcon(extension.getProtocolProvider().getIcon(), 22) val icon = ScaleIcon(extension.getProtocolProvider().getIcon(), 22)
val hostPanel = extension.createProtocolHostPanel() val hostPanel = extension.createProtocolHostPanel(accountOwner)
val button = JToggleButton(protocol, icon).apply { buttonGroup.add(this) } val button = JToggleButton(protocol, icon).apply { buttonGroup.add(this) }
button.setVerticalTextPosition(SwingConstants.BOTTOM) button.setVerticalTextPosition(SwingConstants.BOTTOM)
button.setHorizontalTextPosition(SwingConstants.CENTER) button.setHorizontalTextPosition(SwingConstants.CENTER)

View File

@@ -33,7 +33,7 @@ open class Scope(
return get(clazz) return get(clazz)
} }
synchronized(clazz) { synchronized(this) {
if (beans.containsKey(clazz)) { if (beans.containsKey(clazz)) {
return get(clazz) return get(clazz)
} }

View File

@@ -1,6 +1,7 @@
package app.termora package app.termora
import app.termora.account.AccountManager
import app.termora.actions.* import app.termora.actions.*
import app.termora.database.DatabaseChangedExtension import app.termora.database.DatabaseChangedExtension
import app.termora.database.DatabaseManager import app.termora.database.DatabaseManager
@@ -244,10 +245,15 @@ class TerminalTabbed(
val edit = popupMenu.add(I18n.getString("termora.keymgr.edit")) val edit = popupMenu.add(I18n.getString("termora.keymgr.edit"))
edit.addActionListener(object : AnAction() { edit.addActionListener(object : AnAction() {
private val hostManager get() = HostManager.getInstance() private val hostManager get() = HostManager.getInstance()
private val accountManager get() = AccountManager.getInstance()
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
if (tab is HostTerminalTab) { if (tab is HostTerminalTab) {
val host = hostManager.getHost(tab.host.id) ?: return val host = hostManager.getHost(tab.host.id) ?: return
val dialog = NewHostDialogV2(evt.window, host) val dialog = NewHostDialogV2(
evt.window, host,
accountManager.getOwners().first { it.id == host.ownerId },
)
dialog.setLocationRelativeTo(evt.window) dialog.setLocationRelativeTo(evt.window)
dialog.isVisible = true dialog.isVisible = true

View File

@@ -183,8 +183,8 @@ object AccountHttp {
if (isRefreshing.compareAndSet(false, true)) { if (isRefreshing.compareAndSet(false, true)) {
try { try {
// 刷新 token // 刷新 token 和用户
accountManager.refreshToken() accountManager.refresh()
} finally { } finally {
lock.withLock { lock.withLock {
isRefreshing.set(false) isRefreshing.set(false)

View File

@@ -14,12 +14,15 @@ import okhttp3.RequestBody.Companion.toRequestBody
import org.apache.commons.codec.binary.Base64 import org.apache.commons.codec.binary.Base64
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import java.security.PrivateKey import java.security.PrivateKey
import java.security.PublicKey import java.security.PublicKey
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
class AccountManager private constructor() : ApplicationRunnerExtension { class AccountManager private constructor() : ApplicationRunnerExtension {
companion object { companion object {
private val log = LoggerFactory.getLogger(AccountManager::class.java)
fun getInstance(): AccountManager { fun getInstance(): AccountManager {
return ApplicationScope.forApplicationScope() return ApplicationScope.forApplicationScope()
.getOrCreate(AccountManager::class) { AccountManager() } .getOrCreate(AccountManager::class) { AccountManager() }
@@ -30,6 +33,7 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
} }
} }
private val serverManager get() = ServerManager.getInstance()
private var account = locally() private var account = locally()
private val accountProperties get() = AccountProperties.getInstance() private val accountProperties get() = AccountProperties.getInstance()
@@ -48,10 +52,14 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
fun getAccessToken() = account.accessToken fun getAccessToken() = account.accessToken
fun getRefreshToken() = account.refreshToken fun getRefreshToken() = account.refreshToken
fun getOwnerIds() = account.teams.map { it.id }.toMutableList().apply { add(getAccountId()) }.toSet() fun getOwnerIds() = account.teams.map { it.id }.toMutableList().apply { add(getAccountId()) }.toSet()
fun getOwners() = fun getOwners(): Set<AccountOwner> {
account.teams.map { AccountOwner(it.id, it.name, OwnerType.Team) } val owners = mutableSetOf<AccountOwner>()
.toMutableList().apply { AccountOwner(getAccountId(), getEmail(), OwnerType.User) } owners.add(AccountOwner(getAccountId(), getEmail(), OwnerType.User))
.toSet() for (team in getTeams()) {
owners.add(AccountOwner(team.id, team.name, OwnerType.Team))
}
return owners
}
fun isFreePlan(): Boolean { fun isFreePlan(): Boolean {
return isLocally() || getSubscription().plan == SubscriptionPlan.Free return isLocally() || getSubscription().plan == SubscriptionPlan.Free
@@ -126,6 +134,7 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
* 设置账户信息,可以多次调用,每次修改用户信息都要通过这个方法 * 设置账户信息,可以多次调用,每次修改用户信息都要通过这个方法
*/ */
internal fun login(account: Account) { internal fun login(account: Account) {
synchronized(this) {
val oldAccount = this.account val oldAccount = this.account
@@ -158,6 +167,7 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
// 通知变化 // 通知变化
notifyAccountChanged(oldAccount, account) notifyAccountChanged(oldAccount, account)
} }
}
private fun notifyAccountChanged(oldAccount: Account, newAccount: Account) { private fun notifyAccountChanged(oldAccount: Account, newAccount: Account) {
if (SwingUtilities.isEventDispatchThread()) { if (SwingUtilities.isEventDispatchThread()) {
@@ -220,7 +230,7 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
override fun ready() { override fun ready() {
if (isLocally().not()) { if (isLocally().not()) {
swingCoroutineScope.launch(Dispatchers.IO) { refreshToken() } swingCoroutineScope.launch(Dispatchers.IO) { refresh() }
} }
} }
@@ -228,8 +238,34 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
/** /**
* 刷新用户 * 刷新用户
*/ */
fun refresh(accessToken: String = getAccessToken()) { fun refresh() {
runCatching { refreshToken() }.onSuccess {
refreshAccount()
}.onFailure {
if (log.isErrorEnabled) {
log.error(it.message, it)
}
}
}
fun refreshAccount() {
try {
val me = serverManager.callMe(account.server, getAccessToken())
val teams = me.teams.map {
Team(
id = it.id,
name = it.name,
secretKey = RSA.decrypt(getPrivateKey(), Base64.decodeBase64(it.secretKey)),
role = it.role
)
}
// 重新登录
login(account.copy(teams = teams, subscriptions = me.subscriptions))
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
} }
class AccountApplicationRunnerExtension private constructor() : ApplicationRunnerExtension { class AccountApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {

View File

@@ -75,8 +75,10 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
val subscription = accountManager.getSubscription() val subscription = accountManager.getSubscription()
val isFreePlan = accountManager.isFreePlan() val isFreePlan = accountManager.isFreePlan()
val isLocally = accountManager.isLocally() val isLocally = accountManager.isLocally()
val validTo = if (isFreePlan) "-" else if (subscription.endAt >= Long.MAX_VALUE) val validTo = if (isFreePlan) "-"
I18n.getString("termora.settings.account.lifetime") else else if (subscription.endAt >= Long.MAX_VALUE)
I18n.getString("termora.settings.account.lifetime")
else
DateFormatUtils.format(Date(subscription.endAt), I18n.getString("termora.date-format")) DateFormatUtils.format(Date(subscription.endAt), I18n.getString("termora.date-format"))
val lastSynchronizationOn = if (isFreePlan) "-" else val lastSynchronizationOn = if (isFreePlan) "-" else
DateFormatUtils.format( DateFormatUtils.format(
@@ -158,9 +160,19 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
if (isFreePlan.not()) { if (isFreePlan.not()) {
actions.add(JXHyperlink(object : AnAction(I18n.getString("termora.settings.account.sync-now")) { actions.add(JXHyperlink(object : AnAction(I18n.getString("termora.settings.account.sync-now")) {
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
// 全量同步
accountProperties.nextSynchronizationSince = 0
// 拉取
PullService.getInstance().trigger() PullService.getInstance().trigger()
// 推送
PushService.getInstance().trigger() PushService.getInstance().trigger()
swingCoroutineScope.launch(Dispatchers.IO) { swingCoroutineScope.launch(Dispatchers.IO) {
// 刷新账户
accountManager.refreshAccount()
withContext(Dispatchers.Swing) { withContext(Dispatchers.Swing) {
isEnabled = false isEnabled = false
lastSynchronizationOnLabel.text = DateFormatUtils.format( lastSynchronizationOnLabel.text = DateFormatUtils.format(

View File

@@ -2,5 +2,4 @@ package app.termora.account
import app.termora.database.OwnerType import app.termora.database.OwnerType
data class AccountOwner(val id: String, val name: String, val type: OwnerType) { data class AccountOwner(val id: String, val name: String, val type: OwnerType)
}

View File

@@ -48,6 +48,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
Server(I18n.getString("termora.settings.account.server-singapore"), "https://account.termora.app") Server(I18n.getString("termora.settings.account.server-singapore"), "https://account.termora.app")
private val chinaServer = private val chinaServer =
Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn") Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn")
private val serverManager get() = ServerManager.getInstance()
init { init {
isModal = true isModal = true
@@ -359,7 +360,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
val loginJob = swingCoroutineScope.launch(Dispatchers.IO) { val loginJob = swingCoroutineScope.launch(Dispatchers.IO) {
try { try {
ServerManager.getInstance().login( serverManager.login(
server, usernameTextField.text, server, usernameTextField.text,
String(passwordField.password), mfaTextField.text.trim() String(passwordField.password), mfaTextField.text.trim()
) )

View File

@@ -45,6 +45,15 @@ class PullService private constructor() : SyncService(), Disposable, Application
lastChangeHash = StringUtils.EMPTY lastChangeHash = StringUtils.EMPTY
} }
// 团队变了,全量同步
if (oldAccount.id == newAccount.id) {
if (oldAccount.teams != newAccount.teams) {
accountProperties.nextSynchronizationSince = 0
trigger()
return
}
}
if (oldAccount.isLocally && newAccount.isLocally.not()) { if (oldAccount.isLocally && newAccount.isLocally.not()) {
trigger() trigger()
} }
@@ -281,7 +290,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
ownerId = ownerId, ownerId = ownerId,
ownerType = ownerType, ownerType = ownerType,
type = type, type = type,
data = decryptData(id, data), data = decryptData(id, data, ownerId),
version = version, version = version,
// 因为已经是拉取最新版本了,所以这里无需再同步了 // 因为已经是拉取最新版本了,所以这里无需再同步了
synced = true, synced = true,
@@ -298,11 +307,8 @@ class PullService private constructor() : SyncService(), Disposable, Application
if (log.isDebugEnabled) { if (log.isDebugEnabled) {
log.debug("数据: {}, 类型: {} 云端已经删除,本地即将删除", id, type) log.debug("数据: {}, 类型: {} 云端已经删除,本地即将删除", id, type)
} }
databaseManager.delete(
id, type,
DatabaseChangedExtension.Source.Sync
)
databaseManager.delete(id, type, DatabaseChangedExtension.Source.Sync)
if (log.isInfoEnabled) { if (log.isInfoEnabled) {
log.info("数据: {}, 类型: {} 已从本地删除", id, type) log.info("数据: {}, 类型: {} 已从本地删除", id, type)
@@ -340,7 +346,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
ownerId = ownerId, ownerId = ownerId,
ownerType = ownerType, ownerType = ownerType,
type = type, type = type,
data = decryptData(id, data), data = decryptData(id, data, ownerId),
version = version, version = version,
// 因为已经是拉取最新版本了,所以这里无需再同步了 // 因为已经是拉取最新版本了,所以这里无需再同步了
synced = true, synced = true,
@@ -377,7 +383,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
pullChanges() pullChanges()
// N 秒拉一次 // N 秒拉一次
val result = withTimeoutOrNull(Random.nextInt(5, 15).seconds) { val result = withTimeoutOrNull(Random.nextInt(3, 10).seconds) {
channel.receiveCatching() channel.receiveCatching()
} ?: continue } ?: continue
if (result.isFailure) break if (result.isFailure) break

View File

@@ -4,6 +4,7 @@ import app.termora.*
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.database.Data import app.termora.database.Data
import app.termora.database.DatabaseChangedExtension import app.termora.database.DatabaseChangedExtension
import app.termora.database.OwnerType
import app.termora.plugin.DispatchThread import app.termora.plugin.DispatchThread
import app.termora.plugin.internal.extension.DynamicExtensionHandler import app.termora.plugin.internal.extension.DynamicExtensionHandler
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -106,7 +107,20 @@ class PushService private constructor() : SyncService(), Disposable, Application
.delete() .delete()
.build() .build()
try {
AccountHttp.execute(request = request) AccountHttp.execute(request = request)
} catch (e: Exception) {
if (e is ResponseException) {
if (e.code == 403) {
// 如果是 Team 发现没有权限,那么很有可能是被提出团队
if (data.ownerType == OwnerType.Team.name) {
// 刷新用户
accountManager.refreshAccount()
}
}
}
throw e
}
// 修改为已经同步 // 修改为已经同步
updateData(data.id, synced = true) updateData(data.id, synced = true)
@@ -153,6 +167,12 @@ class PushService private constructor() : SyncService(), Disposable, Application
} }
// 标记为已经同步 // 标记为已经同步
updateData(data.id, synced = true, version = data.version) updateData(data.id, synced = true, version = data.version)
// 如果是 Team 发现没有权限,那么很有可能是被提出团队
if (data.ownerType == OwnerType.Team.name) {
// 刷新用户
accountManager.refreshAccount()
}
return return
} else if (response.code == 409) { // 版本冲突,一般来说是云端版本大于本地版本 } else if (response.code == 409) { // 版本冲突,一般来说是云端版本大于本地版本
val json = ohMyJson.decodeFromString<JsonObject>(text) val json = ohMyJson.decodeFromString<JsonObject>(text)

View File

@@ -54,7 +54,7 @@ class ServerManager private constructor() {
val loginResponse = callLogin(serverInfo, server, username, password, mfa) val loginResponse = callLogin(serverInfo, server, username, password, mfa)
// call me // call me
val meResponse = callMe(server, loginResponse.accessToken) val meResponse = callMe(server.server, loginResponse.accessToken)
// 解密 // 解密
val salt = "${serverInfo.salt}:${username}".toByteArray() val salt = "${serverInfo.salt}:${username}".toByteArray()
@@ -139,9 +139,9 @@ class ServerManager private constructor() {
} }
private fun callMe(server: Server, accessToken: String): MeResponse { fun callMe(server: String, accessToken: String): MeResponse {
val request = Request.Builder() val request = Request.Builder()
.url("${server.server}/v1/users/me") .url("${server}/v1/users/me")
.header("Authorization", "Bearer $accessToken") .header("Authorization", "Bearer $accessToken")
.build() .build()
val text = AccountHttp.execute(request = request) val text = AccountHttp.execute(request = request)
@@ -149,13 +149,13 @@ class ServerManager private constructor() {
} }
@Serializable @Serializable
private data class ServerInfo(val salt: String) data class ServerInfo(val salt: String)
@Serializable @Serializable
private data class LoginResponse(val accessToken: String, val refreshToken: String) data class LoginResponse(val accessToken: String, val refreshToken: String)
@Serializable @Serializable
private data class MeResponse( data class MeResponse(
val id: String, val id: String,
val email: String, val email: String,
val publicKey: String, val publicKey: String,
@@ -167,5 +167,5 @@ class ServerManager private constructor() {
@Serializable @Serializable
private 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: TeamRole, val secretKey: String)
} }

View File

@@ -78,28 +78,23 @@ abstract class SyncService {
protected fun encryptData(id: String, data: String, ownerId: String): String { protected fun encryptData(id: String, data: String, ownerId: String): String {
val iv = DigestUtils.sha256(id).copyOf(12) val iv = DigestUtils.sha256(id).copyOf(12)
var secretKey = EMPTY_BYTE_ARRAY val secretKey = getSecretKey(ownerId)
if (ownerId != accountManager.getAccountId()) {
val team = accountManager.getTeams().firstOrNull { it.id == ownerId }
if (team == null) {
return StringUtils.EMPTY
} else {
secretKey = team.secretKey
}
} else if (ownerId == accountManager.getAccountId()) {
secretKey = accountManager.getSecretKey()
}
if (secretKey.isEmpty()) return StringUtils.EMPTY if (secretKey.isEmpty()) return StringUtils.EMPTY
return Base64.encodeBase64String(AES.GCM.encrypt(secretKey, iv, data.toByteArray())) return Base64.encodeBase64String(AES.GCM.encrypt(secretKey, iv, data.toByteArray()))
} }
protected fun decryptData(id: String, data: String): String { protected fun getSecretKey(ownerId: String): ByteArray {
if (ownerId == accountManager.getAccountId()) {
return accountManager.getSecretKey()
}
val team = accountManager.getTeams().firstOrNull { it.id == ownerId }
return team?.secretKey ?: EMPTY_BYTE_ARRAY
}
protected fun decryptData(id: String, data: String, ownerId: String): String {
val iv = DigestUtils.sha256(id).copyOf(12) val iv = DigestUtils.sha256(id).copyOf(12)
return String( val secretKey = getSecretKey(ownerId)
AES.GCM.decrypt( if (secretKey.isEmpty()) throw IllegalStateException("根据 ownerId 无法获取对应密钥")
accountManager.getSecretKey(), iv, return String(AES.GCM.decrypt(secretKey, iv, Base64.decodeBase64(data)))
Base64.decodeBase64(data)
)
)
} }
} }

View File

@@ -26,4 +26,22 @@ class Team(
* 所属角色 * 所属角色
*/ */
val role: TeamRole, val role: TeamRole,
) ) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Team
if (id != other.id) return false
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + name.hashCode()
return result
}
}

View File

@@ -1,6 +1,7 @@
package app.termora.actions package app.termora.actions
import app.termora.NewHostDialogV2 import app.termora.NewHostDialogV2
import app.termora.account.AccountManager
import app.termora.tree.HostTreeNode import app.termora.tree.HostTreeNode
import javax.swing.tree.TreePath import javax.swing.tree.TreePath
@@ -14,6 +15,8 @@ class NewHostAction : AnAction() {
} }
private val accountManager get() = AccountManager.getInstance()
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
val tree = evt.getData(DataProviders.Welcome.HostTree) ?: return val tree = evt.getData(DataProviders.Welcome.HostTree) ?: return
var lastNode = (tree.lastSelectedPathComponent ?: tree.model.root) as? HostTreeNode ?: return var lastNode = (tree.lastSelectedPathComponent ?: tree.model.root) as? HostTreeNode ?: return
@@ -27,7 +30,7 @@ class NewHostAction : AnAction() {
} }
val lastHost = lastNode.host val lastHost = lastNode.host
val dialog = NewHostDialogV2(evt.window) val dialog = NewHostDialogV2(evt.window, accountOwner = accountManager.getOwners().first { it.id == lastHost.ownerId })
dialog.setLocationRelativeTo(evt.window) dialog.setLocationRelativeTo(evt.window)
dialog.isVisible = true dialog.isVisible = true
val host = (dialog.host ?: return).copy( val host = (dialog.host ?: return).copy(

View File

@@ -11,7 +11,6 @@ import app.termora.highlight.KeywordHighlightManager
import app.termora.keymap.KeymapManager import app.termora.keymap.KeymapManager
import app.termora.keymgr.KeyManager import app.termora.keymgr.KeyManager
import app.termora.macro.MacroManager import app.termora.macro.MacroManager
import app.termora.plugin.ExtensionManager
import app.termora.plugin.internal.extension.DynamicExtensionHandler import app.termora.plugin.internal.extension.DynamicExtensionHandler
import app.termora.snippet.SnippetManager import app.termora.snippet.SnippetManager
import app.termora.terminal.CursorStyle import app.termora.terminal.CursorStyle
@@ -23,6 +22,7 @@ import org.jetbrains.exposed.v1.core.statements.StatementType
import org.jetbrains.exposed.v1.jdbc.* import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.util.* import java.util.*
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
@@ -32,11 +32,8 @@ import kotlin.reflect.KProperty
class DatabaseManager private constructor() : Disposable { class DatabaseManager private constructor() : Disposable {
companion object { companion object {
val log: Logger = LoggerFactory.getLogger(DatabaseManager::class.java)
private const val DB_PASSWORD = "DB_PASSWORD"
private const val DB_SALT = "DB_SALT"
val log = LoggerFactory.getLogger(DatabaseManager::class.java)!!
fun getInstance(): DatabaseManager { fun getInstance(): DatabaseManager {
return ApplicationScope.forApplicationScope() return ApplicationScope.forApplicationScope()
.getOrCreate(DatabaseManager::class) { DatabaseManager() } .getOrCreate(DatabaseManager::class) { DatabaseManager() }
@@ -52,14 +49,6 @@ class DatabaseManager private constructor() : Disposable {
val sftp by lazy { SFTP(this) } val sftp by lazy { SFTP(this) }
@Volatile
internal var dbPassword = StringUtils.EMPTY
private set
@Volatile
internal var dbSalt = StringUtils.EMPTY
private set
private val map = Collections.synchronizedMap<String, String?>(mutableMapOf()) private val map = Collections.synchronizedMap<String, String?>(mutableMapOf())
private val accountManager get() = AccountManager.getInstance() private val accountManager get() = AccountManager.getInstance()
@@ -101,11 +90,6 @@ class DatabaseManager private constructor() : Disposable {
// 注册动态扩展 // 注册动态扩展
registerDynamicExtensions() registerDynamicExtensions()
for (extension in ExtensionManager.getInstance().getExtensions(DatabaseReadyExtension::class.java)) {
extension.ready(this)
}
} }
private fun registerDynamicExtensions() { private fun registerDynamicExtensions() {
@@ -308,6 +292,7 @@ class DatabaseManager private constructor() : Disposable {
DataEntity.update({ DataEntity.id eq id }) { DataEntity.update({ DataEntity.id eq id }) {
it[DataEntity.deleted] = true it[DataEntity.deleted] = true
// 如果是本地用户,那么删除是不需要同步的,云端用户才需要同步 // 如果是本地用户,那么删除是不需要同步的,云端用户才需要同步
// 云端用户也会判断,如果来源 Sync 那么默认同步了
it[DataEntity.synced] = accountManager.isLocally() it[DataEntity.synced] = accountManager.isLocally()
it[DataEntity.data] = StringUtils.EMPTY it[DataEntity.data] = StringUtils.EMPTY
} }
@@ -367,7 +352,6 @@ class DatabaseManager private constructor() : Disposable {
private inner class AccountDataTransferExtension : AccountExtension { private inner class AccountDataTransferExtension : AccountExtension {
private val hostManager get() = HostManager.getInstance()
override fun onAccountChanged(oldAccount: Account, newAccount: Account) { override fun onAccountChanged(oldAccount: Account, newAccount: Account) {
if (oldAccount.isLocally && newAccount.isLocally) { if (oldAccount.isLocally && newAccount.isLocally) {
return return
@@ -481,13 +465,10 @@ class DatabaseManager private constructor() : Disposable {
return return
} }
// 如果团队变更,那么删除所有旧的团队数据,静默删除
if (oldAccount.id == newAccount.id) { if (oldAccount.id == newAccount.id) {
return if (oldAccount.teams != newAccount.teams) {
}
for (team in oldAccount.teams) { for (team in oldAccount.teams) {
// 如果被踢出团队,那么移除该团队的所有资产
if (newAccount.teams.none { it.id == team.id }) {
lock.withLock { lock.withLock {
transaction(database) { transaction(database) {
DataEntity.deleteWhere { DataEntity.deleteWhere {
@@ -499,6 +480,7 @@ class DatabaseManager private constructor() : Disposable {
} }
} }
} }
}
} }

View File

@@ -1,37 +0,0 @@
package app.termora.database
import app.termora.database.DatabaseManager.Companion.log
import app.termora.plugin.DispatchThread
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionManager
import javax.swing.SwingUtilities
interface DatabaseReadyExtension : Extension {
companion object {
fun fireReady(databaseManager: DatabaseManager) {
if (SwingUtilities.isEventDispatchThread()) {
for (extension in ExtensionManager.getInstance().getExtensions(DatabaseReadyExtension::class.java)) {
try {
extension.ready(databaseManager)
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
}
} else {
SwingUtilities.invokeLater { fireReady(databaseManager) }
}
}
}
/**
* 数据库初始化完成
*/
fun ready(databaseManager: DatabaseManager) {}
override fun getDispatchThread(): DispatchThread {
return DispatchThread.BGT
}
}

View File

@@ -19,6 +19,7 @@ class KeyManagerDialog(
owner: Window, owner: Window,
private val selectMode: Boolean = false, private val selectMode: Boolean = false,
size: Dimension = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height")), size: Dimension = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height")),
private val accountOwner: AccountOwner? = null,
) : DialogWrapper(owner) { ) : DialogWrapper(owner) {
var ok: Boolean = false var ok: Boolean = false
@@ -56,12 +57,40 @@ class KeyManagerDialog(
tabbed.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor) tabbed.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
tabbed.tabPlacement = JTabbedPane.TOP tabbed.tabPlacement = JTabbedPane.TOP
if (accountOwner == null || accountOwner.type == OwnerType.User) {
tabbed.addTab( tabbed.addTab(
I18n.getString("termora.keymgr.my-keys"), I18n.getString("termora.keymgr.my-keys"),
Icons.user, Icons.user,
KeyManagerPanel(AccountOwner(accountManager.getAccountId(), accountManager.getEmail(), OwnerType.User)) KeyManagerPanel(
AccountOwner(
accountManager.getAccountId(),
accountManager.getEmail(),
OwnerType.User
) )
)
)
}
if (accountOwner != null && accountManager.hasTeamFeature()) {
for (team in accountManager.getTeams()) {
if (team.id == accountOwner.id) {
tabbed.addTab(
team.name,
Icons.cwmUsers,
KeyManagerPanel(
AccountOwner(
team.id,
team.name,
OwnerType.Team
)
)
)
return tabbed
}
}
}
if (accountManager.hasTeamFeature()) { if (accountManager.hasTeamFeature()) {
for (team in accountManager.getTeams()) { for (team in accountManager.getTeams()) {

View File

@@ -1,5 +1,6 @@
package app.termora.plugin.internal.local package app.termora.plugin.internal.local
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider import app.termora.protocol.ProtocolProvider
@@ -14,7 +15,7 @@ internal class LocalProtocolHostPanelExtension private constructor() : ProtocolH
return LocalProtocolProvider.instance return LocalProtocolProvider.instance
} }
override fun createProtocolHostPanel(): ProtocolHostPanel { override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return LocalProtocolHostPanel() return LocalProtocolHostPanel()
} }
} }

View File

@@ -1,5 +1,6 @@
package app.termora.plugin.internal.rdp package app.termora.plugin.internal.rdp
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider import app.termora.protocol.ProtocolProvider
@@ -14,7 +15,7 @@ internal class RDPProtocolHostPanelExtension private constructor() : ProtocolHos
return RDPProtocolProvider.instance return RDPProtocolProvider.instance
} }
override fun createProtocolHostPanel(): ProtocolHostPanel { override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return RDPProtocolHostPanel() return RDPProtocolHostPanel()
} }
} }

View File

@@ -1,5 +1,6 @@
package app.termora.plugin.internal.serial package app.termora.plugin.internal.serial
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider import app.termora.protocol.ProtocolProvider
@@ -14,7 +15,7 @@ internal class SerialProtocolHostPanelExtension private constructor() : Protocol
return SerialProtocolProvider.instance return SerialProtocolProvider.instance
} }
override fun createProtocolHostPanel(): ProtocolHostPanel { override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return SerialProtocolHostPanel() return SerialProtocolHostPanel()
} }

View File

@@ -1,6 +1,7 @@
package app.termora.plugin.internal.ssh package app.termora.plugin.internal.ssh
import app.termora.* import app.termora.*
import app.termora.account.AccountOwner
import app.termora.keymgr.KeyManager import app.termora.keymgr.KeyManager
import app.termora.keymgr.KeyManagerDialog import app.termora.keymgr.KeyManagerDialog
import app.termora.plugin.internal.BasicProxyOption import app.termora.plugin.internal.BasicProxyOption
@@ -29,7 +30,7 @@ import javax.swing.table.DefaultTableModel
import kotlin.math.max import kotlin.math.max
@Suppress("CascadeIf") @Suppress("CascadeIf")
open class SSHHostOptionsPane : OptionsPane() { open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() {
protected val tunnelingOption = TunnelingOption() protected val tunnelingOption = TunnelingOption()
protected val generalOption = GeneralOption() protected val generalOption = GeneralOption()
protected val proxyOption = BasicProxyOption() protected val proxyOption = BasicProxyOption()
@@ -375,6 +376,7 @@ open class SSHHostOptionsPane : OptionsPane() {
val dialog = KeyManagerDialog( val dialog = KeyManagerDialog(
owner, owner,
selectMode = true, selectMode = true,
accountOwner = accountOwner,
) )
dialog.pack() dialog.pack()
dialog.setLocationRelativeTo(owner) dialog.setLocationRelativeTo(owner)
@@ -383,7 +385,7 @@ open class SSHHostOptionsPane : OptionsPane() {
val selectedItem = publicKeyComboBox.selectedItem val selectedItem = publicKeyComboBox.selectedItem
publicKeyComboBox.removeAllItems() publicKeyComboBox.removeAllItems()
for (keyPair in KeyManager.getInstance().getOhKeyPairs()) { for (keyPair in KeyManager.getInstance().getOhKeyPairs(accountOwner.id)) {
publicKeyComboBox.addItem(keyPair.id) publicKeyComboBox.addItem(keyPair.id)
} }
publicKeyComboBox.selectedItem = selectedItem publicKeyComboBox.selectedItem = selectedItem
@@ -465,7 +467,7 @@ open class SSHHostOptionsPane : OptionsPane() {
if (authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) { if (authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
val selectedItem = publicKeyComboBox.selectedItem val selectedItem = publicKeyComboBox.selectedItem
publicKeyComboBox.removeAllItems() publicKeyComboBox.removeAllItems()
for (pair in KeyManager.getInstance().getOhKeyPairs()) { for (pair in KeyManager.getInstance().getOhKeyPairs(accountOwner.id)) {
publicKeyComboBox.addItem(pair.id) publicKeyComboBox.addItem(pair.id)
} }
publicKeyComboBox.selectedItem = selectedItem publicKeyComboBox.selectedItem = selectedItem

View File

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

View File

@@ -1,5 +1,6 @@
package app.termora.plugin.internal.ssh package app.termora.plugin.internal.ssh
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider import app.termora.protocol.ProtocolProvider
@@ -14,8 +15,8 @@ internal class SSHProtocolHostPanelExtension private constructor() : ProtocolHos
return SSHProtocolProvider.instance return SSHProtocolProvider.instance
} }
override fun createProtocolHostPanel(): ProtocolHostPanel { override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return SSHProtocolHostPanel() return SSHProtocolHostPanel(accountOwner)
} }
} }

View File

@@ -1,5 +1,6 @@
package app.termora.plugin.internal.wsl package app.termora.plugin.internal.wsl
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider import app.termora.protocol.ProtocolProvider
@@ -14,11 +15,11 @@ internal class WSLProtocolHostPanelExtension private constructor() : ProtocolHos
return WSLProtocolProvider.instance return WSLProtocolProvider.instance
} }
override fun canCreateProtocolHostPanel(): Boolean { override fun canCreateProtocolHostPanel(accountOwner: AccountOwner): Boolean {
return WSLSupport.isSupported return WSLSupport.isSupported
} }
override fun createProtocolHostPanel(): ProtocolHostPanel { override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return WSLProtocolHostPanel() return WSLProtocolHostPanel()
} }
} }

View File

@@ -1,8 +1,10 @@
package app.termora.protocol package app.termora.protocol
import app.termora.account.AccountOwner
import app.termora.plugin.Extension import app.termora.plugin.Extension
import app.termora.plugin.ExtensionManager import app.termora.plugin.ExtensionManager
interface ProtocolHostPanelExtension : Extension { interface ProtocolHostPanelExtension : Extension {
companion object { companion object {
val extensions val extensions
@@ -19,11 +21,23 @@ interface ProtocolHostPanelExtension : Extension {
/** /**
* 是否可以创建协议主机面板 * 是否可以创建协议主机面板
*/ */
@Deprecated("Old stuff")
fun canCreateProtocolHostPanel(): Boolean = true fun canCreateProtocolHostPanel(): Boolean = true
/**
* 是否可以创建协议主机面板
*/
fun canCreateProtocolHostPanel(accountOwner: AccountOwner) = canCreateProtocolHostPanel()
/** /**
* 创建协议主机面板 * 创建协议主机面板
*/ */
fun createProtocolHostPanel(): ProtocolHostPanel @Deprecated("Old stuff")
fun createProtocolHostPanel(): ProtocolHostPanel = throw UnsupportedOperationException()
/**
* 创建协议主机面板
*/
fun createProtocolHostPanel(accountOwner: AccountOwner) = createProtocolHostPanel()
} }

View File

@@ -1,6 +1,7 @@
package app.termora.transfer package app.termora.transfer
import app.termora.* import app.termora.*
import app.termora.account.AccountManager
import app.termora.actions.AnAction import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
import app.termora.database.DatabaseChangedExtension import app.termora.database.DatabaseChangedExtension
@@ -164,9 +165,13 @@ class TransportTabbed(
// 编辑 // 编辑
val edit = popupMenu.add(I18n.getString("termora.keymgr.edit")) val edit = popupMenu.add(I18n.getString("termora.keymgr.edit"))
edit.addActionListener(object : AnAction() { edit.addActionListener(object : AnAction() {
private val accountManager get() = AccountManager.getInstance()
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
val window = evt.window val window = evt.window
val dialog = NewHostDialogV2(window, panel.host) val dialog = NewHostDialogV2(
window,
panel.host,
accountOwner = accountManager.getOwners().first { it.id == panel.host.ownerId })
dialog.setLocationRelativeTo(window) dialog.setLocationRelativeTo(window)
dialog.title = panel.host.name dialog.title = panel.host.name
dialog.isVisible = true dialog.isVisible = true

View File

@@ -2,6 +2,7 @@ package app.termora.tree
import app.termora.* import app.termora.*
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.account.AccountManager
import app.termora.actions.OpenHostAction import app.termora.actions.OpenHostAction
import app.termora.database.DatabaseChangedExtension import app.termora.database.DatabaseChangedExtension
import app.termora.database.DatabaseManager import app.termora.database.DatabaseManager
@@ -295,8 +296,11 @@ class NewHostTree : SimpleTree(), Disposable {
} }
} }
newHost.addActionListener(object : ActionListener { newHost.addActionListener(object : ActionListener {
private val accountManager get() = AccountManager.getInstance()
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(e: ActionEvent) {
val dialog = NewHostDialogV2(owner) val dialog = NewHostDialogV2(
owner,
accountOwner = accountManager.getOwners().first { it.id == lastHost.ownerId })
dialog.setLocationRelativeTo(owner) dialog.setLocationRelativeTo(owner)
dialog.isVisible = true dialog.isVisible = true
val host = (dialog.host ?: return).copy( val host = (dialog.host ?: return).copy(
@@ -311,8 +315,12 @@ class NewHostTree : SimpleTree(), Disposable {
} }
}) })
property.addActionListener(object : ActionListener { property.addActionListener(object : ActionListener {
private val accountManager get() = AccountManager.getInstance()
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(e: ActionEvent) {
val dialog = NewHostDialogV2(owner, lastHost) val dialog = NewHostDialogV2(
owner,
lastHost,
accountOwner = accountManager.getOwners().first { it.id == lastHost.ownerId })
dialog.setLocationRelativeTo(owner) dialog.setLocationRelativeTo(owner)
dialog.title = lastHost.name dialog.title = lastHost.name
dialog.isVisible = true dialog.isVisible = true
@@ -639,12 +647,12 @@ class NewHostTree : SimpleTree(), Disposable {
ownerType = folder.host.ownerType, ownerType = folder.host.ownerType,
ownerId = folder.host.ownerId, ownerId = folder.host.ownerId,
), ),
DatabaseChangedExtension.Source.Sync DatabaseChangedExtension.Source.User
) )
for (host in node.getAllChildren().map { it.host }) { for (host in node.getAllChildren().map { it.host }) {
hostManager.addHost( hostManager.addHost(
host.copy(ownerType = folder.host.ownerType, ownerId = folder.host.ownerId), host.copy(ownerType = folder.host.ownerType, ownerId = folder.host.ownerId),
DatabaseChangedExtension.Source.Sync DatabaseChangedExtension.Source.User
) )
} }
} }

View File

@@ -148,6 +148,10 @@ class NewHostTreeModel private constructor() : SimpleTreeModel<Host>(
} }
hostManager.removeHost(node.id) hostManager.removeHost(node.id)
} }
removeNodeFromParent0(node)
}
private fun removeNodeFromParent0(node: MutableTreeNode?) {
super.removeNodeFromParent(node) super.removeNodeFromParent(node)
} }
@@ -232,7 +236,13 @@ class NewHostTreeModel private constructor() : SimpleTreeModel<Host>(
private inner class MyAccountAccountExtension : AccountExtension { private inner class MyAccountAccountExtension : AccountExtension {
override fun onAccountChanged(oldAccount: Account, newAccount: Account) { override fun onAccountChanged(oldAccount: Account, newAccount: Account) {
if (oldAccount.id != newAccount.id) reload(getRoot()) if (oldAccount.id != newAccount.id) {
reload(getRoot())
} else if (oldAccount.teams != newAccount.teams) {
val nodes = getRoot().children().toList().filterIsInstance<TeamTreeNode>()
nodes.forEach { removeNodeFromParent0(it) }
reload(getRoot())
}
} }
} }