@shipfox/api-auth 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +52 -0
  3. package/README.md +71 -37
  4. package/dist/config.d.ts +0 -12
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +0 -62
  7. package/dist/config.js.map +1 -1
  8. package/dist/core/auth.d.ts +21 -2
  9. package/dist/core/auth.d.ts.map +1 -1
  10. package/dist/core/auth.js +109 -77
  11. package/dist/core/auth.js.map +1 -1
  12. package/dist/core/email-owner.d.ts +12 -0
  13. package/dist/core/email-owner.d.ts.map +1 -0
  14. package/dist/core/email-owner.js +20 -0
  15. package/dist/core/email-owner.js.map +1 -0
  16. package/dist/core/job-lease-token.d.ts.map +1 -1
  17. package/dist/core/job-lease-token.js +3 -2
  18. package/dist/core/job-lease-token.js.map +1 -1
  19. package/dist/core/jwt.d.ts +2 -2
  20. package/dist/core/jwt.d.ts.map +1 -1
  21. package/dist/core/jwt.js.map +1 -1
  22. package/dist/core/rate-limit.d.ts.map +1 -1
  23. package/dist/core/rate-limit.js +2 -7
  24. package/dist/core/rate-limit.js.map +1 -1
  25. package/dist/core/runner-session-token.d.ts.map +1 -1
  26. package/dist/core/runner-session-token.js +3 -2
  27. package/dist/core/runner-session-token.js.map +1 -1
  28. package/dist/db/db.d.ts +0 -218
  29. package/dist/db/db.d.ts.map +1 -1
  30. package/dist/db/db.js +0 -2
  31. package/dist/db/db.js.map +1 -1
  32. package/dist/db/users.d.ts +3 -0
  33. package/dist/db/users.d.ts.map +1 -1
  34. package/dist/db/users.js +28 -10
  35. package/dist/db/users.js.map +1 -1
  36. package/dist/index.d.ts +5 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +40 -34
  39. package/dist/index.js.map +1 -1
  40. package/dist/presentation/auth/jwt-auth.d.ts.map +1 -1
  41. package/dist/presentation/auth/jwt-auth.js +2 -2
  42. package/dist/presentation/auth/jwt-auth.js.map +1 -1
  43. package/dist/presentation/e2eRoutes/create-session.d.ts +2 -1
  44. package/dist/presentation/e2eRoutes/create-session.d.ts.map +1 -1
  45. package/dist/presentation/e2eRoutes/create-session.js +42 -39
  46. package/dist/presentation/e2eRoutes/create-session.js.map +1 -1
  47. package/dist/presentation/e2eRoutes/index.d.ts +2 -1
  48. package/dist/presentation/e2eRoutes/index.d.ts.map +1 -1
  49. package/dist/presentation/e2eRoutes/index.js +12 -10
  50. package/dist/presentation/e2eRoutes/index.js.map +1 -1
  51. package/dist/presentation/inter-module.d.ts +4 -0
  52. package/dist/presentation/inter-module.d.ts.map +1 -0
  53. package/dist/presentation/inter-module.js +16 -0
  54. package/dist/presentation/inter-module.js.map +1 -0
  55. package/dist/presentation/routes/email-verification/verify-email-confirm.d.ts +2 -1
  56. package/dist/presentation/routes/email-verification/verify-email-confirm.d.ts.map +1 -1
  57. package/dist/presentation/routes/email-verification/verify-email-confirm.js +36 -25
  58. package/dist/presentation/routes/email-verification/verify-email-confirm.js.map +1 -1
  59. package/dist/presentation/routes/email-verification/verify-email-resend.d.ts.map +1 -1
  60. package/dist/presentation/routes/email-verification/verify-email-resend.js +20 -4
  61. package/dist/presentation/routes/email-verification/verify-email-resend.js.map +1 -1
  62. package/dist/presentation/routes/index.d.ts +2 -2
  63. package/dist/presentation/routes/index.d.ts.map +1 -1
  64. package/dist/presentation/routes/index.js +11 -13
  65. package/dist/presentation/routes/index.js.map +1 -1
  66. package/dist/presentation/routes/password/password-reset-confirm.d.ts +2 -1
  67. package/dist/presentation/routes/password/password-reset-confirm.d.ts.map +1 -1
  68. package/dist/presentation/routes/password/password-reset-confirm.js +29 -26
  69. package/dist/presentation/routes/password/password-reset-confirm.js.map +1 -1
  70. package/dist/presentation/routes/registration/signup.d.ts +2 -1
  71. package/dist/presentation/routes/registration/signup.d.ts.map +1 -1
  72. package/dist/presentation/routes/registration/signup.js +86 -68
  73. package/dist/presentation/routes/registration/signup.js.map +1 -1
  74. package/dist/presentation/routes/session/login.d.ts +2 -1
  75. package/dist/presentation/routes/session/login.d.ts.map +1 -1
  76. package/dist/presentation/routes/session/login.js +40 -37
  77. package/dist/presentation/routes/session/login.js.map +1 -1
  78. package/dist/presentation/routes/session/refresh.d.ts +2 -1
  79. package/dist/presentation/routes/session/refresh.d.ts.map +1 -1
  80. package/dist/presentation/routes/session/refresh.js +42 -39
  81. package/dist/presentation/routes/session/refresh.js.map +1 -1
  82. package/dist/presentation/subscribers/index.d.ts +0 -1
  83. package/dist/presentation/subscribers/index.d.ts.map +1 -1
  84. package/dist/presentation/subscribers/index.js +0 -1
  85. package/dist/presentation/subscribers/index.js.map +1 -1
  86. package/dist/presentation/subscribers/on-password-reset-send-requested.js +1 -1
  87. package/dist/presentation/subscribers/on-password-reset-send-requested.js.map +1 -1
  88. package/dist/tsconfig.test.tsbuildinfo +1 -1
  89. package/drizzle/0000_initial.sql +1 -13
  90. package/drizzle/meta/0000_snapshot.json +0 -92
  91. package/package.json +17 -13
  92. package/src/config.ts +0 -56
  93. package/src/core/auth.test.ts +106 -187
  94. package/src/core/auth.ts +131 -87
  95. package/src/core/email-owner.test.ts +86 -0
  96. package/src/core/email-owner.ts +23 -0
  97. package/src/core/job-lease-token.test.ts +2 -1
  98. package/src/core/job-lease-token.ts +3 -2
  99. package/src/core/jwt.ts +2 -2
  100. package/src/core/rate-limit.test.ts +11 -28
  101. package/src/core/rate-limit.ts +3 -11
  102. package/src/core/runner-session-token.test.ts +2 -1
  103. package/src/core/runner-session-token.ts +3 -2
  104. package/src/db/db.ts +0 -2
  105. package/src/db/users.ts +32 -14
  106. package/src/index.test.ts +15 -11
  107. package/src/index.ts +23 -20
  108. package/src/presentation/auth/jwt-auth.test.ts +2 -1
  109. package/src/presentation/auth/jwt-auth.ts +2 -2
  110. package/src/presentation/e2eRoutes/create-session.ts +34 -30
  111. package/src/presentation/e2eRoutes/index.test.ts +9 -6
  112. package/src/presentation/e2eRoutes/index.ts +8 -5
  113. package/src/presentation/inter-module.ts +13 -0
  114. package/src/presentation/routes/email-verification/verify-email-confirm.ts +42 -24
  115. package/src/presentation/routes/email-verification/verify-email-resend.ts +18 -4
  116. package/src/presentation/routes/index.test.ts +10 -3
  117. package/src/presentation/routes/index.ts +22 -12
  118. package/src/presentation/routes/password/password-reset-confirm.ts +25 -22
  119. package/src/presentation/routes/password/password-reset-request.test.ts +1 -0
  120. package/src/presentation/routes/rate-limit.test.ts +2 -2
  121. package/src/presentation/routes/registration/signup.test.ts +39 -74
  122. package/src/presentation/routes/registration/signup.ts +89 -70
  123. package/src/presentation/routes/session/login.ts +38 -35
  124. package/src/presentation/routes/session/refresh.ts +43 -40
  125. package/src/presentation/subscribers/email-send-requested.test.ts +2 -30
  126. package/src/presentation/subscribers/index.ts +0 -1
  127. package/src/presentation/subscribers/on-password-reset-send-requested.ts +1 -1
  128. package/test/env.ts +1 -3
  129. package/test/globalSetup.ts +10 -1
  130. package/test/routes.ts +51 -84
  131. package/tsconfig.build.tsbuildinfo +1 -1
  132. package/dist/core/entities/email-verification.d.ts +0 -9
  133. package/dist/core/entities/email-verification.d.ts.map +0 -1
  134. package/dist/core/entities/email-verification.js +0 -3
  135. package/dist/core/entities/email-verification.js.map +0 -1
  136. package/dist/db/email-verifications.d.ts +0 -39
  137. package/dist/db/email-verifications.d.ts.map +0 -1
  138. package/dist/db/email-verifications.js +0 -95
  139. package/dist/db/email-verifications.js.map +0 -1
  140. package/dist/db/schema/email-verifications.d.ts +0 -114
  141. package/dist/db/schema/email-verifications.d.ts.map +0 -1
  142. package/dist/db/schema/email-verifications.js +0 -35
  143. package/dist/db/schema/email-verifications.js.map +0 -1
  144. package/dist/presentation/subscribers/on-email-verification-send-requested.d.ts +0 -3
  145. package/dist/presentation/subscribers/on-email-verification-send-requested.d.ts.map +0 -1
  146. package/dist/presentation/subscribers/on-email-verification-send-requested.js +0 -13
  147. package/dist/presentation/subscribers/on-email-verification-send-requested.js.map +0 -1
  148. package/src/core/entities/email-verification.ts +0 -8
  149. package/src/db/email-verifications.test.ts +0 -61
  150. package/src/db/email-verifications.ts +0 -160
  151. package/src/db/schema/email-verifications.ts +0 -37
  152. package/src/presentation/routes/email-verification/verify-email-confirm.test.ts +0 -66
  153. package/src/presentation/routes/email-verification/verify-email-resend.test.ts +0 -100
  154. package/src/presentation/subscribers/on-email-verification-send-requested.ts +0 -10
