Skip to content

Commit fe96eb5

Browse files
committed
Introduce ContextManager to hold context state globally + Rely on secrets to propagate authentication events between windows
1 parent 27569d2 commit fe96eb5

File tree

7 files changed

+99
-45
lines changed

7 files changed

+99
-45
lines changed

src/commands.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { CoderApi } from "./api/coderApi";
1212
import { needToken } from "./api/utils";
1313
import { type CliManager } from "./core/cliManager";
1414
import { type ServiceContainer } from "./core/container";
15+
import { type ContextManager } from "./core/contextManager";
1516
import { type MementoManager } from "./core/mementoManager";
1617
import { type PathResolver } from "./core/pathResolver";
1718
import { 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
/**
@@ -189,6 +192,11 @@ export class Commands {
189192
label?: string;
190193
autoLogin?: boolean;
191194
}): Promise<void> {
195+
if (this.contextManager.get("coder.authenticated")) {
196+
return;
197+
}
198+
this.logger.info("Logging in");
199+
192200
const url = await this.maybeAskUrl(args?.url);
193201
if (!url) {
194202
return; // The user aborted.
@@ -219,13 +227,9 @@ export class Commands {
219227
await this.secretsManager.setSessionToken(res.token);
220228

221229
// These contexts control various menu items and the sidebar.
222-
await vscode.commands.executeCommand(
223-
"setContext",
224-
"coder.authenticated",
225-
true,
226-
);
230+
this.contextManager.set("coder.authenticated", true);
227231
if (res.user.roles.find((role) => role.name === "owner")) {
228-
await vscode.commands.executeCommand("setContext", "coder.isOwner", true);
232+
this.contextManager.set("coder.isOwner", true);
229233
}
230234

231235
vscode.window
@@ -245,6 +249,7 @@ export class Commands {
245249

246250
// Fetch workspaces for the new deployment.
247251
vscode.commands.executeCommand("coder.refreshWorkspaces");
252+
this.secretsManager.triggerLoginStateChange("login");
248253
}
249254

250255
/**
@@ -376,6 +381,10 @@ export class Commands {
376381
}
377382

378383
public async forceLogout(): Promise<void> {
384+
if (!this.contextManager.get("coder.authenticated")) {
385+
return;
386+
}
387+
this.logger.info("Logging out");
379388
// Clear from the REST client. An empty url will indicate to other parts of
380389
// the code that we are logged out.
381390
this.restClient.setHost("");
@@ -385,11 +394,7 @@ export class Commands {
385394
await this.mementoManager.setUrl(undefined);
386395
await this.secretsManager.setSessionToken(undefined);
387396

388-
await vscode.commands.executeCommand(
389-
"setContext",
390-
"coder.authenticated",
391-
false,
392-
);
397+
this.contextManager.set("coder.authenticated", false);
393398
vscode.window
394399
.showInformationMessage("You've been logged out of Coder!", "Login")
395400
.then((action) => {
@@ -400,6 +405,7 @@ export class Commands {
400405

401406
// This will result in clearing the workspace list.
402407
vscode.commands.executeCommand("coder.refreshWorkspaces");
408+
this.secretsManager.triggerLoginStateChange("logout");
403409
}
404410

405411
/**

src/core/container.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as vscode from "vscode";
33
import { type Logger } from "../logging/logger";
44

55
import { CliManager } from "./cliManager";
6+
import { ContextManager } from "./contextManager";
67
import { MementoManager } from "./mementoManager";
78
import { PathResolver } from "./pathResolver";
89
import { SecretsManager } from "./secretsManager";
@@ -17,6 +18,7 @@ export class ServiceContainer implements vscode.Disposable {
1718
private readonly mementoManager: MementoManager;
1819
private readonly secretsManager: SecretsManager;
1920
private readonly cliManager: CliManager;
21+
private readonly contextManager: ContextManager;
2022

2123
constructor(
2224
context: vscode.ExtensionContext,
@@ -34,6 +36,7 @@ export class ServiceContainer implements vscode.Disposable {
3436
this.logger,
3537
this.pathResolver,
3638
);
39+
this.contextManager = new ContextManager();
3740
}
3841

3942
getVsCodeProposed(): typeof vscode {
@@ -60,10 +63,15 @@ export class ServiceContainer implements vscode.Disposable {
6063
return this.cliManager;
6164
}
6265

66+
getContextManager(): ContextManager {
67+
return this.contextManager;
68+
}
69+
6370
/**
6471
* Dispose of all services and clean up resources.
6572
*/
6673
dispose(): void {
74+
this.contextManager.dispose();
6775
this.logger.dispose();
6876
}
6977
}

