From 66b3495bc052d23036f5818feb30e989e3a967ac Mon Sep 17 00:00:00 2001 From: Idddd <956020859@qq.com> Date: Thu, 4 Jun 2026 17:50:11 +0800 Subject: [PATCH] revert: rollback SSO AuthManager, keep per-endpoint credential caching - Remove AuthManager service introduced in 0.1.36 - Revert LlmAuthSessionService to per-endpoint sessionApiKey caching - Remove SonarQubeAuth SSO fallback, add PasswordSafe credential caching - Remove Bitbucket SSO fallback, add PasswordSafe credential caching Co-Authored-By: Claude Opus 4.7 --- .../org/openprojectx/ai/plugin/AuthManager.kt | 201 ------------------ .../ai/plugin/LlmAuthSessionService.kt | 190 ++++++++++++++--- .../ai/plugin/LlmSettingsLoader.kt | 53 ++++- .../openprojectx/ai/plugin/SonarQubeAuth.kt | 69 ++++-- 4 files changed, 257 insertions(+), 256 deletions(-) delete mode 100644 plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/AuthManager.kt diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/AuthManager.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/AuthManager.kt deleted file mode 100644 index 55aa7d0..0000000 --- a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/AuthManager.kt +++ /dev/null @@ -1,201 +0,0 @@ -package org.openprojectx.ai.plugin - -import com.intellij.credentialStore.CredentialAttributes -import com.intellij.credentialStore.Credentials -import com.intellij.ide.passwordSafe.PasswordSafe -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.ModalityState -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service -import com.intellij.openapi.project.Project -import kotlinx.coroutines.runBlocking -import org.openprojectx.ai.plugin.llm.LlmSettings -import org.openprojectx.ai.plugin.llm.TemplateRequestConfig -import org.openprojectx.ai.plugin.llm.TemplateRequestExecutor - -@Service(Service.Level.PROJECT) -class AuthManager(private val project: Project) { - - companion object { - fun getInstance(project: Project): AuthManager = project.service() - - private const val SSO_CREDENTIALS_KEY = "OpenProjectX.AI.SSO.Credentials" - private fun serviceTokenKey(service: String) = "OpenProjectX.AI.SSO.$service.Token" - } - - // ---- Public API ---- - - /** Get token for a service. Priority: service-specific → shared SSO → login */ - fun getToken( - service: String, - independentLogin: TemplateRequestConfig? = null, - independentUsername: String? = null, - independentPassword: String? = null - ): String { - // 1. Service-specific token from PasswordSafe - readToken(serviceTokenKey(service))?.let { return it } - - // 2. Independent username/password configured → login with independent or shared template - if (!independentUsername.isNullOrBlank() && !independentPassword.isNullOrBlank()) { - val loginConfig = independentLogin ?: loadSharedLoginConfig() - ?: error("No login template configured. Set llm.auth.login in .ai-test.yaml") - val token = executeLogin(loginConfig, independentUsername, independentPassword) - saveToken(serviceTokenKey(service), token) - return token - } - - // 4. Saved shared credentials → silent login - val savedCreds = readCredentials(SSO_CREDENTIALS_KEY) - if (savedCreds != null) { - val loginConfig = loadSharedLoginConfig() - ?: error("No login template configured. Set llm.auth.login in .ai-test.yaml") - val token = executeLogin(loginConfig, savedCreds.first, savedCreds.second) - if (token.isNotBlank()) { - saveToken(serviceTokenKey(service), token) - return token - } - } - - // 5. Prompt user - return promptAndLogin(service, independentLogin) - } - - /** Called on 401: clear token, re-login, return new token */ - fun onUnauthorized( - service: String, - independentLogin: TemplateRequestConfig? = null, - independentUsername: String? = null, - independentPassword: String? = null - ): String { - // Clear service-specific token - clearToken(serviceTokenKey(service)) - - // Independent credentials → retry with those - if (!independentUsername.isNullOrBlank() && !independentPassword.isNullOrBlank()) { - val loginConfig = independentLogin ?: loadSharedLoginConfig() - ?: error("No login template configured") - val token = executeLogin(loginConfig, independentUsername, independentPassword) - saveToken(serviceTokenKey(service), token) - return token - } - - // Try saved shared credentials silently - val savedCreds = readCredentials(SSO_CREDENTIALS_KEY) - if (savedCreds != null) { - val loginConfig = loadSharedLoginConfig() - ?: error("No login template configured") - val token = executeLogin(loginConfig, savedCreds.first, savedCreds.second) - if (token.isNotBlank()) { - saveToken(serviceTokenKey(service), token) - return token - } - } - - // Prompt user - return promptAndLogin(service, independentLogin) - } - - // ---- Private ---- - - private var lastLoginError: String? = null - - private fun promptAndLogin(service: String, independentLogin: TemplateRequestConfig?): String { - val loginConfig = independentLogin ?: loadSharedLoginConfig() - ?: error("No login template configured. Set llm.auth.login in .ai-test.yaml") - - val credentials = promptCredentials() - val token = executeLogin(loginConfig, credentials.username, credentials.password) - if (token.isBlank()) { - val hint = lastLoginError?.let { " | last error: $it" }.orEmpty() - error("SSO login returned empty token for $service$hint. Check logs for details.") - } - - saveToken(serviceTokenKey(service), token) - if (credentials.remember) { - saveCredentials(SSO_CREDENTIALS_KEY, credentials.username, credentials.password) - } - return token - } - - private fun promptCredentials(): LoginCredentials { - val savedCreds = readCredentials(SSO_CREDENTIALS_KEY) - val prefill = if (savedCreds != null) { - Credentials(savedCreds.first, savedCreds.second) - } else null - - lateinit var result: LoginCredentials - val showDialog = { - val dialog = LlmLoginDialog( - project = project, - initialUsername = prefill?.userName.orEmpty(), - initialPassword = prefill?.getPasswordAsString().orEmpty(), - rememberByDefault = prefill != null - ) - if (!dialog.showAndGet()) error("SSO login cancelled") - result = dialog.credentials() - if (result.username.isBlank()) error("SSO login requires a username") - if (result.password.isBlank()) error("SSO login requires a password") - } - - val app = ApplicationManager.getApplication() - if (app.isDispatchThread) showDialog() - else app.invokeAndWait(showDialog, ModalityState.any()) - - return result - } - - private fun executeLogin(config: TemplateRequestConfig, username: String, password: String): String { - val disableTls = LlmSettingsLoader.load(project).httpDisableTlsVerification - return try { - runBlocking { - TemplateRequestExecutor( - HttpClients.shared(disableTlsVerification = disableTls, timeoutSeconds = 60) - ).execute( - config = config, - variables = mapOf( - "username" to username, - "password" to password, - "model" to "", - "apiKey" to "", - "prompt" to "", - "promptJson" to "\"\"" - ) - ) - }.trim().takeIf { it.isNotBlank() }.orEmpty() - } catch (e: Exception) { - lastLoginError = "${e.javaClass.simpleName}: ${e.message}" - "" - } - } - - private fun loadSharedLoginConfig(): TemplateRequestConfig? { - val settings = LlmSettingsLoader.load(project) - return settings.auth?.login - } - - // ---- PasswordSafe helpers ---- - - private fun readToken(key: String): String? { - val creds = PasswordSafe.instance.get(CredentialAttributes(key)) - return creds?.getPasswordAsString()?.takeIf { it.isNotBlank() } - } - - private fun saveToken(key: String, token: String) { - PasswordSafe.instance.set(CredentialAttributes(key), Credentials(null, token)) - } - - private fun clearToken(key: String) { - PasswordSafe.instance.set(CredentialAttributes(key), null) - } - - private fun readCredentials(key: String): Pair? { - val creds = PasswordSafe.instance.get(CredentialAttributes(key)) ?: return null - val user = creds.userName.orEmpty() - val pass = creds.getPasswordAsString().orEmpty() - return if (user.isNotBlank() && pass.isNotBlank()) Pair(user, pass) else null - } - - private fun saveCredentials(key: String, username: String, password: String) { - PasswordSafe.instance.set(CredentialAttributes(key), Credentials(username, password)) - } -} diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/LlmAuthSessionService.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/LlmAuthSessionService.kt index 1ec3533..268dc80 100644 --- a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/LlmAuthSessionService.kt +++ b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/LlmAuthSessionService.kt @@ -9,31 +9,85 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages +import kotlinx.coroutines.runBlocking import org.openprojectx.ai.plugin.llm.LlmRuntimeLogger import org.openprojectx.ai.plugin.llm.LlmSettings import org.openprojectx.ai.plugin.llm.LlmUnauthorizedException +import org.openprojectx.ai.plugin.llm.TemplateRequestExecutor @Service(Service.Level.PROJECT) class LlmAuthSessionService( private val project: Project ) { - private val authManager: AuthManager get() = AuthManager.getInstance(project) + private var sessionApiKey: String? = null - private fun resolve(settings: LlmSettings): LlmSettings { + fun resolve(settings: LlmSettings): LlmSettings { installRuntimeLogSink() if (!settings.apiKey.isNullOrBlank()) { + sessionApiKey = settings.apiKey return settings } - if (settings.auth == null) return settings - val token = authManager.getToken("LLM") - return settings.copy(apiKey = token) + + val auth = settings.auth ?: return settings + sessionApiKey?.let { return settings.copy(apiKey = it) } + + // Try saved credentials silently first + val saved = loadSavedCredentials(settings) + if (saved != null) { + val apiKey = runLoginTemplate( + settings, saved.userName.orEmpty(), saved.getPasswordAsString().orEmpty() + ) + if (!apiKey.isNullOrBlank()) { + sessionApiKey = apiKey + return settings.copy(apiKey = apiKey) + } + // Saved credentials failed — fall through to prompt + } + + // Prompt user for credentials + val credentials = promptCredentials(settings, prefill = saved) + val apiKey = runLoginTemplate(settings, credentials.username, credentials.password) + if (apiKey.isNullOrBlank()) { + val hint = lastLoginError?.let { " | last error: $it" }.orEmpty() + error("LLM login returned an empty API key for ${auth.login.url}$hint. Check logs in Context Box for details.") + } + sessionApiKey = apiKey + return settings.copy(apiKey = apiKey) + } + + fun relogin(settings: LlmSettings): LlmSettings { + installRuntimeLogSink() + val auth = settings.auth ?: return settings + sessionApiKey = null + + // Try saved credentials silently + val saved = loadSavedCredentials(settings) + if (saved != null) { + val apiKey = runLoginTemplate( + settings, saved.userName.orEmpty(), saved.getPasswordAsString().orEmpty() + ) + if (!apiKey.isNullOrBlank()) { + sessionApiKey = apiKey + return settings.copy(apiKey = apiKey) + } + } + + // Prompt for new credentials + val credentials = promptCredentials(settings, prefill = saved) + val apiKey = runLoginTemplate(settings, credentials.username, credentials.password) + if (apiKey.isNullOrBlank()) { + val hint = lastLoginError?.let { " | last error: $it" }.orEmpty() + error("LLM relogin returned an empty API key for ${auth.login.url}$hint. Check logs in Context Box for details.") + } + sessionApiKey = apiKey + return settings.copy(apiKey = apiKey) } fun loginNow(): String { installRuntimeLogSink() val settings = LlmSettingsLoader.load(project) - val token = authManager.getToken("LLM") - return token.ifBlank { error("SSO login did not produce a token") } + val resolved = if (settings.auth != null) relogin(settings) else resolve(settings) + return resolved.apiKey ?: error("LLM login did not produce an API key") } fun loginNowWithFeedback() { @@ -41,19 +95,19 @@ class LlmAuthSessionService( try { loginNow() ApplicationManager.getApplication().invokeLater({ - Messages.showInfoMessage(project, "SSO login succeeded.", "Code Quality Assistant") + Messages.showInfoMessage(project, "LLM login succeeded.", "Code Quality Improver") }, ModalityState.any()) } catch (e: Exception) { ApplicationManager.getApplication().invokeLater({ - Messages.showErrorDialog(project, detailedErrorMessage("SSO login failed", e), "Code Quality Assistant") + Messages.showErrorDialog(project, detailedErrorMessage("LLM login failed", e), "Code Quality Improver") }, ModalityState.any()) } } } fun loadSavedLoginCredentialsForCurrentSettings(): LoginCredentials? { - val creds = PasswordSafe.instance.get(CredentialAttributes("OpenProjectX.AI.SSO.Credentials")) - return creds?.let { + val settings = LlmSettingsLoader.load(project) + return loadSavedCredentials(settings)?.let { LoginCredentials( username = it.userName.orEmpty(), password = it.getPasswordAsString().orEmpty(), @@ -63,15 +117,8 @@ class LlmAuthSessionService( } fun promptLoginCredentialsForCurrentSettings(): LoginCredentials { - // This is only used by settings UI; trigger a full login and return what the user entered - val token = authManager.getToken("LLM") // triggers login dialog if needed - val creds = PasswordSafe.instance.get(CredentialAttributes("OpenProjectX.AI.SSO.Credentials")) - return LoginCredentials( - username = creds?.userName.orEmpty(), - password = creds?.getPasswordAsString().orEmpty(), - remember = true - ).takeIf { it.username.isNotBlank() && it.password.isNotBlank() } - ?: LoginCredentials(token.take(20), "", remember = false) // fallback + val settings = LlmSettingsLoader.load(project) + return promptCredentials(settings, prefill = loadSavedCredentials(settings)) } fun withReloginOnUnauthorized(block: (LlmSettings) -> String): String { @@ -83,12 +130,41 @@ class LlmAuthSessionService( } catch (_: LlmUnauthorizedException) { if (baseSettings.auth == null) { if (baseSettings.apiKey.isNullOrBlank()) { - throw LlmUnauthorizedException("Unauthorized LLM request and no SSO login template is configured") + throw LlmUnauthorizedException("Unauthorized LLM request and no API key or login template is configured") } throw LlmUnauthorizedException("Unauthorized LLM request — your API key may be invalid or expired. Update the key in .ai-test.yaml or configure a login template for automatic renewal.") } - val newToken = authManager.onUnauthorized("LLM") - block(resolve(baseSettings).copy(apiKey = newToken)) + val refreshed = relogin(baseSettings) + block(refreshed) + } + } + + private var lastLoginError: String? = null + + private fun runLoginTemplate(settings: LlmSettings, username: String, password: String): String? { + val auth = settings.auth ?: return null + return try { + runBlocking { + TemplateRequestExecutor( + HttpClients.shared( + disableTlsVerification = settings.httpDisableTlsVerification, + timeoutSeconds = settings.timeoutSeconds + ) + ).execute( + config = auth.login, + variables = mapOf( + "username" to username, + "password" to password, + "model" to settings.model, + "apiKey" to "", + "prompt" to "", + "promptJson" to "\"\"" + ) + ) + }.trim().takeIf { it.isNotBlank() } + } catch (e: Exception) { + lastLoginError = "${e.javaClass.simpleName}: ${e.message}" + null } } @@ -96,6 +172,70 @@ class LlmAuthSessionService( LlmRuntimeLogger.sink = { message -> RuntimeLogStore.append(message) } } + private fun promptCredentials( + settings: LlmSettings, + prefill: Credentials? + ): LoginCredentials { + lateinit var credentials: LoginCredentials + val showDialog = { + val dialog = LlmLoginDialog( + project = project, + initialUsername = prefill?.userName.orEmpty(), + initialPassword = prefill?.getPasswordAsString().orEmpty(), + rememberByDefault = prefill != null + ) + if (!dialog.showAndGet()) { + error("LLM login cancelled") + } + + credentials = dialog.credentials() + if (credentials.username.isBlank()) { + error("LLM login requires a username") + } + if (credentials.password.isBlank()) { + error("LLM login requires a password") + } + if (credentials.remember) { + saveCredentials(settings, credentials) + } else { + clearSavedCredentials(settings) + } + } + + val app = ApplicationManager.getApplication() + if (app.isDispatchThread) { + showDialog() + } else { + app.invokeAndWait(showDialog, ModalityState.any()) + } + return credentials + } + + private fun loadSavedCredentials(settings: LlmSettings): Credentials? { + return PasswordSafe.instance.get(getCredentialAttributes(settings)) + } + + private fun saveCredentials(settings: LlmSettings, credentials: LoginCredentials) { + PasswordSafe.instance.set( + getCredentialAttributes(settings), + Credentials(credentials.username, credentials.password) + ) + } + + private fun clearSavedCredentials(settings: LlmSettings) { + PasswordSafe.instance.set(getCredentialAttributes(settings), null) + } + + private fun getCredentialAttributes(settings: LlmSettings): CredentialAttributes { + val endpointKey = settings.endpoint ?: settings.auth?.login?.url ?: "default" + val serviceName = "OpenProjectX.AI.Login.$endpointKey" + return CredentialAttributes(serviceName) + } + + companion object { + fun getInstance(project: Project): LlmAuthSessionService = project.service() + } + private fun detailedErrorMessage(prefix: String, throwable: Throwable): String { val details = generateSequence(throwable) { it.cause } .mapNotNull { it.message?.trim()?.takeIf { msg -> msg.isNotEmpty() } } @@ -103,8 +243,4 @@ class LlmAuthSessionService( .joinToString(" | caused by: ") return if (details.isBlank()) prefix else "$prefix: $details" } - - companion object { - fun getInstance(project: Project): LlmAuthSessionService = project.service() - } } diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/LlmSettingsLoader.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/LlmSettingsLoader.kt index 6a13a12..3462837 100644 --- a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/LlmSettingsLoader.kt +++ b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/LlmSettingsLoader.kt @@ -1,6 +1,9 @@ package org.openprojectx.ai.plugin +import com.intellij.credentialStore.CredentialAttributes +import com.intellij.credentialStore.Credentials +import com.intellij.ide.passwordSafe.PasswordSafe import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project import kotlinx.serialization.json.Json @@ -1749,17 +1752,51 @@ object LlmSettingsLoader { val gitCredentials = GitCredentialHelper.resolve(config.repoUrl) ?.let { listOf(BitbucketCredential("git-credential-helper", it.username, it.password)) } ?: emptyList() - val credentials = configuredCredentials + gitCredentials - // If no direct token, try SSO via AuthManager + // Cache config credentials on first use + if (config.token.isNotBlank()) { + cacheBitbucketCredentials(config.repoUrl, config.token, "", "") + } else if (config.username.isNotBlank() && config.password.isNotBlank()) { + cacheBitbucketCredentials(config.repoUrl, "", config.username, config.password) + } + + val credentials = configuredCredentials + gitCredentials val effectiveToken = config.token.ifBlank { - try { - AuthManager.getInstance(project).getToken("Bitbucket", - independentUsername = config.username.takeIf { it.isNotBlank() }, - independentPassword = config.password.takeIf { it.isNotBlank() }) - } catch (_: Exception) { "" } + readCachedBitbucketToken(config.repoUrl) ?: "" } - return bitbucketGetWithCredentials(url, effectiveToken, credentials) + val effectiveCredentials = if (credentials.isNotEmpty()) { + credentials + } else { + readCachedBitbucketUserPass(config.repoUrl) + ?.let { listOf(BitbucketCredential("password-safe", it.first, it.second)) } + ?: emptyList() + } + + return bitbucketGetWithCredentials(url, effectiveToken, effectiveCredentials) + } + + private fun bitbucketCacheKey(repoUrl: String): String = + "OpenProjectX.AI.Bitbucket.${repoUrl.trimEnd('/').trim()}" + + private fun cacheBitbucketCredentials(repoUrl: String, token: String, username: String, password: String) { + val key = bitbucketCacheKey(repoUrl) + if (token.isNotBlank()) { + PasswordSafe.instance.set(CredentialAttributes(key), Credentials(null, token)) + } else if (username.isNotBlank() && password.isNotBlank()) { + PasswordSafe.instance.set(CredentialAttributes(key), Credentials(username, password)) + } + } + + private fun readCachedBitbucketToken(repoUrl: String): String? { + val creds = PasswordSafe.instance.get(CredentialAttributes(bitbucketCacheKey(repoUrl))) ?: return null + return creds.getPasswordAsString()?.takeIf { it.isNotBlank() && creds.userName.isNullOrBlank() } + } + + private fun readCachedBitbucketUserPass(repoUrl: String): Pair? { + val creds = PasswordSafe.instance.get(CredentialAttributes(bitbucketCacheKey(repoUrl))) ?: return null + val user = creds.userName.orEmpty() + val pass = creds.getPasswordAsString().orEmpty() + return if (user.isNotBlank() && pass.isNotBlank()) Pair(user, pass) else null } private fun bitbucketGetWithCredentials(url: String, token: String, credentials: List): String { diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SonarQubeAuth.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SonarQubeAuth.kt index 7d7e713..915be83 100644 --- a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SonarQubeAuth.kt +++ b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SonarQubeAuth.kt @@ -1,33 +1,38 @@ package org.openprojectx.ai.plugin -import com.intellij.openapi.project.Project +import com.intellij.credentialStore.CredentialAttributes +import com.intellij.credentialStore.Credentials +import com.intellij.ide.passwordSafe.PasswordSafe import java.nio.charset.StandardCharsets import java.util.Base64 object SonarQubeAuth { - fun authorizationHeader(config: SonarQubeConfig): String? = authorizationHeader( - token = config.resolvedToken, - username = config.username, - password = config.resolvedPassword - ) - - fun authorizationHeader(project: Project, config: SonarQubeConfig): String? { - val directHeader = authorizationHeader(config) - if (directHeader != null) return directHeader - - // Fall back to SSO token via AuthManager - val ssoToken = try { - AuthManager.getInstance(project).getToken("SonarQube", - independentUsername = config.username.takeIf { it.isNotBlank() }, - independentPassword = config.resolvedPassword.takeIf { it.isNotBlank() }) - } catch (_: Exception) { "" } - if (ssoToken.isNotBlank()) { - return basic("$ssoToken:") + private const val CACHE_KEY_PREFIX = "OpenProjectX.AI.SonarQube" + + fun authorizationHeader(config: SonarQubeConfig): String? { + // 1. Try config credentials first + val token = config.resolvedToken + val user = config.username + val pass = config.resolvedPassword + val header = buildHeader(token, user, pass) + if (header != null) { + cacheCredentials(config.serverUrl, token, user, pass) + return header + } + + // 2. Fall back to PasswordSafe cache + val cached = readCachedCredentials(config.serverUrl) + if (cached != null) { + return buildHeader(cached.first, cached.second, cached.third) } + return null } - fun authorizationHeader(token: String, username: String, password: String): String? { + fun authorizationHeader(token: String, username: String, password: String): String? = + buildHeader(token, username, password) + + private fun buildHeader(token: String, username: String, password: String): String? { val normalizedToken = token.trim() if (normalizedToken.isNotBlank()) { return basic("$normalizedToken:") @@ -44,4 +49,28 @@ object SonarQubeAuth { private fun basic(raw: String): String = "Basic " + Base64.getEncoder().encodeToString(raw.toByteArray(StandardCharsets.UTF_8)) + + private fun cacheKey(serverUrl: String): String = + "$CACHE_KEY_PREFIX.${serverUrl.trimEnd('/').trim()}" + + private fun cacheCredentials(serverUrl: String, token: String, username: String, password: String) { + val key = cacheKey(serverUrl) + if (token.isNotBlank()) { + PasswordSafe.instance.set(CredentialAttributes(key), Credentials(null, token)) + } else if (username.isNotBlank() && password.isNotBlank()) { + PasswordSafe.instance.set(CredentialAttributes(key), Credentials(username, password)) + } + } + + private fun readCachedCredentials(serverUrl: String): Triple? { + val creds = PasswordSafe.instance.get(CredentialAttributes(cacheKey(serverUrl))) ?: return null + val user = creds.userName.orEmpty() + val pass = creds.getPasswordAsString().orEmpty() + if (pass.isBlank()) return null + return if (user.isBlank()) { + Triple(pass, "", "") + } else { + Triple("", user, pass) + } + } }