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

The admin API's accounts, people, invites and teams lists name their Server-Timing phases #4988

Merged opened this

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

Merged

scout/forge:scout/4978-the-admin-api-s-accounts-people-invites → main 1 commit fork

  • Merged. scout/4978-the-admin-api-s-accounts-people-invites was merged into main as 6e7c8ad.

Review

Sign in to review this change.
2 files changed +61 −7
src/http/api.ts +12 −7
@@ -1692,10 +1692,11 @@ export function apiRoutes(router: Router<Handler>): void {
16921692
16931693 // Crew teams (`core/teams.ts`): a forge admin who is a person; the page's door.
16941694 router.add("GET", "/api/v1/admin/teams", (ctx) => {
1695 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
16951696 const denied = accountAdmin(ctx, "ro");
16961697 if (denied) return denied;
1697 const read = readTeams(ctx.app, ctx.auth.user);
1698 return read.ok ? json({ teams: read.value.teams }) : notFound();
1698 const read = timePhase(ctx.request, "teams", () => readTeams(ctx.app, ctx.auth.user));
1699 return read.ok ? timePhase(ctx.request, "render", () => json({ teams: read.value.teams })) : notFound();
16991700 });
17001701
17011702 router.add("PUT", "/api/v1/admin/teams/:slug", async (ctx) => {
@@ -1775,10 +1776,11 @@ export function apiRoutes(router: Router<Handler>): void {
17751776 });
17761777
17771778 router.add("GET", "/api/v1/admin/invites", (ctx) => {
1779 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
17781780 const denied = accountAdmin(ctx, "ro");
17791781 if (denied) return denied;
1780 const listed = listInviteLinks(ctx.app, ctx.auth.user);
1781 return listed.ok ? json({ items: listed.value }) : notFound();
1782 const listed = timePhase(ctx.request, "invites", () => listInviteLinks(ctx.app, ctx.auth.user));
1783 return listed.ok ? timePhase(ctx.request, "render", () => json({ items: listed.value })) : notFound();
17821784 });
17831785
17841786 router.add("DELETE", "/api/v1/admin/invites/:id", (ctx) => {
@@ -1789,10 +1791,11 @@ export function apiRoutes(router: Router<Handler>): void {
17891791 });
17901792
17911793 router.add("GET", "/api/v1/admin/accounts", (ctx) => {
1794 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
17921795 const denied = accountAdmin(ctx, "ro");
17931796 if (denied) return denied;
1794 const listed = listAccounts(ctx.app, ctx.auth.user);
1795 return listed.ok ? json({ items: listed.value }) : notFound();
1797 const listed = timePhase(ctx.request, "accounts", () => listAccounts(ctx.app, ctx.auth.user));
1798 return listed.ok ? timePhase(ctx.request, "render", () => json({ items: listed.value })) : notFound();
17961799 });
17971800
17981801 router.add("POST", "/api/v1/admin/accounts/:username/suspend", async (ctx) => {
@@ -1836,9 +1839,11 @@ export function apiRoutes(router: Router<Handler>): void {
18361839
18371840 // Every person and their role.
18381841 router.add("GET", "/api/v1/admin/people", (ctx) => {
1842 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
18391843 const denied = accountAdmin(ctx, "admin");
18401844 if (denied) return denied;
1841 return json({ people: listPeople(ctx.app.db) });
1845 const people = timePhase(ctx.request, "people", () => listPeople(ctx.app.db));
1846 return timePhase(ctx.request, "render", () => json({ people }));
18421847 });
18431848
18441849 // Make a person a member (with an optional starter allowance, dollars a month) or a guest again.
test/admin-lists-timing.test.ts added +49
@@ -0,0 +1,49 @@
1 /** The admin API's lists name their phases in Server-Timing. */
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 { serve, type Servers } from "../src/http/server.ts";
8 import { seedAdmin } from "./helpers/admin.ts";
9
10 let dataDir = "";
11 let app: App, servers: Servers, bearer = "";
12
13 beforeAll(async () => {
14 dataDir = await mkdtemp(join(tmpdir(), "forge-atime-"));
15 app = createApp({ dataDir, port: 0 });
16 servers = await serve(app);
17 await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" });
18 const minted = await fetch(`${servers.url}/api/v1/tokens`, {
19 method: "POST",
20 headers: { "content-type": "application/json" },
21 body: JSON.stringify({ adminConfirm: true, username: "alice", password: "correct-horse-battery", name: "cli", scope: "admin" }),
22 });
23 bearer = ((await minted.json()) as { token: string }).token;
24 });
25
26 afterAll(async () => {
27 servers.stop();
28 app.close();
29 await rm(dataDir, { recursive: true, force: true });
30 });
31
32 const names = (res: Response) =>
33 (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!).filter(Boolean);
34
35 for (const [path, read] of [["accounts", "accounts"], ["people", "people"], ["invites", "invites"], ["teams", "teams"]] as const) {
36 test(`GET /admin/${path} names session, ${read}, render in order`, async () => {
37 const res = await fetch(`${servers.url}/api/v1/admin/${path}`, { headers: { authorization: `Bearer ${bearer}` } });
38 await res.arrayBuffer();
39 expect(res.status).toBe(200);
40 expect(names(res).filter((n) => n !== "app")).toEqual(["session", read, "render"]);
41 });
42
43 test(`a refused GET /admin/${path} still names session`, async () => {
44 const res = await fetch(`${servers.url}/api/v1/admin/${path}`);
45 await res.arrayBuffer();
46 expect([401, 403, 404]).toContain(res.status);
47 expect(names(res)).toContain("session");
48 });
49 }

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs