Skip to content

fix: remember token when switching deployments #120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 26, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
- improved workspace status reporting (icon and colors) when it is failed, stopping, deleting, stopped or when we are
establishing the SSH connection.

### Fixed

- tokens are now remembered after switching between multiple deployments

## 0.2.2 - 2025-05-21

### Added
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ class CoderRemoteProvider(
// Store the URL and token for use next time.
context.secrets.lastDeploymentURL = client.url.toString()
context.secrets.lastToken = client.token ?: ""
context.secrets.storeTokenFor(client.url, context.secrets.lastToken)
// Currently we always remember, but this could be made an option.
context.secrets.rememberMe = true
this.client = client
Expand Down
7 changes: 7 additions & 0 deletions src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.coder.toolbox.store

import com.jetbrains.toolbox.api.core.PluginSecretStore
import java.net.URL


/**
Expand All @@ -26,4 +27,10 @@ class CoderSecretsStore(private val store: PluginSecretStore) {
var rememberMe: Boolean
get() = get("remember-me").toBoolean()
set(value) = set("remember-me", value.toString())

fun tokenFor(url: URL): String? = store[url.host]

fun storeTokenFor(url: URL, token: String) {
store[url.host] = token
}
}
15 changes: 12 additions & 3 deletions src/main/kotlin/com/coder/toolbox/views/AuthWizardPage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.coder.toolbox.views
import com.coder.toolbox.CoderToolboxContext
import com.coder.toolbox.cli.CoderCLIManager
import com.coder.toolbox.sdk.CoderRestClient
import com.coder.toolbox.views.state.AuthContext
import com.coder.toolbox.views.state.AuthWizardState
import com.coder.toolbox.views.state.WizardStep
import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription
Expand All @@ -23,9 +24,17 @@ class AuthWizardPage(
private val settingsAction = Action(context.i18n.ptrl("Settings"), actionBlock = {
context.ui.showUiPage(settingsPage)
})
private val signInStep = SignInStep(context, this::notify)
private val tokenStep = TokenStep(context)
private val connectStep = ConnectStep(context, shouldAutoLogin, this::notify, this::displaySteps, onConnect)

private val authContext: AuthContext = AuthContext()
private val signInStep = SignInStep(context, authContext, this::notify)
private val tokenStep = TokenStep(context, authContext)
private val connectStep = ConnectStep(
context,
authContext,
shouldAutoLogin,
this::notify,
this::displaySteps, onConnect
)


/**
Expand Down
13 changes: 8 additions & 5 deletions src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.coder.toolbox.cli.CoderCLIManager
import com.coder.toolbox.cli.ensureCLI
import com.coder.toolbox.plugin.PluginManager
import com.coder.toolbox.sdk.CoderRestClient
import com.coder.toolbox.util.toURL
import com.coder.toolbox.views.state.AuthContext
import com.coder.toolbox.views.state.AuthWizardState
import com.jetbrains.toolbox.api.localization.LocalizableString
import com.jetbrains.toolbox.api.ui.components.LabelField
Expand All @@ -25,6 +25,7 @@ private const val USER_HIT_THE_BACK_BUTTON = "User hit the back button"
*/
class ConnectStep(
private val context: CoderToolboxContext,
private val authContext: AuthContext,
private val shouldAutoLogin: StateFlow<Boolean>,
private val notify: (String, Throwable) -> Unit,
private val refreshWizard: () -> Unit,
Expand All @@ -49,18 +50,18 @@ class ConnectStep(
errorField.textState.update {
context.i18n.pnotr("")
}
if (authContext.isNotReadyForAuth()) return

val url = context.deploymentUrl?.first?.toURL()
statusField.textState.update { context.i18n.pnotr("Connecting to ${url?.host}...") }
statusField.textState.update { context.i18n.pnotr("Connecting to ${authContext.url!!.host}...") }
connect()
}

/**
* Try connecting to Coder with the provided URL and token.
*/
private fun connect() {
val url = context.deploymentUrl?.first?.toURL()
val token = context.getToken(context.deploymentUrl?.first)?.first
val url = authContext.url
val token = authContext.token
if (url == null) {
errorField.textState.update { context.i18n.ptrl("URL is required") }
return
Expand Down Expand Up @@ -96,6 +97,8 @@ class ConnectStep(
// allows interleaving with the back/cancel action
yield()
onConnect(client, cli)

authContext.reset()
AuthWizardState.resetSteps()
} catch (ex: CancellationException) {
if (ex.message != USER_HIT_THE_BACK_BUTTON) {
Expand Down
19 changes: 9 additions & 10 deletions src/main/kotlin/com/coder/toolbox/views/SignInStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,34 @@ package com.coder.toolbox.views

import com.coder.toolbox.CoderToolboxContext
import com.coder.toolbox.util.toURL
import com.coder.toolbox.views.state.AuthContext
import com.coder.toolbox.views.state.AuthWizardState
import com.jetbrains.toolbox.api.localization.LocalizableString
import com.jetbrains.toolbox.api.ui.components.LabelField
import com.jetbrains.toolbox.api.ui.components.RowGroup
import com.jetbrains.toolbox.api.ui.components.TextField
import com.jetbrains.toolbox.api.ui.components.TextType
import com.jetbrains.toolbox.api.ui.components.ValidationErrorField
import kotlinx.coroutines.flow.update
import java.net.MalformedURLException
import java.net.URI

/**
* A page with a field for providing the Coder deployment URL.
*
* Populates with the provided URL, at which point the user can accept or
* enter their own.
*/
class SignInStep(private val context: CoderToolboxContext, private val notify: (String, Throwable) -> Unit) :
class SignInStep(
private val context: CoderToolboxContext,
private val authContext: AuthContext,
private val notify: (String, Throwable) -> Unit
) :
WizardStep {
private val urlField = TextField(context.i18n.ptrl("Deployment URL"), "", TextType.General)
private val descriptionField = LabelField(context.i18n.pnotr(""))
private val errorField = ValidationErrorField(context.i18n.pnotr(""))

override val panel: RowGroup = RowGroup(
RowGroup.RowField(urlField),
RowGroup.RowField(descriptionField),
RowGroup.RowField(errorField)
)

Expand All @@ -37,11 +40,7 @@ class SignInStep(private val context: CoderToolboxContext, private val notify: (
context.i18n.pnotr("")
}
urlField.textState.update {
context.deploymentUrl?.first ?: ""
}

descriptionField.textState.update {
context.i18n.pnotr(context.deploymentUrl?.second?.description("URL") ?: "")
context.secrets.lastDeploymentURL
}
}

Expand All @@ -62,7 +61,7 @@ class SignInStep(private val context: CoderToolboxContext, private val notify: (
notify("URL is invalid", e)
return false
}
context.secrets.lastDeploymentURL = url
authContext.url = URI.create(url).toURL()
AuthWizardState.goToNextStep()
return true
}
Expand Down
23 changes: 11 additions & 12 deletions src/main/kotlin/com/coder/toolbox/views/TokenStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package com.coder.toolbox.views
import com.coder.toolbox.CoderToolboxContext
import com.coder.toolbox.util.toURL
import com.coder.toolbox.util.withPath
import com.coder.toolbox.views.state.AuthContext
import com.coder.toolbox.views.state.AuthWizardState
import com.jetbrains.toolbox.api.localization.LocalizableString
import com.jetbrains.toolbox.api.ui.components.LabelField
import com.jetbrains.toolbox.api.ui.components.LinkField
import com.jetbrains.toolbox.api.ui.components.RowGroup
import com.jetbrains.toolbox.api.ui.components.TextField
Expand All @@ -20,15 +20,16 @@ import kotlinx.coroutines.flow.update
* Populate with the provided token, at which point the user can accept or
* enter their own.
*/
class TokenStep(private val context: CoderToolboxContext) : WizardStep {
class TokenStep(
private val context: CoderToolboxContext,
private val authContext: AuthContext
) : WizardStep {
private val tokenField = TextField(context.i18n.ptrl("Token"), "", TextType.Password)
private val descriptionField = LabelField(context.i18n.pnotr(""))
private val linkField = LinkField(context.i18n.ptrl("Get a token"), "")
private val errorField = ValidationErrorField(context.i18n.pnotr(""))

override val panel: RowGroup = RowGroup(
RowGroup.RowField(tokenField),
RowGroup.RowField(descriptionField),
RowGroup.RowField(linkField),
RowGroup.RowField(errorField)
)
Expand All @@ -39,13 +40,11 @@ class TokenStep(private val context: CoderToolboxContext) : WizardStep {
context.i18n.pnotr("")
}
tokenField.textState.update {
context.getToken(context.deploymentUrl?.first)?.first ?: ""
}
descriptionField.textState.update {
context.i18n.pnotr(
context.getToken(context.deploymentUrl?.first)?.second?.description("token")
?: "No existing token for ${context.deploymentUrl} found."
)
if (authContext.hasUrl()) {
context.secrets.tokenFor(authContext.url!!) ?: ""
} else {
""
}
}
(linkField.urlState as MutableStateFlow).update {
context.deploymentUrl?.first?.toURL()?.withPath("/login?redirect=%2Fcli-auth")?.toString() ?: ""
Expand All @@ -59,7 +58,7 @@ class TokenStep(private val context: CoderToolboxContext) : WizardStep {
return false
}

context.secrets.lastToken = token
authContext.token = token
AuthWizardState.goToNextStep()
return true
}
Expand Down
17 changes: 17 additions & 0 deletions src/main/kotlin/com/coder/toolbox/views/state/AuthContext.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.coder.toolbox.views.state

import java.net.URL

data class AuthContext(
var url: URL? = null,
var token: String? = null
) {
fun hasUrl(): Boolean = url != null

fun isNotReadyForAuth(): Boolean = !(hasUrl() && token != null)

fun reset() {
url = null
token = null
}
}
Loading