@@ -2,7 +2,7 @@ import { Api } from "coder/site/src/api/api"
22import { getErrorMessage } from "coder/site/src/api/errors"
33import { User , Workspace , WorkspaceAgent } from "coder/site/src/api/typesGenerated"
44import * as vscode from "vscode"
5- import { makeCoderSdk } from "./api"
5+ import { makeCoderSdk , needToken } from "./api"
66import { extractAgents } from "./api-helper"
77import { CertificateError } from "./error"
88import { Storage } from "./storage"
@@ -147,78 +147,33 @@ export class Commands {
147147 // a host label.
148148 const label = typeof args [ 2 ] === "undefined" ? toSafeHost ( url ) : args [ 2 ]
149149
150- // Use a temporary client to avoid messing with the global one while trying
151- // to log in.
152- const restClient = await makeCoderSdk ( url , undefined , this . storage )
153-
154- let user : User | undefined
155- let token : string | undefined = args [ 1 ]
156- if ( ! token ) {
157- const opened = await vscode . env . openExternal ( vscode . Uri . parse ( `${ url } /cli-auth` ) )
158- if ( ! opened ) {
159- vscode . window . showWarningMessage ( "You must accept the URL prompt to generate an API key." )
160- return
161- }
162-
163- token = await vscode . window . showInputBox ( {
164- title : "Coder API Key" ,
165- password : true ,
166- placeHolder : "Copy your API key from the opened browser page." ,
167- value : await this . storage . getSessionToken ( ) ,
168- ignoreFocusOut : true ,
169- validateInput : async ( value ) => {
170- restClient . setSessionToken ( value )
171- try {
172- user = await restClient . getAuthenticatedUser ( )
173- if ( ! user ) {
174- throw new Error ( "Failed to get authenticated user" )
175- }
176- } catch ( err ) {
177- // For certificate errors show both a notification and add to the
178- // text under the input box, since users sometimes miss the
179- // notification.
180- if ( err instanceof CertificateError ) {
181- err . showNotification ( )
182-
183- return {
184- message : err . x509Err || err . message ,
185- severity : vscode . InputBoxValidationSeverity . Error ,
186- }
187- }
188- // This could be something like the header command erroring or an
189- // invalid session token.
190- const message = getErrorMessage ( err , "no response from the server" )
191- return {
192- message : "Failed to authenticate: " + message ,
193- severity : vscode . InputBoxValidationSeverity . Error ,
194- }
195- }
196- } ,
197- } )
198- }
199- if ( ! token || ! user ) {
200- return
150+ // Try to get a token from the user, if we need one, and their user.
151+ const res = await this . maybeAskToken ( url , args [ 1 ] )
152+ if ( ! res ) {
153+ return // The user aborted.
201154 }
202155
203- // The URL and token are good; authenticate the global client.
156+ // The URL is good and the token is either good or not required; authorize
157+ // the global client.
204158 this . restClient . setHost ( url )
205- this . restClient . setSessionToken ( token )
159+ this . restClient . setSessionToken ( res . token )
206160
207161 // Store these to be used in later sessions.
208162 await this . storage . setUrl ( url )
209- await this . storage . setSessionToken ( token )
163+ await this . storage . setSessionToken ( res . token )
210164
211165 // Store on disk to be used by the cli.
212- await this . storage . configureCli ( label , url , token )
166+ await this . storage . configureCli ( label , url , res . token )
213167
168+ // These contexts control various menu items and the sidebar.
214169 await vscode . commands . executeCommand ( "setContext" , "coder.authenticated" , true )
215- if ( user . roles . find ( ( role ) => role . name === "owner" ) ) {
170+ if ( res . user . roles . find ( ( role ) => role . name === "owner" ) ) {
216171 await vscode . commands . executeCommand ( "setContext" , "coder.isOwner" , true )
217172 }
218173
219174 vscode . window
220175 . showInformationMessage (
221- `Welcome to Coder, ${ user . username } !` ,
176+ `Welcome to Coder, ${ res . user . username } !` ,
222177 {
223178 detail : "You can now use the Coder extension to manage your Coder instance." ,
224179 } ,
@@ -234,6 +189,69 @@ export class Commands {
234189 vscode . commands . executeCommand ( "coder.refreshWorkspaces" )
235190 }
236191
192+ /**
193+ * If necessary, ask for a token, and keep asking until the token has been
194+ * validated. Return the token and user that was fetched to validate the
195+ * token.
196+ */
197+ private async maybeAskToken ( url : string , token : string ) : Promise < { user : User , token : string } | null > {
198+ const restClient = await makeCoderSdk ( url , token , this . storage )
199+ if ( ! needToken ( ) ) {
200+ return {
201+ // For non-token auth, we write a blank token since the `vscodessh`
202+ // command currently always requires a token file.
203+ token : "" ,
204+ user : await restClient . getAuthenticatedUser ( ) ,
205+ }
206+ }
207+
208+ // This prompt is for convenience; do not error if they close it since
209+ // they may already have a token or already have the page opened.
210+ await vscode . env . openExternal ( vscode . Uri . parse ( `${ url } /cli-auth` ) )
211+
212+ // For token auth, start with the existing token in the prompt or the last
213+ // used token. Once submitted, if there is a failure we will keep asking
214+ // the user for a new token until they quit.
215+ let user : User | undefined
216+ const validatedToken = await vscode . window . showInputBox ( {
217+ title : "Coder API Key" ,
218+ password : true ,
219+ placeHolder : "Paste your API key." ,
220+ value : token || ( await this . storage . getSessionToken ( ) ) ,
221+ ignoreFocusOut : true ,
222+ validateInput : async ( value ) => {
223+ restClient . setSessionToken ( value )
224+ try {
225+ user = await restClient . getAuthenticatedUser ( )
226+ } catch ( err ) {
227+ // For certificate errors show both a notification and add to the
228+ // text under the input box, since users sometimes miss the
229+ // notification.
230+ if ( err instanceof CertificateError ) {
231+ err . showNotification ( )
232+
233+ return {
234+ message : err . x509Err || err . message ,
235+ severity : vscode . InputBoxValidationSeverity . Error ,
236+ }
237+ }
238+ // This could be something like the header command erroring or an
239+ // invalid session token.
240+ const message = getErrorMessage ( err , "no response from the server" )
241+ return {
242+ message : "Failed to authenticate: " + message ,
243+ severity : vscode . InputBoxValidationSeverity . Error ,
244+ }
245+ }
246+ } ,
247+ } )
248+
249+ if ( validatedToken && user ) {
250+ return { token : validatedToken , user }
251+ }
252+ return null
253+ }
254+
237255 /**
238256 * View the logs for the currently connected workspace.
239257 */
0 commit comments