feat(invites): drop literal token after claim; cleaner UI

Once an invite is claimed, the token has no functional role — claims
are one-way and the link is dead. Stop returning the literal token in
the GET /api/invites response for claimed entries (server/invites.ts
toEntry). The audit trail — claimed_at, claimed_by_display — stays.
Helps a little with data minimization: a compromised inviter account
can no longer see used-up invitation URLs.

Type: InviteEntry.token is now string | null. Callers that still need
to use the token (signup-via-invite tests, the cancel button, the
copy button) are guarded so they only run on entries where the token
is present (i.e. unclaimed). The each-key falls back to a synthetic
composite when token is null so Svelte's keyed-each stays stable.

UI: claimed entries collapse to a single muted line, no card frame,
no URL placeholder:
    ✓ Laget DD.MM.YYYY · godtatt av <bruker> DD.MM.YYYY

Unclaimed entries keep the existing card with copy / cancel buttons.
Heading on the invite section also renamed from "Invitasjonslenker"
to "Invitasjoner" — claimed entries don't have a link anymore so the
older label was misleading.

Tests updated to match by created_at instead of token for the
claimed-invite lookup, and to assert that token is null post-claim.
This commit is contained in:
Ole-Morten Duesund 2026-05-25 20:47:33 +02:00
commit 95f989639d
4 changed files with 46 additions and 31 deletions

View file

@ -50,7 +50,12 @@ function toEntry(row: InviteRow): InviteEntry {
}
}
return {
token: row.token,
// Once an invite is claimed the token has no functional role — claim
// is one-way, you can't re-claim — and we don't need the inviter to
// be able to re-share a now-dead link. Stripping it from the response
// also means a compromised inviter account doesn't leak used-up
// links. The audit trail (claimed_at, claimed_by_display) stays.
token: row.claimed_at ? null : row.token,
created_at: row.created_at,
claimed_at: row.claimed_at,
claimed_by_display: claimedByDisplay,