Skip to content

Commit 9ddc6bc

Browse files
fix(teams): accept matching invite tokens for unverified users (#146)
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
1 parent 855e78c commit 9ddc6bc

2 files changed

Lines changed: 12 additions & 12 deletions

File tree

src/teams/invitations-api.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -390,10 +390,6 @@ export function createInvitationsApi(opts: InvitationsApiOptions) {
390390
}
391391
}
392392

393-
if (!currentUser.emailVerified) {
394-
return { succeeded: false, status: 403, error: 'Verify your email before accepting this invitation' }
395-
}
396-
397393
let orgMember = await getOrganizationMember(invitation.organizationId, currentUser.id)
398394
if (orgMember?.role === 'owner' || orgMember?.role === 'admin') {
399395
return { succeeded: false, status: 409, error: 'This user already has account-level access.' }

tests/teams/invitations-api.test.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -207,24 +207,28 @@ describe('getPreview + acceptInvitation', () => {
207207
}
208208
})
209209

210-
it('rejects accept on email mismatch (403) and unverified email (403)', async () => {
210+
it('rejects accept on email mismatch (403)', async () => {
211211
const deps = await seed()
212-
await deps.db.insert(usersTable).values([
213-
{ id: 'inv-mismatch', name: 'X', email: 'wrong@x.com', emailVerified: true },
214-
{ id: 'inv-unverified', name: 'U', email: 'unverified@x.com', emailVerified: false },
215-
])
212+
await deps.db.insert(usersTable).values({ id: 'inv-mismatch', name: 'X', email: 'wrong@x.com', emailVerified: true })
216213
const { api } = makeApi(deps)
217214
const a = await api.createInvitation({ workspaceId: 'ws-1', email: 'target@x.com', permissions: 'editor', invitedByUserId: 'owner-1', origin: ORIGIN })
218215
if (!a.succeeded) throw new Error('setup')
219216
const mismatch = await api.acceptInvitation({ token: a.value.invitation.token, userId: 'inv-mismatch' })
220217
expect(mismatch.succeeded).toBe(false)
221218
if (!mismatch.succeeded) expect(mismatch.status).toBe(403)
219+
})
222220

221+
it('accepts a matching unverified invitee because the invite token proves inbox access', async () => {
222+
const deps = await seed()
223+
await deps.db.insert(usersTable).values({ id: 'inv-unverified', name: 'U', email: 'unverified@x.com', emailVerified: false })
224+
const { api, adds } = makeApi(deps)
223225
const u = await api.createInvitation({ workspaceId: 'ws-1', email: 'unverified@x.com', permissions: 'editor', invitedByUserId: 'owner-1', origin: ORIGIN })
224226
if (!u.succeeded) throw new Error('setup')
225-
const unverified = await api.acceptInvitation({ token: u.value.invitation.token, userId: 'inv-unverified' })
226-
expect(unverified.succeeded).toBe(false)
227-
if (!unverified.succeeded) expect(unverified.status).toBe(403)
227+
const accepted = await api.acceptInvitation({ token: u.value.invitation.token, userId: 'inv-unverified' })
228+
expect(accepted.succeeded).toBe(true)
229+
if (accepted.succeeded) expect(accepted.value.workspaceId).toBe('ws-1')
230+
await flushMicrotasks()
231+
expect(adds).toEqual([{ workspaceId: 'ws-1', userId: 'inv-unverified', role: 'editor' }])
228232
})
229233

230234
it('accepts a matching, verified invite → membership created, add sync fired, idempotent', async () => {

0 commit comments

Comments
 (0)