@@ -12,6 +12,7 @@ import { CoderApi } from "./api/coderApi";
1212import { needToken } from "./api/utils" ;
1313import { type CliManager } from "./core/cliManager" ;
1414import { type ServiceContainer } from "./core/container" ;
15+ import { type ContextManager } from "./core/contextManager" ;
1516import { type MementoManager } from "./core/mementoManager" ;
1617import { type PathResolver } from "./core/pathResolver" ;
1718import { type SecretsManager } from "./core/secretsManager" ;
@@ -32,6 +33,7 @@ export class Commands {
3233 private readonly mementoManager : MementoManager ;
3334 private readonly secretsManager : SecretsManager ;
3435 private readonly cliManager : CliManager ;
36+ private readonly contextManager : ContextManager ;
3537 // These will only be populated when actively connected to a workspace and are
3638 // used in commands. Because commands can be executed by the user, it is not
3739 // possible to pass in arguments, so we have to store the current workspace
@@ -53,6 +55,7 @@ export class Commands {
5355 this . mementoManager = serviceContainer . getMementoManager ( ) ;
5456 this . secretsManager = serviceContainer . getSecretsManager ( ) ;
5557 this . cliManager = serviceContainer . getCliManager ( ) ;
58+ this . contextManager = serviceContainer . getContextManager ( ) ;
5659 }
5760
5861 /**
@@ -179,31 +182,34 @@ export class Commands {
179182 }
180183
181184 /**
182- * Log into the provided deployment. If the deployment URL is not specified,
185+ * Log into the provided deployment. If the deployment URL is not specified,
183186 * ask for it first with a menu showing recent URLs along with the default URL
184187 * and CODER_URL, if those are set.
185188 */
186- public async login ( ...args : string [ ] ) : Promise < void > {
187- // Destructure would be nice but VS Code can pass undefined which errors.
188- const inputUrl = args [ 0 ] ;
189- const inputToken = args [ 1 ] ;
190- const inputLabel = args [ 2 ] ;
191- const isAutologin =
192- typeof args [ 3 ] === "undefined" ? false : Boolean ( args [ 3 ] ) ;
193-
194- const url = await this . maybeAskUrl ( inputUrl ) ;
189+ public async login ( args ?: {
190+ url ?: string ;
191+ token ?: string ;
192+ label ?: string ;
193+ autoLogin ?: boolean ;
194+ } ) : Promise < void > {
195+ if ( this . contextManager . get ( "coder.authenticated" ) ) {
196+ return ;
197+ }
198+ this . logger . info ( "Logging in" ) ;
199+
200+ const url = await this . maybeAskUrl ( args ?. url ) ;
195201 if ( ! url ) {
196202 return ; // The user aborted.
197203 }
198204
199205 // It is possible that we are trying to log into an old-style host, in which
200206 // case we want to write with the provided blank label instead of generating
201207 // a host label.
202- const label =
203- typeof inputLabel === "undefined" ? toSafeHost ( url ) : inputLabel ;
208+ const label = args ?. label === undefined ? toSafeHost ( url ) : args . label ;
204209
205210 // Try to get a token from the user, if we need one, and their user.
206- const res = await this . maybeAskToken ( url , inputToken , isAutologin ) ;
211+ const autoLogin = args ?. autoLogin === true ;
212+ const res = await this . maybeAskToken ( url , args ?. token , autoLogin ) ;
207213 if ( ! res ) {
208214 return ; // The user aborted, or unable to auth.
209215 }
@@ -221,13 +227,9 @@ export class Commands {
221227 await this . cliManager . configure ( label , url , res . token ) ;
222228
223229 // These contexts control various menu items and the sidebar.
224- await vscode . commands . executeCommand (
225- "setContext" ,
226- "coder.authenticated" ,
227- true ,
228- ) ;
230+ this . contextManager . set ( "coder.authenticated" , true ) ;
229231 if ( res . user . roles . find ( ( role ) => role . name === "owner" ) ) {
230- await vscode . commands . executeCommand ( "setContext" , "coder.isOwner" , true ) ;
232+ this . contextManager . set ( "coder.isOwner" , true ) ;
231233 }
232234
233235 vscode . window
@@ -245,6 +247,7 @@ export class Commands {
245247 }
246248 } ) ;
247249
250+ await this . secretsManager . triggerLoginStateChange ( "login" ) ;
248251 // Fetch workspaces for the new deployment.
249252 vscode . commands . executeCommand ( "coder.refreshWorkspaces" ) ;
250253 }
@@ -257,19 +260,21 @@ export class Commands {
257260 */
258261 private async maybeAskToken (
259262 url : string ,
260- token : string ,
261- isAutologin : boolean ,
263+ token : string | undefined ,
264+ isAutoLogin : boolean ,
262265 ) : Promise < { user : User ; token : string } | null > {
263266 const client = CoderApi . create ( url , token , this . logger ) ;
264- if ( ! needToken ( vscode . workspace . getConfiguration ( ) ) ) {
267+ const needsToken = needToken ( vscode . workspace . getConfiguration ( ) ) ;
268+ if ( ! needsToken || token ) {
265269 try {
266270 const user = await client . getAuthenticatedUser ( ) ;
267271 // For non-token auth, we write a blank token since the `vscodessh`
268272 // command currently always requires a token file.
269- return { token : "" , user } ;
273+ // For token auth, we have valid access so we can just return the user here
274+ return { token : needsToken && token ? token : "" , user } ;
270275 } catch ( err ) {
271276 const message = getErrorMessage ( err , "no response from the server" ) ;
272- if ( isAutologin ) {
277+ if ( isAutoLogin ) {
273278 this . logger . warn ( "Failed to log in to Coder server:" , message ) ;
274279 } else {
275280 this . vscodeProposed . window . showErrorMessage (
@@ -301,6 +306,9 @@ export class Commands {
301306 value : token || ( await this . secretsManager . getSessionToken ( ) ) ,
302307 ignoreFocusOut : true ,
303308 validateInput : async ( value ) => {
309+ if ( ! value ) {
310+ return null ;
311+ }
304312 client . setSessionToken ( value ) ;
305313 try {
306314 user = await client . getAuthenticatedUser ( ) ;
@@ -369,7 +377,14 @@ export class Commands {
369377 // Sanity check; command should not be available if no url.
370378 throw new Error ( "You are not logged in" ) ;
371379 }
380+ await this . forceLogout ( ) ;
381+ }
372382
383+ public async forceLogout ( ) : Promise < void > {
384+ if ( ! this . contextManager . get ( "coder.authenticated" ) ) {
385+ return ;
386+ }
387+ this . logger . info ( "Logging out" ) ;
373388 // Clear from the REST client. An empty url will indicate to other parts of
374389 // the code that we are logged out.
375390 this . restClient . setHost ( "" ) ;
@@ -379,19 +394,16 @@ export class Commands {
379394 await this . mementoManager . setUrl ( undefined ) ;
380395 await this . secretsManager . setSessionToken ( undefined ) ;
381396
382- await vscode . commands . executeCommand (
383- "setContext" ,
384- "coder.authenticated" ,
385- false ,
386- ) ;
397+ this . contextManager . set ( "coder.authenticated" , false ) ;
387398 vscode . window
388399 . showInformationMessage ( "You've been logged out of Coder!" , "Login" )
389400 . then ( ( action ) => {
390401 if ( action === "Login" ) {
391- vscode . commands . executeCommand ( "coder. login" ) ;
402+ this . login ( ) ;
392403 }
393404 } ) ;
394405
406+ await this . secretsManager . triggerLoginStateChange ( "logout" ) ;
395407 // This will result in clearing the workspace list.
396408 vscode . commands . executeCommand ( "coder.refreshWorkspaces" ) ;
397409 }
0 commit comments