| 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 |
} |