f0b06dea Andy McCall 1 month ago
1
// The `oxygenhub` authentication provider (spec §4): a session is a hub
2
// base URL and a personal access token, kept in SecretStorage.
3
4
import * as vscode from "vscode";
5
import { HubClient, Session } from "./hub";
6
7
const KEY = "oxygenhub.sessions";
8
9
export class HubAuth implements vscode.AuthenticationProvider, vscode.Disposable {
10
private readonly emitter = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
11
readonly onDidChangeSessions = this.emitter.event;
12
private readonly onDidChangeEmitter = new vscode.EventEmitter<void>();
13
/** Fires when the set of hubs changes. */
14
readonly onDidChange = this.onDidChangeEmitter.event;
15
private sessions: Session[] = [];
16
private readonly disposables: vscode.Disposable[] = [];
17
18
constructor(private readonly secrets: vscode.SecretStorage) {
19
this.disposables.push(vscode.authentication.registerAuthenticationProvider("oxygenhub", "Oxygenhub", this, { supportsMultipleAccounts: true }));
20
}
21
22
async load(): Promise<void> {
23
const raw = await this.secrets.get(KEY);
24
this.sessions = raw ? (JSON.parse(raw) as Session[]) : [];
25
}
26
27
private async save(): Promise<void> {
28
await this.secrets.store(KEY, JSON.stringify(this.sessions));
29
this.onDidChangeEmitter.fire();
30
}
31
32
all(): Session[] {
33
return [...this.sessions];
34
}
35
36
bases(): string[] {
37
const configured = vscode.workspace.getConfiguration("oxygenhub").get<string[]>("hubs") ?? [];
38
return [...new Set([...this.sessions.map((s) => s.base), ...configured.map((b) => b.replace(/\/$/, ""))])];
39
}
40
41
/** The session for a hub base URL, if signed in. */
42
sessionFor(base: string): Session | undefined {
43
return this.sessions.find((s) => s.base === base.replace(/\/$/, ""));
44
}
45
46
clientFor(base: string): HubClient {
47
return new HubClient(base.replace(/\/$/, ""), this.sessionFor(base)?.token);
48
}
49
50
private toVs(s: Session): vscode.AuthenticationSession {
51
return { id: s.base, accessToken: s.token, account: { id: `${s.username ?? "?"}@${s.base}`, label: `${s.username ?? "?"} on ${s.base}` }, scopes: [] };
52
}
53
54
async getSessions(): Promise<vscode.AuthenticationSession[]> {
55
return this.sessions.map((s) => this.toVs(s));
56
}
57
58
async createSession(): Promise<vscode.AuthenticationSession> {
59
const s = await this.signIn();
60
if (!s) {
61
throw new Error("Sign in cancelled");
62
}
63
return this.toVs(s);
64
}
65
66
async removeSession(id: string): Promise<void> {
67
const before = this.sessions.find((s) => s.base === id);
68
this.sessions = this.sessions.filter((s) => s.base !== id);
69
await this.save();
70
if (before) {
71
this.emitter.fire({ added: [], removed: [this.toVs(before)], changed: [] });
72
}
73
}
74
75
/** Ask for a hub and a token, verify them, and keep the session. */
76
async signIn(defaultBase?: string): Promise<Session | undefined> {
77
const base = await vscode.window.showInputBox({ prompt: "Hub URL", value: defaultBase ?? this.bases()[0] ?? "https://", validateInput: (v) => (/^https?:\/\/[^/]+/.test(v) ? undefined : "A URL such as https://hub.example:7343") });
78
if (!base) {
79
return undefined;
80
}
81
const token = await vscode.window.showInputBox({ prompt: `Personal access token for ${base} (Settings › Tokens on the hub; scopes repo:read repo:write proposals:write issues:write)`, password: true });
82
if (!token) {
83
return undefined;
84
}
85
const client = new HubClient(base.replace(/\/$/, ""), token);
86
let username: string | undefined;
87
try {
88
username = (await client.me()).username;
89
} catch (e) {
90
const pick = await vscode.window.showWarningMessage(`Oxygenhub: could not read your account (${(e as Error).message}); the token may lack user:read. Keep it anyway?`, "Keep", "Cancel");
91
if (pick !== "Keep") {
92
return undefined;
93
}
94
}
95
const session: Session = { base: base.replace(/\/$/, ""), token, username };
96
this.sessions = [...this.sessions.filter((s) => s.base !== session.base), session];
97
await this.save();
98
this.emitter.fire({ added: [this.toVs(session)], removed: [], changed: [] });
99
return session;
100
}
101
102
dispose(): void {
103
this.disposables.forEach((d) => d.dispose());
104
}
105
}