@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,89 @@
|
|
|
1
|
+
import type {FastifyInstance} from 'fastify';
|
|
2
|
+
import {
|
|
3
|
+
cookieHeader,
|
|
4
|
+
createAuthTestApp,
|
|
5
|
+
listMembershipsByUserMock,
|
|
6
|
+
resetCapturedMail,
|
|
7
|
+
signupVerifyLogin,
|
|
8
|
+
} from '#test/routes.js';
|
|
9
|
+
|
|
10
|
+
describe('POST /auth/refresh', () => {
|
|
11
|
+
let app: FastifyInstance;
|
|
12
|
+
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
app = await createAuthTestApp();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
resetCapturedMail();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterAll(async () => {
|
|
22
|
+
await app.close();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('returns a fresh access token and rotates the refresh cookie', async () => {
|
|
26
|
+
const account = await signupVerifyLogin(app, 'refresh');
|
|
27
|
+
|
|
28
|
+
const res = await app.inject({
|
|
29
|
+
method: 'POST',
|
|
30
|
+
url: '/auth/refresh',
|
|
31
|
+
headers: {cookie: cookieHeader(account.refreshCookie)},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(res.statusCode).toBe(200);
|
|
35
|
+
expect(res.json().token).toBeDefined();
|
|
36
|
+
expect(res.json().user.email).toBe(account.email);
|
|
37
|
+
expect(res.headers['set-cookie']).toContain('shipfox_refresh_token=');
|
|
38
|
+
expect(res.headers['set-cookie']).toContain('HttpOnly');
|
|
39
|
+
expect(res.headers['set-cookie']).toContain('Secure');
|
|
40
|
+
expect(res.headers['set-cookie']).toContain('SameSite=Lax');
|
|
41
|
+
expect(res.headers['set-cookie']).toContain('Path=/auth');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('transforms missing refresh cookie into 401', async () => {
|
|
45
|
+
const res = await app.inject({
|
|
46
|
+
method: 'POST',
|
|
47
|
+
url: '/auth/refresh',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(res.statusCode).toBe(401);
|
|
51
|
+
expect(res.json().code).toBe('unauthorized');
|
|
52
|
+
expect(res.headers['set-cookie']).toContain('shipfox_refresh_token=;');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('tolerates a concurrent reuse within the grace window without rotating the cookie', async () => {
|
|
56
|
+
const account = await signupVerifyLogin(app, 'refresh-concurrent');
|
|
57
|
+
await app.inject({
|
|
58
|
+
method: 'POST',
|
|
59
|
+
url: '/auth/refresh',
|
|
60
|
+
headers: {cookie: cookieHeader(account.refreshCookie)},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const raced = await app.inject({
|
|
64
|
+
method: 'POST',
|
|
65
|
+
url: '/auth/refresh',
|
|
66
|
+
headers: {cookie: cookieHeader(account.refreshCookie)},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// The racing tab gets a fresh access token but keeps the cookie the winning
|
|
70
|
+
// refresh already installed, so no Set-Cookie is emitted.
|
|
71
|
+
expect(raced.statusCode).toBe(200);
|
|
72
|
+
expect(raced.json().token).toBeDefined();
|
|
73
|
+
expect(raced.headers['set-cookie']).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('transforms membership dependency outages into 503', async () => {
|
|
77
|
+
const account = await signupVerifyLogin(app, 'refresh-workspaces-down');
|
|
78
|
+
listMembershipsByUserMock.mockRejectedValueOnce(new Error('workspaces DB down'));
|
|
79
|
+
|
|
80
|
+
const res = await app.inject({
|
|
81
|
+
method: 'POST',
|
|
82
|
+
url: '/auth/refresh',
|
|
83
|
+
headers: {cookie: cookieHeader(account.refreshCookie)},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(res.statusCode).toBe(503);
|
|
87
|
+
expect(res.json().code).toBe('auth-dependency-unavailable');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {refreshResponseSchema} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
3
|
+
import {refreshAccessToken} from '#core/auth.js';
|
|
4
|
+
import {AuthDependencyUnavailableError, TokenInvalidError} from '#core/errors.js';
|
|
5
|
+
import {
|
|
6
|
+
clearRefreshTokenCookie,
|
|
7
|
+
getRefreshTokenCookie,
|
|
8
|
+
setRefreshTokenCookie,
|
|
9
|
+
} from '#presentation/auth/refresh-cookie.js';
|
|
10
|
+
import {toAuthSessionDto} from '#presentation/dto/user.js';
|
|
11
|
+
|
|
12
|
+
export const refreshRoute = defineRoute({
|
|
13
|
+
method: 'POST',
|
|
14
|
+
path: '/refresh',
|
|
15
|
+
description: 'Refresh the current browser session access token.',
|
|
16
|
+
schema: {
|
|
17
|
+
response: {
|
|
18
|
+
200: refreshResponseSchema,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
errorHandler: (error, _request, reply) => {
|
|
22
|
+
if (error instanceof TokenInvalidError) {
|
|
23
|
+
clearRefreshTokenCookie(reply);
|
|
24
|
+
throw new ClientError('Refresh token is invalid or expired', 'unauthorized', {
|
|
25
|
+
status: 401,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (error instanceof AuthDependencyUnavailableError) {
|
|
29
|
+
throw new ClientError(
|
|
30
|
+
'Authentication dependency unavailable',
|
|
31
|
+
'auth-dependency-unavailable',
|
|
32
|
+
{
|
|
33
|
+
status: 503,
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
},
|
|
39
|
+
handler: async (request, reply) => {
|
|
40
|
+
const refreshToken = getRefreshTokenCookie(request);
|
|
41
|
+
if (!refreshToken) {
|
|
42
|
+
clearRefreshTokenCookie(reply);
|
|
43
|
+
throw new ClientError('Refresh token is invalid or expired', 'unauthorized', {status: 401});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const result = await refreshAccessToken({refreshToken});
|
|
47
|
+
|
|
48
|
+
// Absent on a grace-window hit: keep the existing cookie rather than overwrite it.
|
|
49
|
+
if (result.refreshToken) {
|
|
50
|
+
setRefreshTokenCookie(reply, result.refreshToken);
|
|
51
|
+
}
|
|
52
|
+
return toAuthSessionDto(result);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type {Mailer, MailMessage} from '@shipfox/node-mailer';
|
|
2
|
+
import {onEmailVerificationSendRequested, onPasswordResetSendRequested} from './index.js';
|
|
3
|
+
|
|
4
|
+
const testMailer = vi.hoisted(
|
|
5
|
+
(): {
|
|
6
|
+
captured: MailMessage[];
|
|
7
|
+
send: ReturnType<typeof vi.fn<Mailer['send']>>;
|
|
8
|
+
mailer: Mailer;
|
|
9
|
+
} => {
|
|
10
|
+
const captured: MailMessage[] = [];
|
|
11
|
+
const send = vi.fn<Mailer['send']>((message) => {
|
|
12
|
+
captured.push(message);
|
|
13
|
+
return Promise.resolve();
|
|
14
|
+
});
|
|
15
|
+
return {captured, send, mailer: {send}};
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
vi.mock('#config.js', () => ({
|
|
20
|
+
mailer: testMailer.mailer,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
describe('auth email subscribers', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
testMailer.captured.length = 0;
|
|
26
|
+
testMailer.send.mockReset();
|
|
27
|
+
testMailer.send.mockImplementation((message) => {
|
|
28
|
+
testMailer.captured.push(message);
|
|
29
|
+
return Promise.resolve();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('sends a rendered email verification message', async () => {
|
|
34
|
+
await onEmailVerificationSendRequested({
|
|
35
|
+
email: 'verify@example.com',
|
|
36
|
+
verifyLink: 'https://app.example.test/auth/verify-email?token=verify-token',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(testMailer.captured).toHaveLength(1);
|
|
40
|
+
expect(testMailer.captured[0]).toMatchObject({
|
|
41
|
+
to: 'verify@example.com',
|
|
42
|
+
subject: 'Verify your email',
|
|
43
|
+
});
|
|
44
|
+
expect(testMailer.captured[0]?.text).toContain('/auth/verify-email?token=verify-token');
|
|
45
|
+
expect(testMailer.captured[0]?.html).toContain('/auth/verify-email?token');
|
|
46
|
+
expect(testMailer.captured[0]?.html).toContain('verify-token');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('sends a rendered password reset message', async () => {
|
|
50
|
+
await onPasswordResetSendRequested({
|
|
51
|
+
email: 'reset@example.com',
|
|
52
|
+
resetLink: 'https://app.example.test/auth/reset?token=reset-token',
|
|
53
|
+
expiresInHours: 1,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(testMailer.captured).toHaveLength(1);
|
|
57
|
+
expect(testMailer.captured[0]).toMatchObject({
|
|
58
|
+
to: 'reset@example.com',
|
|
59
|
+
subject: 'Reset your password',
|
|
60
|
+
});
|
|
61
|
+
expect(testMailer.captured[0]?.text).toContain('/auth/reset?token=reset-token');
|
|
62
|
+
expect(testMailer.captured[0]?.html).toContain('/auth/reset?token');
|
|
63
|
+
expect(testMailer.captured[0]?.html).toContain('reset-token');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('rethrows mailer failures so the outbox dispatcher retries', async () => {
|
|
67
|
+
const failure = new Error('smtp unavailable');
|
|
68
|
+
testMailer.send.mockRejectedValueOnce(failure);
|
|
69
|
+
|
|
70
|
+
const promise = onEmailVerificationSendRequested({
|
|
71
|
+
email: 'verify-failure@example.com',
|
|
72
|
+
verifyLink: 'https://app.example.test/auth/verify-email?token=verify-token',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await expect(promise).rejects.toBe(failure);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type {AuthEmailVerificationSendRequestedEvent} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {renderEmail} from '@shipfox/node-email';
|
|
3
|
+
import {mailer} from '#config.js';
|
|
4
|
+
|
|
5
|
+
export async function onEmailVerificationSendRequested(
|
|
6
|
+
payload: AuthEmailVerificationSendRequestedEvent,
|
|
7
|
+
): Promise<void> {
|
|
8
|
+
const email = await renderEmail('verify-email', {verifyLink: payload.verifyLink});
|
|
9
|
+
await mailer.send({to: payload.email, ...email});
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type {AuthPasswordResetSendRequestedEvent} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {renderEmail} from '@shipfox/node-email';
|
|
3
|
+
import {mailer} from '#config.js';
|
|
4
|
+
|
|
5
|
+
export async function onPasswordResetSendRequested(
|
|
6
|
+
payload: AuthPasswordResetSendRequestedEvent,
|
|
7
|
+
): Promise<void> {
|
|
8
|
+
const email = await renderEmail('reset-password', {
|
|
9
|
+
resetLink: payload.resetLink,
|
|
10
|
+
expiresInHours: payload.expiresInHours,
|
|
11
|
+
});
|
|
12
|
+
await mailer.send({to: payload.email, ...email});
|
|
13
|
+
}
|
package/test/env.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
process.env.POSTGRES_HOST ??= 'localhost';
|
|
2
|
+
process.env.POSTGRES_PORT ??= '5432';
|
|
3
|
+
process.env.POSTGRES_USERNAME ??= 'shipfox';
|
|
4
|
+
process.env.POSTGRES_PASSWORD ??= 'password';
|
|
5
|
+
process.env.POSTGRES_DATABASE = 'api_test';
|
|
6
|
+
process.env.POSTGRES_MAX_CONNECTIONS ??= '5';
|
|
7
|
+
process.env.AUTH_JWT_SECRET = 'test-secret';
|
|
8
|
+
process.env.AUTH_JOB_LEASE_TOKEN_SECRET = 'test-lease-secret';
|
|
9
|
+
process.env.AUTH_RUNNER_SESSION_TOKEN_SECRET = 'test-runner-session-secret';
|
|
10
|
+
process.env.TZ = 'UTC';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {userFactory} from './user.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {Factory} from 'fishery';
|
|
2
|
+
import type {User} from '#core/entities/user.js';
|
|
3
|
+
import {hashPassword} from '#core/password.js';
|
|
4
|
+
import {createUser, markEmailVerified} from '#db/users.js';
|
|
5
|
+
|
|
6
|
+
export const userFactory = Factory.define<User & {plainPassword: string}>(
|
|
7
|
+
({sequence, onCreate, params}) => {
|
|
8
|
+
const plainPassword = params.plainPassword ?? 'correct horse battery staple';
|
|
9
|
+
|
|
10
|
+
onCreate(async (user) => {
|
|
11
|
+
const hashedPassword = await hashPassword({password: plainPassword});
|
|
12
|
+
const created = await createUser({
|
|
13
|
+
email: user.email,
|
|
14
|
+
hashedPassword,
|
|
15
|
+
name: user.name,
|
|
16
|
+
});
|
|
17
|
+
if (user.emailVerifiedAt) {
|
|
18
|
+
const verified = await markEmailVerified({userId: created.id});
|
|
19
|
+
return {...(verified ?? created), plainPassword};
|
|
20
|
+
}
|
|
21
|
+
return {...created, plainPassword};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
id: crypto.randomUUID(),
|
|
26
|
+
email: `user-${sequence}-${crypto.randomUUID()}@example.com`,
|
|
27
|
+
hashedPassword: 'placeholder',
|
|
28
|
+
name: `User ${sequence}`,
|
|
29
|
+
emailVerifiedAt: null,
|
|
30
|
+
status: 'active',
|
|
31
|
+
createdAt: new Date(),
|
|
32
|
+
updatedAt: new Date(),
|
|
33
|
+
plainPassword,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import './env.js';
|
|
2
|
+
import {runMigrations} from '@shipfox/node-drizzle';
|
|
3
|
+
import {closePostgresClient, createPostgresClient} from '@shipfox/node-postgres';
|
|
4
|
+
import {sql} from 'drizzle-orm';
|
|
5
|
+
import {closeDb, db} from '#db/db.js';
|
|
6
|
+
import {migrationsPath} from '#db/migrations.js';
|
|
7
|
+
|
|
8
|
+
export async function setup() {
|
|
9
|
+
createPostgresClient();
|
|
10
|
+
|
|
11
|
+
await runMigrations(db(), migrationsPath, '__drizzle_migrations_auth');
|
|
12
|
+
await db().execute(
|
|
13
|
+
sql`TRUNCATE auth_outbox, auth_email_verifications, auth_password_resets, auth_rate_limits, auth_refresh_tokens, auth_users CASCADE`,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
closeDb();
|
|
17
|
+
await closePostgresClient();
|
|
18
|
+
}
|
package/test/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {userFactory} from './factories/index.js';
|
package/test/routes.ts
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import type {OutgoingHttpHeaders} from 'node:http';
|
|
2
|
+
import {
|
|
3
|
+
AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
|
|
4
|
+
type AUTH_PASSWORD_RESET_SEND_REQUESTED,
|
|
5
|
+
} from '@shipfox/api-auth-dto';
|
|
6
|
+
import {type AppConfig, createApp, type FastifyInstance} from '@shipfox/node-fastify';
|
|
7
|
+
import type {Mailer, MailMessage} from '@shipfox/node-mailer';
|
|
8
|
+
import {and, desc, eq, sql} from 'drizzle-orm';
|
|
9
|
+
import {db} from '#db/db.js';
|
|
10
|
+
import {authOutbox} from '#db/schema/outbox.js';
|
|
11
|
+
import {createJwtAuthMethod} from '#presentation/auth/jwt-auth.js';
|
|
12
|
+
import {authRoutes} from '#presentation/routes/index.js';
|
|
13
|
+
|
|
14
|
+
const testConfig = vi.hoisted(
|
|
15
|
+
(): {
|
|
16
|
+
captured: MailMessage[];
|
|
17
|
+
mailer: Mailer;
|
|
18
|
+
secret: string;
|
|
19
|
+
clientBaseUrl: string;
|
|
20
|
+
} => {
|
|
21
|
+
const captured: MailMessage[] = [];
|
|
22
|
+
const mailer: Mailer = {
|
|
23
|
+
send: (message) => {
|
|
24
|
+
captured.push(message);
|
|
25
|
+
return Promise.resolve();
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
captured,
|
|
30
|
+
mailer,
|
|
31
|
+
secret: 'route-tests-secret',
|
|
32
|
+
clientBaseUrl: 'https://app.example.test',
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const workspaceTestDoubles = vi.hoisted(() => {
|
|
38
|
+
class InvitationEmailMismatchError extends Error {
|
|
39
|
+
constructor(message = 'Invitation email does not match') {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = 'InvitationEmailMismatchError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class TokenAlreadyUsedError extends Error {
|
|
46
|
+
constructor(message = 'Invitation token has already been used') {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = 'TokenAlreadyUsedError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class TokenExpiredError extends Error {
|
|
53
|
+
constructor(message = 'Invitation token has expired') {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = 'TokenExpiredError';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class TokenInvalidError extends Error {
|
|
60
|
+
constructor(message = 'Invitation token is invalid') {
|
|
61
|
+
super(message);
|
|
62
|
+
this.name = 'TokenInvalidError';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
acceptWorkspaceInvitation: vi.fn(),
|
|
68
|
+
peekInvitationByRawToken: vi.fn(),
|
|
69
|
+
listMembershipsByUser: vi.fn(() => Promise.resolve([])),
|
|
70
|
+
InvitationEmailMismatchError,
|
|
71
|
+
TokenAlreadyUsedError,
|
|
72
|
+
TokenExpiredError,
|
|
73
|
+
TokenInvalidError,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
vi.mock('#config.js', () => ({
|
|
78
|
+
config: {
|
|
79
|
+
AUTH_JWT_SECRET: testConfig.secret,
|
|
80
|
+
AUTH_JWT_EXPIRES_IN: '15m',
|
|
81
|
+
AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS: 14,
|
|
82
|
+
AUTH_REFRESH_ROTATION_GRACE_SECONDS: 30,
|
|
83
|
+
AUTH_REFRESH_COOKIE_NAME: 'shipfox_refresh_token',
|
|
84
|
+
CLIENT_BASE_URL: testConfig.clientBaseUrl,
|
|
85
|
+
},
|
|
86
|
+
mailer: testConfig.mailer,
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
vi.mock('@shipfox/api-workspaces', () => ({
|
|
90
|
+
acceptWorkspaceInvitation: workspaceTestDoubles.acceptWorkspaceInvitation,
|
|
91
|
+
peekInvitationByRawToken: workspaceTestDoubles.peekInvitationByRawToken,
|
|
92
|
+
InvitationEmailMismatchError: workspaceTestDoubles.InvitationEmailMismatchError,
|
|
93
|
+
listMembershipsByUser: workspaceTestDoubles.listMembershipsByUser,
|
|
94
|
+
TokenAlreadyUsedError: workspaceTestDoubles.TokenAlreadyUsedError,
|
|
95
|
+
TokenExpiredError: workspaceTestDoubles.TokenExpiredError,
|
|
96
|
+
TokenInvalidError: workspaceTestDoubles.TokenInvalidError,
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
const {acceptWorkspaceInvitation, peekInvitationByRawToken, listMembershipsByUser} = await import(
|
|
100
|
+
'@shipfox/api-workspaces'
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const TOKEN_RE = /token=([\w\-_=]+)/;
|
|
104
|
+
type AuthEmailEventType =
|
|
105
|
+
| typeof AUTH_EMAIL_VERIFICATION_SEND_REQUESTED
|
|
106
|
+
| typeof AUTH_PASSWORD_RESET_SEND_REQUESTED;
|
|
107
|
+
|
|
108
|
+
export const ROUTE_TEST_SECRET = testConfig.secret;
|
|
109
|
+
export const acceptWorkspaceInvitationMock = vi.mocked(acceptWorkspaceInvitation);
|
|
110
|
+
export const peekInvitationByRawTokenMock = vi.mocked(peekInvitationByRawToken);
|
|
111
|
+
export const listMembershipsByUserMock = vi.mocked(listMembershipsByUser);
|
|
112
|
+
export const workspaceErrors = {
|
|
113
|
+
InvitationEmailMismatchError: workspaceTestDoubles.InvitationEmailMismatchError,
|
|
114
|
+
TokenAlreadyUsedError: workspaceTestDoubles.TokenAlreadyUsedError,
|
|
115
|
+
TokenExpiredError: workspaceTestDoubles.TokenExpiredError,
|
|
116
|
+
TokenInvalidError: workspaceTestDoubles.TokenInvalidError,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export function resetCapturedMail(): void {
|
|
120
|
+
testConfig.captured.length = 0;
|
|
121
|
+
acceptWorkspaceInvitationMock.mockReset();
|
|
122
|
+
peekInvitationByRawTokenMock.mockReset();
|
|
123
|
+
listMembershipsByUserMock.mockReset();
|
|
124
|
+
listMembershipsByUserMock.mockResolvedValue([]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function capturedMail(): MailMessage[] {
|
|
128
|
+
return testConfig.captured;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function outboxEventsTo(email: string, eventType: AuthEmailEventType) {
|
|
132
|
+
return await db()
|
|
133
|
+
.select()
|
|
134
|
+
.from(authOutbox)
|
|
135
|
+
.where(
|
|
136
|
+
and(eq(authOutbox.eventType, eventType), sql`${authOutbox.payload}->>'email' = ${email}`),
|
|
137
|
+
)
|
|
138
|
+
.orderBy(desc(authOutbox.createdAt));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function uniqueEmail(prefix: string): string {
|
|
142
|
+
return `${prefix}-${crypto.randomUUID()}@example.com`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function extractToken(url: string): string {
|
|
146
|
+
const match = url.match(TOKEN_RE);
|
|
147
|
+
expect(match?.[1]).toBeDefined();
|
|
148
|
+
return match?.[1] ?? '';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function getSetCookie(res: {headers: OutgoingHttpHeaders}): string {
|
|
152
|
+
const header = res.headers['set-cookie'];
|
|
153
|
+
const value = Array.isArray(header) ? header[0] : typeof header === 'string' ? header : undefined;
|
|
154
|
+
expect(value).toBeDefined();
|
|
155
|
+
return value ?? '';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function cookieHeader(setCookie: string): string {
|
|
159
|
+
const [cookie] = setCookie.split(';');
|
|
160
|
+
expect(cookie).toBeDefined();
|
|
161
|
+
return cookie ?? '';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function latestMailTo(email: string): MailMessage {
|
|
165
|
+
const message = [...testConfig.captured].reverse().find((mail) => mail.to === email);
|
|
166
|
+
expect(message).toBeDefined();
|
|
167
|
+
return message as MailMessage;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function latestEmailLinkTo(
|
|
171
|
+
email: string,
|
|
172
|
+
eventType: AuthEmailEventType,
|
|
173
|
+
): Promise<string> {
|
|
174
|
+
const event = (await outboxEventsTo(email, eventType))[0];
|
|
175
|
+
expect(event).toBeDefined();
|
|
176
|
+
const payload = event?.payload as {verifyLink?: string; resetLink?: string} | undefined;
|
|
177
|
+
const link =
|
|
178
|
+
eventType === AUTH_EMAIL_VERIFICATION_SEND_REQUESTED ? payload?.verifyLink : payload?.resetLink;
|
|
179
|
+
expect(link).toBeDefined();
|
|
180
|
+
return link ?? '';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function createAuthTestApp(params?: {
|
|
184
|
+
fastifyOptions?: AppConfig['fastifyOptions'];
|
|
185
|
+
}): Promise<FastifyInstance> {
|
|
186
|
+
const appConfig: AppConfig = {
|
|
187
|
+
auth: [createJwtAuthMethod()],
|
|
188
|
+
routes: [authRoutes],
|
|
189
|
+
swagger: false,
|
|
190
|
+
};
|
|
191
|
+
if (params?.fastifyOptions) appConfig.fastifyOptions = params.fastifyOptions;
|
|
192
|
+
return await createApp(appConfig);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function signup(
|
|
196
|
+
app: FastifyInstance,
|
|
197
|
+
params: {email: string; password: string; name?: string},
|
|
198
|
+
) {
|
|
199
|
+
return await app.inject({
|
|
200
|
+
method: 'POST',
|
|
201
|
+
url: '/auth/signup',
|
|
202
|
+
payload: {name: 'Test User', ...params},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function verifyEmail(app: FastifyInstance, email: string): Promise<void> {
|
|
207
|
+
const token = extractToken(
|
|
208
|
+
await latestEmailLinkTo(email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const res = await app.inject({
|
|
212
|
+
method: 'POST',
|
|
213
|
+
url: '/auth/verify-email/confirm',
|
|
214
|
+
payload: {token},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(res.statusCode).toBe(200);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function login(app: FastifyInstance, params: {email: string; password: string}) {
|
|
221
|
+
return await app.inject({
|
|
222
|
+
method: 'POST',
|
|
223
|
+
url: '/auth/login',
|
|
224
|
+
payload: params,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function signupVerifyLogin(
|
|
229
|
+
app: FastifyInstance,
|
|
230
|
+
prefix: string,
|
|
231
|
+
): Promise<{
|
|
232
|
+
email: string;
|
|
233
|
+
password: string;
|
|
234
|
+
refreshCookie: string;
|
|
235
|
+
token: string;
|
|
236
|
+
userId: string;
|
|
237
|
+
}> {
|
|
238
|
+
const email = uniqueEmail(prefix);
|
|
239
|
+
const password = 'correct horse battery staple';
|
|
240
|
+
await signup(app, {email, password, name: prefix});
|
|
241
|
+
await verifyEmail(app, email);
|
|
242
|
+
|
|
243
|
+
const loginRes = await login(app, {email, password});
|
|
244
|
+
|
|
245
|
+
expect(loginRes.statusCode).toBe(200);
|
|
246
|
+
return {
|
|
247
|
+
email,
|
|
248
|
+
password,
|
|
249
|
+
refreshCookie: getSetCookie(loginRes),
|
|
250
|
+
token: loginRes.json().token,
|
|
251
|
+
userId: loginRes.json().user.id,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function createVerifiedSession(prefix: string): Promise<{
|
|
256
|
+
email: string;
|
|
257
|
+
password: string;
|
|
258
|
+
refreshCookie: string;
|
|
259
|
+
token: string;
|
|
260
|
+
userId: string;
|
|
261
|
+
}> {
|
|
262
|
+
const {createUser, createSessionForUser} = await import('#core/auth.js');
|
|
263
|
+
const email = uniqueEmail(prefix);
|
|
264
|
+
const password = 'correct horse battery staple';
|
|
265
|
+
const user = await createUser({email, password, name: prefix, verified: true});
|
|
266
|
+
const session = await createSessionForUser({userId: user.id});
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
email,
|
|
270
|
+
password,
|
|
271
|
+
refreshCookie: `shipfox_refresh_token=${session.refreshToken}`,
|
|
272
|
+
token: session.token,
|
|
273
|
+
userId: user.id,
|
|
274
|
+
};
|
|
275
|
+
}
|
package/test/setup.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import './env.js';
|
|
2
|
+
import {closePostgresClient, createPostgresClient} from '@shipfox/node-postgres';
|
|
3
|
+
import {afterAll, afterEach, beforeAll, vi} from '@shipfox/vitest/vi';
|
|
4
|
+
import {closeDb} from '#db/db.js';
|
|
5
|
+
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
createPostgresClient();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterAll(async () => {
|
|
15
|
+
closeDb();
|
|
16
|
+
await closePostgresClient();
|
|
17
|
+
});
|