Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,12 @@
"category": "Coder",
"icon": "$(sign-out)"
},
{
"command": "coder.switchDeployment",
"title": "Switch Deployment",
"category": "Coder",
"icon": "$(arrow-swap)"
},
{
"command": "coder.open",
"title": "Open Workspace",
Expand Down Expand Up @@ -282,9 +288,9 @@
"icon": "$(search)"
},
{
"command": "coder.debug.listDeployments",
"title": "List Stored Deployments",
"category": "Coder Debug"
"command": "coder.manageCredentials",
"title": "Manage Stored Credentials",
Comment thread
EhabY marked this conversation as resolved.
Outdated
"category": "Coder"
}
],
"menus": {
Expand All @@ -297,6 +303,10 @@
"command": "coder.logout",
"when": "coder.authenticated"
},
{
"command": "coder.switchDeployment",
"when": "coder.authenticated"
},
{
"command": "coder.createWorkspace",
"when": "coder.authenticated"
Expand Down Expand Up @@ -342,15 +352,18 @@
"when": "false"
},
{
"command": "coder.debug.listDeployments",
"when": "coder.devMode"
"command": "coder.manageCredentials"
}
],
"view/title": [
{
"command": "coder.logout",
"when": "coder.authenticated && view == myWorkspaces"
},
{
"command": "coder.switchDeployment",
"when": "coder.authenticated && view == myWorkspaces"
},
{
"command": "coder.login",
"when": "!coder.authenticated && view == myWorkspaces"
Expand Down
102 changes: 98 additions & 4 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,8 @@ export class Commands {
}

/**
* Log into the provided deployment. If the deployment URL is not specified,
* ask for it first with a menu showing recent URLs along with the default URL
* and CODER_URL, if those are set.
* Log into a deployment. If already authenticated, this is a no-op.
* If no URL is provided, shows a menu of recent URLs plus defaults.
*/
public async login(args?: {
url?: string;
Expand All @@ -85,6 +84,13 @@ export class Commands {
if (this.deploymentManager.isAuthenticated()) {
return;
}
await this.performLogin(args);
}

private async performLogin(args?: {
url?: string;
autoLogin?: boolean;
}): Promise<void> {
this.logger.debug("Logging in");

const currentDeployment = await this.secretsManager.getCurrentDeployment();
Expand Down Expand Up @@ -197,7 +203,7 @@ export class Commands {
}

/**
* Log out from the currently logged-in deployment.
* Log out and clear stored credentials, requiring re-authentication on next login.
*/
public async logout(): Promise<void> {
if (!this.deploymentManager.isAuthenticated()) {
Expand All @@ -206,8 +212,15 @@ export class Commands {

this.logger.debug("Logging out");

const safeHostname =
this.deploymentManager.getCurrentDeployment()?.safeHostname;
Comment thread
EhabY marked this conversation as resolved.

await this.deploymentManager.clearDeployment();

if (safeHostname) {
await this.secretsManager.clearAllAuthData(safeHostname);
}

vscode.window
.showInformationMessage("You've been logged out of Coder!", "Login")
.then((action) => {
Expand All @@ -221,6 +234,87 @@ export class Commands {
this.logger.debug("Logout complete");
}

/**
* Switch to a different deployment without clearing credentials.
* If login fails or user cancels, stays on current deployment.
*/
public async switchDeployment(): Promise<void> {
this.logger.debug("Switching deployment");
await this.performLogin();
}

/**
* Manage stored credentials for all deployments.
* Shows a list of deployments with options to remove individual or all credentials.
*/
public async manageCredentials(): Promise<void> {
try {
const hostnames = await this.secretsManager.getKnownSafeHostnames();
if (hostnames.length === 0) {
vscode.window.showInformationMessage("No stored credentials.");
return;
}

const items: Array<{
label: string;
description: string;
hostname: string | undefined;
}> = hostnames.map((hostname) => ({
label: `$(key) ${hostname}`,
description: "Remove stored credentials",
hostname,
}));

// Only show "Remove All" when there are multiple deployments
if (hostnames.length > 1) {
items.push({
label: "$(trash) Remove All",
description: `Remove credentials for all ${hostnames.length} deployments`,
hostname: undefined,
Comment thread
EhabY marked this conversation as resolved.
Outdated
});
}

const selected = await vscode.window.showQuickPick(items, {
title: "Manage Stored Credentials",
placeHolder: "Select a deployment to remove",
});

if (!selected) {
return;
Comment thread
EhabY marked this conversation as resolved.
}

if (selected.hostname) {
await this.secretsManager.clearAllAuthData(selected.hostname);
vscode.window.showInformationMessage(
`Removed credentials for ${selected.hostname}`,
);
} else {
const confirm = await vscodeProposed.window.showWarningMessage(
`Remove ${hostnames.length} Credentials`,
{
useCustom: true,
modal: true,
detail: `This will remove credentials for: ${hostnames.join(", ")}\n\nYou'll need to log in again to access them.`,
},
"Remove All",
);
if (confirm === "Remove All") {
await Promise.all(
hostnames.map((h) => this.secretsManager.clearAllAuthData(h)),
);
vscode.window.showInformationMessage(
Comment thread
EhabY marked this conversation as resolved.
"Removed credentials for all deployments",
);
}
}
} catch (error: unknown) {
this.logger.error("Failed to manage stored credentials", error);
vscode.window.showErrorMessage(
"Failed to manage stored credentials. Storage may be corrupted.",
);
Comment thread
EhabY marked this conversation as resolved.
}
}

/**
* Create a new workspace for the currently logged-in deployment.
*
Expand Down
44 changes: 7 additions & 37 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
"coder.logout",
commands.logout.bind(commands),
),
vscode.commands.registerCommand(
"coder.switchDeployment",
commands.switchDeployment.bind(commands),
),
vscode.commands.registerCommand("coder.open", commands.open.bind(commands)),
vscode.commands.registerCommand(
"coder.openDevContainer",
Expand Down Expand Up @@ -240,8 +244,9 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
vscode.commands.registerCommand("coder.searchAllWorkspaces", async () =>
showTreeViewSearch(ALL_WORKSPACES_TREE_ID),
),
vscode.commands.registerCommand("coder.debug.listDeployments", () =>
listStoredDeployments(serviceContainer),
vscode.commands.registerCommand(
"coder.manageCredentials",
commands.manageCredentials.bind(commands),
),
);

Expand Down Expand Up @@ -383,38 +388,3 @@ async function showTreeViewSearch(id: string): Promise<void> {
await vscode.commands.executeCommand(`${id}.focus`);
await vscode.commands.executeCommand("list.find");
}

async function listStoredDeployments(
serviceContainer: ServiceContainer,
): Promise<void> {
const secretsManager = serviceContainer.getSecretsManager();
const output = serviceContainer.getLogger();

try {
const hostnames = await secretsManager.getKnownSafeHostnames();
if (hostnames.length === 0) {
vscode.window.showInformationMessage("No deployments stored.");
return;
}

const selected = await vscode.window.showQuickPick(
hostnames.map((hostname) => ({
label: hostname,
description: "Click to forget",
})),
{ placeHolder: "Select a deployment to forget" },
);

if (selected) {
await secretsManager.clearAllAuthData(selected.label);
vscode.window.showInformationMessage(
`Cleared auth data for ${selected.label}`,
);
}
} catch (error: unknown) {
output.error("Failed to list stored deployments", error);
vscode.window.showErrorMessage(
"Failed to list stored deployments. Storage may be corrupted.",
);
}
}