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:
parent
2ac73c3515
commit
95f989639d
4 changed files with 46 additions and 31 deletions
|
|
@ -125,7 +125,7 @@ describe('invites', () => {
|
|||
expect(inv.claimed_at).toBeNull();
|
||||
|
||||
// A new user signs up with that invite token.
|
||||
const claimer = await signupAndGetCookie(ctx, 'inv-claimer@test.invalid', undefined, inv.token);
|
||||
const claimer = await signupAndGetCookie(ctx, 'inv-claimer@test.invalid', undefined, inv.token!);
|
||||
expect(claimer.me.id).toBeTruthy();
|
||||
|
||||
// The invited_by column on the new user row points at the inviter.
|
||||
|
|
@ -133,13 +133,16 @@ describe('invites', () => {
|
|||
.get(claimer.me.id) as { invited_by: string | null };
|
||||
expect(row.invited_by).toBe(inviter.me.id);
|
||||
|
||||
// The invite is now claimed in the inviter's list.
|
||||
// The invite is now claimed in the inviter's list. The server stops
|
||||
// returning the literal token once the invite is claimed, so match by
|
||||
// created_at instead.
|
||||
const myInvites = await reqJson<InviteEntry[]>(ctx, 'GET', '/api/invites', {
|
||||
cookie: inviter.cookie,
|
||||
});
|
||||
const claimed = myInvites.find((i) => i.token === inv.token)!;
|
||||
const claimed = myInvites.find((i) => i.created_at === inv.created_at)!;
|
||||
expect(claimed.claimed_at).not.toBeNull();
|
||||
expect(claimed.claimed_by_display).toBe('inv-claimer');
|
||||
expect(claimed.token).toBeNull();
|
||||
});
|
||||
|
||||
ttest('invite is single-use: re-claim is rejected', async () => {
|
||||
|
|
@ -149,12 +152,12 @@ describe('invites', () => {
|
|||
});
|
||||
|
||||
// First claim succeeds.
|
||||
await signupAndGetCookie(ctx, 'inv-single-1@test.invalid', undefined, inv.token);
|
||||
await signupAndGetCookie(ctx, 'inv-single-1@test.invalid', undefined, inv.token!);
|
||||
|
||||
// Second signup with the same token: with self-registry OPEN (the
|
||||
// default), the bad token is silently dropped and signup proceeds
|
||||
// WITHOUT attribution. Confirm: signup ok, invited_by is null.
|
||||
const second = await signupAndGetCookie(ctx, 'inv-single-2@test.invalid', undefined, inv.token);
|
||||
const second = await signupAndGetCookie(ctx, 'inv-single-2@test.invalid', undefined, inv.token!);
|
||||
const row = getDb().prepare('SELECT invited_by FROM users WHERE id = ?')
|
||||
.get(second.me.id) as { invited_by: string | null };
|
||||
expect(row.invited_by).toBeNull();
|
||||
|
|
@ -176,7 +179,7 @@ describe('invites', () => {
|
|||
const inv = await reqJson<InviteEntry>(ctx, 'POST', '/api/invites', {
|
||||
cookie: inviter.cookie, expect: 201,
|
||||
});
|
||||
await signupAndGetCookie(ctx, 'inv-already-c@test.invalid', undefined, inv.token);
|
||||
await signupAndGetCookie(ctx, 'inv-already-c@test.invalid', undefined, inv.token!);
|
||||
const cancelRes = await req(ctx, 'DELETE', `/api/invites/${inv.token}`, {
|
||||
cookie: inviter.cookie,
|
||||
});
|
||||
|
|
@ -232,7 +235,7 @@ describe('settings + signup gating', () => {
|
|||
const inv = await reqJson<InviteEntry>(ctx, 'POST', '/api/invites', {
|
||||
cookie: admin.cookie, expect: 201,
|
||||
});
|
||||
const claimer = await signupAndGetCookie(ctx, 'gate-claimer@test.invalid', undefined, inv.token);
|
||||
const claimer = await signupAndGetCookie(ctx, 'gate-claimer@test.invalid', undefined, inv.token!);
|
||||
expect(claimer.me.id).toBeTruthy();
|
||||
} finally {
|
||||
// Re-open self-registry so subsequent tests can still sign up.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue