@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
package/drizzle/0000_initial.sql
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
1
|
CREATE TYPE "public"."auth_user_status" AS ENUM('active', 'suspended', 'deleted');--> statement-breakpoint
|
|
2
|
-
CREATE TABLE "auth_email_verifications" (
|
|
3
|
-
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
4
|
-
"user_id" uuid NOT NULL,
|
|
5
|
-
"hashed_token" text NOT NULL,
|
|
6
|
-
"expires_at" timestamp with time zone NOT NULL,
|
|
7
|
-
"used_at" timestamp with time zone,
|
|
8
|
-
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
9
|
-
);
|
|
10
|
-
--> statement-breakpoint
|
|
11
2
|
CREATE TABLE "auth_outbox" (
|
|
12
3
|
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
13
4
|
"event_type" text NOT NULL,
|
|
@@ -65,11 +56,8 @@ CREATE TABLE "auth_users" (
|
|
|
65
56
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
66
57
|
);
|
|
67
58
|
--> statement-breakpoint
|
|
68
|
-
ALTER TABLE "auth_email_verifications" ADD CONSTRAINT "auth_email_verifications_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
69
59
|
ALTER TABLE "auth_password_resets" ADD CONSTRAINT "auth_password_resets_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
70
60
|
ALTER TABLE "auth_refresh_tokens" ADD CONSTRAINT "auth_refresh_tokens_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
71
|
-
CREATE UNIQUE INDEX "auth_email_verifications_hashed_token_unique" ON "auth_email_verifications" USING btree ("hashed_token");--> statement-breakpoint
|
|
72
|
-
CREATE INDEX "auth_email_verifications_user_id_idx" ON "auth_email_verifications" USING btree ("user_id");--> statement-breakpoint
|
|
73
61
|
CREATE INDEX "auth_outbox_pending_idx" ON "auth_outbox" USING btree ("next_dispatch_at","created_at") WHERE "dispatched_at" IS NULL AND "dead_lettered_at" IS NULL;--> statement-breakpoint
|
|
74
62
|
CREATE INDEX "auth_outbox_dispatched_retention_idx" ON "auth_outbox" USING btree ("dispatched_at","id") WHERE "dispatched_at" IS NOT NULL;--> statement-breakpoint
|
|
75
63
|
CREATE UNIQUE INDEX "auth_password_resets_hashed_token_unique" ON "auth_password_resets" USING btree ("hashed_token");--> statement-breakpoint
|
|
@@ -78,4 +66,4 @@ CREATE UNIQUE INDEX "auth_rate_limits_window_unique" ON "auth_rate_limits" USING
|
|
|
78
66
|
CREATE INDEX "auth_rate_limits_expires_at_idx" ON "auth_rate_limits" USING btree ("expires_at");--> statement-breakpoint
|
|
79
67
|
CREATE UNIQUE INDEX "auth_refresh_tokens_hashed_token_unique" ON "auth_refresh_tokens" USING btree ("hashed_token");--> statement-breakpoint
|
|
80
68
|
CREATE INDEX "auth_refresh_tokens_user_id_idx" ON "auth_refresh_tokens" USING btree ("user_id");--> statement-breakpoint
|
|
81
|
-
CREATE UNIQUE INDEX "auth_users_email_unique" ON "auth_users" USING btree ("email");
|
|
69
|
+
CREATE UNIQUE INDEX "auth_users_email_unique" ON "auth_users" USING btree ("email");
|
|
@@ -4,98 +4,6 @@
|
|
|
4
4
|
"version": "7",
|
|
5
5
|
"dialect": "postgresql",
|
|
6
6
|
"tables": {
|
|
7
|
-
"public.auth_email_verifications": {
|
|
8
|
-
"name": "auth_email_verifications",
|
|
9
|
-
"schema": "",
|
|
10
|
-
"columns": {
|
|
11
|
-
"id": {
|
|
12
|
-
"name": "id",
|
|
13
|
-
"type": "uuid",
|
|
14
|
-
"primaryKey": true,
|
|
15
|
-
"notNull": true,
|
|
16
|
-
"default": "uuidv7()"
|
|
17
|
-
},
|
|
18
|
-
"user_id": {
|
|
19
|
-
"name": "user_id",
|
|
20
|
-
"type": "uuid",
|
|
21
|
-
"primaryKey": false,
|
|
22
|
-
"notNull": true
|
|
23
|
-
},
|
|
24
|
-
"hashed_token": {
|
|
25
|
-
"name": "hashed_token",
|
|
26
|
-
"type": "text",
|
|
27
|
-
"primaryKey": false,
|
|
28
|
-
"notNull": true
|
|
29
|
-
},
|
|
30
|
-
"expires_at": {
|
|
31
|
-
"name": "expires_at",
|
|
32
|
-
"type": "timestamp with time zone",
|
|
33
|
-
"primaryKey": false,
|
|
34
|
-
"notNull": true
|
|
35
|
-
},
|
|
36
|
-
"used_at": {
|
|
37
|
-
"name": "used_at",
|
|
38
|
-
"type": "timestamp with time zone",
|
|
39
|
-
"primaryKey": false,
|
|
40
|
-
"notNull": false
|
|
41
|
-
},
|
|
42
|
-
"created_at": {
|
|
43
|
-
"name": "created_at",
|
|
44
|
-
"type": "timestamp with time zone",
|
|
45
|
-
"primaryKey": false,
|
|
46
|
-
"notNull": true,
|
|
47
|
-
"default": "now()"
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
"indexes": {
|
|
51
|
-
"auth_email_verifications_hashed_token_unique": {
|
|
52
|
-
"name": "auth_email_verifications_hashed_token_unique",
|
|
53
|
-
"columns": [
|
|
54
|
-
{
|
|
55
|
-
"expression": "hashed_token",
|
|
56
|
-
"isExpression": false,
|
|
57
|
-
"asc": true,
|
|
58
|
-
"nulls": "last"
|
|
59
|
-
}
|
|
60
|
-
],
|
|
61
|
-
"isUnique": true,
|
|
62
|
-
"concurrently": false,
|
|
63
|
-
"method": "btree",
|
|
64
|
-
"with": {}
|
|
65
|
-
},
|
|
66
|
-
"auth_email_verifications_user_id_idx": {
|
|
67
|
-
"name": "auth_email_verifications_user_id_idx",
|
|
68
|
-
"columns": [
|
|
69
|
-
{
|
|
70
|
-
"expression": "user_id",
|
|
71
|
-
"isExpression": false,
|
|
72
|
-
"asc": true,
|
|
73
|
-
"nulls": "last"
|
|
74
|
-
}
|
|
75
|
-
],
|
|
76
|
-
"isUnique": false,
|
|
77
|
-
"concurrently": false,
|
|
78
|
-
"method": "btree",
|
|
79
|
-
"with": {}
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
"foreignKeys": {
|
|
83
|
-
"auth_email_verifications_user_id_auth_users_id_fk": {
|
|
84
|
-
"name": "auth_email_verifications_user_id_auth_users_id_fk",
|
|
85
|
-
"tableFrom": "auth_email_verifications",
|
|
86
|
-
"tableTo": "auth_users",
|
|
87
|
-
"columnsFrom": ["user_id"],
|
|
88
|
-
"columnsTo": ["id"],
|
|
89
|
-
"onDelete": "cascade",
|
|
90
|
-
"onUpdate": "no action"
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
"compositePrimaryKeys": {},
|
|
94
|
-
"uniqueConstraints": {},
|
|
95
|
-
"policies": {},
|
|
96
|
-
"checkConstraints": {},
|
|
97
|
-
"isRLSEnabled": false
|
|
98
|
-
},
|
|
99
7
|
"public.auth_outbox": {
|
|
100
8
|
"name": "auth_outbox",
|
|
101
9
|
"schema": "",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-auth",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "6.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -30,22 +30,24 @@
|
|
|
30
30
|
"@node-rs/argon2": "^2.0.2",
|
|
31
31
|
"drizzle-orm": "^0.45.2",
|
|
32
32
|
"zod": "^4.4.3",
|
|
33
|
-
"@shipfox/api-auth-context": "
|
|
34
|
-
"@shipfox/api-auth-dto": "
|
|
35
|
-
"@shipfox/api-
|
|
36
|
-
"@shipfox/api-workspaces-dto": "
|
|
33
|
+
"@shipfox/api-auth-context": "6.0.0",
|
|
34
|
+
"@shipfox/api-auth-dto": "6.0.0",
|
|
35
|
+
"@shipfox/api-email-challenges": "0.2.0",
|
|
36
|
+
"@shipfox/api-workspaces-dto": "6.0.0",
|
|
37
|
+
"@shipfox/inter-module": "0.2.0",
|
|
37
38
|
"@shipfox/config": "1.2.2",
|
|
38
|
-
"@shipfox/node-drizzle": "0.3.
|
|
39
|
-
"@shipfox/node-
|
|
40
|
-
"@shipfox/node-
|
|
39
|
+
"@shipfox/node-drizzle": "0.3.2",
|
|
40
|
+
"@shipfox/node-auth-root-key": "0.2.0",
|
|
41
|
+
"@shipfox/node-fastify": "0.2.4",
|
|
42
|
+
"@shipfox/node-jwt": "0.3.0",
|
|
41
43
|
"@shipfox/node-email": "0.2.2",
|
|
42
|
-
"@shipfox/node-mailer": "0.
|
|
43
|
-
"@shipfox/node-module": "0.
|
|
44
|
+
"@shipfox/node-mailer": "0.2.0",
|
|
45
|
+
"@shipfox/node-module": "0.4.0",
|
|
44
46
|
"@shipfox/node-opentelemetry": "0.5.2",
|
|
45
|
-
"@shipfox/node-outbox": "0.2.
|
|
47
|
+
"@shipfox/node-outbox": "0.2.4",
|
|
46
48
|
"@shipfox/node-postgres": "0.4.2",
|
|
47
|
-
"@shipfox/node-rate-limit": "0.
|
|
48
|
-
"@shipfox/node-tokens": "0.
|
|
49
|
+
"@shipfox/node-rate-limit": "0.3.0",
|
|
50
|
+
"@shipfox/node-tokens": "0.3.0"
|
|
49
51
|
},
|
|
50
52
|
"devDependencies": {
|
|
51
53
|
"@types/pg": "^8.15.5",
|
|
@@ -55,6 +57,7 @@
|
|
|
55
57
|
"fishery": "^2.4.0",
|
|
56
58
|
"jose": "^6.0.0",
|
|
57
59
|
"@shipfox/biome": "1.8.2",
|
|
60
|
+
"@shipfox/depcruise": "1.0.2",
|
|
58
61
|
"@shipfox/swc": "1.2.6",
|
|
59
62
|
"@shipfox/ts-config": "1.3.8",
|
|
60
63
|
"@shipfox/typescript": "1.1.7",
|
|
@@ -64,6 +67,7 @@
|
|
|
64
67
|
"build": "shipfox-swc",
|
|
65
68
|
"check": "shipfox-biome-check",
|
|
66
69
|
"check:fix": "shipfox-biome-check --write",
|
|
70
|
+
"depcruise": "shipfox-depcruise",
|
|
67
71
|
"test": "shipfox-vitest-run",
|
|
68
72
|
"test:watch": "shipfox-vitest-watch",
|
|
69
73
|
"type": "shipfox-tsc-check",
|
package/src/config.ts
CHANGED
|
@@ -1,24 +1,14 @@
|
|
|
1
1
|
import {bool, createConfig, num, str} from '@shipfox/config';
|
|
2
|
-
import {createConsoleMailer, createSmtpMailer, type Mailer} from '@shipfox/node-mailer';
|
|
3
2
|
|
|
4
3
|
export const config = createConfig({
|
|
5
|
-
AUTH_JWT_SECRET: str({
|
|
6
|
-
desc: 'Secret used to sign and verify user access tokens (JWTs). Required, with no default, so startup fails when it is missing.',
|
|
7
|
-
}),
|
|
8
4
|
AUTH_JWT_EXPIRES_IN: str({
|
|
9
5
|
desc: 'How long an access token stays valid. Accepts a duration string such as 15m, 1h, or 7d.',
|
|
10
6
|
default: '15m',
|
|
11
7
|
}),
|
|
12
|
-
AUTH_JOB_LEASE_TOKEN_SECRET: str({
|
|
13
|
-
desc: 'Secret used to sign and verify job lease tokens. Required, with no default, so startup fails when it is missing.',
|
|
14
|
-
}),
|
|
15
8
|
AUTH_JOB_LEASE_TOKEN_EXPIRES_IN: str({
|
|
16
9
|
desc: 'How long a job lease token stays valid. Set it longer than the longest job (JOB_MAX_DURATION is 60 minutes) plus a safety margin.',
|
|
17
10
|
default: '90m',
|
|
18
11
|
}),
|
|
19
|
-
AUTH_RUNNER_SESSION_TOKEN_SECRET: str({
|
|
20
|
-
desc: 'Secret used to sign and verify runner session tokens. Required, with no default, so startup fails when it is missing.',
|
|
21
|
-
}),
|
|
22
12
|
AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN: str({
|
|
23
13
|
desc: 'How long a runner session token stays valid. A revoked registration token can leave existing sessions usable until this lifetime ends.',
|
|
24
14
|
default: '1h',
|
|
@@ -39,54 +29,8 @@ export const config = createConfig({
|
|
|
39
29
|
desc: 'Whether password login is available. Use true or false. Defaults to true. When false, password and email-verification routes are not registered, and server startup requires another module to contribute a login method.',
|
|
40
30
|
default: true,
|
|
41
31
|
}),
|
|
42
|
-
RATE_LIMIT_IDENTIFIER_SECRET: str({
|
|
43
|
-
desc: 'Optional secret used to HMAC identifiers before storing rate-limit counters. Leave it unset to derive a stable key from AUTH_JWT_SECRET.',
|
|
44
|
-
default: undefined,
|
|
45
|
-
}),
|
|
46
32
|
CLIENT_BASE_URL: str({
|
|
47
33
|
desc: 'Base URL of the client app. Used to build links in emails such as password resets.',
|
|
48
34
|
default: 'http://localhost:5173',
|
|
49
35
|
}),
|
|
50
|
-
MAILER_TRANSPORT: str({
|
|
51
|
-
desc: 'How emails are delivered. Use console to print them to the log, or smtp to send them through an SMTP server.',
|
|
52
|
-
choices: ['console', 'smtp'],
|
|
53
|
-
default: 'console',
|
|
54
|
-
}),
|
|
55
|
-
MAILER_FROM: str({
|
|
56
|
-
desc: 'Sender address shown on outgoing emails.',
|
|
57
|
-
default: 'noreply@shipfox.local',
|
|
58
|
-
}),
|
|
59
|
-
SMTP_HOST: str({
|
|
60
|
-
desc: 'Hostname of the SMTP server. Required when MAILER_TRANSPORT is smtp.',
|
|
61
|
-
default: undefined,
|
|
62
|
-
}),
|
|
63
|
-
SMTP_PORT: num({
|
|
64
|
-
desc: 'Port of the SMTP server.',
|
|
65
|
-
default: 587,
|
|
66
|
-
}),
|
|
67
|
-
SMTP_USER: str({
|
|
68
|
-
desc: 'Username for SMTP authentication. Leave it unset if the server needs no login.',
|
|
69
|
-
default: undefined,
|
|
70
|
-
}),
|
|
71
|
-
SMTP_PASSWORD: str({
|
|
72
|
-
desc: 'Password for SMTP authentication. Leave it unset if the server needs no login.',
|
|
73
|
-
default: undefined,
|
|
74
|
-
}),
|
|
75
36
|
});
|
|
76
|
-
|
|
77
|
-
function createMailer(): Mailer {
|
|
78
|
-
if (config.MAILER_TRANSPORT === 'smtp') {
|
|
79
|
-
if (!config.SMTP_HOST) throw new Error('SMTP_HOST is required when MAILER_TRANSPORT=smtp');
|
|
80
|
-
return createSmtpMailer({
|
|
81
|
-
host: config.SMTP_HOST,
|
|
82
|
-
port: config.SMTP_PORT,
|
|
83
|
-
...(config.SMTP_USER ? {user: config.SMTP_USER} : {}),
|
|
84
|
-
...(config.SMTP_PASSWORD ? {password: config.SMTP_PASSWORD} : {}),
|
|
85
|
-
from: config.MAILER_FROM,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return createConsoleMailer({from: config.MAILER_FROM});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export const mailer = createMailer();
|
package/src/core/auth.test.ts
CHANGED
|
@@ -1,24 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
AUTH_PASSWORD_RESET_SEND_REQUESTED,
|
|
4
|
-
EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS,
|
|
5
|
-
} from '@shipfox/api-auth-dto';
|
|
1
|
+
import {AUTH_PASSWORD_RESET_SEND_REQUESTED, AUTH_USER_SIGNED_UP} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {userAccessTokenKey} from '@shipfox/node-auth-root-key';
|
|
6
3
|
import type {Mailer, MailMessage} from '@shipfox/node-mailer';
|
|
7
4
|
import {hashOpaqueToken} from '@shipfox/node-tokens';
|
|
8
5
|
import {and, desc, eq, sql} from 'drizzle-orm';
|
|
9
6
|
import {
|
|
10
7
|
changePassword,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
confirmPasswordReset as coreConfirmPasswordReset,
|
|
9
|
+
createSessionForUser as coreCreateSessionForUser,
|
|
10
|
+
login as coreLogin,
|
|
11
|
+
refreshAccessToken as coreRefreshAccessToken,
|
|
14
12
|
getCurrentUser,
|
|
15
|
-
login,
|
|
16
13
|
logout,
|
|
17
14
|
provisionUser,
|
|
18
|
-
refreshAccessToken,
|
|
19
15
|
requestPasswordReset,
|
|
20
|
-
resendEmailVerification,
|
|
21
16
|
signup,
|
|
17
|
+
signupWithInvitation,
|
|
22
18
|
} from '#core/auth.js';
|
|
23
19
|
import {
|
|
24
20
|
AuthDependencyUnavailableError,
|
|
@@ -31,7 +27,6 @@ import {
|
|
|
31
27
|
import {verifyUserToken} from '#core/jwt.js';
|
|
32
28
|
import {db} from '#db/db.js';
|
|
33
29
|
import * as refreshTokenDb from '#db/refresh-tokens.js';
|
|
34
|
-
import {emailVerifications} from '#db/schema/email-verifications.js';
|
|
35
30
|
import {authOutbox} from '#db/schema/outbox.js';
|
|
36
31
|
import {passwordResets} from '#db/schema/password-resets.js';
|
|
37
32
|
import {refreshTokens} from '#db/schema/refresh-tokens.js';
|
|
@@ -62,7 +57,6 @@ const testConfig = vi.hoisted(
|
|
|
62
57
|
|
|
63
58
|
vi.mock('#config.js', () => ({
|
|
64
59
|
config: {
|
|
65
|
-
AUTH_JWT_SECRET: 'auth-core-test-secret',
|
|
66
60
|
AUTH_JWT_EXPIRES_IN: '15m',
|
|
67
61
|
AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS: 14,
|
|
68
62
|
AUTH_REFRESH_ROTATION_GRACE_SECONDS: 30,
|
|
@@ -72,12 +66,25 @@ vi.mock('#config.js', () => ({
|
|
|
72
66
|
mailer: testConfig.mailer,
|
|
73
67
|
}));
|
|
74
68
|
|
|
75
|
-
vi.mock('@shipfox/
|
|
76
|
-
listMembershipsByUser: vi.fn(() => Promise.resolve([])),
|
|
77
|
-
}));
|
|
69
|
+
vi.mock('@shipfox/node-mailer', () => ({mailer: testConfig.mailer}));
|
|
78
70
|
|
|
79
|
-
const
|
|
80
|
-
|
|
71
|
+
const listMembershipsByUserMock = vi.fn(() =>
|
|
72
|
+
Promise.resolve({memberships: [] as Array<{workspaceId: string; role: 'admin'}>}),
|
|
73
|
+
);
|
|
74
|
+
const workspaces = {
|
|
75
|
+
listMembershipsForTokenClaims: listMembershipsByUserMock,
|
|
76
|
+
preflightInvitationAcceptance: vi.fn(),
|
|
77
|
+
acceptInvitation: vi.fn(),
|
|
78
|
+
requireActiveMembership: vi.fn(),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const login = (params: {email: string; password: string}) => coreLogin({...params, workspaces});
|
|
82
|
+
const createSessionForUser = (params: {userId?: string; email?: string}) =>
|
|
83
|
+
coreCreateSessionForUser({...params, workspaces});
|
|
84
|
+
const refreshAccessToken = (params: {refreshToken: string}) =>
|
|
85
|
+
coreRefreshAccessToken({...params, workspaces});
|
|
86
|
+
const confirmPasswordReset = (params: {token: string; newPassword: string}) =>
|
|
87
|
+
coreConfirmPasswordReset({...params, workspaces});
|
|
81
88
|
|
|
82
89
|
const TOKEN_RE = /token=([\w\-_=]+)/;
|
|
83
90
|
|
|
@@ -101,9 +108,8 @@ async function outboxEventsTo(email: string, eventType: string) {
|
|
|
101
108
|
|
|
102
109
|
async function latestEmailLinkTo(email: string, eventType: string): Promise<string> {
|
|
103
110
|
const event = (await outboxEventsTo(email, eventType))[0];
|
|
104
|
-
const payload = event?.payload as {
|
|
105
|
-
const link =
|
|
106
|
-
eventType === AUTH_EMAIL_VERIFICATION_SEND_REQUESTED ? payload?.verifyLink : payload?.resetLink;
|
|
111
|
+
const payload = event?.payload as {resetLink?: string} | undefined;
|
|
112
|
+
const link = payload?.resetLink;
|
|
107
113
|
if (!link) throw new Error(`No ${eventType} outbox link for ${email}`);
|
|
108
114
|
return link;
|
|
109
115
|
}
|
|
@@ -128,14 +134,22 @@ describe('auth core', () => {
|
|
|
128
134
|
expect(user.email).toBe(email);
|
|
129
135
|
expect(user.name).toBe('Sign Up');
|
|
130
136
|
expect(user.emailVerifiedAt).toBeNull();
|
|
131
|
-
expect(
|
|
132
|
-
expect(
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
expect(user.emailChallenge.id).toEqual(expect.any(String));
|
|
138
|
+
expect(user.emailChallenge.nextResendAvailableAt).toBeInstanceOf(Date);
|
|
139
|
+
expect(captured).toHaveLength(1);
|
|
140
|
+
|
|
141
|
+
const events = await outboxEventsTo(email, AUTH_USER_SIGNED_UP);
|
|
142
|
+
expect(events).toHaveLength(1);
|
|
143
|
+
expect(events[0]?.payload).toEqual({
|
|
144
|
+
userId: user.id,
|
|
145
|
+
email,
|
|
146
|
+
name: 'Sign Up',
|
|
147
|
+
viaInvitation: false,
|
|
148
|
+
});
|
|
135
149
|
});
|
|
136
150
|
|
|
137
151
|
test('signup rejects duplicate email with a business error', async () => {
|
|
138
|
-
const existing = await userFactory.create();
|
|
152
|
+
const existing = await userFactory.create({emailVerifiedAt: new Date()});
|
|
139
153
|
|
|
140
154
|
const promise = signup({
|
|
141
155
|
email: existing.email,
|
|
@@ -145,6 +159,33 @@ describe('auth core', () => {
|
|
|
145
159
|
await expect(promise).rejects.toBeInstanceOf(EmailTakenError);
|
|
146
160
|
});
|
|
147
161
|
|
|
162
|
+
test('signup with an invitation writes the signed-up event with its user insert', async () => {
|
|
163
|
+
const email = `signup-invitation-${crypto.randomUUID()}@example.com`;
|
|
164
|
+
const userId = crypto.randomUUID();
|
|
165
|
+
const workspaceId = crypto.randomUUID();
|
|
166
|
+
workspaces.preflightInvitationAcceptance.mockResolvedValueOnce(undefined);
|
|
167
|
+
workspaces.acceptInvitation.mockResolvedValueOnce({
|
|
168
|
+
membership: {id: crypto.randomUUID(), userId, workspaceId},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const result = await signupWithInvitation({
|
|
172
|
+
email,
|
|
173
|
+
password: 'correct horse battery staple',
|
|
174
|
+
name: 'Invited User',
|
|
175
|
+
invitationToken: `invite-${crypto.randomUUID()}`,
|
|
176
|
+
workspaces,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const events = await outboxEventsTo(email, AUTH_USER_SIGNED_UP);
|
|
180
|
+
expect(events).toHaveLength(1);
|
|
181
|
+
expect(events[0]?.payload).toEqual({
|
|
182
|
+
userId: result.user.id,
|
|
183
|
+
email,
|
|
184
|
+
name: 'Invited User',
|
|
185
|
+
viaInvitation: true,
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
148
189
|
test('provisionUser creates a verified, password-less user with a normalized email', async () => {
|
|
149
190
|
const email = `Provision-${crypto.randomUUID()}@EXAMPLE.COM`;
|
|
150
191
|
|
|
@@ -165,11 +206,11 @@ describe('auth core', () => {
|
|
|
165
206
|
const storedSuspended = await findUserById({id: suspended.id});
|
|
166
207
|
|
|
167
208
|
const existingUnverified = await provisionUser({
|
|
168
|
-
email: unverified.email.toUpperCase()
|
|
209
|
+
email: ` ${unverified.email.toUpperCase()} `,
|
|
169
210
|
name: 'Replacement Name',
|
|
170
211
|
});
|
|
171
212
|
const existingSuspended = await provisionUser({
|
|
172
|
-
email: suspended.email.toUpperCase()
|
|
213
|
+
email: ` ${suspended.email.toUpperCase()} `,
|
|
173
214
|
name: 'Replacement Name',
|
|
174
215
|
});
|
|
175
216
|
|
|
@@ -257,6 +298,30 @@ describe('auth core', () => {
|
|
|
257
298
|
);
|
|
258
299
|
});
|
|
259
300
|
|
|
301
|
+
test('createSessionForUser({email}) resolves surrounding whitespace and mixed case', async () => {
|
|
302
|
+
const user = await userFactory.create({emailVerifiedAt: new Date()});
|
|
303
|
+
|
|
304
|
+
const result = await createSessionForUser({email: ` ${user.email.toUpperCase()} `});
|
|
305
|
+
|
|
306
|
+
expect(result.user.id).toBe(user.id);
|
|
307
|
+
expect(result.token).toEqual(expect.any(String));
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('createSessionForUser({email}) retains eligibility checks', async () => {
|
|
311
|
+
const unverified = await userFactory.create();
|
|
312
|
+
const suspended = await userFactory.create({emailVerifiedAt: new Date()});
|
|
313
|
+
await db().update(users).set({status: 'suspended'}).where(eq(users.id, suspended.id));
|
|
314
|
+
|
|
315
|
+
const unverifiedExpectation = expect(
|
|
316
|
+
createSessionForUser({email: unverified.email}),
|
|
317
|
+
).rejects.toBeInstanceOf(EmailNotVerifiedError);
|
|
318
|
+
const suspendedExpectation = expect(
|
|
319
|
+
createSessionForUser({email: suspended.email}),
|
|
320
|
+
).rejects.toBeInstanceOf(InvalidCredentialsError);
|
|
321
|
+
|
|
322
|
+
await Promise.all([unverifiedExpectation, suspendedExpectation]);
|
|
323
|
+
});
|
|
324
|
+
|
|
260
325
|
test('refreshAccessToken rotates the refresh token', async () => {
|
|
261
326
|
const user = await userFactory.create({emailVerifiedAt: new Date()});
|
|
262
327
|
const loginResult = await login({email: user.email, password: user.plainPassword});
|
|
@@ -329,127 +394,6 @@ describe('auth core', () => {
|
|
|
329
394
|
await expect(promise).rejects.toBeInstanceOf(TokenInvalidError);
|
|
330
395
|
});
|
|
331
396
|
|
|
332
|
-
test('confirmEmailVerification marks the user verified, creates a session, and rejects reused tokens', async () => {
|
|
333
|
-
const user = await signup({
|
|
334
|
-
email: `verify-${crypto.randomUUID()}@example.com`,
|
|
335
|
-
password: 'correct horse battery staple',
|
|
336
|
-
});
|
|
337
|
-
const token = extractToken(
|
|
338
|
-
await latestEmailLinkTo(user.email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED),
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
const result = await confirmEmailVerification({token});
|
|
342
|
-
const verified = await findUserById({id: user.id});
|
|
343
|
-
const refreshSession = await refreshTokenDb.findActiveRefreshTokenByHash({
|
|
344
|
-
hashedToken: hashOpaqueToken(result.refreshToken),
|
|
345
|
-
});
|
|
346
|
-
const reused = confirmEmailVerification({token});
|
|
347
|
-
|
|
348
|
-
expect(verified?.emailVerifiedAt).toBeInstanceOf(Date);
|
|
349
|
-
expect(result.token).toEqual(expect.any(String));
|
|
350
|
-
expect(result.user.id).toBe(user.id);
|
|
351
|
-
expect(refreshSession?.userId).toBe(user.id);
|
|
352
|
-
await expect(reused).rejects.toBeInstanceOf(TokenInvalidError);
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
test('resendEmailVerification only sends for active unverified users', async () => {
|
|
356
|
-
const unverified = await userFactory.create();
|
|
357
|
-
const verified = await userFactory.create({emailVerifiedAt: new Date()});
|
|
358
|
-
const inactive = await userFactory.create();
|
|
359
|
-
await db().update(users).set({status: 'suspended'}).where(eq(users.id, inactive.id));
|
|
360
|
-
|
|
361
|
-
const sent = await resendEmailVerification({email: unverified.email});
|
|
362
|
-
const verifiedResult = await resendEmailVerification({email: verified.email});
|
|
363
|
-
const inactiveResult = await resendEmailVerification({email: inactive.email});
|
|
364
|
-
const missingResult = await resendEmailVerification({
|
|
365
|
-
email: `missing-${crypto.randomUUID()}@example.com`,
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
expect(captured).toHaveLength(0);
|
|
369
|
-
expect(
|
|
370
|
-
await outboxEventsTo(unverified.email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED),
|
|
371
|
-
).toHaveLength(1);
|
|
372
|
-
expect(
|
|
373
|
-
await outboxEventsTo(verified.email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED),
|
|
374
|
-
).toHaveLength(0);
|
|
375
|
-
expect(
|
|
376
|
-
await outboxEventsTo(inactive.email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED),
|
|
377
|
-
).toHaveLength(0);
|
|
378
|
-
expect(sent.nextResendAvailableAt).toBeInstanceOf(Date);
|
|
379
|
-
expect(verifiedResult.nextResendAvailableAt).toBeInstanceOf(Date);
|
|
380
|
-
expect(inactiveResult.nextResendAvailableAt).toBeInstanceOf(Date);
|
|
381
|
-
expect(missingResult.nextResendAvailableAt).toBeInstanceOf(Date);
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
test('resendEmailVerification respects cooldown without invalidating the current token', async () => {
|
|
385
|
-
const user = await signup({
|
|
386
|
-
email: `resend-cooldown-${crypto.randomUUID()}@example.com`,
|
|
387
|
-
password: 'correct horse battery staple',
|
|
388
|
-
});
|
|
389
|
-
const token = extractToken(
|
|
390
|
-
await latestEmailLinkTo(user.email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED),
|
|
391
|
-
);
|
|
392
|
-
const existingCreatedAt = new Date(
|
|
393
|
-
Date.now() - (EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS / 2) * 1000,
|
|
394
|
-
);
|
|
395
|
-
await db()
|
|
396
|
-
.update(emailVerifications)
|
|
397
|
-
.set({createdAt: existingCreatedAt})
|
|
398
|
-
.where(eq(emailVerifications.userId, user.id));
|
|
399
|
-
|
|
400
|
-
const beforeCall = Date.now();
|
|
401
|
-
const result = await resendEmailVerification({email: user.email});
|
|
402
|
-
const verified = await confirmEmailVerification({token});
|
|
403
|
-
|
|
404
|
-
// The cooldown branch must return the generic `now + cooldown` estimate, not
|
|
405
|
-
// `latest.createdAt + cooldown`, so the response cannot leak prior timing.
|
|
406
|
-
expect(result.nextResendAvailableAt.getTime()).toBeGreaterThanOrEqual(
|
|
407
|
-
beforeCall + EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS * 1000,
|
|
408
|
-
);
|
|
409
|
-
expect(captured).toHaveLength(0);
|
|
410
|
-
expect(await outboxEventsTo(user.email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED)).toHaveLength(
|
|
411
|
-
1,
|
|
412
|
-
);
|
|
413
|
-
expect(verified.user.id).toBe(user.id);
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
test('resendEmailVerification sends again after cooldown', async () => {
|
|
417
|
-
const user = await signup({
|
|
418
|
-
email: `resend-after-cooldown-${crypto.randomUUID()}@example.com`,
|
|
419
|
-
password: 'correct horse battery staple',
|
|
420
|
-
});
|
|
421
|
-
const staleCreatedAt = new Date(
|
|
422
|
-
Date.now() - (EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS + 1) * 1000,
|
|
423
|
-
);
|
|
424
|
-
await db()
|
|
425
|
-
.update(emailVerifications)
|
|
426
|
-
.set({createdAt: staleCreatedAt})
|
|
427
|
-
.where(eq(emailVerifications.userId, user.id));
|
|
428
|
-
|
|
429
|
-
const result = await resendEmailVerification({email: user.email});
|
|
430
|
-
|
|
431
|
-
expect(result.nextResendAvailableAt).toBeInstanceOf(Date);
|
|
432
|
-
expect(captured).toHaveLength(0);
|
|
433
|
-
expect(await outboxEventsTo(user.email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED)).toHaveLength(
|
|
434
|
-
2,
|
|
435
|
-
);
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
test('resendEmailVerification serializes duplicate requests for one user', async () => {
|
|
439
|
-
const user = await userFactory.create();
|
|
440
|
-
|
|
441
|
-
const results = await Promise.all([
|
|
442
|
-
resendEmailVerification({email: user.email}),
|
|
443
|
-
resendEmailVerification({email: user.email}),
|
|
444
|
-
]);
|
|
445
|
-
|
|
446
|
-
expect(results).toHaveLength(2);
|
|
447
|
-
expect(captured).toHaveLength(0);
|
|
448
|
-
expect(await outboxEventsTo(user.email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED)).toHaveLength(
|
|
449
|
-
1,
|
|
450
|
-
);
|
|
451
|
-
});
|
|
452
|
-
|
|
453
397
|
test('password reset request and confirm update the password and invalidate the token', async () => {
|
|
454
398
|
const user = await userFactory.create({emailVerifiedAt: new Date()});
|
|
455
399
|
const loginResult = await login({email: user.email, password: user.plainPassword});
|
|
@@ -593,32 +537,16 @@ describe('auth core', () => {
|
|
|
593
537
|
const user = await userFactory.create({emailVerifiedAt: new Date()});
|
|
594
538
|
const workspaceA = crypto.randomUUID();
|
|
595
539
|
const workspaceB = crypto.randomUUID();
|
|
596
|
-
listMembershipsByUserMock.mockResolvedValueOnce(
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
workspaceId: workspaceA,
|
|
603
|
-
workspaceName: 'Workspace A',
|
|
604
|
-
createdAt: new Date(),
|
|
605
|
-
updatedAt: new Date(),
|
|
606
|
-
},
|
|
607
|
-
{
|
|
608
|
-
id: crypto.randomUUID(),
|
|
609
|
-
userId: user.id,
|
|
610
|
-
userEmail: user.email,
|
|
611
|
-
userName: null,
|
|
612
|
-
workspaceId: workspaceB,
|
|
613
|
-
workspaceName: 'Workspace B',
|
|
614
|
-
createdAt: new Date(),
|
|
615
|
-
updatedAt: new Date(),
|
|
616
|
-
},
|
|
617
|
-
]);
|
|
540
|
+
listMembershipsByUserMock.mockResolvedValueOnce({
|
|
541
|
+
memberships: [
|
|
542
|
+
{workspaceId: workspaceA, role: 'admin'},
|
|
543
|
+
{workspaceId: workspaceB, role: 'admin'},
|
|
544
|
+
],
|
|
545
|
+
});
|
|
618
546
|
|
|
619
547
|
const result = await login({email: user.email, password: user.plainPassword});
|
|
620
548
|
|
|
621
|
-
const claims = await verifyUserToken({token: result.token, secret:
|
|
549
|
+
const claims = await verifyUserToken({token: result.token, secret: userAccessTokenKey()});
|
|
622
550
|
expect(claims.name).toBe(user.name);
|
|
623
551
|
expect(claims.memberships).toEqual([
|
|
624
552
|
{workspaceId: workspaceA, role: 'admin'},
|
|
@@ -628,27 +556,18 @@ describe('auth core', () => {
|
|
|
628
556
|
|
|
629
557
|
test('refresh re-fetches memberships so newly-added workspaces appear in the next token', async () => {
|
|
630
558
|
const user = await userFactory.create({emailVerifiedAt: new Date()});
|
|
631
|
-
listMembershipsByUserMock.mockResolvedValueOnce([]);
|
|
559
|
+
listMembershipsByUserMock.mockResolvedValueOnce({memberships: []});
|
|
632
560
|
const loginResult = await login({email: user.email, password: user.plainPassword});
|
|
633
561
|
|
|
634
562
|
const newWorkspaceId = crypto.randomUUID();
|
|
635
|
-
listMembershipsByUserMock.mockResolvedValueOnce(
|
|
636
|
-
{
|
|
637
|
-
|
|
638
|
-
userId: user.id,
|
|
639
|
-
userEmail: user.email,
|
|
640
|
-
userName: null,
|
|
641
|
-
workspaceId: newWorkspaceId,
|
|
642
|
-
workspaceName: 'New Workspace',
|
|
643
|
-
createdAt: new Date(),
|
|
644
|
-
updatedAt: new Date(),
|
|
645
|
-
},
|
|
646
|
-
]);
|
|
563
|
+
listMembershipsByUserMock.mockResolvedValueOnce({
|
|
564
|
+
memberships: [{workspaceId: newWorkspaceId, role: 'admin'}],
|
|
565
|
+
});
|
|
647
566
|
const refreshed = await refreshAccessToken({refreshToken: loginResult.refreshToken});
|
|
648
567
|
|
|
649
568
|
const refreshedClaims = await verifyUserToken({
|
|
650
569
|
token: refreshed.token,
|
|
651
|
-
secret:
|
|
570
|
+
secret: userAccessTokenKey(),
|
|
652
571
|
});
|
|
653
572
|
expect(refreshedClaims.memberships).toEqual([{workspaceId: newWorkspaceId, role: 'admin'}]);
|
|
654
573
|
});
|
|
@@ -664,7 +583,7 @@ describe('auth core', () => {
|
|
|
664
583
|
|
|
665
584
|
test('refresh fails closed when listMembershipsByUser throws', async () => {
|
|
666
585
|
const user = await userFactory.create({emailVerifiedAt: new Date()});
|
|
667
|
-
listMembershipsByUserMock.mockResolvedValueOnce([]);
|
|
586
|
+
listMembershipsByUserMock.mockResolvedValueOnce({memberships: []});
|
|
668
587
|
const loginResult = await login({email: user.email, password: user.plainPassword});
|
|
669
588
|
|
|
670
589
|
listMembershipsByUserMock.mockRejectedValueOnce(new Error('workspaces DB down'));
|