@shipfox/api-auth 2.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.
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +116 -0
- package/LICENSE +21 -0
- package/README.md +336 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +95 -0
- package/dist/config.js.map +1 -0
- package/dist/core/auth.d.ts +108 -0
- package/dist/core/auth.d.ts.map +1 -0
- package/dist/core/auth.js +476 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/entities/email-verification.d.ts +9 -0
- package/dist/core/entities/email-verification.d.ts.map +1 -0
- package/dist/core/entities/email-verification.js +3 -0
- package/dist/core/entities/email-verification.js.map +1 -0
- package/dist/core/entities/password-reset.d.ts +9 -0
- package/dist/core/entities/password-reset.d.ts.map +1 -0
- package/dist/core/entities/password-reset.js +3 -0
- package/dist/core/entities/password-reset.js.map +1 -0
- package/dist/core/entities/refresh-token.d.ts +12 -0
- package/dist/core/entities/refresh-token.d.ts.map +1 -0
- package/dist/core/entities/refresh-token.js +3 -0
- package/dist/core/entities/refresh-token.js.map +1 -0
- package/dist/core/entities/user.d.ts +12 -0
- package/dist/core/entities/user.d.ts.map +1 -0
- package/dist/core/entities/user.js +3 -0
- package/dist/core/entities/user.js.map +1 -0
- package/dist/core/errors.d.ts +54 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +106 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/job-lease-token.d.ts +12 -0
- package/dist/core/job-lease-token.d.ts.map +1 -0
- package/dist/core/job-lease-token.js +58 -0
- package/dist/core/job-lease-token.js.map +1 -0
- package/dist/core/jwt.d.ts +37 -0
- package/dist/core/jwt.d.ts.map +1 -0
- package/dist/core/jwt.js +46 -0
- package/dist/core/jwt.js.map +1 -0
- package/dist/core/password.d.ts +10 -0
- package/dist/core/password.d.ts.map +1 -0
- package/dist/core/password.js +13 -0
- package/dist/core/password.js.map +1 -0
- package/dist/core/rate-limit.d.ts +34 -0
- package/dist/core/rate-limit.d.ts.map +1 -0
- package/dist/core/rate-limit.js +76 -0
- package/dist/core/rate-limit.js.map +1 -0
- package/dist/core/runner-session-token.d.ts +6 -0
- package/dist/core/runner-session-token.d.ts.map +1 -0
- package/dist/core/runner-session-token.js +37 -0
- package/dist/core/runner-session-token.js.map +1 -0
- package/dist/db/db.d.ts +1723 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +28 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/email-verifications.d.ts +39 -0
- package/dist/db/email-verifications.d.ts.map +1 -0
- package/dist/db/email-verifications.js +95 -0
- package/dist/db/email-verifications.js.map +1 -0
- package/dist/db/migrations.d.ts +2 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +5 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/password-resets.d.ts +23 -0
- package/dist/db/password-resets.d.ts.map +1 -0
- package/dist/db/password-resets.js +37 -0
- package/dist/db/password-resets.js.map +1 -0
- package/dist/db/rate-limits.d.ts +18 -0
- package/dist/db/rate-limits.d.ts.map +1 -0
- package/dist/db/rate-limits.js +44 -0
- package/dist/db/rate-limits.js.map +1 -0
- package/dist/db/refresh-tokens.d.ts +44 -0
- package/dist/db/refresh-tokens.d.ts.map +1 -0
- package/dist/db/refresh-tokens.js +71 -0
- package/dist/db/refresh-tokens.js.map +1 -0
- package/dist/db/schema/common.d.ts +2 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +4 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/email-verifications.d.ts +114 -0
- package/dist/db/schema/email-verifications.d.ts.map +1 -0
- package/dist/db/schema/email-verifications.js +35 -0
- package/dist/db/schema/email-verifications.js.map +1 -0
- package/dist/db/schema/outbox.d.ts +195 -0
- package/dist/db/schema/outbox.d.ts.map +1 -0
- package/dist/db/schema/outbox.js +5 -0
- package/dist/db/schema/outbox.js.map +1 -0
- package/dist/db/schema/password-resets.d.ts +114 -0
- package/dist/db/schema/password-resets.d.ts.map +1 -0
- package/dist/db/schema/password-resets.js +35 -0
- package/dist/db/schema/password-resets.js.map +1 -0
- package/dist/db/schema/rate-limits.d.ts +145 -0
- package/dist/db/schema/rate-limits.d.ts.map +1 -0
- package/dist/db/schema/rate-limits.js +25 -0
- package/dist/db/schema/rate-limits.js.map +1 -0
- package/dist/db/schema/refresh-tokens.d.ts +165 -0
- package/dist/db/schema/refresh-tokens.d.ts.map +1 -0
- package/dist/db/schema/refresh-tokens.js +47 -0
- package/dist/db/schema/refresh-tokens.js.map +1 -0
- package/dist/db/schema/users.d.ts +149 -0
- package/dist/db/schema/users.d.ts.map +1 -0
- package/dist/db/schema/users.js +40 -0
- package/dist/db/schema/users.js.map +1 -0
- package/dist/db/users.d.ts +28 -0
- package/dist/db/users.d.ts.map +1 -0
- package/dist/db/users.js +83 -0
- package/dist/db/users.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/index.d.ts +2 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +3 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/instance.d.ts +16 -0
- package/dist/metrics/instance.d.ts.map +1 -0
- package/dist/metrics/instance.js +52 -0
- package/dist/metrics/instance.js.map +1 -0
- package/dist/presentation/auth/bearer-token-auth.d.ts +15 -0
- package/dist/presentation/auth/bearer-token-auth.d.ts.map +1 -0
- package/dist/presentation/auth/bearer-token-auth.js +31 -0
- package/dist/presentation/auth/bearer-token-auth.js.map +1 -0
- package/dist/presentation/auth/jwt-auth.d.ts +10 -0
- package/dist/presentation/auth/jwt-auth.d.ts.map +1 -0
- package/dist/presentation/auth/jwt-auth.js +31 -0
- package/dist/presentation/auth/jwt-auth.js.map +1 -0
- package/dist/presentation/auth/lease-token-auth.d.ts +15 -0
- package/dist/presentation/auth/lease-token-auth.d.ts.map +1 -0
- package/dist/presentation/auth/lease-token-auth.js +27 -0
- package/dist/presentation/auth/lease-token-auth.js.map +1 -0
- package/dist/presentation/auth/refresh-cookie.d.ts +7 -0
- package/dist/presentation/auth/refresh-cookie.d.ts.map +1 -0
- package/dist/presentation/auth/refresh-cookie.js +29 -0
- package/dist/presentation/auth/refresh-cookie.js.map +1 -0
- package/dist/presentation/auth/runner-session-auth.d.ts +3 -0
- package/dist/presentation/auth/runner-session-auth.d.ts.map +1 -0
- package/dist/presentation/auth/runner-session-auth.js +16 -0
- package/dist/presentation/auth/runner-session-auth.js.map +1 -0
- package/dist/presentation/dto/user.d.ts +8 -0
- package/dist/presentation/dto/user.d.ts.map +1 -0
- package/dist/presentation/dto/user.js +19 -0
- package/dist/presentation/dto/user.js.map +1 -0
- package/dist/presentation/e2eRoutes/create-session.d.ts +2 -0
- package/dist/presentation/e2eRoutes/create-session.d.ts.map +1 -0
- package/dist/presentation/e2eRoutes/create-session.js +49 -0
- package/dist/presentation/e2eRoutes/create-session.js.map +1 -0
- package/dist/presentation/e2eRoutes/create-user.d.ts +2 -0
- package/dist/presentation/e2eRoutes/create-user.d.ts.map +1 -0
- package/dist/presentation/e2eRoutes/create-user.js +42 -0
- package/dist/presentation/e2eRoutes/create-user.js.map +1 -0
- package/dist/presentation/e2eRoutes/index.d.ts +3 -0
- package/dist/presentation/e2eRoutes/index.d.ts.map +1 -0
- package/dist/presentation/e2eRoutes/index.js +15 -0
- package/dist/presentation/e2eRoutes/index.js.map +1 -0
- package/dist/presentation/routes/email-verification/verify-email-confirm.d.ts +2 -0
- package/dist/presentation/routes/email-verification/verify-email-confirm.d.ts.map +1 -0
- package/dist/presentation/routes/email-verification/verify-email-confirm.js +35 -0
- package/dist/presentation/routes/email-verification/verify-email-confirm.js.map +1 -0
- package/dist/presentation/routes/email-verification/verify-email-resend.d.ts +2 -0
- package/dist/presentation/routes/email-verification/verify-email-resend.d.ts.map +1 -0
- package/dist/presentation/routes/email-verification/verify-email-resend.js +27 -0
- package/dist/presentation/routes/email-verification/verify-email-resend.js.map +1 -0
- package/dist/presentation/routes/index.d.ts +3 -0
- package/dist/presentation/routes/index.d.ts.map +1 -0
- package/dist/presentation/routes/index.js +31 -0
- package/dist/presentation/routes/index.js.map +1 -0
- package/dist/presentation/routes/password/change-password.d.ts +2 -0
- package/dist/presentation/routes/password/change-password.d.ts.map +1 -0
- package/dist/presentation/routes/password/change-password.js +50 -0
- package/dist/presentation/routes/password/change-password.js.map +1 -0
- package/dist/presentation/routes/password/password-reset-confirm.d.ts +2 -0
- package/dist/presentation/routes/password/password-reset-confirm.d.ts.map +1 -0
- package/dist/presentation/routes/password/password-reset-confirm.js +36 -0
- package/dist/presentation/routes/password/password-reset-confirm.js.map +1 -0
- package/dist/presentation/routes/password/password-reset-request.d.ts +2 -0
- package/dist/presentation/routes/password/password-reset-request.d.ts.map +1 -0
- package/dist/presentation/routes/password/password-reset-request.js +26 -0
- package/dist/presentation/routes/password/password-reset-request.js.map +1 -0
- package/dist/presentation/routes/rate-limit.d.ts +10 -0
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -0
- package/dist/presentation/routes/rate-limit.js +101 -0
- package/dist/presentation/routes/rate-limit.js.map +1 -0
- package/dist/presentation/routes/registration/signup.d.ts +2 -0
- package/dist/presentation/routes/registration/signup.d.ts.map +1 -0
- package/dist/presentation/routes/registration/signup.js +82 -0
- package/dist/presentation/routes/registration/signup.js.map +1 -0
- package/dist/presentation/routes/session/login.d.ts +2 -0
- package/dist/presentation/routes/session/login.d.ts.map +1 -0
- package/dist/presentation/routes/session/login.js +48 -0
- package/dist/presentation/routes/session/login.js.map +1 -0
- package/dist/presentation/routes/session/logout.d.ts +2 -0
- package/dist/presentation/routes/session/logout.d.ts.map +1 -0
- package/dist/presentation/routes/session/logout.js +25 -0
- package/dist/presentation/routes/session/logout.js.map +1 -0
- package/dist/presentation/routes/session/me.d.ts +2 -0
- package/dist/presentation/routes/session/me.d.ts.map +1 -0
- package/dist/presentation/routes/session/me.js +42 -0
- package/dist/presentation/routes/session/me.js.map +1 -0
- package/dist/presentation/routes/session/refresh.d.ts +2 -0
- package/dist/presentation/routes/session/refresh.d.ts.map +1 -0
- package/dist/presentation/routes/session/refresh.js +49 -0
- package/dist/presentation/routes/session/refresh.js.map +1 -0
- package/dist/presentation/subscribers/index.d.ts +3 -0
- package/dist/presentation/subscribers/index.d.ts.map +1 -0
- package/dist/presentation/subscribers/index.js +4 -0
- package/dist/presentation/subscribers/index.js.map +1 -0
- package/dist/presentation/subscribers/on-email-verification-send-requested.d.ts +3 -0
- package/dist/presentation/subscribers/on-email-verification-send-requested.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-email-verification-send-requested.js +13 -0
- package/dist/presentation/subscribers/on-email-verification-send-requested.js.map +1 -0
- package/dist/presentation/subscribers/on-password-reset-send-requested.d.ts +3 -0
- package/dist/presentation/subscribers/on-password-reset-send-requested.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-password-reset-send-requested.js +14 -0
- package/dist/presentation/subscribers/on-password-reset-send-requested.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_initial.sql +81 -0
- package/drizzle/meta/0000_snapshot.json +643 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/package.json +88 -0
- package/src/config.ts +88 -0
- package/src/core/auth.test.ts +675 -0
- package/src/core/auth.ts +560 -0
- package/src/core/entities/email-verification.ts +8 -0
- package/src/core/entities/password-reset.ts +8 -0
- package/src/core/entities/refresh-token.ts +11 -0
- package/src/core/entities/user.ts +12 -0
- package/src/core/errors.ts +123 -0
- package/src/core/job-lease-token.test.ts +201 -0
- package/src/core/job-lease-token.ts +83 -0
- package/src/core/jwt.test.ts +180 -0
- package/src/core/jwt.ts +66 -0
- package/src/core/password.test.ts +28 -0
- package/src/core/password.ts +21 -0
- package/src/core/rate-limit.test.ts +462 -0
- package/src/core/rate-limit.ts +130 -0
- package/src/core/runner-session-token.test.ts +126 -0
- package/src/core/runner-session-token.ts +49 -0
- package/src/db/db.ts +28 -0
- package/src/db/email-verifications.test.ts +61 -0
- package/src/db/email-verifications.ts +160 -0
- package/src/db/migrations.ts +4 -0
- package/src/db/password-resets.test.ts +61 -0
- package/src/db/password-resets.ts +69 -0
- package/src/db/rate-limits.ts +69 -0
- package/src/db/refresh-tokens.test.ts +164 -0
- package/src/db/refresh-tokens.ts +149 -0
- package/src/db/schema/common.ts +3 -0
- package/src/db/schema/email-verifications.ts +37 -0
- package/src/db/schema/outbox.ts +4 -0
- package/src/db/schema/password-resets.ts +37 -0
- package/src/db/schema/rate-limits.ts +27 -0
- package/src/db/schema/refresh-tokens.ts +43 -0
- package/src/db/schema/users.ts +37 -0
- package/src/db/users.test.ts +75 -0
- package/src/db/users.ts +121 -0
- package/src/index.test.ts +39 -0
- package/src/index.ts +69 -0
- package/src/metrics/index.ts +13 -0
- package/src/metrics/instance.test.ts +69 -0
- package/src/metrics/instance.ts +77 -0
- package/src/presentation/auth/bearer-token-auth.test.ts +99 -0
- package/src/presentation/auth/bearer-token-auth.ts +51 -0
- package/src/presentation/auth/jwt-auth.test.ts +136 -0
- package/src/presentation/auth/jwt-auth.ts +39 -0
- package/src/presentation/auth/lease-token-auth.test.ts +56 -0
- package/src/presentation/auth/lease-token-auth.ts +25 -0
- package/src/presentation/auth/refresh-cookie.test.ts +39 -0
- package/src/presentation/auth/refresh-cookie.ts +34 -0
- package/src/presentation/auth/runner-session-auth.test.ts +54 -0
- package/src/presentation/auth/runner-session-auth.ts +13 -0
- package/src/presentation/dto/user.ts +21 -0
- package/src/presentation/e2eRoutes/create-session.ts +39 -0
- package/src/presentation/e2eRoutes/create-user.ts +38 -0
- package/src/presentation/e2eRoutes/index.test.ts +102 -0
- package/src/presentation/e2eRoutes/index.ts +10 -0
- package/src/presentation/routes/email-verification/verify-email-confirm.test.ts +66 -0
- package/src/presentation/routes/email-verification/verify-email-confirm.ts +37 -0
- package/src/presentation/routes/email-verification/verify-email-resend.test.ts +100 -0
- package/src/presentation/routes/email-verification/verify-email-resend.ts +26 -0
- package/src/presentation/routes/index.ts +29 -0
- package/src/presentation/routes/password/change-password.test.ts +106 -0
- package/src/presentation/routes/password/change-password.ts +45 -0
- package/src/presentation/routes/password/password-reset-confirm.test.ts +63 -0
- package/src/presentation/routes/password/password-reset-confirm.ts +35 -0
- package/src/presentation/routes/password/password-reset-request.test.ts +56 -0
- package/src/presentation/routes/password/password-reset-request.ts +25 -0
- package/src/presentation/routes/rate-limit.test.ts +341 -0
- package/src/presentation/routes/rate-limit.ts +119 -0
- package/src/presentation/routes/registration/signup.test.ts +284 -0
- package/src/presentation/routes/registration/signup.ts +81 -0
- package/src/presentation/routes/session/login.test.ts +77 -0
- package/src/presentation/routes/session/login.ts +50 -0
- package/src/presentation/routes/session/logout.test.ts +55 -0
- package/src/presentation/routes/session/logout.ts +23 -0
- package/src/presentation/routes/session/me.test.ts +56 -0
- package/src/presentation/routes/session/me.ts +35 -0
- package/src/presentation/routes/session/refresh.test.ts +89 -0
- package/src/presentation/routes/session/refresh.ts +54 -0
- package/src/presentation/subscribers/email-send-requested.test.ts +77 -0
- package/src/presentation/subscribers/index.ts +2 -0
- package/src/presentation/subscribers/on-email-verification-send-requested.ts +10 -0
- package/src/presentation/subscribers/on-password-reset-send-requested.ts +13 -0
- package/test/env.ts +10 -0
- package/test/factories/index.ts +1 -0
- package/test/factories/user.ts +36 -0
- package/test/globalSetup.ts +18 -0
- package/test/index.ts +1 -0
- package/test/routes.ts +275 -0
- package/test/setup.ts +17 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +4 -0
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {and, eq, gt, isNull, ne, sql} from 'drizzle-orm';
|
|
2
|
+
import type {RefreshToken} from '#core/entities/refresh-token.js';
|
|
3
|
+
import {db} from './db.js';
|
|
4
|
+
import {refreshTokens, toRefreshToken} from './schema/refresh-tokens.js';
|
|
5
|
+
|
|
6
|
+
export interface CreateRefreshTokenParams {
|
|
7
|
+
userId: string;
|
|
8
|
+
hashedToken: string;
|
|
9
|
+
expiresAt: Date;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function createRefreshToken(params: CreateRefreshTokenParams): Promise<RefreshToken> {
|
|
13
|
+
const rows = await db()
|
|
14
|
+
.insert(refreshTokens)
|
|
15
|
+
.values({
|
|
16
|
+
userId: params.userId,
|
|
17
|
+
hashedToken: params.hashedToken,
|
|
18
|
+
expiresAt: params.expiresAt,
|
|
19
|
+
})
|
|
20
|
+
.returning();
|
|
21
|
+
|
|
22
|
+
const row = rows[0];
|
|
23
|
+
if (!row) throw new Error('Insert returned no rows');
|
|
24
|
+
return toRefreshToken(row);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Looks up the live session token by hash — one that can still authenticate as
|
|
29
|
+
* the current session. Returns `undefined` for revoked, expired, or already
|
|
30
|
+
* rotated tokens; rotated rows survive in the table only for the grace window
|
|
31
|
+
* and are not "active".
|
|
32
|
+
*/
|
|
33
|
+
export async function findActiveRefreshTokenByHash(params: {
|
|
34
|
+
hashedToken: string;
|
|
35
|
+
}): Promise<RefreshToken | undefined> {
|
|
36
|
+
const rows = await db()
|
|
37
|
+
.select()
|
|
38
|
+
.from(refreshTokens)
|
|
39
|
+
.where(
|
|
40
|
+
and(
|
|
41
|
+
eq(refreshTokens.hashedToken, params.hashedToken),
|
|
42
|
+
isNull(refreshTokens.revokedAt),
|
|
43
|
+
isNull(refreshTokens.rotatedAt),
|
|
44
|
+
gt(refreshTokens.expiresAt, sql`now()`),
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
.limit(1);
|
|
48
|
+
|
|
49
|
+
const row = rows[0];
|
|
50
|
+
if (!row) return undefined;
|
|
51
|
+
return toRefreshToken(row);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Looks up a non-revoked, non-expired token by hash, including ones already
|
|
56
|
+
* rotated. Rotation reads {@link RefreshToken.rotatedAt} to decide whether to
|
|
57
|
+
* rotate, honour the grace window, or reject reuse, so — unlike
|
|
58
|
+
* {@link findActiveRefreshTokenByHash} — it must still see rotated rows.
|
|
59
|
+
*/
|
|
60
|
+
export async function findRefreshTokenByHash(params: {
|
|
61
|
+
hashedToken: string;
|
|
62
|
+
}): Promise<RefreshToken | undefined> {
|
|
63
|
+
const rows = await db()
|
|
64
|
+
.select()
|
|
65
|
+
.from(refreshTokens)
|
|
66
|
+
.where(
|
|
67
|
+
and(
|
|
68
|
+
eq(refreshTokens.hashedToken, params.hashedToken),
|
|
69
|
+
isNull(refreshTokens.revokedAt),
|
|
70
|
+
gt(refreshTokens.expiresAt, sql`now()`),
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
.limit(1);
|
|
74
|
+
|
|
75
|
+
const row = rows[0];
|
|
76
|
+
if (!row) return undefined;
|
|
77
|
+
return toRefreshToken(row);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Atomically claims rotation and inserts the successor token. The predecessor is
|
|
82
|
+
* marked rather than deleted so later reuse of the retired token can be detected.
|
|
83
|
+
* If the successor insert fails, the predecessor rotation rolls back.
|
|
84
|
+
*/
|
|
85
|
+
export async function rotateRefreshToken(params: {
|
|
86
|
+
id: string;
|
|
87
|
+
currentHashedToken: string;
|
|
88
|
+
nextHashedToken: string;
|
|
89
|
+
expiresAt: Date;
|
|
90
|
+
}): Promise<RefreshToken | undefined> {
|
|
91
|
+
return await db().transaction(async (tx) => {
|
|
92
|
+
const rotatedRows = await tx
|
|
93
|
+
.update(refreshTokens)
|
|
94
|
+
.set({
|
|
95
|
+
rotatedAt: sql`now()`,
|
|
96
|
+
lastUsedAt: sql`now()`,
|
|
97
|
+
updatedAt: sql`now()`,
|
|
98
|
+
})
|
|
99
|
+
.where(
|
|
100
|
+
and(
|
|
101
|
+
eq(refreshTokens.id, params.id),
|
|
102
|
+
eq(refreshTokens.hashedToken, params.currentHashedToken),
|
|
103
|
+
isNull(refreshTokens.revokedAt),
|
|
104
|
+
isNull(refreshTokens.rotatedAt),
|
|
105
|
+
gt(refreshTokens.expiresAt, sql`now()`),
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
.returning();
|
|
109
|
+
|
|
110
|
+
const rotated = rotatedRows[0];
|
|
111
|
+
if (!rotated) return undefined;
|
|
112
|
+
|
|
113
|
+
const successorRows = await tx
|
|
114
|
+
.insert(refreshTokens)
|
|
115
|
+
.values({
|
|
116
|
+
userId: rotated.userId,
|
|
117
|
+
hashedToken: params.nextHashedToken,
|
|
118
|
+
expiresAt: params.expiresAt,
|
|
119
|
+
})
|
|
120
|
+
.returning();
|
|
121
|
+
|
|
122
|
+
if (!successorRows[0]) throw new Error('Insert returned no rows');
|
|
123
|
+
|
|
124
|
+
return toRefreshToken(rotated);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function revokeRefreshTokenByHash(params: {hashedToken: string}): Promise<void> {
|
|
129
|
+
await db()
|
|
130
|
+
.update(refreshTokens)
|
|
131
|
+
.set({revokedAt: sql`now()`, updatedAt: sql`now()`})
|
|
132
|
+
.where(and(eq(refreshTokens.hashedToken, params.hashedToken), isNull(refreshTokens.revokedAt)));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function revokeRefreshTokensForUser(params: {
|
|
136
|
+
userId: string;
|
|
137
|
+
exceptRefreshTokenId?: string | undefined;
|
|
138
|
+
}): Promise<void> {
|
|
139
|
+
await db()
|
|
140
|
+
.update(refreshTokens)
|
|
141
|
+
.set({revokedAt: sql`now()`, updatedAt: sql`now()`})
|
|
142
|
+
.where(
|
|
143
|
+
and(
|
|
144
|
+
eq(refreshTokens.userId, params.userId),
|
|
145
|
+
isNull(refreshTokens.revokedAt),
|
|
146
|
+
params.exceptRefreshTokenId ? ne(refreshTokens.id, params.exceptRefreshTokenId) : undefined,
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
|
|
2
|
+
import {index, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';
|
|
3
|
+
import type {PasswordReset} from '#core/entities/password-reset.js';
|
|
4
|
+
import {pgTable} from './common.js';
|
|
5
|
+
import {users} from './users.js';
|
|
6
|
+
|
|
7
|
+
export const passwordResets = pgTable(
|
|
8
|
+
'password_resets',
|
|
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_password_resets_hashed_token_unique').on(table.hashedToken),
|
|
21
|
+
index('auth_password_resets_user_id_idx').on(table.userId),
|
|
22
|
+
],
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export type PasswordResetDb = typeof passwordResets.$inferSelect;
|
|
26
|
+
export type PasswordResetCreateDb = typeof passwordResets.$inferInsert;
|
|
27
|
+
|
|
28
|
+
export function toPasswordReset(row: PasswordResetDb): PasswordReset {
|
|
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
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {index, integer, text, timestamp, uniqueIndex} from 'drizzle-orm/pg-core';
|
|
2
|
+
import {pgTable} from './common.js';
|
|
3
|
+
|
|
4
|
+
export const authRateLimits = pgTable(
|
|
5
|
+
'rate_limits',
|
|
6
|
+
{
|
|
7
|
+
action: text('action').notNull(),
|
|
8
|
+
scope: text('scope').notNull(),
|
|
9
|
+
identifierHmac: text('identifier_hmac').notNull(),
|
|
10
|
+
windowStart: timestamp('window_start', {withTimezone: true}).notNull(),
|
|
11
|
+
count: integer('count').notNull().default(1),
|
|
12
|
+
expiresAt: timestamp('expires_at', {withTimezone: true}).notNull(),
|
|
13
|
+
createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
|
|
14
|
+
updatedAt: timestamp('updated_at', {withTimezone: true}).notNull().defaultNow(),
|
|
15
|
+
},
|
|
16
|
+
(table) => [
|
|
17
|
+
uniqueIndex('auth_rate_limits_window_unique').on(
|
|
18
|
+
table.action,
|
|
19
|
+
table.scope,
|
|
20
|
+
table.identifierHmac,
|
|
21
|
+
table.windowStart,
|
|
22
|
+
),
|
|
23
|
+
index('auth_rate_limits_expires_at_idx').on(table.expiresAt),
|
|
24
|
+
],
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
export type AuthRateLimitDb = typeof authRateLimits.$inferSelect;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
|
|
2
|
+
import {index, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';
|
|
3
|
+
import type {RefreshToken} from '#core/entities/refresh-token.js';
|
|
4
|
+
import {pgTable} from './common.js';
|
|
5
|
+
import {users} from './users.js';
|
|
6
|
+
|
|
7
|
+
export const refreshTokens = pgTable(
|
|
8
|
+
'refresh_tokens',
|
|
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
|
+
revokedAt: timestamp('revoked_at', {withTimezone: true}),
|
|
17
|
+
rotatedAt: timestamp('rotated_at', {withTimezone: true}),
|
|
18
|
+
lastUsedAt: timestamp('last_used_at', {withTimezone: true}),
|
|
19
|
+
createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
|
|
20
|
+
updatedAt: timestamp('updated_at', {withTimezone: true}).notNull().defaultNow(),
|
|
21
|
+
},
|
|
22
|
+
(table) => [
|
|
23
|
+
uniqueIndex('auth_refresh_tokens_hashed_token_unique').on(table.hashedToken),
|
|
24
|
+
index('auth_refresh_tokens_user_id_idx').on(table.userId),
|
|
25
|
+
],
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export type RefreshTokenDb = typeof refreshTokens.$inferSelect;
|
|
29
|
+
export type RefreshTokenCreateDb = typeof refreshTokens.$inferInsert;
|
|
30
|
+
|
|
31
|
+
export function toRefreshToken(row: RefreshTokenDb): RefreshToken {
|
|
32
|
+
return {
|
|
33
|
+
id: row.id,
|
|
34
|
+
userId: row.userId,
|
|
35
|
+
hashedToken: row.hashedToken,
|
|
36
|
+
expiresAt: row.expiresAt,
|
|
37
|
+
revokedAt: row.revokedAt,
|
|
38
|
+
rotatedAt: row.rotatedAt,
|
|
39
|
+
lastUsedAt: row.lastUsedAt,
|
|
40
|
+
createdAt: row.createdAt,
|
|
41
|
+
updatedAt: row.updatedAt,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
|
|
2
|
+
import {pgEnum, text, timestamp, uniqueIndex} from 'drizzle-orm/pg-core';
|
|
3
|
+
import type {User, UserStatus} from '#core/entities/user.js';
|
|
4
|
+
import {pgTable} from './common.js';
|
|
5
|
+
|
|
6
|
+
export const userStatusEnum = pgEnum('auth_user_status', ['active', 'suspended', 'deleted']);
|
|
7
|
+
|
|
8
|
+
export const users = pgTable(
|
|
9
|
+
'users',
|
|
10
|
+
{
|
|
11
|
+
id: uuidv7PrimaryKey(),
|
|
12
|
+
email: text('email').notNull(),
|
|
13
|
+
hashedPassword: text('hashed_password'),
|
|
14
|
+
name: text('name'),
|
|
15
|
+
emailVerifiedAt: timestamp('email_verified_at', {withTimezone: true}),
|
|
16
|
+
status: userStatusEnum('status').notNull().default('active'),
|
|
17
|
+
createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
|
|
18
|
+
updatedAt: timestamp('updated_at', {withTimezone: true}).notNull().defaultNow(),
|
|
19
|
+
},
|
|
20
|
+
(table) => [uniqueIndex('auth_users_email_unique').on(table.email)],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export type UserDb = typeof users.$inferSelect;
|
|
24
|
+
export type UserCreateDb = typeof users.$inferInsert;
|
|
25
|
+
|
|
26
|
+
export function toUser(row: UserDb): User {
|
|
27
|
+
return {
|
|
28
|
+
id: row.id,
|
|
29
|
+
email: row.email,
|
|
30
|
+
hashedPassword: row.hashedPassword,
|
|
31
|
+
name: row.name,
|
|
32
|
+
emailVerifiedAt: row.emailVerifiedAt,
|
|
33
|
+
status: row.status as UserStatus,
|
|
34
|
+
createdAt: row.createdAt,
|
|
35
|
+
updatedAt: row.updatedAt,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {hashOpaqueToken} from '@shipfox/node-tokens';
|
|
2
|
+
import {EmailTakenError} from '#core/errors.js';
|
|
3
|
+
import {
|
|
4
|
+
createUser,
|
|
5
|
+
findUserByEmail,
|
|
6
|
+
findUserById,
|
|
7
|
+
markEmailVerified,
|
|
8
|
+
updateUserPassword,
|
|
9
|
+
} from './users.js';
|
|
10
|
+
|
|
11
|
+
function emailFor(suffix: string): string {
|
|
12
|
+
return `${suffix}-${crypto.randomUUID()}@example.com`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('users db', () => {
|
|
16
|
+
test('creates a user with email_verified_at null and finds by email and id', async () => {
|
|
17
|
+
const email = emailFor('alice');
|
|
18
|
+
|
|
19
|
+
const user = await createUser({email, hashedPassword: 'h', name: 'Alice'});
|
|
20
|
+
const byEmail = await findUserByEmail({email});
|
|
21
|
+
const byId = await findUserById({id: user.id});
|
|
22
|
+
|
|
23
|
+
expect(user.id).toBeDefined();
|
|
24
|
+
expect(user.email).toBe(email);
|
|
25
|
+
expect(user.emailVerifiedAt).toBeNull();
|
|
26
|
+
expect(byEmail?.id).toBe(user.id);
|
|
27
|
+
expect(byId?.id).toBe(user.id);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('creates a user with email_verified_at set when provided', async () => {
|
|
31
|
+
const emailVerifiedAt = new Date('2026-01-01T00:00:00.000Z');
|
|
32
|
+
|
|
33
|
+
const user = await createUser({
|
|
34
|
+
email: emailFor('verified-insert'),
|
|
35
|
+
hashedPassword: 'h',
|
|
36
|
+
emailVerifiedAt,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(user.emailVerifiedAt).toEqual(emailVerifiedAt);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('rejects duplicate email', async () => {
|
|
43
|
+
const email = emailFor('dup');
|
|
44
|
+
await createUser({email, hashedPassword: 'h'});
|
|
45
|
+
|
|
46
|
+
await expect(createUser({email, hashedPassword: 'h2'})).rejects.toThrow();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('remaps email unique violation to EmailTakenError', async () => {
|
|
50
|
+
const email = emailFor('race');
|
|
51
|
+
await createUser({email, hashedPassword: 'h'});
|
|
52
|
+
|
|
53
|
+
await expect(createUser({email, hashedPassword: 'h2'})).rejects.toBeInstanceOf(EmailTakenError);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('updateUserPassword updates the hashed password', async () => {
|
|
57
|
+
const user = await createUser({email: emailFor('pwbump'), hashedPassword: 'old'});
|
|
58
|
+
|
|
59
|
+
const updated = await updateUserPassword({userId: user.id, hashedPassword: 'new'});
|
|
60
|
+
|
|
61
|
+
expect(updated?.hashedPassword).toBe('new');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('markEmailVerified sets emailVerifiedAt', async () => {
|
|
65
|
+
const user = await createUser({email: emailFor('verify'), hashedPassword: 'h'});
|
|
66
|
+
|
|
67
|
+
const verified = await markEmailVerified({userId: user.id});
|
|
68
|
+
|
|
69
|
+
expect(verified?.emailVerifiedAt).toBeInstanceOf(Date);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('opaque token helpers remain available', () => {
|
|
73
|
+
expect(typeof hashOpaqueToken).toBe('function');
|
|
74
|
+
});
|
|
75
|
+
});
|
package/src/db/users.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {eq} from 'drizzle-orm';
|
|
2
|
+
import type {User} from '#core/entities/user.js';
|
|
3
|
+
import {EmailTakenError} from '#core/errors.js';
|
|
4
|
+
import {db} from './db.js';
|
|
5
|
+
import {toUser, type UserDb, users} from './schema/users.js';
|
|
6
|
+
|
|
7
|
+
export interface CreateUserParams {
|
|
8
|
+
email: string;
|
|
9
|
+
hashedPassword: string | null;
|
|
10
|
+
name?: string | null;
|
|
11
|
+
emailVerifiedAt?: Date | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ProvisionUserParams {
|
|
15
|
+
email: string;
|
|
16
|
+
name?: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Drizzle wraps the underlying Postgres error; walk `.cause` to reach it.
|
|
20
|
+
function isAuthUsersEmailUniqueViolation(error: unknown): boolean {
|
|
21
|
+
let current: unknown = error;
|
|
22
|
+
for (let depth = 0; depth < 5 && current != null; depth += 1) {
|
|
23
|
+
if (typeof current !== 'object') return false;
|
|
24
|
+
const {code, constraint} = current as {code?: unknown; constraint?: unknown};
|
|
25
|
+
if (code === '23505' && constraint === 'auth_users_email_unique') return true;
|
|
26
|
+
current = (current as {cause?: unknown}).cause;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function createUser(params: CreateUserParams): Promise<User> {
|
|
32
|
+
let rows: UserDb[];
|
|
33
|
+
try {
|
|
34
|
+
rows = await db()
|
|
35
|
+
.insert(users)
|
|
36
|
+
.values({
|
|
37
|
+
email: params.email,
|
|
38
|
+
hashedPassword: params.hashedPassword,
|
|
39
|
+
name: params.name ?? null,
|
|
40
|
+
emailVerifiedAt: params.emailVerifiedAt ?? null,
|
|
41
|
+
})
|
|
42
|
+
.returning();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (isAuthUsersEmailUniqueViolation(error)) {
|
|
45
|
+
throw new EmailTakenError(params.email);
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const row = rows[0];
|
|
51
|
+
if (!row) throw new Error('Insert returned no rows');
|
|
52
|
+
return toUser(row);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function provisionUser(params: ProvisionUserParams): Promise<User> {
|
|
56
|
+
const rows = await db()
|
|
57
|
+
.insert(users)
|
|
58
|
+
.values({
|
|
59
|
+
email: params.email,
|
|
60
|
+
hashedPassword: null,
|
|
61
|
+
name: params.name ?? null,
|
|
62
|
+
emailVerifiedAt: new Date(),
|
|
63
|
+
})
|
|
64
|
+
.onConflictDoNothing({target: users.email})
|
|
65
|
+
.returning();
|
|
66
|
+
|
|
67
|
+
const row = rows[0];
|
|
68
|
+
if (row) return toUser(row);
|
|
69
|
+
|
|
70
|
+
const existing = await findUserByEmail({email: params.email});
|
|
71
|
+
if (existing) return existing;
|
|
72
|
+
throw new Error('Provisioning user conflict returned no user');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function findUserByEmail(params: {email: string}): Promise<User | undefined> {
|
|
76
|
+
const rows = await db().select().from(users).where(eq(users.email, params.email)).limit(1);
|
|
77
|
+
const row = rows[0];
|
|
78
|
+
if (!row) return undefined;
|
|
79
|
+
return toUser(row);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function findUserById(params: {id: string}): Promise<User | undefined> {
|
|
83
|
+
const rows = await db().select().from(users).where(eq(users.id, params.id)).limit(1);
|
|
84
|
+
const row = rows[0];
|
|
85
|
+
if (!row) return undefined;
|
|
86
|
+
return toUser(row);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface UpdateUserPasswordParams {
|
|
90
|
+
userId: string;
|
|
91
|
+
hashedPassword: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function updateUserPassword(
|
|
95
|
+
params: UpdateUserPasswordParams,
|
|
96
|
+
): Promise<User | undefined> {
|
|
97
|
+
const rows = await db()
|
|
98
|
+
.update(users)
|
|
99
|
+
.set({
|
|
100
|
+
hashedPassword: params.hashedPassword,
|
|
101
|
+
updatedAt: new Date(),
|
|
102
|
+
})
|
|
103
|
+
.where(eq(users.id, params.userId))
|
|
104
|
+
.returning();
|
|
105
|
+
|
|
106
|
+
const row = rows[0];
|
|
107
|
+
if (!row) return undefined;
|
|
108
|
+
return toUser(row);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function markEmailVerified(params: {userId: string}): Promise<User | undefined> {
|
|
112
|
+
const rows = await db()
|
|
113
|
+
.update(users)
|
|
114
|
+
.set({emailVerifiedAt: new Date(), updatedAt: new Date()})
|
|
115
|
+
.where(eq(users.id, params.userId))
|
|
116
|
+
.returning();
|
|
117
|
+
|
|
118
|
+
const row = rows[0];
|
|
119
|
+
if (!row) return undefined;
|
|
120
|
+
return toUser(row);
|
|
121
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
|
|
3
|
+
AUTH_PASSWORD_RESET_SEND_REQUESTED,
|
|
4
|
+
authEventSchemas,
|
|
5
|
+
} from '@shipfox/api-auth-dto';
|
|
6
|
+
import {authModule} from './index.js';
|
|
7
|
+
|
|
8
|
+
vi.mock('#config.js', () => ({
|
|
9
|
+
config: {
|
|
10
|
+
AUTH_JWT_SECRET: 'auth-module-test-secret',
|
|
11
|
+
AUTH_JWT_EXPIRES_IN: '15m',
|
|
12
|
+
AUTH_JOB_LEASE_TOKEN_SECRET: 'auth-module-test-job-secret',
|
|
13
|
+
AUTH_JOB_LEASE_TOKEN_EXPIRES_IN: '90m',
|
|
14
|
+
AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS: 14,
|
|
15
|
+
AUTH_REFRESH_ROTATION_GRACE_SECONDS: 30,
|
|
16
|
+
AUTH_REFRESH_COOKIE_NAME: 'shipfox_refresh_token',
|
|
17
|
+
CLIENT_BASE_URL: 'https://app.example.test',
|
|
18
|
+
},
|
|
19
|
+
mailer: {send: vi.fn()},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe('authModule', () => {
|
|
23
|
+
test('registers auth email outbox publisher and subscribers', () => {
|
|
24
|
+
const publisher = authModule.publishers?.find((pub) => pub.name === 'auth');
|
|
25
|
+
const events = authModule.subscribers?.map((subscriber) => subscriber.event);
|
|
26
|
+
|
|
27
|
+
expect(publisher?.eventSchemas).toBe(authEventSchemas);
|
|
28
|
+
expect(Object.keys(publisher?.eventSchemas ?? {}).sort()).toEqual([
|
|
29
|
+
AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
|
|
30
|
+
AUTH_PASSWORD_RESET_SEND_REQUESTED,
|
|
31
|
+
]);
|
|
32
|
+
expect(events).toEqual(
|
|
33
|
+
expect.arrayContaining([
|
|
34
|
+
AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
|
|
35
|
+
AUTH_PASSWORD_RESET_SEND_REQUESTED,
|
|
36
|
+
]),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
|
|
3
|
+
AUTH_PASSWORD_RESET_SEND_REQUESTED,
|
|
4
|
+
type AuthEventMap,
|
|
5
|
+
authEventSchemas,
|
|
6
|
+
} from '@shipfox/api-auth-dto';
|
|
7
|
+
import type {ShipfoxModule} from '@shipfox/node-module';
|
|
8
|
+
import {subscriberFactory} from '@shipfox/node-module';
|
|
9
|
+
import {db} from '#db/db.js';
|
|
10
|
+
import {migrationsPath} from '#db/migrations.js';
|
|
11
|
+
import {authOutbox} from '#db/schema/outbox.js';
|
|
12
|
+
import {createJwtAuthMethod} from '#presentation/auth/jwt-auth.js';
|
|
13
|
+
import {createLeaseTokenAuthMethod} from '#presentation/auth/lease-token-auth.js';
|
|
14
|
+
import {createRunnerSessionAuthMethod} from '#presentation/auth/runner-session-auth.js';
|
|
15
|
+
import {authE2eRoutes} from '#presentation/e2eRoutes/index.js';
|
|
16
|
+
import {authRoutes} from '#presentation/routes/index.js';
|
|
17
|
+
import {
|
|
18
|
+
onEmailVerificationSendRequested,
|
|
19
|
+
onPasswordResetSendRequested,
|
|
20
|
+
} from '#presentation/subscribers/index.js';
|
|
21
|
+
|
|
22
|
+
export type {JobLeaseTokenClaims, RunnerSessionTokenClaims} from '@shipfox/api-auth-dto';
|
|
23
|
+
export type {
|
|
24
|
+
CreateSessionForUserError,
|
|
25
|
+
CreateSessionForUserParams,
|
|
26
|
+
CreateSessionForUserResult,
|
|
27
|
+
ProvisionUserParams,
|
|
28
|
+
} from '#core/auth.js';
|
|
29
|
+
export {createSessionForUser, provisionUser} from '#core/auth.js';
|
|
30
|
+
export type {User, UserStatus} from '#core/entities/user.js';
|
|
31
|
+
export {
|
|
32
|
+
AuthDependencyUnavailableError,
|
|
33
|
+
EmailNotVerifiedError,
|
|
34
|
+
InvalidCredentialsError,
|
|
35
|
+
UserNotFoundError,
|
|
36
|
+
} from '#core/errors.js';
|
|
37
|
+
export {
|
|
38
|
+
issueJobLeaseToken,
|
|
39
|
+
jobLeaseParamsFrom,
|
|
40
|
+
verifyJobLeaseToken,
|
|
41
|
+
} from '#core/job-lease-token.js';
|
|
42
|
+
export {
|
|
43
|
+
issueRunnerSessionToken,
|
|
44
|
+
verifyRunnerSessionToken,
|
|
45
|
+
} from '#core/runner-session-token.js';
|
|
46
|
+
export {createJwtAuthMethod} from '#presentation/auth/jwt-auth.js';
|
|
47
|
+
export {createLeaseTokenAuthMethod} from '#presentation/auth/lease-token-auth.js';
|
|
48
|
+
export {
|
|
49
|
+
authCookiePlugin,
|
|
50
|
+
clearRefreshTokenCookie,
|
|
51
|
+
getRefreshTokenCookie,
|
|
52
|
+
setRefreshTokenCookie,
|
|
53
|
+
} from '#presentation/auth/refresh-cookie.js';
|
|
54
|
+
export {createRunnerSessionAuthMethod} from '#presentation/auth/runner-session-auth.js';
|
|
55
|
+
|
|
56
|
+
const subscriber = subscriberFactory<AuthEventMap>();
|
|
57
|
+
|
|
58
|
+
export const authModule: ShipfoxModule = {
|
|
59
|
+
name: 'auth',
|
|
60
|
+
database: {db, migrationsPath},
|
|
61
|
+
auth: [createJwtAuthMethod(), createLeaseTokenAuthMethod(), createRunnerSessionAuthMethod()],
|
|
62
|
+
routes: [authRoutes],
|
|
63
|
+
e2eRoutes: [authE2eRoutes],
|
|
64
|
+
publishers: [{name: 'auth', table: authOutbox, db, eventSchemas: authEventSchemas}],
|
|
65
|
+
subscribers: [
|
|
66
|
+
subscriber(AUTH_EMAIL_VERIFICATION_SEND_REQUESTED, onEmailVerificationSendRequested),
|
|
67
|
+
subscriber(AUTH_PASSWORD_RESET_SEND_REQUESTED, onPasswordResetSendRequested),
|
|
68
|
+
],
|
|
69
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type AuthRateLimitAction,
|
|
3
|
+
type AuthRateLimitOutcome,
|
|
4
|
+
type AuthRateLimitScope,
|
|
5
|
+
type AuthTokenRefreshOutcome,
|
|
6
|
+
type AuthTokenType,
|
|
7
|
+
type AuthTokenVerificationOutcome,
|
|
8
|
+
recordAuthRateLimitCheck,
|
|
9
|
+
recordAuthRateLimitPruneFailure,
|
|
10
|
+
recordTokenIssued,
|
|
11
|
+
recordTokenRefreshed,
|
|
12
|
+
recordTokenVerified,
|
|
13
|
+
} from './instance.js';
|