Public: anyone can read this; the crew can change it.
Revoke on an invite link at /admin/limits goes through the confirm page, which says what revoking does #7970
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/7962-revoke-on-an-invite-link-at-admin-limits → main 1 commit fork
3 files
changed +22
−6
src/web/pages/admin-limits.ts +2 −5
| @@ -20,7 +20,7 @@ import { notFoundPage, settingsLayout } from "../chrome.ts"; | ||
| 20 | 20 | import { field, readForm } from "../form.ts"; |
| 21 | 21 | import { html, when } from "../html.ts"; |
| 22 | 22 | import { flashTo, page } from "../layout.ts"; |
| 23 | import { btn, card, icon, row, time } from "../ui.ts"; | |
| 23 | import { btn, card, confirmAction, icon, row, time } from "../ui.ts"; | |
| 24 | 24 | import { accountSections } from "./origins.ts"; |
| 25 | 25 | |
| 26 | 26 | // GET /admin/limits |
| @@ -165,9 +165,7 @@ export function limitsPage(ctx: Ctx): Response { | ||
| 165 | 165 | meta: html`Made ${time(i.createdAt)} · expires ${time(i.expiresAt)}`, |
| 166 | 166 | trail: |
| 167 | 167 | i.state === "open" |
| 168 | ? html`<form class="row-fields" method="post" action="/admin/invites/${i.id}/revoke"> | |
| 169 | ${btn("Revoke", { size: "sm", label: `Revoke invite ${i.id}` })} | |
| 170 | </form>` | |
| 168 | ? confirmAction({ action: `/admin/invites/${i.id}/revoke`, verb: "Revoke", size: "sm" }) | |
| 171 | 169 | : html``, |
| 172 | 170 | }), |
| 173 | 171 | ); |
| @@ -175,7 +173,6 @@ export function limitsPage(ctx: Ctx): Response { | ||
| 175 | 173 | html`<div class="card-body"> |
| 176 | 174 | <p class="muted small"> |
| 177 | 175 | A link is shown once, when it is made, and is good for one signup. |
| 178 | Nothing reads these yet. | |
| 179 | 176 | </p> |
| 180 | 177 | <form class="row-fields" method="post" action="/admin/invites"> |
| 181 | 178 | <label class="small">expires in (days) |
src/web/pages/search.ts +10
| @@ -717,6 +717,16 @@ const DESTRUCTIVE: { | ||
| 717 | 717 | }; |
| 718 | 718 | }, |
| 719 | 719 | }, |
| 720 | { | |
| 721 | pattern: /^\/admin\/invites\/([^/]+)\/revoke$/, | |
| 722 | describe: (m) => ({ | |
| 723 | verb: "Revoke", | |
| 724 | title: `Revoke invite ${decodeURIComponent(m[1] ?? "")}?`, | |
| 725 | detail: | |
| 726 | "Whoever holds this link can no longer sign up with it. A new link has to be minted to let them in.", | |
| 727 | phrase: "", | |
| 728 | }), | |
| 729 | }, | |
| 720 | 730 | ]; |
| 721 | 731 | |
| 722 | 732 | function describeTarget(to: string, ctx: Ctx): Destructive | undefined { |
test/invites.test.ts +10 −1
| @@ -142,7 +142,16 @@ describe("invites: the settings form", () => { | ||
| 142 | 142 | expect(page).not.toContain("invite_"); |
| 143 | 143 | const [item] = listInvites(app.db); |
| 144 | 144 | expect(item!.state).toBe("open"); |
| 145 | expect(page).toContain(`/admin/invites/${item!.id}/revoke`); | |
| 145 | const to = `/admin/invites/${item!.id}/revoke`; | |
| 146 | expect(page).toContain(`href="/-/confirm?to=${encodeURIComponent(to)}"`); | |
| 147 | expect(page).not.toContain(`action="${to}"`); | |
| 148 | expect(page).not.toContain("Nothing reads these yet."); | |
| 149 | const confirm = await fetch(`${servers.url}/-/confirm?to=${encodeURIComponent(to)}`, { headers: { cookie: alice } }); | |
| 150 | expect(confirm.status).toBe(200); | |
| 151 | const words = await confirm.text(); | |
| 152 | expect(words).toContain(`Revoke invite ${item!.id}?`); | |
| 153 | expect(words).toContain("Whoever holds this link can no longer sign up with it."); | |
| 154 | expect(words).toContain(`action="${to}"`); | |
| 146 | 155 | expect((await post(alice, `/admin/invites/${item!.id}/revoke`, {})).status).toBe(303); |
| 147 | 156 | expect(listInvites(app.db)[0]!.state).toBe("revoked"); |
| 148 | 157 | }); |
new above
offline