From 9b86093c82ed5ed31327eb22ac5354ca64f62f65 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Thu, 25 Jun 2026 14:03:05 +0700 Subject: [PATCH] fix(teams): accept matching invite tokens for unverified users Treat a valid invite token for the matching email address as proof of inbox access, so invite-first signup does not require a second verification email before accepting the invite.\n\nCloses #145 --- src/teams/invitations-api.ts | 4 ---- tests/teams/invitations-api.test.ts | 20 ++++++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/teams/invitations-api.ts b/src/teams/invitations-api.ts index 1324fa1..16dcc63 100644 --- a/src/teams/invitations-api.ts +++ b/src/teams/invitations-api.ts @@ -390,10 +390,6 @@ export function createInvitationsApi(opts: InvitationsApiOptions) { } } - if (!currentUser.emailVerified) { - return { succeeded: false, status: 403, error: 'Verify your email before accepting this invitation' } - } - let orgMember = await getOrganizationMember(invitation.organizationId, currentUser.id) if (orgMember?.role === 'owner' || orgMember?.role === 'admin') { return { succeeded: false, status: 409, error: 'This user already has account-level access.' } diff --git a/tests/teams/invitations-api.test.ts b/tests/teams/invitations-api.test.ts index f6437fa..52e96a6 100644 --- a/tests/teams/invitations-api.test.ts +++ b/tests/teams/invitations-api.test.ts @@ -207,24 +207,28 @@ describe('getPreview + acceptInvitation', () => { } }) - it('rejects accept on email mismatch (403) and unverified email (403)', async () => { + it('rejects accept on email mismatch (403)', async () => { const deps = await seed() - await deps.db.insert(usersTable).values([ - { id: 'inv-mismatch', name: 'X', email: 'wrong@x.com', emailVerified: true }, - { id: 'inv-unverified', name: 'U', email: 'unverified@x.com', emailVerified: false }, - ]) + await deps.db.insert(usersTable).values({ id: 'inv-mismatch', name: 'X', email: 'wrong@x.com', emailVerified: true }) const { api } = makeApi(deps) const a = await api.createInvitation({ workspaceId: 'ws-1', email: 'target@x.com', permissions: 'editor', invitedByUserId: 'owner-1', origin: ORIGIN }) if (!a.succeeded) throw new Error('setup') const mismatch = await api.acceptInvitation({ token: a.value.invitation.token, userId: 'inv-mismatch' }) expect(mismatch.succeeded).toBe(false) if (!mismatch.succeeded) expect(mismatch.status).toBe(403) + }) + it('accepts a matching unverified invitee because the invite token proves inbox access', async () => { + const deps = await seed() + await deps.db.insert(usersTable).values({ id: 'inv-unverified', name: 'U', email: 'unverified@x.com', emailVerified: false }) + const { api, adds } = makeApi(deps) const u = await api.createInvitation({ workspaceId: 'ws-1', email: 'unverified@x.com', permissions: 'editor', invitedByUserId: 'owner-1', origin: ORIGIN }) if (!u.succeeded) throw new Error('setup') - const unverified = await api.acceptInvitation({ token: u.value.invitation.token, userId: 'inv-unverified' }) - expect(unverified.succeeded).toBe(false) - if (!unverified.succeeded) expect(unverified.status).toBe(403) + const accepted = await api.acceptInvitation({ token: u.value.invitation.token, userId: 'inv-unverified' }) + expect(accepted.succeeded).toBe(true) + if (accepted.succeeded) expect(accepted.value.workspaceId).toBe('ws-1') + await flushMicrotasks() + expect(adds).toEqual([{ workspaceId: 'ws-1', userId: 'inv-unverified', role: 'editor' }]) }) it('accepts a matching, verified invite → membership created, add sync fired, idempotent', async () => {