Skip to content
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