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

An admin reads the last 20 refused first asks on /forges/refused #11589

opened this

Check · Review · Land · Live — Its check is red. Next: builder, its check is red.

Ready to land

scout/forge:scout/11586-an-admin-reads-the-last-20-refused-first → main 2 commits fork

Review

Sign in to review this change.
4 files changed +103 −1
scripts/hollow-allow.ts +3 −1
@@ -18,4 +18,6 @@ export type Allowed = {
1818 issue: number;
1919 };
2020
21 export const ALLOWED: readonly Allowed[] = [];
21 export const ALLOWED: readonly Allowed[] = [
22 { orphan: "GET /forges/refused", reason: "the page exists; /forges links to it in the next pull", issue: 11587 },
23 ];
src/http/server.ts +2
@@ -280,6 +280,7 @@ import { firstAdminOptions, firstAdminPage, firstAdminSubmit } from "../web/page
280280 import { familyPage } from "../web/pages/family.ts";
281281 import { forgeLinkRevoke, forgeLinkSubmit, forgeLinkUndo, forgeRemoveConfirm, forgeRemoveSubmit, forgeStopSubmit,
282282 forgeOwnCodeSubmit, forgesPage } from "../web/pages/forges.ts";
283 import { forgesRefusedPage } from "../web/pages/forges-refused.ts";
283284 import { forgeItSubmit } from "../web/pages/forge-it.ts";
284285 import { funnelSubmit } from "./funnel.ts";
285286 import { pricingDone, pricingOrder, pricingPage, resendPage, resendSubmit } from "../web/pages/pricing.ts";
@@ -856,6 +857,7 @@ export function buildRouter(): Router<Handler> {
856857 router.add("POST", "/first-admin/options", firstAdminOptions);
857858 // "+ New forge": the spawn door as a page, any signed-in person (spawn/door.ts).
858859 router.add("GET", "/forges", forgesPage);
860 router.add("GET", "/forges/refused", forgesRefusedPage);
859861 router.add("GET", "/family", familyPage);
860862 router.add("GET", "/forges/name/:name", forgeName);
861863 router.add("POST", "/forges/:name/claim-link", forgeLinkSubmit);
src/web/pages/forges-refused.ts added +40
@@ -0,0 +1,40 @@
1 /**
2 * `/forges/refused`: the last 20 first asks a stranger made and the forge
3 * refused, with the refusal's words — an admin's only, the same answer
4 * `/forges` gives anyone else. One `listRefusals` call; the ask is text,
5 * escaped; no email or claim code is held by the rows, so none is shown.
6 */
7
8 import type { Ctx } from "../../http/context.ts";
9 import { listRefusals } from "../../core/first_ask_refusals.ts";
10 import { notFoundPage, pageHead } from "../chrome.ts";
11 import { html } from "../html.ts";
12 import { page } from "../layout.ts";
13 import { time } from "../ui.ts";
14
15 export const REFUSED_SHOWN = 20;
16
17 // GET /forges/refused — an admin's only.
18 export function forgesRefusedPage(ctx: Ctx): Response {
19 if (!ctx.auth.user?.isAdmin) return notFoundPage(ctx);
20 const rows = listRefusals(ctx.app.db, REFUSED_SHOWN, Date.now());
21 return page({
22 ctx,
23 title: "Refused first asks",
24 body: html`<div class="narrow-wide" data-refused>
25 ${pageHead("Refused first asks", { sub: "The last 20 first asks the forge turned away, newest first." })}
26 ${rows.length === 0
27 ? html`<p class="muted">No first ask has been refused.</p>`
28 : html`<ul class="list setting-list">
29 ${rows.map(
30 (r) => html`<li data-refusal>
31 <p>${time(new Date(r.at).toISOString())} · <span class="muted">${r.actorId ?? "no one signed in"}</span></p>
32 <p>${r.ask}</p>
33 <p class="muted small">${r.words}</p>
34 </li>`,
35 )}
36 </ul>`}
37 <p class="mt"><a href="/forges">Forges you sent</a></p>
38 </div>`,
39 });
40 }
test/forges-refused.test.ts added +58
@@ -0,0 +1,58 @@
1 /** GET /forges/refused (#11586): an admin reads the last 20 refused first asks. */
2
3 import { describe, expect, test } from "bun:test";
4 import { mkdtempSync } from "node:fs";
5 import { tmpdir } from "node:os";
6 import { join } from "node:path";
7 import { createApp } from "../src/app.ts";
8 import { recordRefusal } from "../src/core/first_ask_refusals.ts";
9 import { forgesPage } from "../src/web/pages/forges.ts";
10 import { forgesRefusedPage } from "../src/web/pages/forges-refused.ts";
11
12 const fresh = () => createApp({ dataDir: mkdtempSync(join(tmpdir(), "forge-refused-")), url: "https://x.test", port: 0 });
13 const ctxOf = (app: ReturnType<typeof fresh>, user: unknown) => {
14 const request = new Request("https://x.test/forges/refused");
15 return { app, request, auth: { user }, params: {}, url: new URL(request.url) } as never;
16 };
17
18 describe("GET /forges/refused", () => {
19 test("an admin sees the last 20, newest first, with the reason", async () => {
20 const app = fresh();
21 for (let i = 0; i < 25; i++) recordRefusal(app.db, { actorId: `u${i}`, ask: `ask number ${i}`, words: "Not today." }, Date.now() - (25 - i) * 1000);
22 const res = forgesRefusedPage(ctxOf(app, { id: "a", isAdmin: true }));
23 expect(res.status).toBe(200);
24 const body = await res.text();
25 expect(body.match(/data-refusal/g)).toHaveLength(20);
26 expect(body).toContain("ask number 24");
27 expect(body).not.toContain("ask number 4<");
28 expect(body.indexOf("ask number 24")).toBeLessThan(body.indexOf("ask number 23"));
29 expect(body).toContain("Not today.");
30 });
31 test("an empty list is one quiet line", async () => {
32 const body = await forgesRefusedPage(ctxOf(fresh(), { id: "a", isAdmin: true })).text();
33 expect(body).toContain("No first ask has been refused.");
34 expect(body).not.toContain("data-refusal");
35 });
36 test("a non-admin gets exactly what /forges gives", async () => {
37 const app = fresh();
38 for (const user of [undefined, { id: "b", isAdmin: false }]) {
39 const mine = forgesRefusedPage(ctxOf(app, user));
40 const theirs = await forgesPage(ctxOf(app, user));
41 expect(mine.status).toBe(theirs.status);
42 expect(await mine.text()).toBe(await theirs.text());
43 }
44 });
45 test("markup in an ask renders as text", async () => {
46 const app = fresh();
47 recordRefusal(app.db, { actorId: null, ask: "<script>alert(1)</script>", words: "no" }, Date.now());
48 const body = await forgesRefusedPage(ctxOf(app, { id: "a", isAdmin: true })).text();
49 expect(body).not.toContain("<script>alert(1)");
50 expect(body).toContain("&lt;script&gt;alert(1)&lt;/script&gt;");
51 });
52 test("no email or claim code appears", async () => {
53 const app = fresh();
54 recordRefusal(app.db, { actorId: "u1", ask: "build me a site", words: "no" }, Date.now());
55 const body = await forgesRefusedPage(ctxOf(app, { id: "a", isAdmin: true, email: "[email protected]" })).text();
56 expect(body).not.toMatch(/@x\.test|claim/i);
57 });
58 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs