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
Prev Previous commit
Next Next commit
fix: auto-login was no longer loading the last url and token
On top of that the shared auth context was reset each time the TBX window was hided and then made
visible again. The auth context is now a singleton object shared between the wizard steps
  • Loading branch information
fioan89 committed May 23, 2025
commit d706582a79a5cd8751b7df3428bf2ee120d5d0ba
4 changes: 2 additions & 2 deletions src/main/kotlin/com/coder/toolbox/browser/BrowserUtil.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.coder.toolbox.browser

import com.coder.toolbox.util.toURL
import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
import java.net.URI


suspend fun LocalDesktopManager.browse(rawUrl: String, errorHandler: suspend (BrowserException) -> Unit) {
try {
val url = URI.create(rawUrl).toURL()
val url = rawUrl.toURL()
this.openUrl(url)
} catch (e: Exception) {
errorHandler(
Expand Down
18 changes: 12 additions & 6 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.util.toURL
import com.coder.toolbox.views.state.AuthContext
import com.coder.toolbox.views.state.AuthWizardState
import com.coder.toolbox.views.state.WizardStep
Expand All @@ -25,24 +26,29 @@ class AuthWizardPage(
context.ui.showUiPage(settingsPage)
})

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


/**
* Fields for this page, displayed in order.
*/
override val fields: MutableStateFlow<List<UiField>> = MutableStateFlow(emptyList())
override val actionButtons: MutableStateFlow<List<RunnableActionDescription>> = MutableStateFlow(emptyList())

init {
if (shouldAutoLogin.value) {
AuthContext.url = context.secrets.lastDeploymentURL.toURL()
AuthContext.token = context.secrets.lastToken
}
}

override fun beforeShow() {
displaySteps()
}
Expand Down
35 changes: 19 additions & 16 deletions src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ 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 @@ -50,35 +49,39 @@ class ConnectStep(
errorField.textState.update {
context.i18n.pnotr("")
}
if (authContext.isNotReadyForAuth()) return

statusField.textState.update { context.i18n.pnotr("Connecting to ${authContext.url!!.host}...") }
if (AuthContext.isNotReadyForAuth()) {
errorField.textState.update {
context.i18n.pnotr("URL and token were not properly configured. Please go back and provide a proper URL and token!")
}
return
}

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 = authContext.url
val token = authContext.token
if (url == null) {
if (!AuthContext.hasUrl()) {
errorField.textState.update { context.i18n.ptrl("URL is required") }
return
}

if (token.isNullOrBlank()) {
if (!AuthContext.hasToken()) {
errorField.textState.update { context.i18n.ptrl("Token is required") }
return
}
signInJob?.cancel()
signInJob = context.cs.launch {
try {
statusField.textState.update { (context.i18n.ptrl("Authenticating to ${url.host}...")) }
statusField.textState.update { (context.i18n.ptrl("Authenticating to ${AuthContext.url!!.host}...")) }
val client = CoderRestClient(
context,
url,
token,
AuthContext.url!!,
AuthContext.token!!,
PluginManager.pluginInfo.version,
)
// allows interleaving with the back/cancel action
Expand All @@ -93,21 +96,20 @@ class ConnectStep(
yield()
cli.login(client.token)
}
statusField.textState.update { (context.i18n.ptrl("Successfully configured ${url.host}...")) }
statusField.textState.update { (context.i18n.ptrl("Successfully configured ${AuthContext.url!!.host}...")) }
// allows interleaving with the back/cancel action
yield()
onConnect(client, cli)

authContext.reset()
AuthContext.reset()
AuthWizardState.resetSteps()
onConnect(client, cli)
} catch (ex: CancellationException) {
if (ex.message != USER_HIT_THE_BACK_BUTTON) {
notify("Connection to ${url.host} was configured", ex)
notify("Connection to ${AuthContext.url!!.host} was configured", ex)
onBack()
refreshWizard()
}
} catch (ex: Exception) {
notify("Failed to configure ${url.host}", ex)
notify("Failed to configure ${AuthContext.url!!.host}", ex)
onBack()
refreshWizard()
}
Expand All @@ -123,6 +125,7 @@ class ConnectStep(
signInJob?.cancel(CancellationException(USER_HIT_THE_BACK_BUTTON))
} finally {
if (shouldAutoLogin.value) {
AuthContext.reset()
AuthWizardState.resetSteps()
context.secrets.rememberMe = false
} else {
Expand Down
10 changes: 4 additions & 6 deletions src/main/kotlin/com/coder/toolbox/views/SignInStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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
import java.net.URL

/**
* A page with a field for providing the Coder deployment URL.
Expand All @@ -21,7 +21,6 @@ import java.net.URI
*/
class SignInStep(
private val context: CoderToolboxContext,
private val authContext: AuthContext,
private val notify: (String, Throwable) -> Unit
) :
WizardStep {
Expand Down Expand Up @@ -56,22 +55,21 @@ class SignInStep(
url
}
try {
validateRawUrl(url)
AuthContext.url = validateRawUrl(url)
} catch (e: MalformedURLException) {
notify("URL is invalid", e)
return false
}
authContext.url = URI.create(url).toURL()
AuthWizardState.goToNextStep()
return true
}

/**
* Throws [MalformedURLException] if the given string violates RFC-2396
*/
private fun validateRawUrl(url: String) {
private fun validateRawUrl(url: String): URL {
try {
url.toURL()
return url.toURL()
} catch (e: Exception) {
throw MalformedURLException(e.message)
}
Expand Down
19 changes: 10 additions & 9 deletions src/main/kotlin/com/coder/toolbox/views/TokenStep.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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
Expand All @@ -22,7 +21,6 @@ import kotlinx.coroutines.flow.update
*/
class TokenStep(
private val context: CoderToolboxContext,
private val authContext: AuthContext
) : WizardStep {
private val tokenField = TextField(context.i18n.ptrl("Token"), "", TextType.Password)
private val linkField = LinkField(context.i18n.ptrl("Get a token"), "")
Expand All @@ -39,15 +37,18 @@ class TokenStep(
errorField.textState.update {
context.i18n.pnotr("")
}
tokenField.textState.update {
if (authContext.hasUrl()) {
context.secrets.tokenFor(authContext.url!!) ?: ""
} else {
""
if (AuthContext.hasUrl()) {
tokenField.textState.update {
context.secrets.tokenFor(AuthContext.url!!) ?: ""
}
} else {
errorField.textState.update {
context.i18n.pnotr("URL not configure in the previous step. Please go back and provide a proper URL.")
return
}
}
(linkField.urlState as MutableStateFlow).update {
context.deploymentUrl?.first?.toURL()?.withPath("/login?redirect=%2Fcli-auth")?.toString() ?: ""
AuthContext.url!!.withPath("/login?redirect=%2Fcli-auth")?.toString() ?: ""
}
}

Expand All @@ -58,7 +59,7 @@ class TokenStep(
return false
}

authContext.token = token
AuthContext.token = token
AuthWizardState.goToNextStep()
return true
}
Expand Down
34 changes: 31 additions & 3 deletions src/main/kotlin/com/coder/toolbox/views/state/AuthContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,42 @@ package com.coder.toolbox.views.state

import java.net.URL

data class AuthContext(
var url: URL? = null,
/**
* Singleton that holds authentication context (URL and token) across multiple
* Toolbox window lifecycle events.
*
* This ensures that user input (URL and token) is not lost when the Toolbox
* window is temporarily closed or recreated.
*/
object AuthContext {
/**
* The currently entered URL.
*/
var url: URL? = null

/**
* The token associated with the URL.
*/
var token: String? = null
) {

/**
* Returns true if a URL is currently set.
*/
fun hasUrl(): Boolean = url != null

/**
* Returns true if a token is currently set.
*/
fun hasToken(): Boolean = !token.isNullOrBlank()

/**
* Returns true if URL or token is missing and auth is not yet possible.
*/
fun isNotReadyForAuth(): Boolean = !(hasUrl() && token != null)

/**
* Resets both URL and token to null.
*/
fun reset() {
url = null
token = null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package com.coder.toolbox.views.state


/**
* A singleton that maintains the state of the authorization wizard across Toolbox window lifecycle events.
*
* This is used to persist the wizard's progress (i.e., current step) between visibility changes
* of the Toolbox window. Without this object, closing and reopening the window would reset the wizard
* to its initial state by creating a new instance.
*/
object AuthWizardState {
private var currentStep = WizardStep.URL_REQUEST

Expand Down