Public: anyone can read this; the crew can change it.
Behind an edge, the terminal's sign-up cannot take a name the edge will later vouch for #2830
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/2829-behind-an-edge-the-terminal-s-sign-up-ca → main 1 commit fork
2 files
changed +68
−3
src/tui/screens/signin.ts +10 −3
| @@ -42,6 +42,9 @@ const CREATE: Field[] = [ | ||
| 42 | 42 | const INVITE_BLANK_MESSAGE = "Paste the token from your invite link."; |
| 43 | 43 | const INVITE: Field = { label: "Invite", secret: true, hint: "The token from your invite link." }; |
| 44 | 44 | |
| 45 | /** Behind an edge the terminal cannot know the login the edge will vouch for. */ | |
| 46 | const EDGE_SIGNUP_MESSAGE = "Sign up on the page: accounts here are tied to the login the edge vouches for."; | |
| 47 | ||
| 45 | 48 | export function signInScreen(f: Session, ctx: Ctx, onDone: () => void): Screen { |
| 46 | 49 | // "Is this forge empty?" is a existence question, not a count: `COUNT(*)` |
| 47 | 50 | // walks the whole user table on the one screen every session paints. |
| @@ -50,7 +53,11 @@ export function signInScreen(f: Session, ctx: Ctx, onDone: () => void): Screen { | ||
| 50 | 53 | // With signup = invite the form is offered, and asks for the invite. |
| 51 | 54 | // Both are read where they are used, so the screen follows the live setting. |
| 52 | 55 | const invited = (): boolean => getSignup(f.db) === "invite"; |
| 53 | const closed = (): boolean => !invited() && signupClosed(f.app); | |
| 56 | // Behind an edge no account is made here: it would be one the edge may later | |
| 57 | // sign a platform user of the same name into. Sign-in is unchanged. | |
| 58 | const edge = f.app.config.edgeToken !== undefined; | |
| 59 | const closed = (): boolean => edge || (!invited() && signupClosed(f.app)); | |
| 60 | const closedText = (): string => (edge ? EDGE_SIGNUP_MESSAGE : SIGNUP_CLOSED_MESSAGE); | |
| 54 | 61 | let mode: "signin" | "create" = first && !closed() ? "create" : "signin"; |
| 55 | 62 | let values = ["", "", "", ""]; |
| 56 | 63 | let focus = 0; |
| @@ -62,7 +69,7 @@ export function signInScreen(f: Session, ctx: Ctx, onDone: () => void): Screen { | ||
| 62 | 69 | const submit = (): void => { |
| 63 | 70 | if (busy) return; |
| 64 | 71 | if (closed() && mode === "create") { |
| 65 | error = SIGNUP_CLOSED_MESSAGE; | |
| 72 | error = closedText(); | |
| 66 | 73 | return; |
| 67 | 74 | } |
| 68 | 75 | const needsInvite = mode === "create" && invited(); |
| @@ -141,7 +148,7 @@ export function signInScreen(f: Session, ctx: Ctx, onDone: () => void): Screen { | ||
| 141 | 148 | head!.y + 1, |
| 142 | 149 | truncate( |
| 143 | 150 | closed() |
| 144 | ? SIGNUP_CLOSED_MESSAGE | |
| 151 | ? closedText() | |
| 145 | 152 | : first |
| 146 | 153 | ? "This forge has no accounts yet. The operator makes the admin." |
| 147 | 154 | : `Connected from ${f.identity.remoteAddress} with ${f.identity.fingerprint ?? "no key"}`, |
test/tui-signup-edge.test.ts added +58
| @@ -0,0 +1,58 @@ | ||
| 1 | /** | |
| 2 | * Behind an edge the terminal cannot know the login the edge will vouch for, | |
| 3 | * so its create-account mode makes no row and points at the page. | |
| 4 | */ | |
| 5 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 6 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 7 | import { tmpdir } from "node:os"; | |
| 8 | import { join } from "node:path"; | |
| 9 | import { createApp, type App as Forge } from "../src/app.ts"; | |
| 10 | import { getUserByName } from "../src/core/users.ts"; | |
| 11 | import { createForgeApp } from "../src/tui/forge.ts"; | |
| 12 | import type { TuiIdentity } from "../src/tui/session.ts"; | |
| 13 | import { headlessTerminal } from "../src/tui/term/terminal.ts"; | |
| 14 | import { quietLogs } from "./helpers/globals.ts"; | |
| 15 | import { settleApp } from "./helpers/tui.ts"; | |
| 16 | ||
| 17 | quietLogs(); | |
| 18 | ||
| 19 | const KEY = | |
| 20 | "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB2S7bF0R4NmuTUvVJXPPjdI1AF8XHXeYSNfr8DJ5eDo terminal-test"; | |
| 21 | ||
| 22 | let dir: string; | |
| 23 | let forge: Forge; | |
| 24 | ||
| 25 | beforeAll(async () => { | |
| 26 | dir = await mkdtemp(join(tmpdir(), "forge-tui-edge-signup-")); | |
| 27 | forge = createApp({ dataDir: dir, port: 0, edgeToken: "edge-secret", disk: () => ({ freeBytes: 95, totalBytes: 100 }) }); | |
| 28 | }); | |
| 29 | afterAll(async () => { | |
| 30 | forge.close(); | |
| 31 | await rm(dir, { recursive: true, force: true }); | |
| 32 | }); | |
| 33 | ||
| 34 | test("with an edge token the terminal sign-up makes no row", async () => { | |
| 35 | const identity: TuiIdentity = { sshUsername: "git", remoteAddress: "198.51.100.7", fingerprint: "SHA256:x", publicKey: KEY }; | |
| 36 | const { app } = createForgeApp(forge, identity, { terminal: headlessTerminal(120, 32), exitHooks: false }); | |
| 37 | app.start(); | |
| 38 | const key = (k: string, ctrl = false): void => | |
| 39 | app.dispatch({ type: "key", key: k, ctrl, alt: false, shift: false }); | |
| 40 | const type = (t: string): void => [...t].forEach((c) => key(c)); | |
| 41 | try { | |
| 42 | await settleApp(app); | |
| 43 | key("s"); | |
| 44 | await settleApp(app); | |
| 45 | key("t", true); // no create mode to switch to | |
| 46 | await settleApp(app); | |
| 47 | expect(app.screen.toText()).not.toContain("Email"); | |
| 48 | expect(app.screen.toText()).toContain("Sign up on the page"); | |
| 49 | type("mallory"); | |
| 50 | key("tab"); | |
| 51 | type("a-long-passphrase"); | |
| 52 | key("enter"); | |
| 53 | await settleApp(app); | |
| 54 | expect(getUserByName(forge.db, "mallory")).toBeFalsy(); | |
| 55 | } finally { | |
| 56 | app.stop(); | |
| 57 | } | |
| 58 | }); |
new above
offline