Skip to content

Commit 03ea427

Browse files
committed
Improve consistency in client-side login/logout experience
1 parent 67e85e6 commit 03ea427

File tree

4 files changed

+68
-47
lines changed

4 files changed

+68
-47
lines changed

src/commands.ts

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -179,31 +179,32 @@ export class Commands {
179179
}
180180

181181
/**
182-
* Log into the provided deployment. If the deployment URL is not specified,
182+
* Log into the provided deployment. If the deployment URL is not specified,
183183
* ask for it first with a menu showing recent URLs along with the default URL
184184
* and CODER_URL, if those are set.
185185
*/
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);
186+
public async login(args?: {
187+
url?: string;
188+
token?: string;
189+
label?: string;
190+
autoLogin?: boolean;
191+
}): Promise<void> {
192+
const url = await this.maybeAskUrl(args?.url);
195193
if (!url) {
196194
return; // The user aborted.
197195
}
198196

199197
// It is possible that we are trying to log into an old-style host, in which
200198
// case we want to write with the provided blank label instead of generating
201199
// a host label.
202-
const label =
203-
typeof inputLabel === "undefined" ? toSafeHost(url) : inputLabel;
200+
const label = args?.label === undefined ? toSafeHost(url) : args?.label;
204201

205202
// Try to get a token from the user, if we need one, and their user.
206-
const res = await this.maybeAskToken(url, inputToken, isAutologin);
203+
const res = await this.maybeAskToken(
204+
url,
205+
args?.token,
206+
args?.autoLogin === true,
207+
);
207208
if (!res) {
208209
return; // The user aborted, or unable to auth.
209210
}
@@ -257,21 +258,23 @@ export class Commands {
257258
*/
258259
private async maybeAskToken(
259260
url: string,
260-
token: string,
261-
isAutologin: boolean,
261+
token: string | undefined,
262+
isAutoLogin: boolean,
262263
): Promise<{ user: User; token: string } | null> {
263264
const client = CoderApi.create(url, token, this.logger, () =>
264265
vscode.workspace.getConfiguration(),
265266
);
266-
if (!needToken(vscode.workspace.getConfiguration())) {
267-
try {
268-
const user = await client.getAuthenticatedUser();
269-
// For non-token auth, we write a blank token since the `vscodessh`
270-
// command currently always requires a token file.
271-
return { token: "", user };
272-
} catch (err) {
267+
const needsToken = needToken(vscode.workspace.getConfiguration());
268+
try {
269+
const user = await client.getAuthenticatedUser();
270+
// For non-token auth, we write a blank token since the `vscodessh`
271+
// command currently always requires a token file.
272+
// For token auth, we have valid access so we can just return the user here
273+
return { token: needsToken && token ? token : "", user };
274+
} catch (err) {
275+
if (!needToken(vscode.workspace.getConfiguration())) {
273276
const message = getErrorMessage(err, "no response from the server");
274-
if (isAutologin) {
277+
if (isAutoLogin) {
275278
this.logger.warn("Failed to log in to Coder server:", message);
276279
} else {
277280
this.vscodeProposed.window.showErrorMessage(
@@ -303,6 +306,9 @@ export class Commands {
303306
value: token || (await this.secretsManager.getSessionToken()),
304307
ignoreFocusOut: true,
305308
validateInput: async (value) => {
309+
if (!value) {
310+
return null;
311+
}
306312
client.setSessionToken(value);
307313
try {
308314
user = await client.getAuthenticatedUser();
@@ -371,7 +377,10 @@ export class Commands {
371377
// Sanity check; command should not be available if no url.
372378
throw new Error("You are not logged in");
373379
}
380+
await this.forceLogout();
381+
}
374382

383+
public async forceLogout(): Promise<void> {
375384
// Clear from the REST client. An empty url will indicate to other parts of
376385
// the code that we are logged out.
377386
this.restClient.setHost("");
@@ -390,7 +399,7 @@ export class Commands {
390399
.showInformationMessage("You've been logged out of Coder!", "Login")
391400
.then((action) => {
392401
if (action === "Login") {
393-
vscode.commands.executeCommand("coder.login");
402+
this.login();
394403
}
395404
});
396405

src/core/secretsManager.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import type { SecretStorage } from "vscode";
1+
import type { SecretStorage, Disposable } from "vscode";
2+
3+
const SESSION_TOKEN_KEY = "sessionToken";
24

35
export class SecretsManager {
46
constructor(private readonly secrets: SecretStorage) {}
@@ -8,9 +10,9 @@ export class SecretsManager {
810
*/
911
public async setSessionToken(sessionToken?: string): Promise<void> {
1012
if (!sessionToken) {
11-
await this.secrets.delete("sessionToken");
13+
await this.secrets.delete(SESSION_TOKEN_KEY);
1214
} else {
13-
await this.secrets.store("sessionToken", sessionToken);
15+
await this.secrets.store(SESSION_TOKEN_KEY, sessionToken);
1416
}
1517
}
1618

@@ -19,11 +21,22 @@ export class SecretsManager {
1921
*/
2022
public async getSessionToken(): Promise<string | undefined> {
2123
try {
22-
return await this.secrets.get("sessionToken");
24+
return await this.secrets.get(SESSION_TOKEN_KEY);
2325
} catch {
2426
// The VS Code session store has become corrupt before, and
2527
// will fail to get the session token...
2628
return undefined;
2729
}
2830
}
31+
32+
/**
33+
* Subscribe to changes to the session token which can be used to indicate user login status.
34+
*/
35+
public onDidChangeSessionToken(listener: () => Promise<void>): Disposable {
36+
return this.secrets.onDidChange((e) => {
37+
if (e.key === SESSION_TOKEN_KEY) {
38+
listener();
39+
}
40+
});
41+
}
2942
}

src/extension.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,21 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
299299
commands.viewLogs.bind(commands),
300300
);
301301

302+
ctx.subscriptions.push(
303+
secretsManager.onDidChangeSessionToken(async () => {
304+
const token = await secretsManager.getSessionToken();
305+
const url = mementoManager.getUrl();
306+
if (!token) {
307+
output.info("Logging out");
308+
await commands.forceLogout();
309+
} else if (url) {
310+
output.info("Logging in");
311+
// Should login the user directly if the URL+Token are valid
312+
await commands.login({ url, token });
313+
}
314+
}),
315+
);
316+
302317
// Since the "onResolveRemoteAuthority:ssh-remote" activation event exists
303318
// in package.json we're able to perform actions before the authority is
304319
// resolved by the remote SSH extension.
@@ -410,13 +425,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
410425
cfg.get<string>("coder.defaultUrl")?.trim() ||
411426
process.env.CODER_URL?.trim();
412427
if (defaultUrl) {
413-
vscode.commands.executeCommand(
414-
"coder.login",
415-
defaultUrl,
416-
undefined,
417-
undefined,
418-
"true",
419-
);
428+
commands.login({ url: defaultUrl, autoLogin: true });
420429
}
421430
}
422431
}

src/remote/remote.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -257,12 +257,7 @@ export class Remote {
257257
await this.closeRemote();
258258
} else {
259259
// Log in then try again.
260-
await vscode.commands.executeCommand(
261-
"coder.login",
262-
baseUrlRaw,
263-
undefined,
264-
parts.label,
265-
);
260+
await this.commands.login({ url: baseUrlRaw, label: parts.label });
266261
await this.setup(remoteAuthority, firstConnect);
267262
}
268263
return;
@@ -382,12 +377,7 @@ export class Remote {
382377
if (!result) {
383378
await this.closeRemote();
384379
} else {
385-
await vscode.commands.executeCommand(
386-
"coder.login",
387-
baseUrlRaw,
388-
undefined,
389-
parts.label,
390-
);
380+
await this.commands.login({ url: baseUrlRaw, label: parts.label });
391381
await this.setup(remoteAuthority, firstConnect);
392382
}
393383
return;

0 commit comments

Comments
 (0)