@shipfox/api-auth 5.0.0 → 7.0.1
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 +69 -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 +18 -14
- 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
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {JOB_LEASE_TOKEN_AUDIENCE, RUNNER_SESSION_TOKEN_AUDIENCE} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {runnerSessionTokenKey} from '@shipfox/node-auth-root-key';
|
|
2
3
|
import {signHs256} from '@shipfox/node-jwt';
|
|
3
4
|
import {generateKeyPair, SignJWT} from 'jose';
|
|
4
5
|
import {issueJobLeaseToken, verifyJobLeaseToken} from './job-lease-token.js';
|
|
5
6
|
import {issueRunnerSessionToken, verifyRunnerSessionToken} from './runner-session-token.js';
|
|
6
7
|
|
|
7
|
-
const SECRET =
|
|
8
|
+
const SECRET = runnerSessionTokenKey();
|
|
8
9
|
|
|
9
10
|
function claims() {
|
|
10
11
|
return {
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type RunnerSessionTokenClaims,
|
|
4
4
|
runnerSessionTokenClaimsSchema,
|
|
5
5
|
} from '@shipfox/api-auth-dto';
|
|
6
|
+
import {runnerSessionTokenKey} from '@shipfox/node-auth-root-key';
|
|
6
7
|
import {signHs256, verifyHs256} from '@shipfox/node-jwt';
|
|
7
8
|
import {config} from '#config.js';
|
|
8
9
|
import {recordTokenIssued, recordTokenVerified} from '#metrics/index.js';
|
|
@@ -21,7 +22,7 @@ export async function issueRunnerSessionToken(
|
|
|
21
22
|
labels: claims.labels,
|
|
22
23
|
maxClaims: claims.maxClaims,
|
|
23
24
|
},
|
|
24
|
-
secret:
|
|
25
|
+
secret: runnerSessionTokenKey(),
|
|
25
26
|
expiresIn: config.AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN,
|
|
26
27
|
audience: RUNNER_SESSION_TOKEN_AUDIENCE,
|
|
27
28
|
});
|
|
@@ -36,7 +37,7 @@ export async function verifyRunnerSessionToken(
|
|
|
36
37
|
try {
|
|
37
38
|
const claims = await verifyHs256({
|
|
38
39
|
token,
|
|
39
|
-
secret:
|
|
40
|
+
secret: runnerSessionTokenKey(),
|
|
40
41
|
schema: runnerSessionTokenClaimsSchema,
|
|
41
42
|
audience: RUNNER_SESSION_TOKEN_AUDIENCE,
|
|
42
43
|
});
|
package/src/db/db.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {drizzle, type NodePgDatabase} from '@shipfox/node-drizzle';
|
|
2
2
|
import {pgClient} from '@shipfox/node-postgres';
|
|
3
|
-
import {emailVerifications} from './schema/email-verifications.js';
|
|
4
3
|
import {authOutbox} from './schema/outbox.js';
|
|
5
4
|
import {passwordResets} from './schema/password-resets.js';
|
|
6
5
|
import {authRateLimits} from './schema/rate-limits.js';
|
|
@@ -11,7 +10,6 @@ export const schema = {
|
|
|
11
10
|
users,
|
|
12
11
|
passwordResets,
|
|
13
12
|
refreshTokens,
|
|
14
|
-
emailVerifications,
|
|
15
13
|
authOutbox,
|
|
16
14
|
authRateLimits,
|
|
17
15
|
};
|
package/src/db/users.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import {AUTH_USER_SIGNED_UP, type AuthEventMap} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {writeOutboxEvent} from '@shipfox/node-outbox';
|
|
1
3
|
import {eq} from 'drizzle-orm';
|
|
2
4
|
import type {User} from '#core/entities/user.js';
|
|
3
5
|
import {EmailTakenError} from '#core/errors.js';
|
|
4
6
|
import {db} from './db.js';
|
|
7
|
+
import {authOutbox} from './schema/outbox.js';
|
|
5
8
|
import {toUser, type UserDb, users} from './schema/users.js';
|
|
6
9
|
|
|
7
10
|
export interface CreateUserParams {
|
|
@@ -9,6 +12,7 @@ export interface CreateUserParams {
|
|
|
9
12
|
hashedPassword: string | null;
|
|
10
13
|
name?: string | null;
|
|
11
14
|
emailVerifiedAt?: Date | null;
|
|
15
|
+
signedUp?: {viaInvitation: boolean};
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
export interface ProvisionUserParams {
|
|
@@ -29,27 +33,41 @@ function isAuthUsersEmailUniqueViolation(error: unknown): boolean {
|
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
export async function createUser(params: CreateUserParams): Promise<User> {
|
|
32
|
-
let rows: UserDb[];
|
|
33
36
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
return await db().transaction(async (tx) => {
|
|
38
|
+
const rows: UserDb[] = await tx
|
|
39
|
+
.insert(users)
|
|
40
|
+
.values({
|
|
41
|
+
email: params.email,
|
|
42
|
+
hashedPassword: params.hashedPassword,
|
|
43
|
+
name: params.name ?? null,
|
|
44
|
+
emailVerifiedAt: params.emailVerifiedAt ?? null,
|
|
45
|
+
})
|
|
46
|
+
.returning();
|
|
47
|
+
|
|
48
|
+
const row = rows[0];
|
|
49
|
+
if (!row) throw new Error('Insert returned no rows');
|
|
50
|
+
|
|
51
|
+
const user = toUser(row);
|
|
52
|
+
if (params.signedUp) {
|
|
53
|
+
await writeOutboxEvent<AuthEventMap>(tx, authOutbox, {
|
|
54
|
+
type: AUTH_USER_SIGNED_UP,
|
|
55
|
+
payload: {
|
|
56
|
+
userId: user.id,
|
|
57
|
+
email: user.email,
|
|
58
|
+
...(user.name ? {name: user.name} : {}),
|
|
59
|
+
viaInvitation: params.signedUp.viaInvitation,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return user;
|
|
64
|
+
});
|
|
43
65
|
} catch (error) {
|
|
44
66
|
if (isAuthUsersEmailUniqueViolation(error)) {
|
|
45
67
|
throw new EmailTakenError(params.email);
|
|
46
68
|
}
|
|
47
69
|
throw error;
|
|
48
70
|
}
|
|
49
|
-
|
|
50
|
-
const row = rows[0];
|
|
51
|
-
if (!row) throw new Error('Insert returned no rows');
|
|
52
|
-
return toUser(row);
|
|
53
71
|
}
|
|
54
72
|
|
|
55
73
|
export async function provisionUser(params: ProvisionUserParams): Promise<User> {
|
package/src/index.test.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
|
|
3
2
|
AUTH_PASSWORD_RESET_SEND_REQUESTED,
|
|
3
|
+
AUTH_USER_SIGNED_UP,
|
|
4
4
|
authEventSchemas,
|
|
5
5
|
} from '@shipfox/api-auth-dto';
|
|
6
|
-
import {
|
|
6
|
+
import {createAuthModule} from './index.js';
|
|
7
7
|
import {passwordLoginMethods} from './login-methods.js';
|
|
8
8
|
|
|
9
9
|
vi.mock('#config.js', () => ({
|
|
10
10
|
config: {
|
|
11
|
-
AUTH_JWT_SECRET: 'auth-module-test-secret',
|
|
12
11
|
AUTH_JWT_EXPIRES_IN: '15m',
|
|
13
|
-
AUTH_JOB_LEASE_TOKEN_SECRET: 'auth-module-test-job-secret',
|
|
14
12
|
AUTH_JOB_LEASE_TOKEN_EXPIRES_IN: '90m',
|
|
15
13
|
AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS: 14,
|
|
16
14
|
AUTH_REFRESH_ROTATION_GRACE_SECONDS: 30,
|
|
@@ -18,10 +16,21 @@ vi.mock('#config.js', () => ({
|
|
|
18
16
|
AUTH_PASSWORD_ENABLED: true,
|
|
19
17
|
CLIENT_BASE_URL: 'https://app.example.test',
|
|
20
18
|
},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock('@shipfox/node-mailer', () => ({
|
|
21
22
|
mailer: {send: vi.fn()},
|
|
22
23
|
}));
|
|
23
24
|
|
|
24
25
|
describe('authModule', () => {
|
|
26
|
+
const authModule = createAuthModule({
|
|
27
|
+
workspaces: {
|
|
28
|
+
listMembershipsForTokenClaims: vi.fn(),
|
|
29
|
+
preflightInvitationAcceptance: vi.fn(),
|
|
30
|
+
acceptInvitation: vi.fn(),
|
|
31
|
+
requireActiveMembership: vi.fn(),
|
|
32
|
+
},
|
|
33
|
+
});
|
|
25
34
|
test('declares password login only when password login is enabled', () => {
|
|
26
35
|
expect(passwordLoginMethods(true)).toEqual([{id: 'password'}]);
|
|
27
36
|
expect(passwordLoginMethods(false)).toEqual([]);
|
|
@@ -34,14 +43,9 @@ describe('authModule', () => {
|
|
|
34
43
|
|
|
35
44
|
expect(publisher?.eventSchemas).toBe(authEventSchemas);
|
|
36
45
|
expect(Object.keys(publisher?.eventSchemas ?? {}).sort()).toEqual([
|
|
37
|
-
AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
|
|
38
46
|
AUTH_PASSWORD_RESET_SEND_REQUESTED,
|
|
47
|
+
AUTH_USER_SIGNED_UP,
|
|
39
48
|
]);
|
|
40
|
-
expect(events).toEqual(
|
|
41
|
-
expect.arrayContaining([
|
|
42
|
-
AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
|
|
43
|
-
AUTH_PASSWORD_RESET_SEND_REQUESTED,
|
|
44
|
-
]),
|
|
45
|
-
);
|
|
49
|
+
expect(events).toEqual(expect.arrayContaining([AUTH_PASSWORD_RESET_SEND_REQUESTED]));
|
|
46
50
|
});
|
|
47
51
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
|
|
3
2
|
AUTH_PASSWORD_RESET_SEND_REQUESTED,
|
|
4
3
|
type AuthEventMap,
|
|
5
4
|
authEventSchemas,
|
|
@@ -13,12 +12,10 @@ import {authOutbox} from '#db/schema/outbox.js';
|
|
|
13
12
|
import {createJwtAuthMethod} from '#presentation/auth/jwt-auth.js';
|
|
14
13
|
import {createLeaseTokenAuthMethod} from '#presentation/auth/lease-token-auth.js';
|
|
15
14
|
import {createRunnerSessionAuthMethod} from '#presentation/auth/runner-session-auth.js';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
onPasswordResetSendRequested,
|
|
21
|
-
} from '#presentation/subscribers/index.js';
|
|
15
|
+
import {createAuthE2eRoutes} from '#presentation/e2eRoutes/index.js';
|
|
16
|
+
import {createAuthInterModulePresentation} from '#presentation/inter-module.js';
|
|
17
|
+
import {buildAuthRoutes} from '#presentation/routes/index.js';
|
|
18
|
+
import {onPasswordResetSendRequested} from '#presentation/subscribers/index.js';
|
|
22
19
|
import {passwordLoginMethods} from './login-methods.js';
|
|
23
20
|
|
|
24
21
|
export type {JobLeaseTokenClaims, RunnerSessionTokenClaims} from '@shipfox/api-auth-dto';
|
|
@@ -29,6 +26,8 @@ export type {
|
|
|
29
26
|
ProvisionUserParams,
|
|
30
27
|
} from '#core/auth.js';
|
|
31
28
|
export {createSessionForUser, provisionUser} from '#core/auth.js';
|
|
29
|
+
export type {EmailOwner, FindUserByEmailParams} from '#core/email-owner.js';
|
|
30
|
+
export {findUserByEmail} from '#core/email-owner.js';
|
|
32
31
|
export type {User, UserStatus} from '#core/entities/user.js';
|
|
33
32
|
export {
|
|
34
33
|
AuthDependencyUnavailableError,
|
|
@@ -57,16 +56,20 @@ export {createRunnerSessionAuthMethod} from '#presentation/auth/runner-session-a
|
|
|
57
56
|
|
|
58
57
|
const subscriber = subscriberFactory<AuthEventMap>();
|
|
59
58
|
|
|
60
|
-
export
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
59
|
+
export function createAuthModule({
|
|
60
|
+
workspaces,
|
|
61
|
+
}: {
|
|
62
|
+
workspaces: import('@shipfox/api-workspaces-dto/inter-module').WorkspacesInterModuleClient;
|
|
63
|
+
}): ShipfoxModule {
|
|
64
|
+
return {
|
|
65
|
+
name: 'auth',
|
|
66
|
+
database: {db, migrationsPath},
|
|
67
|
+
auth: [createJwtAuthMethod(), createLeaseTokenAuthMethod(), createRunnerSessionAuthMethod()],
|
|
68
|
+
loginMethods: passwordLoginMethods(config.AUTH_PASSWORD_ENABLED),
|
|
69
|
+
routes: [buildAuthRoutes(config.AUTH_PASSWORD_ENABLED, workspaces)],
|
|
70
|
+
e2eRoutes: [createAuthE2eRoutes(workspaces)],
|
|
71
|
+
publishers: [{name: 'auth', table: authOutbox, db, eventSchemas: authEventSchemas}],
|
|
72
|
+
subscribers: [subscriber(AUTH_PASSWORD_RESET_SEND_REQUESTED, onPasswordResetSendRequested)],
|
|
73
|
+
interModulePresentations: [createAuthInterModulePresentation()],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {userAccessTokenKey} from '@shipfox/node-auth-root-key';
|
|
1
2
|
import type {FastifyInstance} from 'fastify';
|
|
2
3
|
import Fastify from 'fastify';
|
|
3
4
|
import {serializerCompiler, validatorCompiler} from 'fastify-type-provider-zod';
|
|
@@ -5,7 +6,7 @@ import {signUserToken} from '#core/jwt.js';
|
|
|
5
6
|
import {createUser, findUserById} from '#db/users.js';
|
|
6
7
|
import {createJwtAuthMethod, getClientContext} from './jwt-auth.js';
|
|
7
8
|
|
|
8
|
-
const SECRET =
|
|
9
|
+
const SECRET = userAccessTokenKey();
|
|
9
10
|
|
|
10
11
|
function emailFor(suffix: string): string {
|
|
11
12
|
return `${suffix}-${crypto.randomUUID()}@example.com`;
|
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
setUserContext,
|
|
6
6
|
type UserContext,
|
|
7
7
|
} from '@shipfox/api-auth-context';
|
|
8
|
+
import {userAccessTokenKey} from '@shipfox/node-auth-root-key';
|
|
8
9
|
import type {AuthMethod} from '@shipfox/node-fastify';
|
|
9
10
|
import type {FastifyRequest} from 'fastify';
|
|
10
|
-
import {config} from '#config.js';
|
|
11
11
|
import {verifyUserToken} from '#core/jwt.js';
|
|
12
12
|
import {createBearerTokenAuthMethod} from './bearer-token-auth.js';
|
|
13
13
|
|
|
@@ -24,7 +24,7 @@ export function getClientContext(request: FastifyRequest): ClientContext | null
|
|
|
24
24
|
export function createJwtAuthMethod(): AuthMethod {
|
|
25
25
|
return createBearerTokenAuthMethod({
|
|
26
26
|
name: AUTH_USER,
|
|
27
|
-
verifyToken: (token) => verifyUserToken({token, secret:
|
|
27
|
+
verifyToken: (token) => verifyUserToken({token, secret: userAccessTokenKey()}),
|
|
28
28
|
invalidTokenError: {message: 'Invalid or expired token', code: 'unauthorized'},
|
|
29
29
|
setContext: (request, claims) => {
|
|
30
30
|
const clientContext: ClientContext = buildUserContext({
|
|
@@ -1,39 +1,43 @@
|
|
|
1
1
|
import {e2eCreateSessionBodySchema, e2eCreateSessionResponseSchema} 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 {createSessionForUser} from '#core/auth.js';
|
|
4
5
|
import {EmailNotVerifiedError, InvalidCredentialsError, UserNotFoundError} from '#core/errors.js';
|
|
5
6
|
import {setRefreshTokenCookie} from '#presentation/auth/refresh-cookie.js';
|
|
6
7
|
import {toAuthSessionDto} from '#presentation/dto/user.js';
|
|
7
8
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
export function createE2eSessionRoute(workspaces: WorkspacesInterModuleClient) {
|
|
10
|
+
return defineRoute({
|
|
11
|
+
method: 'POST',
|
|
12
|
+
path: '/sessions',
|
|
13
|
+
description: 'Create an authenticated browser session for E2E tests.',
|
|
14
|
+
schema: {
|
|
15
|
+
body: e2eCreateSessionBodySchema,
|
|
16
|
+
response: {
|
|
17
|
+
200: e2eCreateSessionResponseSchema,
|
|
18
|
+
},
|
|
16
19
|
},
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
20
|
+
errorHandler: (error) => {
|
|
21
|
+
if (error instanceof UserNotFoundError) {
|
|
22
|
+
throw new ClientError('User not found', 'user-not-found', {status: 404});
|
|
23
|
+
}
|
|
24
|
+
if (error instanceof EmailNotVerifiedError) {
|
|
25
|
+
throw new ClientError('Email not verified', 'email-not-verified', {status: 403});
|
|
26
|
+
}
|
|
27
|
+
if (error instanceof InvalidCredentialsError) {
|
|
28
|
+
throw new ClientError('User is not active', 'user-inactive', {status: 409});
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
},
|
|
32
|
+
handler: async (request, reply) => {
|
|
33
|
+
const result = await createSessionForUser({
|
|
34
|
+
...(request.body.user_id ? {userId: request.body.user_id} : {}),
|
|
35
|
+
...(request.body.email ? {email: request.body.email} : {}),
|
|
36
|
+
workspaces,
|
|
37
|
+
});
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
39
|
+
setRefreshTokenCookie(reply, result.refreshToken);
|
|
40
|
+
return toAuthSessionDto(result);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import {createApp} from '@shipfox/node-fastify';
|
|
2
2
|
import type {FastifyInstance} from 'fastify';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
vi.mock('@shipfox/api-workspaces', () => ({
|
|
6
|
-
listMembershipsByUser: vi.fn(() => Promise.resolve([])),
|
|
7
|
-
}));
|
|
3
|
+
import {createAuthE2eRoutes} from './index.js';
|
|
8
4
|
|
|
9
5
|
describe('auth E2E routes', () => {
|
|
10
6
|
let app: FastifyInstance;
|
|
11
7
|
|
|
12
8
|
beforeAll(async () => {
|
|
13
9
|
app = await createApp({
|
|
14
|
-
routes: [
|
|
10
|
+
routes: [
|
|
11
|
+
createAuthE2eRoutes({
|
|
12
|
+
listMembershipsForTokenClaims: vi.fn(() => Promise.resolve({memberships: []})),
|
|
13
|
+
preflightInvitationAcceptance: vi.fn(),
|
|
14
|
+
acceptInvitation: vi.fn(),
|
|
15
|
+
requireActiveMembership: vi.fn(),
|
|
16
|
+
}),
|
|
17
|
+
],
|
|
15
18
|
swagger: false,
|
|
16
19
|
});
|
|
17
20
|
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import cookie from '@fastify/cookie';
|
|
2
|
+
import type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';
|
|
2
3
|
import type {RouteGroup} from '@shipfox/node-fastify';
|
|
3
4
|
import {createE2eSessionRoute} from './create-session.js';
|
|
4
5
|
import {createE2eUserRoute} from './create-user.js';
|
|
5
6
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
export function createAuthE2eRoutes(workspaces: WorkspacesInterModuleClient): RouteGroup {
|
|
8
|
+
return {
|
|
9
|
+
prefix: '/auth',
|
|
10
|
+
plugins: [cookie],
|
|
11
|
+
routes: [createE2eUserRoute, createE2eSessionRoute(workspaces)],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {authInterModuleContract} from '@shipfox/api-auth-dto/inter-module';
|
|
2
|
+
import {defineInterModulePresentation, type InterModulePresentation} from '@shipfox/inter-module';
|
|
3
|
+
import {issueJobLeaseToken} from '#core/job-lease-token.js';
|
|
4
|
+
import {issueRunnerSessionToken} from '#core/runner-session-token.js';
|
|
5
|
+
|
|
6
|
+
export function createAuthInterModulePresentation(): InterModulePresentation<
|
|
7
|
+
typeof authInterModuleContract
|
|
8
|
+
> {
|
|
9
|
+
return defineInterModulePresentation(authInterModuleContract, {
|
|
10
|
+
mintRunnerSessionToken: async (claims) => ({token: await issueRunnerSessionToken(claims)}),
|
|
11
|
+
mintJobLeaseToken: async (claims) => ({token: await issueJobLeaseToken(claims)}),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -2,36 +2,54 @@ import {
|
|
|
2
2
|
verifyEmailConfirmBodySchema,
|
|
3
3
|
verifyEmailConfirmResponseSchema,
|
|
4
4
|
} from '@shipfox/api-auth-dto';
|
|
5
|
+
import {EmailChallengeError} from '@shipfox/api-email-challenges';
|
|
6
|
+
import type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';
|
|
5
7
|
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
6
8
|
import {confirmEmailVerification} from '#core/auth.js';
|
|
7
9
|
import {TokenInvalidError} from '#core/errors.js';
|
|
8
10
|
import {setRefreshTokenCookie} from '#presentation/auth/refresh-cookie.js';
|
|
9
11
|
import {toAuthSessionDto} from '#presentation/dto/user.js';
|
|
10
12
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
export function createVerifyEmailConfirmRoute(workspaces: WorkspacesInterModuleClient) {
|
|
14
|
+
return defineRoute({
|
|
15
|
+
method: 'POST',
|
|
16
|
+
path: '/verify-email/confirm',
|
|
17
|
+
description: 'Verify a user email address with the code sent by email.',
|
|
18
|
+
schema: {
|
|
19
|
+
body: verifyEmailConfirmBodySchema,
|
|
20
|
+
response: {
|
|
21
|
+
200: verifyEmailConfirmResponseSchema,
|
|
22
|
+
},
|
|
19
23
|
},
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
errorHandler: (error) => {
|
|
25
|
+
if (error instanceof TokenInvalidError) {
|
|
26
|
+
throw new ClientError(
|
|
27
|
+
'Verification code is invalid or expired',
|
|
28
|
+
'email-challenge-invalid',
|
|
29
|
+
{
|
|
30
|
+
status: 410,
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (error instanceof EmailChallengeError) {
|
|
35
|
+
throw new ClientError(error.message, `email-challenge-${error.code}`, {
|
|
36
|
+
status: error.code === 'expired' || error.code === 'exhausted' ? 410 : 400,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
},
|
|
41
|
+
handler: async (request, reply) => {
|
|
42
|
+
const {email, challenge_id, code} = request.body;
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
const result = await confirmEmailVerification({
|
|
45
|
+
email,
|
|
46
|
+
challengeId: challenge_id,
|
|
47
|
+
code,
|
|
48
|
+
workspaces,
|
|
49
|
+
});
|
|
50
|
+
setRefreshTokenCookie(reply, result.refreshToken);
|
|
34
51
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
52
|
+
return toAuthSessionDto(result);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {verifyEmailResendBodySchema, verifyEmailResendResponseSchema} from '@shipfox/api-auth-dto';
|
|
2
|
-
import {
|
|
2
|
+
import {EmailChallengeError} from '@shipfox/api-email-challenges';
|
|
3
|
+
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
3
4
|
import {resendEmailVerification} from '#core/auth.js';
|
|
4
5
|
import {createAuthRateLimitPreHandler} from '#presentation/routes/rate-limit.js';
|
|
5
6
|
|
|
6
7
|
export const verifyEmailResendRoute = defineRoute({
|
|
7
8
|
method: 'POST',
|
|
8
9
|
path: '/verify-email/resend',
|
|
9
|
-
description: 'Send a new email verification
|
|
10
|
+
description: 'Send a new email verification code if the account can be verified.',
|
|
10
11
|
schema: {
|
|
11
12
|
body: verifyEmailResendBodySchema,
|
|
12
13
|
response: {
|
|
@@ -14,10 +15,23 @@ export const verifyEmailResendRoute = defineRoute({
|
|
|
14
15
|
},
|
|
15
16
|
},
|
|
16
17
|
preHandler: createAuthRateLimitPreHandler('email-send'),
|
|
18
|
+
errorHandler: (error) => {
|
|
19
|
+
if (error instanceof EmailChallengeError) {
|
|
20
|
+
throw new ClientError(error.message, `email-challenge-${error.code}`, {
|
|
21
|
+
status: error.code === 'limited' ? 429 : error.code === 'expired' ? 410 : 400,
|
|
22
|
+
...(error.retryAt ? {details: {retry_at: error.retryAt.toISOString()}} : {}),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
},
|
|
17
27
|
handler: async (request, reply) => {
|
|
18
|
-
const {email} = request.body;
|
|
28
|
+
const {email, challenge_id} = request.body;
|
|
19
29
|
|
|
20
|
-
const result = await resendEmailVerification({
|
|
30
|
+
const result = await resendEmailVerification({
|
|
31
|
+
email,
|
|
32
|
+
challengeId: challenge_id,
|
|
33
|
+
sourceIp: request.ip,
|
|
34
|
+
});
|
|
21
35
|
|
|
22
36
|
reply.code(200).send({
|
|
23
37
|
next_resend_available_at: result.nextResendAvailableAt.toISOString(),
|
|
@@ -2,6 +2,13 @@ import {createApp} from '@shipfox/node-fastify';
|
|
|
2
2
|
import {createJwtAuthMethod} from '#presentation/auth/jwt-auth.js';
|
|
3
3
|
import {buildAuthRoutes} from './index.js';
|
|
4
4
|
|
|
5
|
+
const workspaces = {
|
|
6
|
+
listMembershipsForTokenClaims: vi.fn(() => Promise.resolve({memberships: []})),
|
|
7
|
+
preflightInvitationAcceptance: vi.fn(),
|
|
8
|
+
acceptInvitation: vi.fn(),
|
|
9
|
+
requireActiveMembership: vi.fn(),
|
|
10
|
+
};
|
|
11
|
+
|
|
5
12
|
const disabledPasswordPaths = [
|
|
6
13
|
'/auth/signup',
|
|
7
14
|
'/auth/verify-email/confirm',
|
|
@@ -14,7 +21,7 @@ const disabledPasswordPaths = [
|
|
|
14
21
|
|
|
15
22
|
describe('buildAuthRoutes', () => {
|
|
16
23
|
test('preserves the default route table when password login is enabled', () => {
|
|
17
|
-
const routes = buildAuthRoutes(true);
|
|
24
|
+
const routes = buildAuthRoutes(true, workspaces);
|
|
18
25
|
|
|
19
26
|
expect(routes.routes.map((route) => ('path' in route ? route.path : route.prefix))).toEqual([
|
|
20
27
|
'/signup',
|
|
@@ -31,7 +38,7 @@ describe('buildAuthRoutes', () => {
|
|
|
31
38
|
});
|
|
32
39
|
|
|
33
40
|
test('keeps only session lifecycle routes when password login is disabled', () => {
|
|
34
|
-
const routes = buildAuthRoutes(false);
|
|
41
|
+
const routes = buildAuthRoutes(false, workspaces);
|
|
35
42
|
|
|
36
43
|
expect(routes.routes.map((route) => ('path' in route ? route.path : route.prefix))).toEqual([
|
|
37
44
|
'/refresh',
|
|
@@ -43,7 +50,7 @@ describe('buildAuthRoutes', () => {
|
|
|
43
50
|
test('does not register password or email-verification routes when password login is disabled', async () => {
|
|
44
51
|
const app = await createApp({
|
|
45
52
|
auth: [createJwtAuthMethod()],
|
|
46
|
-
routes: [buildAuthRoutes(false)],
|
|
53
|
+
routes: [buildAuthRoutes(false, workspaces)],
|
|
47
54
|
swagger: false,
|
|
48
55
|
});
|
|
49
56
|
|
|
@@ -1,20 +1,28 @@
|
|
|
1
|
+
import type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';
|
|
1
2
|
import type {RouteGroup} from '@shipfox/node-fastify';
|
|
2
|
-
import {config} from '#config.js';
|
|
3
3
|
import {authCookiePlugin} from '#presentation/auth/refresh-cookie.js';
|
|
4
|
-
import {
|
|
4
|
+
import {createVerifyEmailConfirmRoute} from './email-verification/verify-email-confirm.js';
|
|
5
5
|
import {verifyEmailResendRoute} from './email-verification/verify-email-resend.js';
|
|
6
6
|
import {changePasswordRoute} from './password/change-password.js';
|
|
7
|
-
import {
|
|
7
|
+
import {createPasswordResetConfirmRoute} from './password/password-reset-confirm.js';
|
|
8
8
|
import {passwordResetRequestRoute} from './password/password-reset-request.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import {createSignupRoute} from './registration/signup.js';
|
|
10
|
+
import {createLoginRoute} from './session/login.js';
|
|
11
11
|
import {logoutRoute} from './session/logout.js';
|
|
12
12
|
import {meRoute} from './session/me.js';
|
|
13
|
-
import {
|
|
13
|
+
import {createRefreshRoute} from './session/refresh.js';
|
|
14
14
|
|
|
15
|
-
export function buildAuthRoutes(
|
|
15
|
+
export function buildAuthRoutes(
|
|
16
|
+
passwordEnabled: boolean,
|
|
17
|
+
workspaces: WorkspacesInterModuleClient,
|
|
18
|
+
): RouteGroup {
|
|
16
19
|
const passwordRoutes = passwordEnabled
|
|
17
|
-
? [
|
|
20
|
+
? [
|
|
21
|
+
createSignupRoute(workspaces),
|
|
22
|
+
createVerifyEmailConfirmRoute(workspaces),
|
|
23
|
+
verifyEmailResendRoute,
|
|
24
|
+
createLoginRoute(workspaces),
|
|
25
|
+
]
|
|
18
26
|
: [];
|
|
19
27
|
|
|
20
28
|
return {
|
|
@@ -22,14 +30,16 @@ export function buildAuthRoutes(passwordEnabled: boolean): RouteGroup {
|
|
|
22
30
|
plugins: [authCookiePlugin],
|
|
23
31
|
routes: [
|
|
24
32
|
...passwordRoutes,
|
|
25
|
-
|
|
33
|
+
createRefreshRoute(workspaces),
|
|
26
34
|
logoutRoute,
|
|
27
35
|
meRoute,
|
|
28
36
|
...(passwordEnabled
|
|
29
|
-
? [
|
|
37
|
+
? [
|
|
38
|
+
changePasswordRoute,
|
|
39
|
+
passwordResetRequestRoute,
|
|
40
|
+
createPasswordResetConfirmRoute(workspaces),
|
|
41
|
+
]
|
|
30
42
|
: []),
|
|
31
43
|
],
|
|
32
44
|
};
|
|
33
45
|
}
|
|
34
|
-
|
|
35
|
-
export const authRoutes = buildAuthRoutes(config.AUTH_PASSWORD_ENABLED);
|