Public: anyone can read this; the crew can change it.

Connecting an origin, the device code and the authorize step, speaks the home page's language #9908

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/9905-connecting-an-origin-the-device-code-and → main 3 commits fork 2 threads · 2 unresolved

  • Merged. scout/9905-connecting-an-origin-the-device-code-and was merged into main as 93518de.

Review

Sign in to review this change.
2 files changed +123 −54
src/web/pages/origin-flow.ts +44 −54
@@ -17,7 +17,7 @@ import { readForm } from "../form.ts";
1717 import { html, raw, type Html } from "../html.ts";
1818 import { page, redirect } from "../layout.ts";
1919 import { pollJs } from "../client.ts";
20 import { btn, card, icon, row } from "../ui.ts";
20 import { btn, icon, row } from "../ui.ts";
2121 import {
2222 accountOnly,
2323 hostLabel,
@@ -64,25 +64,25 @@ export function originFlowPage(ctx: Ctx): Response {
6464 base === "" || flow.shortCode === "" ? "" : `${base}/go/${flow.shortCode}`;
6565 const body =
6666 flow.flow === "device"
67 ? html`${card(
68 html`<p class="muted small">
69 Type this code at
70 <a href="${flow.verificationUri}" rel="noreferrer noopener"
71 >${flow.verificationUri}</a
72 >. Any device with a browser will do — this page waits.
73 </p>
74 <div class="bar">
75 <p class="device-code" id="device-code">${flow.userCode}</p>
76 ${copyBtn("device-code", "Copy the code")}
77 </div>
78 ${handoff(flow.verificationUri, shortLink)}`,
79 {
80 head: html`${icon("github")}
81 <h2>Enter this code</h2>`,
82 },
83 )}`
84 : html`${card(
85 authorize === undefined
67 ? html`<section aria-labelledby="h-flow">
68 <h2 id="h-flow">Enter this code</h2>

Principle 1: the heading says 'Enter this code' and the line below says 'Type this code at'. Say it once, e.g. 'Type it at'.

69 <p class="muted">
70 Type it at
71 <a href="${flow.verificationUri}" rel="noreferrer noopener"
72 >${flow.verificationUri}</a
73 >. Any device with a browser will do — this page waits.
74 </p>
75 <div class="bar">
76 <p class="device-code" id="device-code">${flow.userCode}</p>
77 ${copyBtn("device-code", "Copy the code")}
78 </div>
79 ${handoff(flow.verificationUri, shortLink)}
80 </section>`
81 : html`<section aria-labelledby="h-flow">
82 ${forge
83 ? html`<h2 id="h-flow">Sign in at ${hostLabel(flow.webUrl)}</h2>`
84 : html`<h2 id="h-flow">Authorize on the instance</h2>`}
85 ${authorize === undefined
8686 ? html`<p>
8787 This connect cannot be continued from here — the authorization
8888 link was lost. Start it again from Origins.
@@ -98,15 +98,8 @@ export function originFlowPage(ctx: Ctx): Response {
9898 <span class="mono">${hostLabel(flow.webUrl)}</span> and
9999 ${forge ? "say yes" : "approve the application"}. You will come straight back here.
100100 </p>
101 ${handoff(authorize, shortLink, forge ? `Continue to ${hostLabel(flow.webUrl)}` : "Continue to the instance")}`,
102 {
103 head: forge
104 ? html`${icon("globe")}
105 <h2>Sign in at ${hostLabel(flow.webUrl)}</h2>`
106 : html`${icon("forgejo")}
107 <h2>Authorize on the instance</h2>`,
108 },
109 )}`;
101 ${handoff(authorize, shortLink, forge ? `Continue to ${hostLabel(flow.webUrl)}` : "Continue to the instance")}`}
102 </section>`;
110103
111104 const waiting = html`<form
112105 class="bar mt"
@@ -266,31 +259,28 @@ function expiredFlow(
266259 title: "Connecting an origin",
267260 body: html`<div class="page-head"><h1>Connecting an origin</h1></div>
268261 <div class="narrow-wide">
269 ${card(
270 html`<p class="mb">
271 This connect link expired. Nothing was connected. Start again:
272 </p>
273 <form method="post" action="/settings/origins/oauth">
274 <input type="hidden" name="host" value="${flow.webUrl}" />
275 ${flow.kind === "github"
276 ? html`<input type="hidden" name="kind" value="github" />`
277 : null}
278 ${then === undefined
279 ? null
280 : html`<input type="hidden" name="then" value="${then === "/bring" ? "bring" : "migrate"}" />`}
281 ${link === undefined
282 ? null
283 : html`<input type="hidden" name="link" value="${link}" />`}
284 ${btn(`Connect ${hostLabel(flow.webUrl)}`, {
285 variant: "primary",
286 icon: "external",
287 })}
288 </form>`,
289 {
290 head: html`${icon("alert")}
291 <h2>This connect link expired</h2>`,
292 },
293 )}
262 <section aria-labelledby="h-expired">
263 <h2 id="h-expired">This connect link expired</h2>
264 <p class="mb">
265 Nothing was connected. Start again:
266 </p>
267 <form method="post" action="/settings/origins/oauth">
268 <input type="hidden" name="host" value="${flow.webUrl}" />
269 ${flow.kind === "github"
270 ? html`<input type="hidden" name="kind" value="github" />`
271 : null}
272 ${then === undefined
273 ? null
274 : html`<input type="hidden" name="then" value="${then === "/bring" ? "bring" : "migrate"}" />`}

Principle 1 and 8: the heading already says the link expired. Start the paragraph with 'Nothing was connected. Start again:'.

275 ${link === undefined
276 ? null
277 : html`<input type="hidden" name="link" value="${link}" />`}
278 ${btn(`Connect ${hostLabel(flow.webUrl)}`, {
279 variant: "primary",
280 icon: "external",
281 })}
282 </form>
283 </section>
294284 </div>`,
295285 });
296286 }
test/origin-flow-language.test.ts added +79
@@ -0,0 +1,79 @@
1 /** Connecting an origin speaks the home page's language (#9905): one heading, plain sections, no card. */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { createSession } from "../src/core/sessions.ts";
8 import { createUser } from "../src/core/users.ts";
9 import { createFlow } from "../src/origins/oauth.ts";
10 import { serve, type Servers } from "../src/http/server.ts";
11 import { everyoneIsAMember } from "./helpers/members.ts";
12
13 let dir = "";
14 let app: App;
15 let servers: Servers;
16 let cookie = "";
17 let userId = "";
18
19 beforeAll(async () => {
20 dir = await mkdtemp(join(tmpdir(), "forge-flow-lang-"));
21 app = createApp({ dataDir: dir, port: 0 });
22 everyoneIsAMember(app.db);
23 servers = await serve(app);
24 const made = await createUser(app.db, {
25 username: "ada", email: "[email protected]", password: "a-long-flow-lang-passphrase",
26 origin: "seed", trusted: true,
27 });
28 if (!made.ok) throw new Error(made.error);
29 userId = made.value.id;
30 cookie = `forge_session=${createSession(app.db, userId).token}`;
31 });
32 afterAll(async () => {
33 servers.stop();
34 app.close();
35 await rm(dir, { recursive: true, force: true });
36 });
37
38 const device = (ttlMs?: number) =>
39 createFlow(app.db, dir, {
40 userId,
41 flow: "device",
42 app: { kind: "github", apiUrl: "https://api.github.com", webUrl: "https://github.com", clientId: "cid", clientSecret: "" } as never,
43 deviceCode: "secret-device-code-xyz",
44 userCode: "WDJB-MJHT",
45 verificationUri: "https://github.com/login/device",
46 target: "https://github.com/login/device",
47 ...(ttlMs === undefined ? {} : { ttlMs }),
48 });
49
50 const get = async (id: string): Promise<string> => {
51 const res = await fetch(`${servers.url}/settings/origins/flows/${id}`, { headers: { cookie } });
52 expect(res.status).toBe(200);
53 const text = await res.text();
54 return text.slice(text.indexOf("<main"), text.indexOf("</main>"));
55 };
56
57 function plain(page: string): void {
58 expect(page.match(/<h1[\s>]/g)?.length).toBe(1);
59 expect(page).not.toContain('class="card');
60 expect(page).not.toContain("card-body");
61 expect(page).not.toContain("secret-device-code-xyz");
62 }
63
64 test("a waiting device grant: the code and where to type it, no card", async () => {
65 const page = await get(device().id);
66 plain(page);
67 expect(page).toContain("WDJB-MJHT");
68 expect(page).toMatch(/<a href="https:\/\/github.com\/login\/device" rel="noreferrer noopener"/);
69 expect(page).toContain("/poll");
70 });
71
72 test("an expired flow: the notice and the start-again form, no card", async () => {
73 const flow = device(-60_000);
74 const page = await get(flow.id);
75 plain(page);
76 expect(page).toContain("This connect link expired");
77 expect(page).toContain('action="/settings/origins/oauth"');
78 expect(page).not.toContain("WDJB-MJHT");
79 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs