Skip to content

Commit 1896c54

Browse files
committed
Move memento and secrets managment from storage.ts
1 parent cfb7593 commit 1896c54

File tree

7 files changed

+184
-156
lines changed

7 files changed

+184
-156
lines changed

src/commands.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import { createWorkspaceIdentifier, extractAgents } from "./api/api-helper";
1010
import { CoderApi } from "./api/coderApi";
1111
import { needToken } from "./api/utils";
1212
import { CliConfigManager } from "./core/cliConfig";
13+
import { MementoManager } from "./core/mementoManager";
1314
import { PathResolver } from "./core/pathResolver";
15+
import { SecretsManager } from "./core/secretsManager";
1416
import { CertificateError } from "./error";
1517
import { getGlobalFlags } from "./globalFlags";
1618
import { Storage } from "./storage";
@@ -39,6 +41,8 @@ export class Commands {
3941
private readonly restClient: Api,
4042
private readonly storage: Storage,
4143
private readonly pathResolver: PathResolver,
44+
private readonly mementoManager: MementoManager,
45+
private readonly secretsManager: SecretsManager,
4246
) {
4347
this.cliConfigManager = new CliConfigManager(pathResolver);
4448
}
@@ -108,7 +112,7 @@ export class Commands {
108112
quickPick.title = "Enter the URL of your Coder deployment.";
109113

110114
// Initial items.
111-
quickPick.items = this.storage
115+
quickPick.items = this.mementoManager
112116
.withUrlHistory(defaultURL, process.env.CODER_URL)
113117
.map((url) => ({
114118
alwaysShow: true,
@@ -119,7 +123,7 @@ export class Commands {
119123
// an option in case the user wants to connect to something that is not in
120124
// the list.
121125
quickPick.onDidChangeValue((value) => {
122-
quickPick.items = this.storage
126+
quickPick.items = this.mementoManager
123127
.withUrlHistory(defaultURL, process.env.CODER_URL, value)
124128
.map((url) => ({
125129
alwaysShow: true,
@@ -199,8 +203,8 @@ export class Commands {
199203
this.restClient.setSessionToken(res.token);
200204

201205
// Store these to be used in later sessions.
202-
await this.storage.setUrl(url);
203-
await this.storage.setSessionToken(res.token);
206+
await this.mementoManager.setUrl(url);
207+
await this.secretsManager.setSessionToken(res.token);
204208

205209
// Store on disk to be used by the cli.
206210
await this.cliConfigManager.configure(label, url, res.token);
@@ -288,7 +292,7 @@ export class Commands {
288292
title: "Coder API Key",
289293
password: true,
290294
placeHolder: "Paste your API key.",
291-
value: token || (await this.storage.getSessionToken()),
295+
value: token || (await this.secretsManager.getSessionToken()),
292296
ignoreFocusOut: true,
293297
validateInput: async (value) => {
294298
client.setSessionToken(value);
@@ -354,7 +358,7 @@ export class Commands {
354358
* Log out from the currently logged-in deployment.
355359
*/
356360
public async logout(): Promise<void> {
357-
const url = this.storage.getUrl();
361+
const url = this.mementoManager.getUrl();
358362
if (!url) {
359363
// Sanity check; command should not be available if no url.
360364
throw new Error("You are not logged in");
@@ -366,8 +370,8 @@ export class Commands {
366370
this.restClient.setSessionToken("");
367371

368372
// Clear from memory.
369-
await this.storage.setUrl(undefined);
370-
await this.storage.setSessionToken(undefined);
373+
await this.mementoManager.setUrl(undefined);
374+
await this.secretsManager.setSessionToken(undefined);
371375

372376
await vscode.commands.executeCommand(
373377
"setContext",
@@ -392,7 +396,7 @@ export class Commands {
392396
* Must only be called if currently logged in.
393397
*/
394398
public async createWorkspace(): Promise<void> {
395-
const uri = this.storage.getUrl() + "/templates";
399+
const uri = this.mementoManager.getUrl() + "/templates";
396400
await vscode.commands.executeCommand("vscode.open", uri);
397401
}
398402

@@ -407,7 +411,7 @@ export class Commands {
407411
public async navigateToWorkspace(item: OpenableTreeItem) {
408412
if (item) {
409413
const workspaceId = createWorkspaceIdentifier(item.workspace);
410-
const uri = this.storage.getUrl() + `/@${workspaceId}`;
414+
const uri = this.mementoManager.getUrl() + `/@${workspaceId}`;
411415
await vscode.commands.executeCommand("vscode.open", uri);
412416
} else if (this.workspace && this.workspaceRestClient) {
413417
const baseUrl =
@@ -430,7 +434,7 @@ export class Commands {
430434
public async navigateToWorkspaceSettings(item: OpenableTreeItem) {
431435
if (item) {
432436
const workspaceId = createWorkspaceIdentifier(item.workspace);
433-
const uri = this.storage.getUrl() + `/@${workspaceId}/settings`;
437+
const uri = this.mementoManager.getUrl() + `/@${workspaceId}/settings`;
434438
await vscode.commands.executeCommand("vscode.open", uri);
435439
} else if (this.workspace && this.workspaceRestClient) {
436440
const baseUrl =
@@ -508,7 +512,7 @@ export class Commands {
508512

509513
// If workspace_name is provided, run coder ssh before the command
510514

511-
const url = this.storage.getUrl();
515+
const url = this.mementoManager.getUrl();
512516
if (!url) {
513517
throw new Error("No coder url found for sidebar");
514518
}
@@ -650,8 +654,8 @@ export class Commands {
650654
newWindow = false;
651655
}
652656

653-
// Only set the memento if when opening a new folder
654-
await this.storage.setFirstConnect();
657+
// Only set the memento when opening a new folder
658+
await this.mementoManager.setFirstConnect();
655659
await vscode.commands.executeCommand(
656660
"vscode.openFolder",
657661
vscode.Uri.from({
@@ -831,8 +835,8 @@ export class Commands {
831835
}
832836
}
833837

834-
// Only set the memento if when opening a new folder/window
835-
await this.storage.setFirstConnect();
838+
// Only set the memento when opening a new folder/window
839+
await this.mementoManager.setFirstConnect();
836840
if (folderPath) {
837841
await vscode.commands.executeCommand(
838842
"vscode.openFolder",

src/core/mementoManager.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type { Memento } from "vscode";
2+
3+
// Maximum number of recent URLs to store.
4+
const MAX_URLS = 10;
5+
6+
export class MementoManager {
7+
constructor(private readonly memento: Memento) {}
8+
9+
/**
10+
* Add the URL to the list of recently accessed URLs in global storage, then
11+
* set it as the last used URL.
12+
*
13+
* If the URL is falsey, then remove it as the last used URL and do not touch
14+
* the history.
15+
*/
16+
public async setUrl(url?: string): Promise<void> {
17+
await this.memento.update("url", url);
18+
if (url) {
19+
const history = this.withUrlHistory(url);
20+
await this.memento.update("urlHistory", history);
21+
}
22+
}
23+
24+
/**
25+
* Get the last used URL.
26+
*/
27+
public getUrl(): string | undefined {
28+
return this.memento.get("url");
29+
}
30+
31+
/**
32+
* Get the most recently accessed URLs (oldest to newest) with the provided
33+
* values appended. Duplicates will be removed.
34+
*/
35+
public withUrlHistory(...append: (string | undefined)[]): string[] {
36+
const val = this.memento.get("urlHistory");
37+
const urls = Array.isArray(val) ? new Set(val) : new Set();
38+
for (const url of append) {
39+
if (url) {
40+
// It might exist; delete first so it gets appended.
41+
urls.delete(url);
42+
urls.add(url);
43+
}
44+
}
45+
// Slice off the head if the list is too large.
46+
return urls.size > MAX_URLS
47+
? Array.from(urls).slice(urls.size - MAX_URLS, urls.size)
48+
: Array.from(urls);
49+
}
50+
51+
/**
52+
* Mark this as the first connection to a workspace, which influences whether
53+
* the workspace startup confirmation is shown to the user.
54+
*/
55+
public async setFirstConnect(): Promise<void> {
56+
return this.memento.update("firstConnect", true);
57+
}
58+
59+
/**
60+
* Check if this is the first connection to a workspace and clear the flag.
61+
* Used to determine whether to automatically start workspaces without
62+
* prompting the user for confirmation.
63+
*/
64+
public async getAndClearFirstConnect(): Promise<boolean> {
65+
const isFirst = this.memento.get<boolean>("firstConnect");
66+
if (isFirst !== undefined) {
67+
await this.memento.update("firstConnect", undefined);
68+
}
69+
return isFirst === true;
70+
}
71+
}

src/core/pathResolver.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { WorkspaceConfiguration } from "vscode";
44
export class PathResolver {
55
constructor(
66
private readonly basePath: string,
7+
private readonly codeLogPath: string,
78
private readonly configurations: WorkspaceConfiguration,
89
) {}
910

@@ -100,4 +101,16 @@ export class PathResolver {
100101
public getUrlPath(label: string): string {
101102
return path.join(this.getGlobalConfigDir(label), "url");
102103
}
104+
105+
/**
106+
* The uri of a directory in which the extension can create log files.
107+
*
108+
* The directory might not exist on disk and creation is up to the extension.
109+
* However, the parent directory is guaranteed to be existent.
110+
*
111+
* This directory is provided by VS Code and may not be the same as the directory where the Coder CLI writes its log files.
112+
*/
113+
public getCodeLogDir(): string {
114+
return this.codeLogPath;
115+
}
103116
}

src/core/secretsManager.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { SecretStorage } from "vscode";
2+
3+
export class SecretsManager {
4+
constructor(private readonly secrets: SecretStorage) {}
5+
6+
/**
7+
* Set or unset the last used token.
8+
*/
9+
public async setSessionToken(sessionToken?: string): Promise<void> {
10+
if (!sessionToken) {
11+
await this.secrets.delete("sessionToken");
12+
} else {
13+
await this.secrets.store("sessionToken", sessionToken);
14+
}
15+
}
16+
17+
/**
18+
* Get the last used token.
19+
*/
20+
public async getSessionToken(): Promise<string | undefined> {
21+
try {
22+
return await this.secrets.get("sessionToken");
23+
} catch (ex) {
24+
// The VS Code session store has become corrupt before, and
25+
// will fail to get the session token...
26+
return undefined;
27+
}
28+
}
29+
}

src/extension.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import { CoderApi } from "./api/coderApi";
88
import { needToken } from "./api/utils";
99
import { Commands } from "./commands";
1010
import { CliConfigManager } from "./core/cliConfig";
11+
import { MementoManager } from "./core/mementoManager";
1112
import { PathResolver } from "./core/pathResolver";
13+
import { SecretsManager } from "./core/secretsManager";
1214
import { CertificateError, getErrorDetail } from "./error";
1315
import { Remote } from "./remote";
1416
import { Storage } from "./storage";
@@ -52,28 +54,26 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
5254

5355
const pathResolver = new PathResolver(
5456
ctx.globalStorageUri.fsPath,
57+
ctx.logUri.fsPath,
5558
vscode.workspace.getConfiguration(),
5659
);
60+
const cliConfigManager = new CliConfigManager(pathResolver);
61+
const mementoManager = new MementoManager(ctx.globalState);
62+
const secretsManager = new SecretsManager(ctx.secrets);
63+
5764
const output = vscode.window.createOutputChannel("Coder", { log: true });
58-
const storage = new Storage(
59-
vscodeProposed,
60-
output,
61-
ctx.globalState,
62-
ctx.secrets,
63-
ctx.logUri,
64-
pathResolver,
65-
);
65+
const storage = new Storage(output, pathResolver);
6666

67-
// Try to clear this flag ASAP then pass it around if needed
68-
const isFirstConnect = await storage.getAndClearFirstConnect();
67+
// Try to clear this flag ASAP
68+
const isFirstConnect = await mementoManager.getAndClearFirstConnect();
6969

7070
// This client tracks the current login and will be used through the life of
7171
// the plugin to poll workspaces for the current login, as well as being used
7272
// in commands that operate on the current login.
73-
const url = storage.getUrl();
73+
const url = mementoManager.getUrl();
7474
const client = CoderApi.create(
7575
url || "",
76-
await storage.getSessionToken(),
76+
await secretsManager.getSessionToken(),
7777
storage.output,
7878
() => vscode.workspace.getConfiguration(),
7979
);
@@ -108,8 +108,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
108108
allWorkspacesProvider.setVisibility(event.visible);
109109
});
110110

111-
const cliConfigManager = new CliConfigManager(pathResolver);
112-
113111
// Handle vscode:// URIs.
114112
vscode.window.registerUriHandler({
115113
handleUri: async (uri) => {
@@ -137,11 +135,11 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
137135
// hit enter and move on.
138136
const url = await commands.maybeAskUrl(
139137
params.get("url"),
140-
storage.getUrl(),
138+
mementoManager.getUrl(),
141139
);
142140
if (url) {
143141
client.setHost(url);
144-
await storage.setUrl(url);
142+
await mementoManager.setUrl(url);
145143
} else {
146144
throw new Error(
147145
"url must be provided or specified as a query parameter",
@@ -159,7 +157,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
159157
: (params.get("token") ?? "");
160158
if (token) {
161159
client.setSessionToken(token);
162-
await storage.setSessionToken(token);
160+
await secretsManager.setSessionToken(token);
163161
}
164162

165163
// Store on disk to be used by the cli.
@@ -219,11 +217,11 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
219217
// hit enter and move on.
220218
const url = await commands.maybeAskUrl(
221219
params.get("url"),
222-
storage.getUrl(),
220+
mementoManager.getUrl(),
223221
);
224222
if (url) {
225223
client.setHost(url);
226-
await storage.setUrl(url);
224+
await mementoManager.setUrl(url);
227225
} else {
228226
throw new Error(
229227
"url must be provided or specified as a query parameter",
@@ -261,7 +259,14 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
261259

262260
// Register globally available commands. Many of these have visibility
263261
// controlled by contexts, see `when` in the package.json.
264-
const commands = new Commands(vscodeProposed, client, storage, pathResolver);
262+
const commands = new Commands(
263+
vscodeProposed,
264+
client,
265+
storage,
266+
pathResolver,
267+
mementoManager,
268+
secretsManager,
269+
);
265270
vscode.commands.registerCommand("coder.login", commands.login.bind(commands));
266271
vscode.commands.registerCommand(
267272
"coder.logout",

0 commit comments

Comments
 (0)