src/core/contextManager.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import * as vscode from "vscode";
2+
3+
const CONTEXT_DEFAULTS = {
4+
"coder.authenticated": false,
5+
"coder.isOwner": false,
6+
"coder.loaded": false,
7+
"coder.workspace.updatable": false,
8+
} as const;
9+
10+
type CoderContext = keyof typeof CONTEXT_DEFAULTS;
11+
12+
export class ContextManager implements vscode.Disposable {
13+
private readonly context = new Map<CoderContext, boolean>();
14+
15+
public constructor() {
16+
(Object.keys(CONTEXT_DEFAULTS) as CoderContext[]).forEach((key) => {
17+
this.set(key, CONTEXT_DEFAULTS[key]);
18+
});
19+
}
20+
21+
public set(key: CoderContext, value: boolean): void {
22+
this.context.set(key, value);
23+
vscode.commands.executeCommand("setContext", key, value);
24+
}
25+
26+
public get(key: CoderContext): boolean {
27+
return this.context.get(key) ?? CONTEXT_DEFAULTS[key];
28+
}
29+
30+
public dispose() {
31+
this.context.clear();
32+
}
33+
}

src/core/secretsManager.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@ import type { SecretStorage, Disposable } from "vscode";
22

33
const SESSION_TOKEN_KEY = "sessionToken";
44