@@ -1,9 +0,0 @@
1
- export interface EmailVerification {
2
- id: string;
3
- userId: string;
4
- hashedToken: string;
5
- expiresAt: Date;
6
- usedAt: Date | null;
7
- createdAt: Date;
8
- }
9
- //# sourceMappingURL=email-verification.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"email-verification.d.ts","sourceRoot":"","sources":["../../../src/core/entities/email-verification.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC;IAChB,MAAM,EAAE,IAAI,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC;CACjB"}
@@ -1,3 +0,0 @@
1
- export { };
2
-
3
- //# sourceMappingURL=email-verification.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../src/core/entities/email-verification.ts"],"sourcesContent":["export interface EmailVerification {\n id: string;\n userId: string;\n hashedToken: string;\n expiresAt: Date;\n usedAt: Date | null;\n createdAt: Date;\n}\n"],"names":[],"mappings":"AAAA,WAOC"}
@@ -1,39 +0,0 @@
1
- import type { EmailVerification } from '#core/entities/email-verification.js';
2
- import type { User } from '#core/entities/user.js';
3
- interface CreateEmailVerificationBaseParams {
4
- userId: string;
5
- hashedToken: string;
6
- expiresAt: Date;
7
- }
8
- export type CreateEmailVerificationParams = CreateEmailVerificationBaseParams & ({
9
- sendEmail: {
10
- email: string;
11
- verifyLink: string;
12
- };
13
- skipEmail?: never;
14
- } | {
15
- sendEmail?: never;
16
- skipEmail: true;
17
- });
18
- export declare function createEmailVerification(params: CreateEmailVerificationParams): Promise<EmailVerification>;
19
- export interface CreateResendEmailVerificationParams {
20
- email: string;
21
- hashedToken: string;
22
- expiresAt: Date;
23
- cooldownSeconds: number;
24
- sendEmail: {
25
- verifyLink: string;
26
- };
27
- now?: Date | undefined;
28
- }
29
- export interface CreateResendEmailVerificationResult {
30
- user?: User | undefined;
31
- verification?: EmailVerification | undefined;
32
- nextResendAvailableAt: Date;
33
- }
34
- export declare function createResendEmailVerification(params: CreateResendEmailVerificationParams): Promise<CreateResendEmailVerificationResult>;
35
- export declare function consumeEmailVerification(params: {
36
- hashedToken: string;
37
- }): Promise<EmailVerification | undefined>;
38
- export {};
39
- //# sourceMappingURL=email-verifications.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"email-verifications.d.ts","sourceRoot":"","sources":["../../src/db/email-verifications.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,sCAAsC,CAAC;AAC5E,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAMjD,UAAU,iCAAiC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,MAAM,6BAA6B,GAAG,iCAAiC,GAC3E,CACI;IAAC,SAAS,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAC,CAAC;IAAC,SAAS,CAAC,EAAE,KAAK,CAAA;CAAC,GACnE;IAAC,SAAS,CAAC,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAC,CACvC,CAAC;AAEJ,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,6BAA6B,GACpC,OAAO,CAAC,iBAAiB,CAAC,CA0B5B;AAED,MAAM,WAAW,mCAAmC;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE;QAAC,UAAU,EAAE,MAAM,CAAA;KAAC,CAAC;IAChC,GAAG,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;CACxB;AAED,MAAM,WAAW,mCAAmC;IAClD,IAAI,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;IACxB,YAAY,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAC7C,qBAAqB,EAAE,IAAI,CAAC;CAC7B;AAMD,wBAAsB,6BAA6B,CACjD,MAAM,EAAE,mCAAmC,GAC1C,OAAO,CAAC,mCAAmC,CAAC,CAkE9C;AAED,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAgBzC"}
@@ -1,95 +0,0 @@
1
- import { AUTH_EMAIL_VERIFICATION_SEND_REQUESTED } from '@shipfox/api-auth-dto';
2
- import { writeOutboxEvent } from '@shipfox/node-outbox';
3
- import { and, desc, eq, gt, isNull, sql } from 'drizzle-orm';
4
- import { db } from './db.js';
5
- import { emailVerifications, toEmailVerification } from './schema/email-verifications.js';
6
- import { authOutbox } from './schema/outbox.js';
7
- import { toUser, users } from './schema/users.js';
8
- export async function createEmailVerification(params) {
9
- return await db().transaction(async (tx)=>{
10
- await tx.update(emailVerifications).set({
11
- usedAt: sql`now()`
12
- }).where(and(eq(emailVerifications.userId, params.userId), isNull(emailVerifications.usedAt)));
13
- const rows = await tx.insert(emailVerifications).values({
14
- userId: params.userId,
15
- hashedToken: params.hashedToken,
16
- expiresAt: params.expiresAt
17
- }).returning();
18
- const row = rows[0];
19
- if (!row) throw new Error('Insert returned no rows');
20
- if (params.sendEmail) {
21
- await writeOutboxEvent(tx, authOutbox, {
22
- type: AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
23
- payload: params.sendEmail
24
- });
25
- }
26
- return toEmailVerification(row);
27
- });
28
- }
29
- function secondsAfter(date, seconds) {
30
- return new Date(date.getTime() + seconds * 1000);
31
- }
32
- export async function createResendEmailVerification(params) {
33
- const now = params.now ?? new Date();
34
- const genericNextResendAvailableAt = secondsAfter(now, params.cooldownSeconds);
35
- return await db().transaction(async (tx)=>{
36
- const userRows = await tx.select().from(users).where(eq(users.email, params.email)).limit(1).for('update');
37
- const userRow = userRows[0];
38
- if (!userRow) {
39
- return {
40
- nextResendAvailableAt: genericNextResendAvailableAt
41
- };
42
- }
43
- const user = toUser(userRow);
44
- if (user.status !== 'active' || user.emailVerifiedAt !== null) {
45
- return {
46
- nextResendAvailableAt: genericNextResendAvailableAt
47
- };
48
- }
49
- const latestRows = await tx.select().from(emailVerifications).where(and(eq(emailVerifications.userId, user.id), isNull(emailVerifications.usedAt))).orderBy(desc(emailVerifications.createdAt)).limit(1);
50
- const latest = latestRows[0] ? toEmailVerification(latestRows[0]) : undefined;
51
- if (latest && secondsAfter(latest.createdAt, params.cooldownSeconds) > now) {
52
- // Return the same public retry estimate as no-op cases so the endpoint does
53
- // not expose existing account state or prior verification timing.
54
- return {
55
- user,
56
- nextResendAvailableAt: genericNextResendAvailableAt
57
- };
58
- }
59
- await tx.update(emailVerifications).set({
60
- usedAt: sql`now()`
61
- }).where(and(eq(emailVerifications.userId, user.id), isNull(emailVerifications.usedAt)));
62
- const rows = await tx.insert(emailVerifications).values({
63
- userId: user.id,
64
- hashedToken: params.hashedToken,
65
- expiresAt: params.expiresAt
66
- }).returning();
67
- const row = rows[0];
68
- if (!row) throw new Error('Insert returned no rows');
69
- const verification = toEmailVerification(row);
70
- if (params.sendEmail) {
71
- await writeOutboxEvent(tx, authOutbox, {
72
- type: AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
73
- payload: {
74
- email: user.email,
75
- verifyLink: params.sendEmail.verifyLink
76
- }
77
- });
78
- }
79
- return {
80
- user,
81
- verification,
82
- nextResendAvailableAt: secondsAfter(verification.createdAt, params.cooldownSeconds)
83
- };
84
- });
85
- }
86
- export async function consumeEmailVerification(params) {
87
- const rows = await db().update(emailVerifications).set({
88
- usedAt: sql`now()`
89
- }).where(and(eq(emailVerifications.hashedToken, params.hashedToken), isNull(emailVerifications.usedAt), gt(emailVerifications.expiresAt, sql`now()`))).returning();
90
- const row = rows[0];
91
- if (!row) return undefined;
92
- return toEmailVerification(row);
93
- }
94
-
95
- //# sourceMappingURL=email-verifications.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/db/email-verifications.ts"],"sourcesContent":["import {AUTH_EMAIL_VERIFICATION_SEND_REQUESTED, type AuthEventMap} from '@shipfox/api-auth-dto';\nimport {writeOutboxEvent} from '@shipfox/node-outbox';\nimport {and, desc, eq, gt, isNull, sql} from 'drizzle-orm';\nimport type {EmailVerification} from '#core/entities/email-verification.js';\nimport type {User} from '#core/entities/user.js';\nimport {db} from './db.js';\nimport {emailVerifications, toEmailVerification} from './schema/email-verifications.js';\nimport {authOutbox} from './schema/outbox.js';\nimport {toUser, users} from './schema/users.js';\n\ninterface CreateEmailVerificationBaseParams {\n userId: string;\n hashedToken: string;\n expiresAt: Date;\n}\n\nexport type CreateEmailVerificationParams = CreateEmailVerificationBaseParams &\n (\n | {sendEmail: {email: string; verifyLink: string}; skipEmail?: never}\n | {sendEmail?: never; skipEmail: true}\n );\n\nexport async function createEmailVerification(\n params: CreateEmailVerificationParams,\n): Promise<EmailVerification> {\n return await db().transaction(async (tx) => {\n await tx\n .update(emailVerifications)\n .set({usedAt: sql`now()`})\n .where(and(eq(emailVerifications.userId, params.userId), isNull(emailVerifications.usedAt)));\n\n const rows = await tx\n .insert(emailVerifications)\n .values({\n userId: params.userId,\n hashedToken: params.hashedToken,\n expiresAt: params.expiresAt,\n })\n .returning();\n\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n if (params.sendEmail) {\n await writeOutboxEvent<AuthEventMap>(tx, authOutbox, {\n type: AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,\n payload: params.sendEmail,\n });\n }\n return toEmailVerification(row);\n });\n}\n\nexport interface CreateResendEmailVerificationParams {\n email: string;\n hashedToken: string;\n expiresAt: Date;\n cooldownSeconds: number;\n sendEmail: {verifyLink: string};\n now?: Date | undefined;\n}\n\nexport interface CreateResendEmailVerificationResult {\n user?: User | undefined;\n verification?: EmailVerification | undefined;\n nextResendAvailableAt: Date;\n}\n\nfunction secondsAfter(date: Date, seconds: number): Date {\n return new Date(date.getTime() + seconds * 1000);\n}\n\nexport async function createResendEmailVerification(\n params: CreateResendEmailVerificationParams,\n): Promise<CreateResendEmailVerificationResult> {\n const now = params.now ?? new Date();\n const genericNextResendAvailableAt = secondsAfter(now, params.cooldownSeconds);\n\n return await db().transaction(async (tx) => {\n const userRows = await tx\n .select()\n .from(users)\n .where(eq(users.email, params.email))\n .limit(1)\n .for('update');\n const userRow = userRows[0];\n if (!userRow) {\n return {nextResendAvailableAt: genericNextResendAvailableAt};\n }\n\n const user = toUser(userRow);\n if (user.status !== 'active' || user.emailVerifiedAt !== null) {\n return {nextResendAvailableAt: genericNextResendAvailableAt};\n }\n\n const latestRows = await tx\n .select()\n .from(emailVerifications)\n .where(and(eq(emailVerifications.userId, user.id), isNull(emailVerifications.usedAt)))\n .orderBy(desc(emailVerifications.createdAt))\n .limit(1);\n const latest = latestRows[0] ? toEmailVerification(latestRows[0]) : undefined;\n if (latest && secondsAfter(latest.createdAt, params.cooldownSeconds) > now) {\n // Return the same public retry estimate as no-op cases so the endpoint does\n // not expose existing account state or prior verification timing.\n return {user, nextResendAvailableAt: genericNextResendAvailableAt};\n }\n\n await tx\n .update(emailVerifications)\n .set({usedAt: sql`now()`})\n .where(and(eq(emailVerifications.userId, user.id), isNull(emailVerifications.usedAt)));\n\n const rows = await tx\n .insert(emailVerifications)\n .values({\n userId: user.id,\n hashedToken: params.hashedToken,\n expiresAt: params.expiresAt,\n })\n .returning();\n\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n const verification = toEmailVerification(row);\n if (params.sendEmail) {\n await writeOutboxEvent<AuthEventMap>(tx, authOutbox, {\n type: AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,\n payload: {\n email: user.email,\n verifyLink: params.sendEmail.verifyLink,\n },\n });\n }\n return {\n user,\n verification,\n nextResendAvailableAt: secondsAfter(verification.createdAt, params.cooldownSeconds),\n };\n });\n}\n\nexport async function consumeEmailVerification(params: {\n hashedToken: string;\n}): Promise<EmailVerification | undefined> {\n const rows = await db()\n .update(emailVerifications)\n .set({usedAt: sql`now()`})\n .where(\n and(\n eq(emailVerifications.hashedToken, params.hashedToken),\n isNull(emailVerifications.usedAt),\n gt(emailVerifications.expiresAt, sql`now()`),\n ),\n )\n .returning();\n\n const row = rows[0];\n if (!row) return undefined;\n return toEmailVerification(row);\n}\n"],"names":["AUTH_EMAIL_VERIFICATION_SEND_REQUESTED","writeOutboxEvent","and","desc","eq","gt","isNull","sql","db","emailVerifications","toEmailVerification","authOutbox","toUser","users","createEmailVerification","params","transaction","tx","update","set","usedAt","where","userId","rows","insert","values","hashedToken","expiresAt","returning","row","Error","sendEmail","type","payload","secondsAfter","date","seconds","Date","getTime","createResendEmailVerification","now","genericNextResendAvailableAt","cooldownSeconds","userRows","select","from","email","limit","for","userRow","nextResendAvailableAt","user","status","emailVerifiedAt","latestRows","id","orderBy","createdAt","latest","undefined","verification","verifyLink","consumeEmailVerification"],"mappings":"AAAA,SAAQA,sCAAsC,QAA0B,wBAAwB;AAChG,SAAQC,gBAAgB,QAAO,uBAAuB;AACtD,SAAQC,GAAG,EAAEC,IAAI,EAAEC,EAAE,EAAEC,EAAE,EAAEC,MAAM,EAAEC,GAAG,QAAO,cAAc;AAG3D,SAAQC,EAAE,QAAO,UAAU;AAC3B,SAAQC,kBAAkB,EAAEC,mBAAmB,QAAO,kCAAkC;AACxF,SAAQC,UAAU,QAAO,qBAAqB;AAC9C,SAAQC,MAAM,EAAEC,KAAK,QAAO,oBAAoB;AAchD,OAAO,eAAeC,wBACpBC,MAAqC;IAErC,OAAO,MAAMP,KAAKQ,WAAW,CAAC,OAAOC;QACnC,MAAMA,GACHC,MAAM,CAACT,oBACPU,GAAG,CAAC;YAACC,QAAQb,GAAG,CAAC,KAAK,CAAC;QAAA,GACvBc,KAAK,CAACnB,IAAIE,GAAGK,mBAAmBa,MAAM,EAAEP,OAAOO,MAAM,GAAGhB,OAAOG,mBAAmBW,MAAM;QAE3F,MAAMG,OAAO,MAAMN,GAChBO,MAAM,CAACf,oBACPgB,MAAM,CAAC;YACNH,QAAQP,OAAOO,MAAM;YACrBI,aAAaX,OAAOW,WAAW;YAC/BC,WAAWZ,OAAOY,SAAS;QAC7B,GACCC,SAAS;QAEZ,MAAMC,MAAMN,IAAI,CAAC,EAAE;QACnB,IAAI,CAACM,KAAK,MAAM,IAAIC,MAAM;QAC1B,IAAIf,OAAOgB,SAAS,EAAE;YACpB,MAAM9B,iBAA+BgB,IAAIN,YAAY;gBACnDqB,MAAMhC;gBACNiC,SAASlB,OAAOgB,SAAS;YAC3B;QACF;QACA,OAAOrB,oBAAoBmB;IAC7B;AACF;AAiBA,SAASK,aAAaC,IAAU,EAAEC,OAAe;IAC/C,OAAO,IAAIC,KAAKF,KAAKG,OAAO,KAAKF,UAAU;AAC7C;AAEA,OAAO,eAAeG,8BACpBxB,MAA2C;IAE3C,MAAMyB,MAAMzB,OAAOyB,GAAG,IAAI,IAAIH;IAC9B,MAAMI,+BAA+BP,aAAaM,KAAKzB,OAAO2B,eAAe;IAE7E,OAAO,MAAMlC,KAAKQ,WAAW,CAAC,OAAOC;QACnC,MAAM0B,WAAW,MAAM1B,GACpB2B,MAAM,GACNC,IAAI,CAAChC,OACLQ,KAAK,CAACjB,GAAGS,MAAMiC,KAAK,EAAE/B,OAAO+B,KAAK,GAClCC,KAAK,CAAC,GACNC,GAAG,CAAC;QACP,MAAMC,UAAUN,QAAQ,CAAC,EAAE;QAC3B,IAAI,CAACM,SAAS;YACZ,OAAO;gBAACC,uBAAuBT;YAA4B;QAC7D;QAEA,MAAMU,OAAOvC,OAAOqC;QACpB,IAAIE,KAAKC,MAAM,KAAK,YAAYD,KAAKE,eAAe,KAAK,MAAM;YAC7D,OAAO;gBAACH,uBAAuBT;YAA4B;QAC7D;QAEA,MAAMa,aAAa,MAAMrC,GACtB2B,MAAM,GACNC,IAAI,CAACpC,oBACLY,KAAK,CAACnB,IAAIE,GAAGK,mBAAmBa,MAAM,EAAE6B,KAAKI,EAAE,GAAGjD,OAAOG,mBAAmBW,MAAM,IAClFoC,OAAO,CAACrD,KAAKM,mBAAmBgD,SAAS,GACzCV,KAAK,CAAC;QACT,MAAMW,SAASJ,UAAU,CAAC,EAAE,GAAG5C,oBAAoB4C,UAAU,CAAC,EAAE,IAAIK;QACpE,IAAID,UAAUxB,aAAawB,OAAOD,SAAS,EAAE1C,OAAO2B,eAAe,IAAIF,KAAK;YAC1E,4EAA4E;YAC5E,kEAAkE;YAClE,OAAO;gBAACW;gBAAMD,uBAAuBT;YAA4B;QACnE;QAEA,MAAMxB,GACHC,MAAM,CAACT,oBACPU,GAAG,CAAC;YAACC,QAAQb,GAAG,CAAC,KAAK,CAAC;QAAA,GACvBc,KAAK,CAACnB,IAAIE,GAAGK,mBAAmBa,MAAM,EAAE6B,KAAKI,EAAE,GAAGjD,OAAOG,mBAAmBW,MAAM;QAErF,MAAMG,OAAO,MAAMN,GAChBO,MAAM,CAACf,oBACPgB,MAAM,CAAC;YACNH,QAAQ6B,KAAKI,EAAE;YACf7B,aAAaX,OAAOW,WAAW;YAC/BC,WAAWZ,OAAOY,SAAS;QAC7B,GACCC,SAAS;QAEZ,MAAMC,MAAMN,IAAI,CAAC,EAAE;QACnB,IAAI,CAACM,KAAK,MAAM,IAAIC,MAAM;QAC1B,MAAM8B,eAAelD,oBAAoBmB;QACzC,IAAId,OAAOgB,SAAS,EAAE;YACpB,MAAM9B,iBAA+BgB,IAAIN,YAAY;gBACnDqB,MAAMhC;gBACNiC,SAAS;oBACPa,OAAOK,KAAKL,KAAK;oBACjBe,YAAY9C,OAAOgB,SAAS,CAAC8B,UAAU;gBACzC;YACF;QACF;QACA,OAAO;YACLV;YACAS;YACAV,uBAAuBhB,aAAa0B,aAAaH,SAAS,EAAE1C,OAAO2B,eAAe;QACpF;IACF;AACF;AAEA,OAAO,eAAeoB,yBAAyB/C,MAE9C;IACC,MAAMQ,OAAO,MAAMf,KAChBU,MAAM,CAACT,oBACPU,GAAG,CAAC;QAACC,QAAQb,GAAG,CAAC,KAAK,CAAC;IAAA,GACvBc,KAAK,CACJnB,IACEE,GAAGK,mBAAmBiB,WAAW,EAAEX,OAAOW,WAAW,GACrDpB,OAAOG,mBAAmBW,MAAM,GAChCf,GAAGI,mBAAmBkB,SAAS,EAAEpB,GAAG,CAAC,KAAK,CAAC,IAG9CqB,SAAS;IAEZ,MAAMC,MAAMN,IAAI,CAAC,EAAE;IACnB,IAAI,CAACM,KAAK,OAAO8B;IACjB,OAAOjD,oBAAoBmB;AAC7B"}
@@ -1,114 +0,0 @@
1
- import type { EmailVerification } from '#core/entities/email-verification.js';
2
- export declare const emailVerifications: import("drizzle-orm/pg-core").PgTableWithColumns<{
3
- name: "email_verifications";
4
- schema: undefined;
5
- columns: {
6
- id: import("drizzle-orm/pg-core").PgColumn<{
7
- name: "id";
8
- tableName: "email_verifications";
9
- dataType: "string";
10
- columnType: "PgUUID";
11
- data: string;
12
- driverParam: string;
13
- notNull: true;
14
- hasDefault: true;
15
- isPrimaryKey: true;
16
- isAutoincrement: false;
17
- hasRuntimeDefault: false;
18
- enumValues: undefined;
19
- baseColumn: never;
20
- identity: undefined;
21
- generated: undefined;
22
- }, {}, {}>;
23
- userId: import("drizzle-orm/pg-core").PgColumn<{
24
- name: "user_id";
25
- tableName: "email_verifications";
26
- dataType: "string";
27
- columnType: "PgUUID";
28
- data: string;
29
- driverParam: string;
30
- notNull: true;
31
- hasDefault: false;
32
- isPrimaryKey: false;
33
- isAutoincrement: false;
34
- hasRuntimeDefault: false;
35
- enumValues: undefined;
36
- baseColumn: never;
37
- identity: undefined;
38
- generated: undefined;
39
- }, {}, {}>;
40
- hashedToken: import("drizzle-orm/pg-core").PgColumn<{
41
- name: "hashed_token";
42
- tableName: "email_verifications";
43
- dataType: "string";
44
- columnType: "PgText";
45
- data: string;
46
- driverParam: string;
47
- notNull: true;
48
- hasDefault: false;
49
- isPrimaryKey: false;
50
- isAutoincrement: false;
51
- hasRuntimeDefault: false;
52
- enumValues: [string, ...string[]];
53
- baseColumn: never;
54
- identity: undefined;
55
- generated: undefined;
56
- }, {}, {}>;
57
- expiresAt: import("drizzle-orm/pg-core").PgColumn<{
58
- name: "expires_at";
59
- tableName: "email_verifications";
60
- dataType: "date";
61
- columnType: "PgTimestamp";
62
- data: Date;
63
- driverParam: string;
64
- notNull: true;
65
- hasDefault: false;
66
- isPrimaryKey: false;
67
- isAutoincrement: false;
68
- hasRuntimeDefault: false;
69
- enumValues: undefined;
70
- baseColumn: never;
71
- identity: undefined;
72
- generated: undefined;
73
- }, {}, {}>;
74
- usedAt: import("drizzle-orm/pg-core").PgColumn<{
75
- name: "used_at";
76
- tableName: "email_verifications";
77
- dataType: "date";
78
- columnType: "PgTimestamp";
79
- data: Date;
80
- driverParam: string;
81
- notNull: false;
82
- hasDefault: false;
83
- isPrimaryKey: false;
84
- isAutoincrement: false;
85
- hasRuntimeDefault: false;
86
- enumValues: undefined;
87
- baseColumn: never;
88
- identity: undefined;
89
- generated: undefined;
90
- }, {}, {}>;
91
- createdAt: import("drizzle-orm/pg-core").PgColumn<{
92
- name: "created_at";
93
- tableName: "email_verifications";
94
- dataType: "date";
95
- columnType: "PgTimestamp";
96
- data: Date;
97
- driverParam: string;
98
- notNull: true;
99
- hasDefault: true;
100
- isPrimaryKey: false;
101
- isAutoincrement: false;
102
- hasRuntimeDefault: false;
103
- enumValues: undefined;
104
- baseColumn: never;
105
- identity: undefined;
106
- generated: undefined;
107
- }, {}, {}>;
108
- };
109
- dialect: "pg";
110
- }>;
111
- export type EmailVerificationDb = typeof emailVerifications.$inferSelect;
112
- export type EmailVerificationCreateDb = typeof emailVerifications.$inferInsert;
113
- export declare function toEmailVerification(row: EmailVerificationDb): EmailVerification;
114
- //# sourceMappingURL=email-verifications.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"email-verifications.d.ts","sourceRoot":"","sources":["../../../src/db/schema/email-verifications.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,sCAAsC,CAAC;AAI5E,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgB9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,OAAO,kBAAkB,CAAC,YAAY,CAAC;AACzE,MAAM,MAAM,yBAAyB,GAAG,OAAO,kBAAkB,CAAC,YAAY,CAAC;AAE/E,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,mBAAmB,GAAG,iBAAiB,CAS/E"}
@@ -1,35 +0,0 @@
1
- import { uuidv7PrimaryKey } from '@shipfox/node-drizzle';
2
- import { index, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core';
3
- import { pgTable } from './common.js';
4
- import { users } from './users.js';
5
- export const emailVerifications = pgTable('email_verifications', {
6
- id: uuidv7PrimaryKey(),
7
- userId: uuid('user_id').notNull().references(()=>users.id, {
8
- onDelete: 'cascade'
9
- }),
10
- hashedToken: text('hashed_token').notNull(),
11
- expiresAt: timestamp('expires_at', {
12
- withTimezone: true
13
- }).notNull(),
14
- usedAt: timestamp('used_at', {
15
- withTimezone: true
16
- }),
17
- createdAt: timestamp('created_at', {
18
- withTimezone: true
19
- }).notNull().defaultNow()
20
- }, (table)=>[
21
- uniqueIndex('auth_email_verifications_hashed_token_unique').on(table.hashedToken),
22
- index('auth_email_verifications_user_id_idx').on(table.userId)
23
- ]);
24
- export function toEmailVerification(row) {
25
- return {
26
- id: row.id,
27
- userId: row.userId,
28
- hashedToken: row.hashedToken,
29
- expiresAt: row.expiresAt,
30
- usedAt: row.usedAt,
31
- createdAt: row.createdAt
32
- };
33
- }
34
-
35
- //# sourceMappingURL=email-verifications.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../src/db/schema/email-verifications.ts"],"sourcesContent":["import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';\nimport {index, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';\nimport type {EmailVerification} from '#core/entities/email-verification.js';\nimport {pgTable} from './common.js';\nimport {users} from './users.js';\n\nexport const emailVerifications = pgTable(\n 'email_verifications',\n {\n id: uuidv7PrimaryKey(),\n userId: uuid('user_id')\n .notNull()\n .references(() => users.id, {onDelete: 'cascade'}),\n hashedToken: text('hashed_token').notNull(),\n expiresAt: timestamp('expires_at', {withTimezone: true}).notNull(),\n usedAt: timestamp('used_at', {withTimezone: true}),\n createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),\n },\n (table) => [\n uniqueIndex('auth_email_verifications_hashed_token_unique').on(table.hashedToken),\n index('auth_email_verifications_user_id_idx').on(table.userId),\n ],\n);\n\nexport type EmailVerificationDb = typeof emailVerifications.$inferSelect;\nexport type EmailVerificationCreateDb = typeof emailVerifications.$inferInsert;\n\nexport function toEmailVerification(row: EmailVerificationDb): EmailVerification {\n return {\n id: row.id,\n userId: row.userId,\n hashedToken: row.hashedToken,\n expiresAt: row.expiresAt,\n usedAt: row.usedAt,\n createdAt: row.createdAt,\n };\n}\n"],"names":["uuidv7PrimaryKey","index","text","timestamp","uniqueIndex","uuid","pgTable","users","emailVerifications","id","userId","notNull","references","onDelete","hashedToken","expiresAt","withTimezone","usedAt","createdAt","defaultNow","table","on","toEmailVerification","row"],"mappings":"AAAA,SAAQA,gBAAgB,QAAO,wBAAwB;AACvD,SAAQC,KAAK,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,IAAI,QAAO,sBAAsB;AAE9E,SAAQC,OAAO,QAAO,cAAc;AACpC,SAAQC,KAAK,QAAO,aAAa;AAEjC,OAAO,MAAMC,qBAAqBF,QAChC,uBACA;IACEG,IAAIT;IACJU,QAAQL,KAAK,WACVM,OAAO,GACPC,UAAU,CAAC,IAAML,MAAME,EAAE,EAAE;QAACI,UAAU;IAAS;IAClDC,aAAaZ,KAAK,gBAAgBS,OAAO;IACzCI,WAAWZ,UAAU,cAAc;QAACa,cAAc;IAAI,GAAGL,OAAO;IAChEM,QAAQd,UAAU,WAAW;QAACa,cAAc;IAAI;IAChDE,WAAWf,UAAU,cAAc;QAACa,cAAc;IAAI,GAAGL,OAAO,GAAGQ,UAAU;AAC/E,GACA,CAACC,QAAU;QACThB,YAAY,gDAAgDiB,EAAE,CAACD,MAAMN,WAAW;QAChFb,MAAM,wCAAwCoB,EAAE,CAACD,MAAMV,MAAM;KAC9D,EACD;AAKF,OAAO,SAASY,oBAAoBC,GAAwB;IAC1D,OAAO;QACLd,IAAIc,IAAId,EAAE;QACVC,QAAQa,IAAIb,MAAM;QAClBI,aAAaS,IAAIT,WAAW;QAC5BC,WAAWQ,IAAIR,SAAS;QACxBE,QAAQM,IAAIN,MAAM;QAClBC,WAAWK,IAAIL,SAAS;IAC1B;AACF"}
@@ -1,3 +0,0 @@
1
- import type { AuthEmailVerificationSendRequestedEvent } from '@shipfox/api-auth-dto';
2
- export declare function onEmailVerificationSendRequested(payload: AuthEmailVerificationSendRequestedEvent): Promise<void>;
3
- //# sourceMappingURL=on-email-verification-send-requested.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"on-email-verification-send-requested.d.ts","sourceRoot":"","sources":["../../../src/presentation/subscribers/on-email-verification-send-requested.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,uCAAuC,EAAC,MAAM,uBAAuB,CAAC;AAInF,wBAAsB,gCAAgC,CACpD,OAAO,EAAE,uCAAuC,GAC/C,OAAO,CAAC,IAAI,CAAC,CAGf"}
@@ -1,13 +0,0 @@
1
- import { renderEmail } from '@shipfox/node-email';
2
- import { mailer } from '#config.js';
3
- export async function onEmailVerificationSendRequested(payload) {
4
- const email = await renderEmail('verify-email', {
5
- verifyLink: payload.verifyLink
6
- });
7
- await mailer.send({
8
- to: payload.email,
9
- ...email
10
- });
11
- }
12
-
13
- //# sourceMappingURL=on-email-verification-send-requested.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../src/presentation/subscribers/on-email-verification-send-requested.ts"],"sourcesContent":["import type {AuthEmailVerificationSendRequestedEvent} from '@shipfox/api-auth-dto';\nimport {renderEmail} from '@shipfox/node-email';\nimport {mailer} from '#config.js';\n\nexport async function onEmailVerificationSendRequested(\n payload: AuthEmailVerificationSendRequestedEvent,\n): Promise<void> {\n const email = await renderEmail('verify-email', {verifyLink: payload.verifyLink});\n await mailer.send({to: payload.email, ...email});\n}\n"],"names":["renderEmail","mailer","onEmailVerificationSendRequested","payload","email","verifyLink","send","to"],"mappings":"AACA,SAAQA,WAAW,QAAO,sBAAsB;AAChD,SAAQC,MAAM,QAAO,aAAa;AAElC,OAAO,eAAeC,iCACpBC,OAAgD;IAEhD,MAAMC,QAAQ,MAAMJ,YAAY,gBAAgB;QAACK,YAAYF,QAAQE,UAAU;IAAA;IAC/E,MAAMJ,OAAOK,IAAI,CAAC;QAACC,IAAIJ,QAAQC,KAAK;QAAE,GAAGA,KAAK;IAAA;AAChD"}
@@ -1,8 +0,0 @@
1
- export interface EmailVerification {
2
- id: string;
3
- userId: string;
4
- hashedToken: string;
5
- expiresAt: Date;
6
- usedAt: Date | null;
7
- createdAt: Date;
8
- }
@@ -1,61 +0,0 @@
1
- import {hashOpaqueToken} from '@shipfox/node-tokens';
2
- import {consumeEmailVerification, createEmailVerification} from './email-verifications.js';
3
- import {createUser} from './users.js';
4
-
5
- function emailFor(suffix: string): string {
6
- return `${suffix}-${crypto.randomUUID()}@example.com`;
7
- }
8
-
9
- describe('email-verifications db', () => {
10
- test('creates a verification and consumes it once', async () => {
11
- const user = await createUser({email: emailFor('ev1'), hashedPassword: 'h'});
12
- const raw = `verification-${crypto.randomUUID()}`;
13
- const verification = await createEmailVerification({
14
- userId: user.id,
15
- hashedToken: hashOpaqueToken(raw),
16
- expiresAt: new Date(Date.now() + 60_000),
17
- skipEmail: true,
18
- });
19
-
20
- const first = await consumeEmailVerification({hashedToken: verification.hashedToken});
21
- const second = await consumeEmailVerification({hashedToken: verification.hashedToken});
22
-
23
- expect(first?.id).toBe(verification.id);
24
- expect(second).toBeUndefined();
25
- });
26
-
27
- test('rejects expired verification', async () => {
28
- const user = await createUser({email: emailFor('ev2'), hashedPassword: 'h'});
29
- const verification = await createEmailVerification({
30
- userId: user.id,
31
- hashedToken: hashOpaqueToken(`expired-v-${crypto.randomUUID()}`),
32
- expiresAt: new Date(Date.now() - 60_000),
33
- skipEmail: true,
34
- });
35
-
36
- const consumed = await consumeEmailVerification({hashedToken: verification.hashedToken});
37
-
38
- expect(consumed).toBeUndefined();
39
- });
40
-
41
- test('creating a new verification invalidates prior unused for the same user', async () => {
42
- const user = await createUser({email: emailFor('ev3'), hashedPassword: 'h'});
43
- const first = await createEmailVerification({
44
- userId: user.id,
45
- hashedToken: hashOpaqueToken(`first-v-${crypto.randomUUID()}`),
46
- expiresAt: new Date(Date.now() + 60_000),
47
- skipEmail: true,
48
- });
49
-
50
- await createEmailVerification({
51
- userId: user.id,
52
- hashedToken: hashOpaqueToken(`second-v-${crypto.randomUUID()}`),
53
- expiresAt: new Date(Date.now() + 60_000),
54
- skipEmail: true,
55
- });
56
-
57
- const consumeFirst = await consumeEmailVerification({hashedToken: first.hashedToken});
58
-
59
- expect(consumeFirst).toBeUndefined();
60
- });
61
- });
@@ -1,160 +0,0 @@
1
- import {AUTH_EMAIL_VERIFICATION_SEND_REQUESTED, type AuthEventMap} from '@shipfox/api-auth-dto';
2
- import {writeOutboxEvent} from '@shipfox/node-outbox';
3
- import {and, desc, eq, gt, isNull, sql} from 'drizzle-orm';
4
- import type {EmailVerification} from '#core/entities/email-verification.js';
5
- import type {User} from '#core/entities/user.js';
6
- import {db} from './db.js';
7
- import {emailVerifications, toEmailVerification} from './schema/email-verifications.js';
8
- import {authOutbox} from './schema/outbox.js';
9
- import {toUser, users} from './schema/users.js';
10
-
11
- interface CreateEmailVerificationBaseParams {
12
- userId: string;
13
- hashedToken: string;
14
- expiresAt: Date;
15
- }
16
-
17
- export type CreateEmailVerificationParams = CreateEmailVerificationBaseParams &
18
- (
19
- | {sendEmail: {email: string; verifyLink: string}; skipEmail?: never}
20
- | {sendEmail?: never; skipEmail: true}
21
- );
22
-
23
- export async function createEmailVerification(
24
- params: CreateEmailVerificationParams,
25
- ): Promise<EmailVerification> {
26
- return await db().transaction(async (tx) => {
27
- await tx
28
- .update(emailVerifications)
29
- .set({usedAt: sql`now()`})
30
- .where(and(eq(emailVerifications.userId, params.userId), isNull(emailVerifications.usedAt)));
31
-
32
- const rows = await tx
33
- .insert(emailVerifications)
34
- .values({
35
- userId: params.userId,
36
- hashedToken: params.hashedToken,
37
- expiresAt: params.expiresAt,
38
- })
39
- .returning();
40
-
41
- const row = rows[0];
42
- if (!row) throw new Error('Insert returned no rows');
43
- if (params.sendEmail) {
44
- await writeOutboxEvent<AuthEventMap>(tx, authOutbox, {
45
- type: AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
46
- payload: params.sendEmail,
47
- });
48
- }
49
- return toEmailVerification(row);
50
- });
51
- }
52
-
53
- export interface CreateResendEmailVerificationParams {
54
- email: string;
55
- hashedToken: string;
56
- expiresAt: Date;
57
- cooldownSeconds: number;
58
- sendEmail: {verifyLink: string};
59
- now?: Date | undefined;
60
- }
61
-
62
- export interface CreateResendEmailVerificationResult {
63
- user?: User | undefined;
64
- verification?: EmailVerification | undefined;
65
- nextResendAvailableAt: Date;
66
- }
67
-
68
- function secondsAfter(date: Date, seconds: number): Date {
69
- return new Date(date.getTime() + seconds * 1000);
70
- }
71
-
72
- export async function createResendEmailVerification(
73
- params: CreateResendEmailVerificationParams,
74
- ): Promise<CreateResendEmailVerificationResult> {
75
- const now = params.now ?? new Date();
76
- const genericNextResendAvailableAt = secondsAfter(now, params.cooldownSeconds);
77
-
78
- return await db().transaction(async (tx) => {
79
- const userRows = await tx
80
- .select()
81
- .from(users)
82
- .where(eq(users.email, params.email))
83
- .limit(1)
84
- .for('update');
85
- const userRow = userRows[0];
86
- if (!userRow) {
87
- return {nextResendAvailableAt: genericNextResendAvailableAt};
88
- }
89
-
90
- const user = toUser(userRow);
91
- if (user.status !== 'active' || user.emailVerifiedAt !== null) {
92
- return {nextResendAvailableAt: genericNextResendAvailableAt};
93
- }
94
-
95
- const latestRows = await tx
96
- .select()
97
- .from(emailVerifications)
98
- .where(and(eq(emailVerifications.userId, user.id), isNull(emailVerifications.usedAt)))
99
- .orderBy(desc(emailVerifications.createdAt))
100
- .limit(1);
101
- const latest = latestRows[0] ? toEmailVerification(latestRows[0]) : undefined;
102
- if (latest && secondsAfter(latest.createdAt, params.cooldownSeconds) > now) {
103
- // Return the same public retry estimate as no-op cases so the endpoint does
104
- // not expose existing account state or prior verification timing.
105
- return {user, nextResendAvailableAt: genericNextResendAvailableAt};
106
- }
107
-
108
- await tx
109
- .update(emailVerifications)
110
- .set({usedAt: sql`now()`})
111
- .where(and(eq(emailVerifications.userId, user.id), isNull(emailVerifications.usedAt)));
112
-
113
- const rows = await tx
114
- .insert(emailVerifications)
115
- .values({
116
- userId: user.id,
117
- hashedToken: params.hashedToken,
118
- expiresAt: params.expiresAt,
119
- })
120
- .returning();
121
-
122
- const row = rows[0];
123
- if (!row) throw new Error('Insert returned no rows');
124
- const verification = toEmailVerification(row);
125
- if (params.sendEmail) {
126
- await writeOutboxEvent<AuthEventMap>(tx, authOutbox, {
127
- type: AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
128
- payload: {
129
- email: user.email,
130
- verifyLink: params.sendEmail.verifyLink,
131
- },
132
- });
133
- }
134
- return {
135
- user,
136
- verification,
137
- nextResendAvailableAt: secondsAfter(verification.createdAt, params.cooldownSeconds),
138
- };
139
- });
140
- }
141
-
142
- export async function consumeEmailVerification(params: {
143
- hashedToken: string;
144
- }): Promise<EmailVerification | undefined> {
145
- const rows = await db()
146
- .update(emailVerifications)
147
- .set({usedAt: sql`now()`})
148
- .where(
149
- and(
150
- eq(emailVerifications.hashedToken, params.hashedToken),
151
- isNull(emailVerifications.usedAt),
152
- gt(emailVerifications.expiresAt, sql`now()`),
153
- ),
154
- )
155
- .returning();
156
-
157
- const row = rows[0];
158
- if (!row) return undefined;
159
- return toEmailVerification(row);
160
- }
@@ -1,37 +0,0 @@
1
- import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
2
- import {index, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';
3
- import type {EmailVerification} from '#core/entities/email-verification.js';
4
- import {pgTable} from './common.js';
5
- import {users} from './users.js';
6
-
7
- export const emailVerifications = pgTable(
8
- 'email_verifications',
9
- {
10
- id: uuidv7PrimaryKey(),
11
- userId: uuid('user_id')
12
- .notNull()
13
- .references(() => users.id, {onDelete: 'cascade'}),
14
- hashedToken: text('hashed_token').notNull(),
15
- expiresAt: timestamp('expires_at', {withTimezone: true}).notNull(),
16
- usedAt: timestamp('used_at', {withTimezone: true}),
17
- createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
18
- },
19
- (table) => [
20
- uniqueIndex('auth_email_verifications_hashed_token_unique').on(table.hashedToken),
21
- index('auth_email_verifications_user_id_idx').on(table.userId),
22
- ],
23
- );
24
-
25
- export type EmailVerificationDb = typeof emailVerifications.$inferSelect;
26
- export type EmailVerificationCreateDb = typeof emailVerifications.$inferInsert;
27
-
28
- export function toEmailVerification(row: EmailVerificationDb): EmailVerification {
29
- return {
30
- id: row.id,
31
- userId: row.userId,
32
- hashedToken: row.hashedToken,
33
- expiresAt: row.expiresAt,
34
- usedAt: row.usedAt,
35
- createdAt: row.createdAt,
36
- };
37
- }