OXYGENHUB
# f0b06deac4b9 oxygenhub-vscode / src / auth.ts
4.1 KiB · last changed 0.1.0: sign in, hub client with generated types, remote provider, proposals and issues views, review threads on atoms, seal and land through oxy, checks status bar 1 month ago source blame history raw
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
}