@@ -12,6 +12,7 @@ import { CoderApi } from "./api/coderApi";
12
12
import { needToken } from "./api/utils" ;
13
13
import { type CliManager } from "./core/cliManager" ;
14
14
import { type ServiceContainer } from "./core/container" ;
15
+ import { type ContextManager } from "./core/contextManager" ;
15
16
import { type MementoManager } from "./core/mementoManager" ;
16
17
import { type PathResolver } from "./core/pathResolver" ;
17
18
import { type SecretsManager } from "./core/secretsManager" ;
@@ -32,6 +33,7 @@ export class Commands {
32
33
private readonly mementoManager : MementoManager ;
33
34
private readonly secretsManager : SecretsManager ;
34
35
private readonly cliManager : CliManager ;
36
+ private readonly contextManager : ContextManager ;
35
37
// These will only be populated when actively connected to a workspace and are
36
38
// used in commands. Because commands can be executed by the user, it is not
37
39
// possible to pass in arguments, so we have to store the current workspace
@@ -53,6 +55,7 @@ export class Commands {
53
55
this . mementoManager = serviceContainer . getMementoManager ( ) ;
54
56
this . secretsManager = serviceContainer . getSecretsManager ( ) ;
55
57
this . cliManager = serviceContainer . getCliManager ( ) ;
58
+ this . contextManager = serviceContainer . getContextManager ( ) ;
56
59
}
57
60
58
61
/**
@@ -179,31 +182,34 @@ export class Commands {
179
182
}
180
183
181
184
/**
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,
183
186
* ask for it first with a menu showing recent URLs along with the default URL
184
187
* and CODER_URL, if those are set.
185
188
*/
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 ) ;
195
201
if ( ! url ) {
196
202
return ; // The user aborted.
197
203
}
198
204
199
205
// It is possible that we are trying to log into an old-style host, in which
200
206
// case we want to write with the provided blank label instead of generating
201
207
// a host label.
202
- const label =
203
- typeof inputLabel === "undefined" ? toSafeHost ( url ) : inputLabel ;
208
+ const label = args ?. label === undefined ? toSafeHost ( url ) : args . label ;
204
209
205
210
// 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 ) ;
207
213
if ( ! res ) {
208
214
return ; // The user aborted, or unable to auth.
209
215
}
@@ -221,13 +227,9 @@ export class Commands {
221
227
await this . cliManager . configure ( label , url , res . token ) ;
222
228
223
229
// 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 ) ;
229
231
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 ) ;
231
233
}
232
234
233
235
vscode . window
@@ -245,6 +247,7 @@ export class Commands {
245
247
}
246
248
} ) ;
247
249
250
+ await this . secretsManager . triggerLoginStateChange ( "login" ) ;
248
251
// Fetch workspaces for the new deployment.
249
252
vscode . commands . executeCommand ( "coder.refreshWorkspaces" ) ;
250
253
}
@@ -257,19 +260,21 @@ export class Commands {
257
260
*/
258
261
private async maybeAskToken (
259
262
url : string ,
260
- token : string ,
261
- isAutologin : boolean ,
263
+ token : string | undefined ,
264
+ isAutoLogin : boolean ,
262
265
) : Promise < { user : User ; token : string } | null > {
263
266
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 ) {
265
269
try {
266
270
const user = await client . getAuthenticatedUser ( ) ;
267
271
// For non-token auth, we write a blank token since the `vscodessh`
268
272
// 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 } ;
270
275
} catch ( err ) {
271
276
const message = getErrorMessage ( err , "no response from the server" ) ;
272
- if ( isAutologin ) {
277
+ if ( isAutoLogin ) {
273
278
this . logger . warn ( "Failed to log in to Coder server:" , message ) ;
274
279
} else {
275
280
this . vscodeProposed . window . showErrorMessage (
@@ -301,6 +306,9 @@ export class Commands {
301
306
value : token || ( await this . secretsManager . getSessionToken ( ) ) ,
302
307
ignoreFocusOut : true ,
303
308
validateInput : async ( value ) => {
309
+ if ( ! value ) {
310
+ return null ;
311
+ }
304
312
client . setSessionToken ( value ) ;
305
313
try {
306
314
user = await client . getAuthenticatedUser ( ) ;
@@ -369,7 +377,14 @@ export class Commands {
369
377
// Sanity check; command should not be available if no url.
370
378
throw new Error ( "You are not logged in" ) ;
371
379
}
380
+ await this . forceLogout ( ) ;
381
+ }
372
382
383
+ public async forceLogout ( ) : Promise < void > {
384
+ if ( ! this . contextManager . get ( "coder.authenticated" ) ) {
385
+ return ;
386
+ }
387
+ this . logger . info ( "Logging out" ) ;
373
388
// Clear from the REST client. An empty url will indicate to other parts of
374
389
// the code that we are logged out.
375
390
this . restClient . setHost ( "" ) ;
@@ -379,19 +394,16 @@ export class Commands {
379
394
await this . mementoManager . setUrl ( undefined ) ;
380
395
await this . secretsManager . setSessionToken ( undefined ) ;
381
396
382
- await vscode . commands . executeCommand (
383
- "setContext" ,
384
- "coder.authenticated" ,
385
- false ,
386
- ) ;
397
+ this . contextManager . set ( "coder.authenticated" , false ) ;
387
398
vscode . window
388
399
. showInformationMessage ( "You've been logged out of Coder!" , "Login" )
389
400
. then ( ( action ) => {
390
401
if ( action === "Login" ) {
391
- vscode . commands . executeCommand ( "coder. login" ) ;
402
+ this . login ( ) ;
392
403
}
393
404
} ) ;
394
405
406
+ await this . secretsManager . triggerLoginStateChange ( "logout" ) ;
395
407
// This will result in clearing the workspace list.
396
408
vscode . commands . executeCommand ( "coder.refreshWorkspaces" ) ;
397
409
}
0 commit comments