@@ -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"
@@ -137,88 +137,48 @@ export class Commands {
137137 * ask for it first with a menu showing recent URLs and CODER_URL, if set.
138138 */
139139 public async login ( ...args : string [ ] ) : Promise < void > {
140- const url = await this . maybeAskUrl ( args [ 0 ] )
140+ // Destructure would be nice but VS Code can pass undefined which errors.
141+ const inputUrl = args [ 0 ]
142+ const inputToken = args [ 1 ]
143+ const inputLabel = args [ 2 ]
144+
145+ const url = await this . maybeAskUrl ( inputUrl )
141146 if ( ! url ) {
142147 return
143148 }
144149
145150 // It is possible that we are trying to log into an old-style host, in which
146151 // case we want to write with the provided blank label instead of generating
147152 // a host label.
148- const label = typeof args [ 2 ] === "undefined" ? toSafeHost ( url ) : args [ 2 ]
149-
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 ( )
153+ const label = typeof inputLabel === "undefined" ? toSafeHost ( url ) : inputLabel
182154
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
155+ // Try to get a token from the user, if we need one, and their user.
156+ const res = await this . maybeAskToken ( url , inputToken )
157+ if ( ! res ) {
158+ return // The user aborted.
201159 }
202160
203- // The URL and token are good; authenticate the global client.
161+ // The URL is good and the token is either good or not required; authorize
162+ // the global client.
204163 this . restClient . setHost ( url )
205- this . restClient . setSessionToken ( token )
164+ this . restClient . setSessionToken ( res . token )
206165
207166 // Store these to be used in later sessions.
208167 await this . storage . setUrl ( url )
209- await this . storage . setSessionToken ( token )
168+ await this . storage . setSessionToken ( res . token )
210169
211170 // Store on disk to be used by the cli.
212- await this . storage . configureCli ( label , url , token )
171+ await this . storage . configureCli ( label , url , res . token )
213172
173+ // These contexts control various menu items and the sidebar.
214174 await vscode . commands . executeCommand ( "setContext" , "coder.authenticated" , true )
215- if ( user . roles . find ( ( role ) => role . name === "owner" ) ) {
175+ if ( res . user . roles . find ( ( role ) => role . name === "owner" ) ) {
216176 await vscode . commands . executeCommand ( "setContext" , "coder.isOwner" , true )
217177 }
218178
219179 vscode . window
220180 . showInformationMessage (
221- `Welcome to Coder, ${ user . username } !` ,
181+ `Welcome to Coder, ${ res . user . username } !` ,
222182 {
223183 detail : "You can now use the Coder extension to manage your Coder instance." ,
224184 } ,
@@ -234,6 +194,71 @@ export class Commands {
234194 vscode . commands . executeCommand ( "coder.refreshWorkspaces" )
235195 }
236196
197+ /**
198+ * If necessary, ask for a token, and keep asking until the token has been
199+ * validated. Return the token and user that was fetched to validate the
200+ * token.
201+ */
202+ private async maybeAskToken ( url : string , token : string ) : Promise < { user : User ; token : string } | null > {
203+ const restClient = await makeCoderSdk ( url , token , this . storage )
204+ if ( ! needToken ( ) ) {
205+ return {
206+ // For non-token auth, we write a blank token since the `vscodessh`
207+ // command currently always requires a token file.
208+ token : "" ,
209+ user : await restClient . getAuthenticatedUser ( ) ,
210+ }
211+ }
212+
213+ // This prompt is for convenience; do not error if they close it since
214+ // they may already have a token or already have the page opened.
215+ await vscode . env . openExternal ( vscode . Uri . parse ( `${ url } /cli-auth` ) )
216+
217+ // For token auth, start with the existing token in the prompt or the last
218+ // used token. Once submitted, if there is a failure we will keep asking
219+ // the user for a new token until they quit.
220+ let user : User | undefined
221+ const validatedToken = await vscode . window . showInputBox ( {
222+ title : "Coder API Key" ,
223+ password : true ,
224+ placeHolder : "Paste your API key." ,
225+ value : token || ( await this . storage . getSessionToken ( ) ) ,
226+ ignoreFocusOut : true ,
227+ validateInput : async ( value ) => {
228+ restClient . setSessionToken ( value )
229+ try {
230+ user = await restClient . getAuthenticatedUser ( )
231+ } catch ( err ) {
232+ // For certificate errors show both a notification and add to the
233+ // text under the input box, since users sometimes miss the
234+ // notification.
235+ if ( err instanceof CertificateError ) {
236+ err . showNotification ( )
237+
238+ return {
239+ message : err . x509Err || err . message ,
240+ severity : vscode . InputBoxValidationSeverity . Error ,
241+ }
242+ }
243+ // This could be something like the header command erroring or an
244+ // invalid session token.
245+ const message = getErrorMessage ( err , "no response from the server" )
246+ return {
247+ message : "Failed to authenticate: " + message ,
248+ severity : vscode . InputBoxValidationSeverity . Error ,
249+ }
250+ }
251+ } ,
252+ } )
253+
254+ if ( validatedToken && user ) {
255+ return { token : validatedToken , user }
256+ }
257+
258+ // User aborted.
259+ return null
260+ }
261+
237262 /**
238263 * View the logs for the currently connected workspace.
239264 */
0 commit comments