@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +52 -0
- package/README.md +71 -37
- package/dist/config.d.ts +0 -12
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -62
- package/dist/config.js.map +1 -1
- package/dist/core/auth.d.ts +21 -2
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +109 -77
- package/dist/core/auth.js.map +1 -1
- package/dist/core/email-owner.d.ts +12 -0
- package/dist/core/email-owner.d.ts.map +1 -0
- package/dist/core/email-owner.js +20 -0
- package/dist/core/email-owner.js.map +1 -0
- package/dist/core/job-lease-token.d.ts.map +1 -1
- package/dist/core/job-lease-token.js +3 -2
- package/dist/core/job-lease-token.js.map +1 -1
- package/dist/core/jwt.d.ts +2 -2
- package/dist/core/jwt.d.ts.map +1 -1
- package/dist/core/jwt.js.map +1 -1
- package/dist/core/rate-limit.d.ts.map +1 -1
- package/dist/core/rate-limit.js +2 -7
- package/dist/core/rate-limit.js.map +1 -1
- package/dist/core/runner-session-token.d.ts.map +1 -1
- package/dist/core/runner-session-token.js +3 -2
- package/dist/core/runner-session-token.js.map +1 -1
- package/dist/db/db.d.ts +0 -218
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +0 -2
- package/dist/db/db.js.map +1 -1
- package/dist/db/users.d.ts +3 -0
- package/dist/db/users.d.ts.map +1 -1
- package/dist/db/users.js +28 -10
- package/dist/db/users.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -34
- package/dist/index.js.map +1 -1
- package/dist/presentation/auth/jwt-auth.d.ts.map +1 -1
- package/dist/presentation/auth/jwt-auth.js +2 -2
- package/dist/presentation/auth/jwt-auth.js.map +1 -1
- package/dist/presentation/e2eRoutes/create-session.d.ts +2 -1
- package/dist/presentation/e2eRoutes/create-session.d.ts.map +1 -1
- package/dist/presentation/e2eRoutes/create-session.js +42 -39
- package/dist/presentation/e2eRoutes/create-session.js.map +1 -1
- package/dist/presentation/e2eRoutes/index.d.ts +2 -1
- package/dist/presentation/e2eRoutes/index.d.ts.map +1 -1
- package/dist/presentation/e2eRoutes/index.js +12 -10
- package/dist/presentation/e2eRoutes/index.js.map +1 -1
- package/dist/presentation/inter-module.d.ts +4 -0
- package/dist/presentation/inter-module.d.ts.map +1 -0
- package/dist/presentation/inter-module.js +16 -0
- package/dist/presentation/inter-module.js.map +1 -0
- package/dist/presentation/routes/email-verification/verify-email-confirm.d.ts +2 -1
- package/dist/presentation/routes/email-verification/verify-email-confirm.d.ts.map +1 -1
- package/dist/presentation/routes/email-verification/verify-email-confirm.js +36 -25
- package/dist/presentation/routes/email-verification/verify-email-confirm.js.map +1 -1
- package/dist/presentation/routes/email-verification/verify-email-resend.d.ts.map +1 -1
- package/dist/presentation/routes/email-verification/verify-email-resend.js +20 -4
- package/dist/presentation/routes/email-verification/verify-email-resend.js.map +1 -1
- package/dist/presentation/routes/index.d.ts +2 -2
- package/dist/presentation/routes/index.d.ts.map +1 -1
- package/dist/presentation/routes/index.js +11 -13
- package/dist/presentation/routes/index.js.map +1 -1
- package/dist/presentation/routes/password/password-reset-confirm.d.ts +2 -1
- package/dist/presentation/routes/password/password-reset-confirm.d.ts.map +1 -1
- package/dist/presentation/routes/password/password-reset-confirm.js +29 -26
- package/dist/presentation/routes/password/password-reset-confirm.js.map +1 -1
- package/dist/presentation/routes/registration/signup.d.ts +2 -1
- package/dist/presentation/routes/registration/signup.d.ts.map +1 -1
- package/dist/presentation/routes/registration/signup.js +86 -68
- package/dist/presentation/routes/registration/signup.js.map +1 -1
- package/dist/presentation/routes/session/login.d.ts +2 -1
- package/dist/presentation/routes/session/login.d.ts.map +1 -1
- package/dist/presentation/routes/session/login.js +40 -37
- package/dist/presentation/routes/session/login.js.map +1 -1
- package/dist/presentation/routes/session/refresh.d.ts +2 -1
- package/dist/presentation/routes/session/refresh.d.ts.map +1 -1
- package/dist/presentation/routes/session/refresh.js +42 -39
- package/dist/presentation/routes/session/refresh.js.map +1 -1
- package/dist/presentation/subscribers/index.d.ts +0 -1
- package/dist/presentation/subscribers/index.d.ts.map +1 -1
- package/dist/presentation/subscribers/index.js +0 -1
- package/dist/presentation/subscribers/index.js.map +1 -1
- package/dist/presentation/subscribers/on-password-reset-send-requested.js +1 -1
- package/dist/presentation/subscribers/on-password-reset-send-requested.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0000_initial.sql +1 -13
- package/drizzle/meta/0000_snapshot.json +0 -92
- package/package.json +17 -13
- package/src/config.ts +0 -56
- package/src/core/auth.test.ts +106 -187
- package/src/core/auth.ts +131 -87
- package/src/core/email-owner.test.ts +86 -0
- package/src/core/email-owner.ts +23 -0
- package/src/core/job-lease-token.test.ts +2 -1
- package/src/core/job-lease-token.ts +3 -2
- package/src/core/jwt.ts +2 -2
- package/src/core/rate-limit.test.ts +11 -28
- package/src/core/rate-limit.ts +3 -11
- package/src/core/runner-session-token.test.ts +2 -1
- package/src/core/runner-session-token.ts +3 -2
- package/src/db/db.ts +0 -2
- package/src/db/users.ts +32 -14
- package/src/index.test.ts +15 -11
- package/src/index.ts +23 -20
- package/src/presentation/auth/jwt-auth.test.ts +2 -1
- package/src/presentation/auth/jwt-auth.ts +2 -2
- package/src/presentation/e2eRoutes/create-session.ts +34 -30
- package/src/presentation/e2eRoutes/index.test.ts +9 -6
- package/src/presentation/e2eRoutes/index.ts +8 -5
- package/src/presentation/inter-module.ts +13 -0
- package/src/presentation/routes/email-verification/verify-email-confirm.ts +42 -24
- package/src/presentation/routes/email-verification/verify-email-resend.ts +18 -4
- package/src/presentation/routes/index.test.ts +10 -3
- package/src/presentation/routes/index.ts +22 -12
- package/src/presentation/routes/password/password-reset-confirm.ts +25 -22
- package/src/presentation/routes/password/password-reset-request.test.ts +1 -0
- package/src/presentation/routes/rate-limit.test.ts +2 -2
- package/src/presentation/routes/registration/signup.test.ts +39 -74
- package/src/presentation/routes/registration/signup.ts +89 -70
- package/src/presentation/routes/session/login.ts +38 -35
- package/src/presentation/routes/session/refresh.ts +43 -40
- package/src/presentation/subscribers/email-send-requested.test.ts +2 -30
- package/src/presentation/subscribers/index.ts +0 -1
- package/src/presentation/subscribers/on-password-reset-send-requested.ts +1 -1
- package/test/env.ts +1 -3
- package/test/globalSetup.ts +10 -1
- package/test/routes.ts +51 -84
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/core/entities/email-verification.d.ts +0 -9
- package/dist/core/entities/email-verification.d.ts.map +0 -1
- package/dist/core/entities/email-verification.js +0 -3
- package/dist/core/entities/email-verification.js.map +0 -1
- package/dist/db/email-verifications.d.ts +0 -39
- package/dist/db/email-verifications.d.ts.map +0 -1
- package/dist/db/email-verifications.js +0 -95
- package/dist/db/email-verifications.js.map +0 -1
- package/dist/db/schema/email-verifications.d.ts +0 -114
- package/dist/db/schema/email-verifications.d.ts.map +0 -1
- package/dist/db/schema/email-verifications.js +0 -35
- package/dist/db/schema/email-verifications.js.map +0 -1
- package/dist/presentation/subscribers/on-email-verification-send-requested.d.ts +0 -3
- package/dist/presentation/subscribers/on-email-verification-send-requested.d.ts.map +0 -1
- package/dist/presentation/subscribers/on-email-verification-send-requested.js +0 -13
- package/dist/presentation/subscribers/on-email-verification-send-requested.js.map +0 -1
- package/src/core/entities/email-verification.ts +0 -8
- package/src/db/email-verifications.test.ts +0 -61
- package/src/db/email-verifications.ts +0 -160
- package/src/db/schema/email-verifications.ts +0 -37
- package/src/presentation/routes/email-verification/verify-email-confirm.test.ts +0 -66
- package/src/presentation/routes/email-verification/verify-email-resend.test.ts +0 -100
- package/src/presentation/subscribers/on-email-verification-send-requested.ts +0 -10
|
@@ -2,34 +2,37 @@ import {
|
|
|
2
2
|
passwordResetConfirmBodySchema,
|
|
3
3
|
passwordResetConfirmResponseSchema,
|
|
4
4
|
} from '@shipfox/api-auth-dto';
|
|
5
|
+
import type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';
|
|
5
6
|
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
6
7
|
import {confirmPasswordReset} from '#core/auth.js';
|
|
7
8
|
import {TokenInvalidError} from '#core/errors.js';
|
|
8
9
|
import {setRefreshTokenCookie} from '#presentation/auth/refresh-cookie.js';
|
|
9
10
|
import {toAuthSessionDto} from '#presentation/dto/user.js';
|
|
10
11
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
export function createPasswordResetConfirmRoute(workspaces: WorkspacesInterModuleClient) {
|
|
13
|
+
return defineRoute({
|
|
14
|
+
method: 'POST',
|
|
15
|
+
path: '/password-reset/confirm',
|
|
16
|
+
description: 'Set a new password using the link sent by email.',
|
|
17
|
+
schema: {
|
|
18
|
+
body: passwordResetConfirmBodySchema,
|
|
19
|
+
response: {
|
|
20
|
+
200: passwordResetConfirmResponseSchema,
|
|
21
|
+
},
|
|
19
22
|
},
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const {token, new_password} = request.body;
|
|
23
|
+
errorHandler: (error) => {
|
|
24
|
+
if (error instanceof TokenInvalidError) {
|
|
25
|
+
throw new ClientError('Reset token is invalid or expired', 'token-invalid', {status: 410});
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
},
|
|
29
|
+
handler: async (request, reply) => {
|
|
30
|
+
const {token, new_password} = request.body;
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
const result = await confirmPasswordReset({token, newPassword: new_password, workspaces});
|
|
33
|
+
setRefreshTokenCookie(reply, result.refreshToken);
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
});
|
|
35
|
+
return toAuthSessionDto(result);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -27,6 +27,7 @@ describe('POST /auth/password-reset', () => {
|
|
|
27
27
|
|
|
28
28
|
test('returns 204 and sends reset mail for an active account', async () => {
|
|
29
29
|
const account = await signupVerifyLogin(app, 'reset-request');
|
|
30
|
+
resetCapturedMail();
|
|
30
31
|
|
|
31
32
|
const res = await app.inject({
|
|
32
33
|
method: 'POST',
|
|
@@ -210,13 +210,13 @@ describe('auth rate-limit routes', () => {
|
|
|
210
210
|
method: 'POST',
|
|
211
211
|
url: '/auth/password-reset',
|
|
212
212
|
headers: {'x-forwarded-for': uniqueIp()},
|
|
213
|
-
payload: {email: account.email},
|
|
213
|
+
payload: {email: account.email, challenge_id: account.emailChallengeId},
|
|
214
214
|
});
|
|
215
215
|
const blockedResend = await app.inject({
|
|
216
216
|
method: 'POST',
|
|
217
217
|
url: '/auth/verify-email/resend',
|
|
218
218
|
headers: {'x-forwarded-for': uniqueIp()},
|
|
219
|
-
payload: {email: account.email},
|
|
219
|
+
payload: {email: account.email, challenge_id: account.emailChallengeId},
|
|
220
220
|
});
|
|
221
221
|
|
|
222
222
|
expect(blockedReset.statusCode).toBe(429);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {workspacesInterModuleContract} from '@shipfox/api-workspaces-dto/inter-module';
|
|
2
|
+
import {createInterModuleKnownError} from '@shipfox/inter-module';
|
|
2
3
|
import type {FastifyInstance} from 'fastify';
|
|
3
4
|
import {verifyUserToken} from '#core/jwt.js';
|
|
4
5
|
import {
|
|
@@ -6,9 +7,7 @@ import {
|
|
|
6
7
|
capturedMail,
|
|
7
8
|
createAuthTestApp,
|
|
8
9
|
getSetCookie,
|
|
9
|
-
latestEmailLinkTo,
|
|
10
10
|
listMembershipsByUserMock,
|
|
11
|
-
outboxEventsTo,
|
|
12
11
|
peekInvitationByRawTokenMock,
|
|
13
12
|
ROUTE_TEST_SECRET,
|
|
14
13
|
resetCapturedMail,
|
|
@@ -36,7 +35,7 @@ describe('POST /auth/signup', () => {
|
|
|
36
35
|
const email = uniqueEmail('signup');
|
|
37
36
|
|
|
38
37
|
const res = await signup(app, {
|
|
39
|
-
email: email.toUpperCase()
|
|
38
|
+
email: ` ${email.toUpperCase()} `,
|
|
40
39
|
password,
|
|
41
40
|
name: ' New User ',
|
|
42
41
|
});
|
|
@@ -45,10 +44,11 @@ describe('POST /auth/signup', () => {
|
|
|
45
44
|
expect(res.json().user.email).toBe(email);
|
|
46
45
|
expect(res.json().user.name).toBe('New User');
|
|
47
46
|
expect(res.json().user.email_verified_at).toBeNull();
|
|
48
|
-
expect(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
expect(res.json().email_challenge).toEqual({
|
|
48
|
+
id: expect.any(String),
|
|
49
|
+
next_resend_available_at: expect.any(String),
|
|
50
|
+
});
|
|
51
|
+
expect(capturedMail()).toHaveLength(1);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
test.each([
|
|
@@ -72,14 +72,24 @@ describe('POST /auth/signup', () => {
|
|
|
72
72
|
expect(res.statusCode).toBe(400);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
test('
|
|
75
|
+
test('resumes verification when an unverified user retries signup with the same password', async () => {
|
|
76
76
|
const email = uniqueEmail('duplicate');
|
|
77
77
|
await signup(app, {email, password});
|
|
78
78
|
|
|
79
79
|
const res = await signup(app, {email, password});
|
|
80
80
|
|
|
81
|
-
expect(res.statusCode).toBe(
|
|
82
|
-
expect(res.json().
|
|
81
|
+
expect(res.statusCode).toBe(201);
|
|
82
|
+
expect(res.json().email_challenge.id).toEqual(expect.any(String));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('normalizes email when resuming verification', async () => {
|
|
86
|
+
const email = uniqueEmail('duplicate-equivalent');
|
|
87
|
+
await signup(app, {email, password});
|
|
88
|
+
|
|
89
|
+
const res = await signup(app, {email: ` ${email.toUpperCase()} `, password});
|
|
90
|
+
|
|
91
|
+
expect(res.statusCode).toBe(201);
|
|
92
|
+
expect(res.json().email_challenge.id).toEqual(expect.any(String));
|
|
83
93
|
});
|
|
84
94
|
|
|
85
95
|
test('accepts a valid invitation during signup and skips verification email', async () => {
|
|
@@ -93,6 +103,7 @@ describe('POST /auth/signup', () => {
|
|
|
93
103
|
email,
|
|
94
104
|
hashedToken: 'hashed',
|
|
95
105
|
expiresAt: new Date(Date.now() + 86_400_000),
|
|
106
|
+
revokedAt: null,
|
|
96
107
|
acceptedAt: null,
|
|
97
108
|
acceptedByUserId: null,
|
|
98
109
|
invitedByUserId: crypto.randomUUID(),
|
|
@@ -107,6 +118,7 @@ describe('POST /auth/signup', () => {
|
|
|
107
118
|
email,
|
|
108
119
|
hashedToken: 'hashed',
|
|
109
120
|
expiresAt: new Date(Date.now() + 86_400_000),
|
|
121
|
+
revokedAt: null,
|
|
110
122
|
acceptedAt: new Date(),
|
|
111
123
|
acceptedByUserId: params.userId,
|
|
112
124
|
invitedByUserId: crypto.randomUUID(),
|
|
@@ -126,18 +138,9 @@ describe('POST /auth/signup', () => {
|
|
|
126
138
|
},
|
|
127
139
|
alreadyMember: false,
|
|
128
140
|
}));
|
|
129
|
-
listMembershipsByUserMock.mockResolvedValueOnce(
|
|
130
|
-
{
|
|
131
|
-
|
|
132
|
-
userId: crypto.randomUUID(),
|
|
133
|
-
userEmail: email,
|
|
134
|
-
userName: 'Invitee',
|
|
135
|
-
workspaceId,
|
|
136
|
-
workspaceName: 'Acme',
|
|
137
|
-
createdAt: new Date(),
|
|
138
|
-
updatedAt: new Date(),
|
|
139
|
-
},
|
|
140
|
-
]);
|
|
141
|
+
listMembershipsByUserMock.mockResolvedValueOnce({
|
|
142
|
+
memberships: [{workspaceId, role: 'admin' as const}],
|
|
143
|
+
});
|
|
141
144
|
const res = await app.inject({
|
|
142
145
|
method: 'POST',
|
|
143
146
|
url: '/auth/signup',
|
|
@@ -162,7 +165,6 @@ describe('POST /auth/signup', () => {
|
|
|
162
165
|
});
|
|
163
166
|
expect(body.accept_error).toBeUndefined();
|
|
164
167
|
expect(claims.memberships).toEqual([{workspaceId, role: 'admin'}]);
|
|
165
|
-
expect(await outboxEventsTo(email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED)).toHaveLength(0);
|
|
166
168
|
expect(capturedMail()).toHaveLength(0);
|
|
167
169
|
});
|
|
168
170
|
|
|
@@ -175,6 +177,7 @@ describe('POST /auth/signup', () => {
|
|
|
175
177
|
email,
|
|
176
178
|
hashedToken: 'hashed',
|
|
177
179
|
expiresAt: new Date(Date.now() + 86_400_000),
|
|
180
|
+
revokedAt: null,
|
|
178
181
|
acceptedAt: null,
|
|
179
182
|
acceptedByUserId: null,
|
|
180
183
|
invitedByUserId: crypto.randomUUID(),
|
|
@@ -206,65 +209,27 @@ describe('POST /auth/signup', () => {
|
|
|
206
209
|
});
|
|
207
210
|
|
|
208
211
|
test.each([
|
|
209
|
-
{
|
|
210
|
-
name: 'invalid token',
|
|
211
|
-
invitation: undefined,
|
|
212
|
-
expectedStatus: 410,
|
|
213
|
-
expectedCode: 'invitation-token-invalid',
|
|
214
|
-
},
|
|
212
|
+
{name: 'invalid token', expectedCode: 'invitation-token-invalid' as const, expectedStatus: 410},
|
|
215
213
|
{
|
|
216
214
|
name: 'already accepted token',
|
|
217
|
-
|
|
218
|
-
acceptedAt: new Date(),
|
|
219
|
-
expiresAt: new Date(Date.now() + 86_400_000),
|
|
220
|
-
email: uniqueEmail('signup-invite-used'),
|
|
221
|
-
},
|
|
222
|
-
expectedStatus: 410,
|
|
223
|
-
expectedCode: 'invitation-token-used',
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
name: 'expired token',
|
|
227
|
-
invitation: {
|
|
228
|
-
acceptedAt: null,
|
|
229
|
-
expiresAt: new Date(Date.now() - 60_000),
|
|
230
|
-
email: uniqueEmail('signup-invite-expired'),
|
|
231
|
-
},
|
|
215
|
+
expectedCode: 'invitation-token-used' as const,
|
|
232
216
|
expectedStatus: 410,
|
|
233
|
-
expectedCode: 'invitation-token-expired',
|
|
234
217
|
},
|
|
218
|
+
{name: 'expired token', expectedCode: 'invitation-token-expired' as const, expectedStatus: 410},
|
|
235
219
|
{
|
|
236
220
|
name: 'email mismatch',
|
|
237
|
-
|
|
238
|
-
acceptedAt: null,
|
|
239
|
-
expiresAt: new Date(Date.now() + 86_400_000),
|
|
240
|
-
email: uniqueEmail('signup-invite-other'),
|
|
241
|
-
},
|
|
221
|
+
expectedCode: 'invitation-email-mismatch' as const,
|
|
242
222
|
expectedStatus: 403,
|
|
243
|
-
expectedCode: 'invitation-email-mismatch',
|
|
244
223
|
},
|
|
245
|
-
])('rejects signup with invitation for $name', async ({
|
|
246
|
-
invitation,
|
|
247
|
-
expectedStatus,
|
|
248
|
-
expectedCode,
|
|
249
|
-
}) => {
|
|
224
|
+
])('rejects signup with invitation for $name', async ({expectedCode, expectedStatus}) => {
|
|
250
225
|
const email = uniqueEmail('signup-invite-error');
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
hashedToken: 'hashed',
|
|
259
|
-
expiresAt: invitation.expiresAt,
|
|
260
|
-
acceptedAt: invitation.acceptedAt,
|
|
261
|
-
acceptedByUserId: null,
|
|
262
|
-
invitedByUserId: crypto.randomUUID(),
|
|
263
|
-
invitedByDisplay: null,
|
|
264
|
-
createdAt: new Date(),
|
|
265
|
-
updatedAt: new Date(),
|
|
266
|
-
});
|
|
267
|
-
}
|
|
226
|
+
peekInvitationByRawTokenMock.mockRejectedValueOnce(
|
|
227
|
+
createInterModuleKnownError(
|
|
228
|
+
workspacesInterModuleContract.methods.preflightInvitationAcceptance,
|
|
229
|
+
expectedCode,
|
|
230
|
+
{},
|
|
231
|
+
),
|
|
232
|
+
);
|
|
268
233
|
|
|
269
234
|
const res = await app.inject({
|
|
270
235
|
method: 'POST',
|
|
@@ -1,81 +1,100 @@
|
|
|
1
1
|
import {signupBodySchema, signupResponseSchema} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {EmailChallengeError} from '@shipfox/api-email-challenges';
|
|
3
|
+
import type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';
|
|
4
|
+
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
5
|
+
import {signup, signupWithInvitation} from '#core/auth.js';
|
|
2
6
|
import {
|
|
7
|
+
EmailTakenError,
|
|
3
8
|
InvitationEmailMismatchError,
|
|
4
9
|
TokenAlreadyUsedError,
|
|
5
10
|
TokenExpiredError,
|
|
6
|
-
TokenInvalidError
|
|
7
|
-
} from '
|
|
8
|
-
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
9
|
-
import {signup, signupWithInvitation} from '#core/auth.js';
|
|
10
|
-
import {EmailTakenError} from '#core/errors.js';
|
|
11
|
+
TokenInvalidError,
|
|
12
|
+
} from '#core/errors.js';
|
|
11
13
|
import {setRefreshTokenCookie} from '#presentation/auth/refresh-cookie.js';
|
|
12
14
|
import {toUserDto} from '#presentation/dto/user.js';
|
|
13
15
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
export function createSignupRoute(workspaces: WorkspacesInterModuleClient) {
|
|
17
|
+
return defineRoute({
|
|
18
|
+
method: 'POST',
|
|
19
|
+
path: '/signup',
|
|
20
|
+
description:
|
|
21
|
+
'Create a user account. Sends a verification email unless an invitation token is supplied.',
|
|
22
|
+
schema: {
|
|
23
|
+
body: signupBodySchema,
|
|
24
|
+
response: {
|
|
25
|
+
201: signupResponseSchema,
|
|
26
|
+
},
|
|
23
27
|
},
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'invitation-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
28
|
+
errorHandler: (error) => {
|
|
29
|
+
if (error instanceof EmailChallengeError) {
|
|
30
|
+
throw new ClientError(error.message, `email-challenge-${error.code}`, {
|
|
31
|
+
status: error.code === 'limited' ? 429 : 400,
|
|
32
|
+
...(error.retryAt ? {details: {retry_at: error.retryAt.toISOString()}} : {}),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (error instanceof EmailTakenError) {
|
|
36
|
+
throw new ClientError('Email already registered', 'email-taken', {status: 409});
|
|
37
|
+
}
|
|
38
|
+
if (error instanceof TokenInvalidError) {
|
|
39
|
+
throw new ClientError('Invitation token is invalid', 'invitation-token-invalid', {
|
|
40
|
+
status: 410,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (error instanceof TokenAlreadyUsedError) {
|
|
44
|
+
throw new ClientError('Invitation has already been accepted', 'invitation-token-used', {
|
|
45
|
+
status: 410,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (error instanceof TokenExpiredError) {
|
|
49
|
+
throw new ClientError('Invitation has expired', 'invitation-token-expired', {
|
|
50
|
+
status: 410,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (error instanceof InvitationEmailMismatchError) {
|
|
54
|
+
throw new ClientError(
|
|
55
|
+
'Signup email does not match the invitation',
|
|
56
|
+
'invitation-email-mismatch',
|
|
57
|
+
{status: 403},
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
},
|
|
62
|
+
handler: async (request, reply) => {
|
|
63
|
+
const {email, password, name, invitation_token} = request.body;
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
if (invitation_token === undefined) {
|
|
66
|
+
const result = await signup({email, password, name, sourceIp: request.ip});
|
|
67
|
+
reply.code(201);
|
|
68
|
+
return {
|
|
69
|
+
user: toUserDto(result),
|
|
70
|
+
email_challenge: {
|
|
71
|
+
id: result.emailChallenge.id,
|
|
72
|
+
next_resend_available_at: result.emailChallenge.nextResendAvailableAt.toISOString(),
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
77
|
+
const result = await signupWithInvitation({
|
|
78
|
+
email,
|
|
79
|
+
password,
|
|
80
|
+
name,
|
|
81
|
+
invitationToken: invitation_token,
|
|
82
|
+
workspaces,
|
|
83
|
+
});
|
|
84
|
+
setRefreshTokenCookie(reply, result.refreshToken);
|
|
85
|
+
reply.code(201);
|
|
86
|
+
return {
|
|
87
|
+
user: toUserDto(result.user),
|
|
88
|
+
token: result.token,
|
|
89
|
+
membership: result.membership
|
|
90
|
+
? {
|
|
91
|
+
id: result.membership.id,
|
|
92
|
+
user_id: result.membership.userId,
|
|
93
|
+
workspace_id: result.membership.workspaceId,
|
|
94
|
+
}
|
|
95
|
+
: null,
|
|
96
|
+
...(result.acceptError ? {accept_error: result.acceptError} : {}),
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {loginBodySchema, loginResponseSchema} from '@shipfox/api-auth-dto';
|
|
2
|
+
import type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';
|
|
2
3
|
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
3
4
|
import {login} from '#core/auth.js';
|
|
4
5
|
import {
|
|
@@ -10,41 +11,43 @@ import {setRefreshTokenCookie} from '#presentation/auth/refresh-cookie.js';
|
|
|
10
11
|
import {toAuthSessionDto} from '#presentation/dto/user.js';
|
|
11
12
|
import {createAuthRateLimitPreHandler} from '#presentation/routes/rate-limit.js';
|
|
12
13
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
export function createLoginRoute(workspaces: WorkspacesInterModuleClient) {
|
|
15
|
+
return defineRoute({
|
|
16
|
+
method: 'POST',
|
|
17
|
+
path: '/login',
|
|
18
|
+
description: 'Sign in with an email address and password.',
|
|
19
|
+
schema: {
|
|
20
|
+
body: loginBodySchema,
|
|
21
|
+
response: {
|
|
22
|
+
200: loginResponseSchema,
|
|
23
|
+
},
|
|
21
24
|
},
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const {email, password} = request.body;
|
|
25
|
+
preHandler: createAuthRateLimitPreHandler('login'),
|
|
26
|
+
errorHandler: (error) => {
|
|
27
|
+
if (error instanceof InvalidCredentialsError) {
|
|
28
|
+
throw new ClientError('Invalid credentials', 'invalid-credentials', {status: 401});
|
|
29
|
+
}
|
|
30
|
+
if (error instanceof EmailNotVerifiedError) {
|
|
31
|
+
throw new ClientError('Email not verified', 'email-not-verified', {status: 403});
|
|
32
|
+
}
|
|
33
|
+
if (error instanceof AuthDependencyUnavailableError) {
|
|
34
|
+
throw new ClientError(
|
|
35
|
+
'Authentication dependency unavailable',
|
|
36
|
+
'auth-dependency-unavailable',
|
|
37
|
+
{
|
|
38
|
+
status: 503,
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
},
|
|
44
|
+
handler: async (request, reply) => {
|
|
45
|
+
const {email, password} = request.body;
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
const result = await login({email, password, workspaces});
|
|
48
|
+
setRefreshTokenCookie(reply, result.refreshToken);
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
});
|
|
50
|
+
return toAuthSessionDto(result);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {refreshResponseSchema} from '@shipfox/api-auth-dto';
|
|
2
|
+
import type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';
|
|
2
3
|
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
3
4
|
import {refreshAccessToken} from '#core/auth.js';
|
|
4
5
|
import {AuthDependencyUnavailableError, TokenInvalidError} from '#core/errors.js';
|
|
@@ -9,46 +10,48 @@ import {
|
|
|
9
10
|
} from '#presentation/auth/refresh-cookie.js';
|
|
10
11
|
import {toAuthSessionDto} from '#presentation/dto/user.js';
|
|
11
12
|
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
export function createRefreshRoute(workspaces: WorkspacesInterModuleClient) {
|
|
14
|
+
return defineRoute({
|
|
15
|
+
method: 'POST',
|
|
16
|
+
path: '/refresh',
|
|
17
|
+
description: 'Refresh the current browser session access token.',
|
|
18
|
+
schema: {
|
|
19
|
+
response: {
|
|
20
|
+
200: refreshResponseSchema,
|
|
21
|
+
},
|
|
19
22
|
},
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
23
|
+
errorHandler: (error, _request, reply) => {
|
|
24
|
+
if (error instanceof TokenInvalidError) {
|
|
25
|
+
clearRefreshTokenCookie(reply);
|
|
26
|
+
throw new ClientError('Refresh token is invalid or expired', 'unauthorized', {
|
|
27
|
+
status: 401,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (error instanceof AuthDependencyUnavailableError) {
|
|
31
|
+
throw new ClientError(
|
|
32
|
+
'Authentication dependency unavailable',
|
|
33
|
+
'auth-dependency-unavailable',
|
|
34
|
+
{
|
|
35
|
+
status: 503,
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
},
|
|
41
|
+
handler: async (request, reply) => {
|
|
42
|
+
const refreshToken = getRefreshTokenCookie(request);
|
|
43
|
+
if (!refreshToken) {
|
|
44
|
+
clearRefreshTokenCookie(reply);
|
|
45
|
+
throw new ClientError('Refresh token is invalid or expired', 'unauthorized', {status: 401});
|
|
46
|
+
}
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
const result = await refreshAccessToken({refreshToken, workspaces});
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
});
|
|
50
|
+
// Absent on a grace-window hit: keep the existing cookie rather than overwrite it.
|
|
51
|
+
if (result.refreshToken) {
|
|
52
|
+
setRefreshTokenCookie(reply, result.refreshToken);
|
|
53
|
+
}
|
|
54
|
+
return toAuthSessionDto(result);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|