5+
const LOGIN_STATE_KEY = "loginState";
6+
7+
type AuthAction = "login" | "logout";
58
export class SecretsManager {
6-
constructor(private readonly secrets: SecretStorage) {}
9+
constructor(private readonly secrets: SecretStorage) {
10+
void this.secrets.delete(LOGIN_STATE_KEY);
11+
}
712

813
/**
914
* Set or unset the last used token.
@@ -29,13 +34,17 @@ export class SecretsManager {
2934
}
3035
}
3136

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();
37+
public triggerLoginStateChange(action: AuthAction): void {
38+
this.secrets.store(LOGIN_STATE_KEY, action);
39+
}
40+
41+
public onDidChangeLoginState(
42+
listener: (state?: AuthAction) => Promise<void>,
43+
): Disposable {
44+
return this.secrets.onDidChange(async (e) => {
45+
if (e.key === LOGIN_STATE_KEY) {
46+
const state = await this.secrets.get(LOGIN_STATE_KEY);
47+
listener(state as AuthAction | undefined);
3948
}
4049
});
4150
}

src/extension.ts

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
6262
const output = serviceContainer.getLogger();
6363
const mementoManager = serviceContainer.getMementoManager();
6464
const secretsManager = serviceContainer.getSecretsManager();
65+
const contextManager = serviceContainer.getContextManager();
6566

6667
// Try to clear this flag ASAP
6768
const isFirstConnect = await mementoManager.getAndClearFirstConnect();
@@ -331,18 +332,21 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
331332
const remote = new Remote(serviceContainer, commands, ctx.extensionMode);
332333

333334
ctx.subscriptions.push(
334-
secretsManager.onDidChangeSessionToken(async () => {
335-
const token = await secretsManager.getSessionToken();
336-
const url = mementoManager.getUrl();
337-
if (!token) {
338-
output.info("Logging out");
339-
await commands.forceLogout();
340-
} else if (url) {
341-
output.info("Logging in");
335+
secretsManager.onDidChangeLoginState(async (state) => {
336+
if (state === undefined) {
337+
// Initalization - Ignore those events
338+
return;
339+
}
340+
341+
if (state === "login") {
342+
const token = await secretsManager.getSessionToken();
343+
const url = mementoManager.getUrl();
342344
// Should login the user directly if the URL+Token are valid
343345
await commands.login({ url, token });
344346
// Resolve any pending login detection promises
345347
remote.resolveLoginDetected();
348+
} else {
349+
await commands.forceLogout();
346350
}
347351
}),
348352
);
@@ -413,20 +417,12 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
413417
output.info(`Logged in to ${baseUrl}; checking credentials`);
414418
client
415419
.getAuthenticatedUser()
416-
.then(async (user) => {
420+
.then((user) => {
417421
if (user && user.roles) {
418422
output.info("Credentials are valid");
419-
vscode.commands.executeCommand(
420-
"setContext",
421-
"coder.authenticated",
422-
true,
423-
);
423+
contextManager.set("coder.authenticated", true);
424424
if (user.roles.find((role) => role.name === "owner")) {
425-
await vscode.commands.executeCommand(
426-
"setContext",
427-
"coder.isOwner",
428-
true,
429-
);
425+
contextManager.set("coder.isOwner", true);
430426
}
431427

432428
// Fetch and monitor workspaces, now that we know the client is good.
@@ -445,11 +441,11 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
445441
);
446442
})
447443
.finally(() => {
448-
vscode.commands.executeCommand("setContext", "coder.loaded", true);
444+
contextManager.set("coder.loaded", true);
449445
});
450446
} else {
451447
output.info("Not currently logged in");
452-
vscode.commands.executeCommand("setContext", "coder.loaded", true);
448+
contextManager.set("coder.loaded", true);
453449

454450
// Handle autologin, if not already logged in.
455451
const cfg = vscode.workspace.getConfiguration();

src/remote/remote.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { type Commands } from "../commands";
3030
import { type CliManager } from "../core/cliManager";
3131
import * as cliUtils from "../core/cliUtils";
3232
import { type ServiceContainer } from "../core/container";
33+
import { type ContextManager } from "../core/contextManager";
3334
import { type PathResolver } from "../core/pathResolver";
3435
import { featureSetForVersion, type FeatureSet } from "../featureSet";
3536
import { getGlobalFlags } from "../globalFlags";
@@ -58,6 +59,7 @@ export class Remote {
5859
private readonly logger: Logger;
5960
private readonly pathResolver: PathResolver;
6061
private readonly cliManager: CliManager;
62+
private readonly contextManager: ContextManager;
6163

6264
// Used to race between the login dialog and the logging in from a different window
6365
private loginDetectedResolver: (() => void) | undefined;
@@ -73,6 +75,7 @@ export class Remote {
7375
this.logger = serviceContainer.getLogger();
7476
this.pathResolver = serviceContainer.getPathResolver();
7577
this.cliManager = serviceContainer.getCliManager();
78+
this.contextManager = serviceContainer.getContextManager();
7679
}
7780

7881
/**
@@ -545,6 +548,7 @@ export class Remote {
545548
workspaceClient,
546549
this.logger,
547550
this.vscodeProposed,
551+
this.contextManager,
548552
);
549553
disposables.push(monitor);
550554
disposables.push(

src/workspace/workspaceMonitor.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as vscode from "vscode";
77

88
import { createWorkspaceIdentifier, errToStr } from "../api/api-helper";
99
import { type CoderApi } from "../api/coderApi";
10+
import { type ContextManager } from "../core/contextManager";
1011
import { type Logger } from "../logging/logger";
1112
import { type OneWayWebSocket } from "../websocket/oneWayWebSocket";
1213

@@ -41,6 +42,7 @@ export class WorkspaceMonitor implements vscode.Disposable {
4142
private readonly logger: Logger,
4243
// We use the proposed API to get access to useCustom in dialogs.
4344
private readonly vscodeProposed: typeof vscode,
45+
private readonly contextManager: ContextManager,
4446
) {
4547
this.name = createWorkspaceIdentifier(workspace);
4648
const socket = this.client.watchWorkspace(workspace);
@@ -217,11 +219,7 @@ export class WorkspaceMonitor implements vscode.Disposable {
217219
}
218220

219221
private updateContext(workspace: Workspace) {
220-
vscode.commands.executeCommand(
221-
"setContext",
222-
"coder.workspace.updatable",
223-
workspace.outdated,
224-
);
222+
this.contextManager.set("coder.workspace.updatable", workspace.outdated);
225223
}
226224

227225
private updateStatusBar(workspace: Workspace) {

0 commit comments

Comments
 (0)