Public: anyone can read this; the crew can change it.
Revoking an invite link in the terminal asks first: `x` names the link, `y` revokes, `n` or esc keeps it open #7965
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/7960-revoking-an-invite-link-in-the-terminal → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +43
−4
src/tui/screens/invites.ts +16 −3
| @@ -3,7 +3,7 @@ | ||
| 3 | 3 | * state, when it was made and when it expires, as Invite links on /admin/limits |
| 4 | 4 | * shows them. `m` mints one from a number of days (1 to 30, 7 by default) and |
| 5 | 5 | * prints the link once, on this screen, until the next key of any kind; `x` revokes the |
| 6 | * selected open one. Both go through the actions, so the gate is theirs: an | |
| 6 | * selected open one after a confirm (`y` revokes, `n` or esc keeps it). Both go through the actions, so the gate is theirs: an | |
| 7 | 7 | * admin who is a person; anyone else sees nothing and mints nothing. The top |
| 8 | 8 | * line says who may sign up now, as MCP `signup` does; `s` picks open, invite |
| 9 | 9 | * or closed, shows from → to and changes it only after a confirm. |
| @@ -120,8 +120,21 @@ export function invitesScreen(f: Session, ctx: Ctx): Screen { | ||
| 120 | 120 | } else if (e.key === "x") { |
| 121 | 121 | const row = all[state.index]; |
| 122 | 122 | if (row === undefined) return false; |
| 123 | note = revoke(f.app, f.user, row.id).ok ? `Revoked ${row.id}.` : INVITE_NOT_OPEN; | |
| 124 | ctx.invalidate(); | |
| 123 | if (row.state !== "open") note = INVITE_NOT_OPEN; | |
| 124 | else | |
| 125 | ctx.push( | |
| 126 | confirmScreen(ctx, screen, { | |
| 127 | title: `Revoke invite ${row.id}?`, | |
| 128 | body: [`Expires ${row.expiresAt.slice(0, 10)}; holder can no longer sign up.`], | |
| 129 | actions: [{ label: "Revoke", variant: "danger" }, { label: "Keep" }], | |
| 130 | yesNo: true, | |
| 131 | onChoose: (i) => { | |
| 132 | if (i !== 0) return; | |
| 133 | note = revoke(f.app, f.user, row.id).ok ? `Revoked ${row.id}.` : INVITE_NOT_OPEN; | |
| 134 | ctx.invalidate(); | |
| 135 | }, | |
| 136 | }), | |
| 137 | ); | |
| 125 | 138 | } else if (e.key === "s") { |
| 126 | 139 | const now = signup(); |
| 127 | 140 | if (!now.ok) return false; |
test/tui-invites.test.ts +27 −1
| @@ -91,8 +91,34 @@ describe("invite links in the terminal", () => { | ||
| 91 | 91 | await h.press("j"); |
| 92 | 92 | expect(h.text()).not.toContain("invite_"); |
| 93 | 93 | await h.press("x"); |
| 94 | expect(listInvites(forge.db)[0]!.state).toBe("revoked"); | |
| 94 | expect(h.text()).toContain(`Revoke invite ${made!.id}?`); | |
| 95 | 95 | expect(h.text()).not.toContain("invite_"); |
| 96 | expect(listInvites(forge.db)[0]!.state).toBe("open"); | |
| 97 | await h.press("y"); | |
| 98 | expect(listInvites(forge.db)[0]!.state).toBe("revoked"); | |
| 99 | expect(h.text()).toContain(`Revoked ${made!.id}.`); | |
| 100 | expect(h.text()).not.toContain("invite_"); | |
| 101 | await h.press("x"); | |
| 102 | expect(h.text()).not.toContain("Revoke invite"); | |
| 103 | expect(h.text()).toContain("not open"); | |
| 104 | h.stop(); | |
| 105 | }); | |
| 106 | ||
| 107 | test("n or esc on the revoke confirm keeps the link open", async () => { | |
| 108 | const h = boot("alice"); | |
| 109 | await open(h); | |
| 110 | await h.press("m"); | |
| 111 | await h.press("enter"); | |
| 112 | const made = listInvites(forge.db).find((i) => i.state === "open")!; | |
| 113 | for (const k of ["n", "esc"]) { | |
| 114 | await h.press("x"); | |
| 115 | expect(h.text()).toContain(`Revoke invite ${made.id}?`); | |
| 116 | expect(h.text()).toContain(`Expires ${made.expiresAt.slice(0, 10)}; holder can no longer sign up.`); | |
| 117 | expect(h.text()).not.toContain("invite_"); | |
| 118 | await h.press(k); | |
| 119 | expect(h.text()).not.toContain("Revoke invite"); | |
| 120 | expect(listInvites(forge.db).find((i) => i.id === made.id)!.state).toBe("open"); | |
| 121 | } | |
| 96 | 122 | h.stop(); |
| 97 | 123 | }); |
| 98 | 124 |
new above
offline
This line is much longer than its neighbours. Reflow the paragraph.