Skip to content

Commit e164c59

Browse files
committed
feat: add and use vfn identifier fn so we know which email
1 parent e8765f3 commit e164c59

File tree

4 files changed

+108
-20
lines changed

4 files changed

+108
-20
lines changed

apps/web/src/server/modules/auth/__tests__/auth.service.spec.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { db } from '@acme/db'
88

99
import * as mailService from '../../mail/mail.service'
1010
import { emailLogin, emailVerifyOtp } from '../auth.service'
11-
import { createAuthToken } from '../auth.utils'
11+
import { createAuthToken, createVfnIdentifier } from '../auth.utils'
1212

1313
const mockedMailService = mock(mailService)
1414

@@ -30,9 +30,10 @@ describe('auth.service', () => {
3030
otpPrefix: expect.any(String),
3131
})
3232

33-
// Verify token was created in database with nonce as identifier
33+
// Verify token was created in database with vfnIdentifier
34+
const vfnIdentifier = createVfnIdentifier({ email, nonce })
3435
const token = await db.verificationToken.findUnique({
35-
where: { identifier: nonce },
36+
where: { identifier: vfnIdentifier },
3637
})
3738
expect(token).toBeDefined()
3839
expect(mockedMailService.sendMail).toHaveBeenCalledWith({
@@ -49,17 +50,18 @@ describe('auth.service', () => {
4950
// First login
5051
await emailLogin({ email, nonce })
5152

53+
const vfnIdentifier = createVfnIdentifier({ email, nonce })
5254
// Simulate failed attempts
5355
await db.verificationToken.update({
54-
where: { identifier: nonce },
56+
where: { identifier: vfnIdentifier },
5557
data: { attempts: 3 },
5658
})
5759

5860
// Second login should reset attempts
5961
await emailLogin({ email, nonce })
6062

6163
const token = await db.verificationToken.findUnique({
62-
where: { identifier: nonce },
64+
where: { identifier: vfnIdentifier },
6365
})
6466
expect(token?.attempts).toBe(0)
6567
})
@@ -71,9 +73,10 @@ describe('auth.service', () => {
7173
await emailLogin({ email, nonce })
7274
await emailLogin({ email, nonce })
7375

76+
const vfnIdentifier = createVfnIdentifier({ email, nonce })
7477
// Should only have one record
7578
const tokens = await db.verificationToken.findMany({
76-
where: { identifier: nonce },
79+
where: { identifier: vfnIdentifier },
7780
})
7881
expect(tokens).toHaveLength(1)
7982
})
@@ -87,11 +90,13 @@ describe('auth.service', () => {
8790
await emailLogin({ email, nonce: nonce2 })
8891

8992
// Should have two records with different nonces
93+
const vfnIdentifier1 = createVfnIdentifier({ email, nonce: nonce1 })
94+
const vfnIdentifier2 = createVfnIdentifier({ email, nonce: nonce2 })
9095
const token1 = await db.verificationToken.findUnique({
91-
where: { identifier: nonce1 },
96+
where: { identifier: vfnIdentifier1 },
9297
})
9398
const token2 = await db.verificationToken.findUnique({
94-
where: { identifier: nonce2 },
99+
where: { identifier: vfnIdentifier2 },
95100
})
96101

97102
expect(token1).toBeDefined()
@@ -114,8 +119,9 @@ describe('auth.service', () => {
114119
).resolves.not.toThrow()
115120

116121
// Token should be deleted after successful verification
122+
const vfnIdentifier = createVfnIdentifier({ email, nonce })
117123
const verificationToken = await db.verificationToken.findUnique({
118-
where: { identifier: nonce },
124+
where: { identifier: vfnIdentifier },
119125
})
120126
expect(verificationToken).toBeNull()
121127
})
@@ -137,11 +143,12 @@ describe('auth.service', () => {
137143

138144
const { token, hashedToken } = createAuthToken({ email, nonce })
139145

146+
const vfnIdentifier = createVfnIdentifier({ email, nonce })
140147
// Create a verification token with an old issuedAt date
141148
const oldDate = add(new Date(), { seconds: -700 }) // 700 seconds ago (beyond 600s expiry)
142149
await db.verificationToken.create({
143150
data: {
144-
identifier: nonce,
151+
identifier: vfnIdentifier,
145152
token: hashedToken,
146153
issuedAt: oldDate,
147154
},
@@ -159,17 +166,18 @@ describe('auth.service', () => {
159166

160167
await emailLogin({ email, nonce })
161168

169+
const vfnIdentifier = createVfnIdentifier({ email, nonce })
162170
// First attempt
163171
await expect(emailVerifyOtp({ email, token, nonce })).rejects.toThrow()
164172
let verificationToken = await db.verificationToken.findUnique({
165-
where: { identifier: nonce },
173+
where: { identifier: vfnIdentifier },
166174
})
167175
expect(verificationToken?.attempts).toBe(1)
168176

169177
// Second attempt
170178
await expect(emailVerifyOtp({ email, token, nonce })).rejects.toThrow()
171179
verificationToken = await db.verificationToken.findUnique({
172-
where: { identifier: nonce },
180+
where: { identifier: vfnIdentifier },
173181
})
174182
expect(verificationToken?.attempts).toBe(2)
175183
})
@@ -210,8 +218,9 @@ describe('auth.service', () => {
210218
await emailVerifyOtp({ email, token, nonce })
211219

212220
// Token should be deleted
221+
const vfnIdentifier = createVfnIdentifier({ email, nonce })
213222
const verificationToken = await db.verificationToken.findUnique({
214-
where: { identifier: nonce },
223+
where: { identifier: vfnIdentifier },
215224
})
216225
expect(verificationToken).toBeNull()
217226
})
@@ -245,8 +254,9 @@ describe('auth.service', () => {
245254
).rejects.toThrow('Invalid login email or missing nonce')
246255

247256
// Original token should still exist
257+
const vfnIdentifier1 = createVfnIdentifier({ email, nonce: nonce1 })
248258
const verificationToken = await db.verificationToken.findUnique({
249-
where: { identifier: nonce1 },
259+
where: { identifier: vfnIdentifier1 },
250260
})
251261
expect(verificationToken).toBeDefined()
252262
})

apps/web/src/server/modules/auth/__tests__/auth.utils.spec.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,65 @@
1-
import { createAuthToken, createVfnPrefix, isValidToken } from '../auth.utils'
1+
import {
2+
createAuthToken,
3+
createVfnIdentifier,
4+
createVfnPrefix,
5+
isValidToken,
6+
} from '../auth.utils'
27

38
describe('auth.utils', () => {
9+
describe('createVfnIdentifier', () => {
10+
it('should create identifier in format "nonce:email"', () => {
11+
const email = '[email protected]'
12+
const nonce = 'test-nonce-123'
13+
14+
const identifier = createVfnIdentifier({ email, nonce })
15+
16+
expect(identifier).toBe('test-nonce-123:[email protected]')
17+
})
18+
19+
it('should create different identifiers for different nonces with same email', () => {
20+
const email = '[email protected]'
21+
const nonce1 = 'nonce-1'
22+
const nonce2 = 'nonce-2'
23+
24+
const identifier1 = createVfnIdentifier({ email, nonce: nonce1 })
25+
const identifier2 = createVfnIdentifier({ email, nonce: nonce2 })
26+
27+
expect(identifier1).not.toBe(identifier2)
28+
expect(identifier1).toBe('nonce-1:[email protected]')
29+
expect(identifier2).toBe('nonce-2:[email protected]')
30+
})
31+
32+
it('should handle special characters in email', () => {
33+
const email = '[email protected]'
34+
const nonce = 'test-nonce'
35+
36+
const identifier = createVfnIdentifier({ email, nonce })
37+
38+
expect(identifier).toBe('test-nonce:[email protected]')
39+
})
40+
41+
it('should handle special characters in nonce', () => {
42+
const email = '[email protected]'
43+
const nonce = 'nonce-with-special_chars.123'
44+
45+
const identifier = createVfnIdentifier({ email, nonce })
46+
47+
expect(identifier).toBe('nonce-with-special_chars.123:[email protected]')
48+
})
49+
50+
it('should create deterministic identifiers', () => {
51+
const email = '[email protected]'
52+
const nonce = 'test-nonce'
53+
54+
const identifier1 = createVfnIdentifier({ email, nonce })
55+
const identifier2 = createVfnIdentifier({ email, nonce })
56+
const identifier3 = createVfnIdentifier({ email, nonce })
57+
58+
expect(identifier1).toBe(identifier2)
59+
expect(identifier2).toBe(identifier3)
60+
})
61+
})
62+
463
describe('createVfnPrefix', () => {
564
it('should generate a 3-character prefix', () => {
665
const prefix = createVfnPrefix()

apps/web/src/server/modules/auth/auth.service.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import { Prisma } from '@acme/db/client'
88
import { env } from '~/env'
99
import { getBaseUrl } from '~/utils/get-base-url'
1010
import { sendMail } from '../mail/mail.service'
11-
import { createAuthToken, createVfnPrefix, isValidToken } from './auth.utils'
11+
import {
12+
createAuthToken,
13+
createVfnIdentifier,
14+
createVfnPrefix,
15+
isValidToken,
16+
} from './auth.utils'
1217

1318
export const emailLogin = async ({
1419
email,
@@ -22,17 +27,19 @@ export const emailLogin = async ({
2227

2328
const url = new URL(getBaseUrl())
2429

30+
const vfnIdentifier = createVfnIdentifier({ email, nonce })
31+
2532
const { issuedAt } = await db.verificationToken.upsert({
2633
where: {
27-
identifier: nonce,
34+
identifier: vfnIdentifier,
2835
},
2936
update: {
3037
token: hashedToken,
3138
attempts: 0,
3239
issuedAt: new Date(),
3340
},
3441
create: {
35-
identifier: nonce,
42+
identifier: vfnIdentifier,
3643
token: hashedToken,
3744
issuedAt: new Date(),
3845
},
@@ -69,11 +76,13 @@ export const emailVerifyOtp = async ({
6976
token: string
7077
nonce: string
7178
}) => {
79+
const vfnIdentifier = createVfnIdentifier({ email, nonce })
80+
7281
try {
7382
// Not in transaction, because we do not want it to rollback
7483
const hashedToken = await db.verificationToken.update({
7584
where: {
76-
identifier: nonce,
85+
identifier: vfnIdentifier,
7786
},
7887
data: {
7988
attempts: {
@@ -106,7 +115,7 @@ export const emailVerifyOtp = async ({
106115
// Valid token, delete record to prevent reuse
107116
return db.verificationToken.delete({
108117
where: {
109-
identifier: nonce,
118+
identifier: vfnIdentifier,
110119
},
111120
})
112121
} catch (error) {

apps/web/src/server/modules/auth/auth.utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ export const createVfnPrefix = customAlphabet(
1414
OTP_PREFIX_LENGTH,
1515
)
1616

17+
export const createVfnIdentifier = ({
18+
email,
19+
nonce,
20+
}: {
21+
email: string
22+
nonce: string
23+
}) => {
24+
return `${nonce}:${email}`
25+
}
26+
1727
const createTokenHash = ({
1828
token,
1929
email,

0 commit comments

Comments
 (0)