@shipfox/api-auth 9.1.0 → 9.2.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 +20 -0
- package/README.md +28 -3
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +22 -0
- package/dist/config.js.map +1 -1
- package/dist/core/admin-role-model.d.ts +5 -0
- package/dist/core/admin-role-model.d.ts.map +1 -0
- package/dist/core/admin-role-model.js +18 -0
- package/dist/core/admin-role-model.js.map +1 -0
- package/dist/core/admin-role.d.ts +13 -0
- package/dist/core/admin-role.d.ts.map +1 -0
- package/dist/core/admin-role.js +21 -0
- package/dist/core/admin-role.js.map +1 -0
- package/dist/core/auth.d.ts +9 -0
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +71 -20
- package/dist/core/auth.js.map +1 -1
- package/dist/core/entities/admin-grant.d.ts +10 -0
- package/dist/core/entities/admin-grant.d.ts.map +1 -0
- package/dist/core/entities/admin-grant.js +3 -0
- package/dist/core/entities/admin-grant.js.map +1 -0
- package/dist/core/errors.d.ts +10 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +19 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/ports.d.ts +13 -0
- package/dist/core/ports.d.ts.map +1 -0
- package/dist/core/ports.js +3 -0
- package/dist/core/ports.js.map +1 -0
- package/dist/core/signup-policy.d.ts +10 -0
- package/dist/core/signup-policy.d.ts.map +1 -0
- package/dist/core/signup-policy.js +39 -0
- package/dist/core/signup-policy.js.map +1 -0
- package/dist/db/admin-grants.d.ts +15 -0
- package/dist/db/admin-grants.d.ts.map +1 -0
- package/dist/db/admin-grants.js +49 -0
- package/dist/db/admin-grants.js.map +1 -0
- package/dist/db/db.d.ts +218 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +2 -0
- package/dist/db/db.js.map +1 -1
- package/dist/db/schema/admin-grants.d.ts +115 -0
- package/dist/db/schema/admin-grants.d.ts.map +1 -0
- package/dist/db/schema/admin-grants.js +42 -0
- package/dist/db/schema/admin-grants.js.map +1 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/presentation/dto/user.d.ts +2 -1
- package/dist/presentation/dto/user.d.ts.map +1 -1
- package/dist/presentation/dto/user.js +2 -1
- package/dist/presentation/dto/user.js.map +1 -1
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +26 -2
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/presentation/routes/index.d.ts +2 -1
- package/dist/presentation/routes/index.d.ts.map +1 -1
- package/dist/presentation/routes/index.js +2 -2
- package/dist/presentation/routes/index.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 +14 -3
- package/dist/presentation/routes/registration/signup.js.map +1 -1
- package/dist/presentation/routes/session/me.d.ts.map +1 -1
- package/dist/presentation/routes/session/me.js +5 -1
- package/dist/presentation/routes/session/me.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0001_brainy_shriek.sql +14 -0
- package/drizzle/meta/0001_snapshot.json +702 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +5 -5
- package/src/config.test.ts +39 -0
- package/src/config.ts +30 -0
- package/src/core/admin-role-model.ts +25 -0
- package/src/core/admin-role.test.ts +39 -0
- package/src/core/admin-role.ts +25 -0
- package/src/core/auth.test.ts +110 -0
- package/src/core/auth.ts +78 -21
- package/src/core/entities/admin-grant.ts +10 -0
- package/src/core/errors.ts +24 -0
- package/src/core/jwt.test.ts +1 -0
- package/src/core/ports.ts +7 -0
- package/src/core/signup-policy.test.ts +78 -0
- package/src/core/signup-policy.ts +42 -0
- package/src/db/admin-grants.test.ts +57 -0
- package/src/db/admin-grants.ts +83 -0
- package/src/db/db.ts +2 -0
- package/src/db/schema/admin-grants.ts +49 -0
- package/src/index.test.ts +53 -0
- package/src/index.ts +28 -5
- package/src/presentation/dto/user.ts +7 -2
- package/src/presentation/inter-module.test.ts +47 -0
- package/src/presentation/inter-module.ts +22 -1
- package/src/presentation/routes/index.ts +3 -1
- package/src/presentation/routes/registration/signup.test.ts +26 -0
- package/src/presentation/routes/registration/signup.ts +22 -2
- package/src/presentation/routes/session/login.test.ts +1 -0
- package/src/presentation/routes/session/me.test.ts +18 -2
- package/src/presentation/routes/session/me.ts +5 -1
- package/test/globalSetup.ts +1 -1
- package/tsconfig.build.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
$ shipfox-swc
|
|
2
|
-
Successfully compiled:
|
|
2
|
+
Successfully compiled: 59 files with swc (416.7ms)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @shipfox/api-auth
|
|
2
2
|
|
|
3
|
+
## 9.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- bd7c46b: Thread an optional signup policy through API Auth module composition in preparation for signup enforcement.
|
|
8
|
+
- 456c884: Add Auth-owned local administrator grants and server-side role evaluation.
|
|
9
|
+
- 36adf25: Guards password and external-identity account creation with the signup policy.
|
|
10
|
+
- f31bee6: Adds the environment-variable signup policy and its Auth-prefixed configuration.
|
|
11
|
+
- 63c0e5b: Adds the signup policy port and denial error to API Auth.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- b321700: Default the Auth module to its environment-backed signup policy when no custom policy is provided.
|
|
16
|
+
- ad36e26: Return a bounded signup-not-allowed message from the password signup route.
|
|
17
|
+
- Updated dependencies [456c884]
|
|
18
|
+
- @shipfox/api-auth-dto@9.2.0
|
|
19
|
+
- @shipfox/api-email-challenges@1.1.4
|
|
20
|
+
- @shipfox/api-workspaces-dto@9.2.0
|
|
21
|
+
- @shipfox/api-auth-context@9.2.0
|
|
22
|
+
|
|
3
23
|
## 9.1.0
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -63,10 +63,21 @@ Required environment:
|
|
|
63
63
|
| `AUTH_REFRESH_ROTATION_GRACE_SECONDS` | `30` | Grace window for accepting a just-rotated refresh token during concurrent refreshes. |
|
|
64
64
|
| `AUTH_REFRESH_COOKIE_NAME` | `shipfox_refresh_token` | HTTP cookie name for refresh sessions. |
|
|
65
65
|
| `AUTH_PASSWORD_ENABLED` | `true` | Enables password and email-verification routes. Set to `false` when another module provides login. Server construction fails if no login method is available. |
|
|
66
|
+
| `AUTH_SIGNUP_GATE_ENABLED` | `false` | Restricts new account creation to the configured signup email allowlist. Requires `AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS` or `AUTH_SIGNUP_ALLOWED_EMAILS` to be set. |
|
|
67
|
+
| `AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS` | none | Comma-separated email domains allowed to create accounts, such as `shipfox.io,acme.com`. |
|
|
68
|
+
| `AUTH_SIGNUP_ALLOWED_EMAILS` | none | Comma-separated exact email addresses allowed to create accounts. |
|
|
69
|
+
| `AUTH_SIGNUP_NOT_ALLOWED_MESSAGE` | none | Description returned when the signup gate blocks an account. Defaults to "This Shipfox deployment does not accept new accounts right now." |
|
|
66
70
|
| `CLIENT_BASE_URL` | `http://localhost:5173` | Base URL used in email verification and password reset links. |
|
|
67
71
|
|
|
68
72
|
Email delivery uses the shared `@shipfox/node-mailer` configuration.
|
|
69
73
|
|
|
74
|
+
The signup gate is off by default. When it is on, provide at least one non-empty
|
|
75
|
+
entry in `AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS` or `AUTH_SIGNUP_ALLOWED_EMAILS`.
|
|
76
|
+
Startup fails when both lists are empty. Values are trimmed and lowercased.
|
|
77
|
+
The policy matches exact email addresses or exact domains. It does not match
|
|
78
|
+
subdomains or wildcards. `createAuthModule` applies these settings by default;
|
|
79
|
+
pass a `signupPolicy` to replace the environment-backed policy.
|
|
80
|
+
|
|
70
81
|
When `AUTH_PASSWORD_ENABLED=false`, the module does not register signup, login, password-reset, password-change, or email-verification routes. Refresh, logout, and current-session routes stay available. Another module must contribute a login method before the API server starts.
|
|
71
82
|
|
|
72
83
|
## Security model
|
|
@@ -202,7 +213,7 @@ All routes are mounted under `/auth`.
|
|
|
202
213
|
|
|
203
214
|
| Method | Path | Auth | Result |
|
|
204
215
|
| --- | --- | --- | --- |
|
|
205
|
-
| `POST` | `/signup` | none | Creates a user and sends an eight-digit email verification code. |
|
|
216
|
+
| `POST` | `/signup` | none | Creates a user and sends an eight-digit email verification code. A denied new signup returns `403` with error code `signup-not-allowed` and the policy message. |
|
|
206
217
|
| `POST` | `/verify-email/confirm` | none | Confirms a verification code, returns an access token, and sets the refresh cookie. |
|
|
207
218
|
| `POST` | `/verify-email/resend` | none | Sends a new verification code when the account is eligible. |
|
|
208
219
|
| `POST` | `/login` | none | Returns an access token and sets the refresh cookie. |
|
|
@@ -244,7 +255,9 @@ application composition:
|
|
|
244
255
|
```ts
|
|
245
256
|
import {createAuthModule} from '@shipfox/api-auth';
|
|
246
257
|
|
|
247
|
-
const authModule = createAuthModule({
|
|
258
|
+
const authModule = createAuthModule({
|
|
259
|
+
workspaces,
|
|
260
|
+
});
|
|
248
261
|
```
|
|
249
262
|
|
|
250
263
|
It also exports lower-level pieces for tests and advanced integration:
|
|
@@ -256,6 +269,7 @@ It also exports lower-level pieces for tests and advanced integration:
|
|
|
256
269
|
- `createLeaseTokenAuthMethod()`: the Fastify auth method for job lease tokens.
|
|
257
270
|
- `issueRunnerSessionToken(claims)` / `verifyRunnerSessionToken(token)`: mint and verify runner session tokens.
|
|
258
271
|
- `issueJobLeaseToken(claims)` / `verifyJobLeaseToken(token)`: mint and verify job lease tokens.
|
|
272
|
+
- `createEnvironmentSignupPolicy()`: creates the environment-backed signup policy used by default. Pass `signupPolicy` to `createAuthModule` to replace it.
|
|
259
273
|
- `getClientContext(request)`: reads the authenticated user context from a Fastify request.
|
|
260
274
|
- `getAuthenticatedSessionContext(request)`: resolves an authenticated request to its user ID and active refresh-session ID.
|
|
261
275
|
- `findUserByEmail({email})`: read-only lookup of the current owner of a normalized email; see below.
|
|
@@ -270,18 +284,20 @@ create a normal Shipfox session and set its refresh cookie:
|
|
|
270
284
|
import {
|
|
271
285
|
authCookiePlugin,
|
|
272
286
|
createSessionForUser,
|
|
287
|
+
createEnvironmentSignupPolicy,
|
|
273
288
|
provisionUser,
|
|
274
289
|
setRefreshTokenCookie,
|
|
275
290
|
} from '@shipfox/api-auth';
|
|
276
291
|
import type {FastifyReply} from 'fastify';
|
|
277
292
|
|
|
278
293
|
export const callbackRoutePlugins = [authCookiePlugin];
|
|
294
|
+
const signupPolicy = createEnvironmentSignupPolicy();
|
|
279
295
|
|
|
280
296
|
export async function completeProviderCallback(
|
|
281
297
|
reply: FastifyReply,
|
|
282
298
|
profile: {email: string; name?: string},
|
|
283
299
|
): Promise<string> {
|
|
284
|
-
const user = await provisionUser(profile);
|
|
300
|
+
const user = await provisionUser({...profile, signupPolicy});
|
|
285
301
|
const session = await createSessionForUser({userId: user.id});
|
|
286
302
|
|
|
287
303
|
setRefreshTokenCookie(reply, session.refreshToken);
|
|
@@ -295,6 +311,12 @@ For OAuth providers, require a verified-email claim such as `email_verified`.
|
|
|
295
311
|
session for any active, verified account. An unverified provider email could
|
|
296
312
|
otherwise sign in as an existing password account with the same address.
|
|
297
313
|
|
|
314
|
+
Pass the same `signupPolicy` to `provisionUser` when signup gating is enabled.
|
|
315
|
+
The function checks for an existing user before it calls the policy, so existing
|
|
316
|
+
users keep signing in even when their address is no longer allowed. For a new
|
|
317
|
+
user, a denied policy throws `SignupNotAllowedError`. Call
|
|
318
|
+
`createSessionForUser` only after provisioning succeeds.
|
|
319
|
+
|
|
298
320
|
Add `authCookiePlugin` to the callback route group before calling a cookie
|
|
299
321
|
helper. `getRefreshTokenCookie`, `setRefreshTokenCookie`, and
|
|
300
322
|
`clearRefreshTokenCookie` use the configured cookie name and the `/auth` path.
|
|
@@ -373,6 +395,9 @@ Passwords use Argon2id. Password reset tokens and refresh tokens are opaque toke
|
|
|
373
395
|
## Behavior Notes
|
|
374
396
|
|
|
375
397
|
- Signup sends an eight-digit verification code and returns the new user with its challenge ID.
|
|
398
|
+
- Signup gating blocks account creation only. Existing users keep signing in, even when their address is not on the current allowlist.
|
|
399
|
+
- A denied password signup returns `403` with error code `signup-not-allowed`. The policy message is capped at 500 characters.
|
|
400
|
+
- Invitation signup validates the invitation and bypasses the signup policy. Invitations remain a separate authorization path.
|
|
376
401
|
- Login only succeeds for active users with verified email addresses and a password hash.
|
|
377
402
|
- Refresh tokens rotate on each refresh.
|
|
378
403
|
- Password reset tokens and email challenge proofs are consumed once.
|
package/dist/config.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export declare const config: Readonly<{
|
|
|
6
6
|
AUTH_REFRESH_ROTATION_GRACE_SECONDS: number;
|
|
7
7
|
AUTH_REFRESH_COOKIE_NAME: string;
|
|
8
8
|
AUTH_PASSWORD_ENABLED: boolean;
|
|
9
|
+
AUTH_SIGNUP_GATE_ENABLED: boolean;
|
|
10
|
+
AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS: string;
|
|
11
|
+
AUTH_SIGNUP_ALLOWED_EMAILS: string;
|
|
12
|
+
AUTH_SIGNUP_NOT_ALLOWED_MESSAGE: string | undefined;
|
|
9
13
|
CLIENT_BASE_URL: string;
|
|
10
14
|
} & import("@shipfox/config").CleanedEnvAccessors>;
|
|
11
15
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM;;;;;;;;;;;;;kDAiDjB,CAAC"}
|
package/dist/config.js
CHANGED
|
@@ -28,10 +28,32 @@ export const config = createConfig({
|
|
|
28
28
|
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.',
|
|
29
29
|
default: true
|
|
30
30
|
}),
|
|
31
|
+
AUTH_SIGNUP_GATE_ENABLED: bool({
|
|
32
|
+
desc: 'Whether new account creation is restricted to the configured signup email allowlist. Defaults to false, which allows every signup.',
|
|
33
|
+
default: false
|
|
34
|
+
}),
|
|
35
|
+
AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS: str({
|
|
36
|
+
desc: 'Comma-separated email domains allowed to create accounts, such as shipfox.io,acme.com. Required when AUTH_SIGNUP_GATE_ENABLED is true unless AUTH_SIGNUP_ALLOWED_EMAILS is set.',
|
|
37
|
+
default: ''
|
|
38
|
+
}),
|
|
39
|
+
AUTH_SIGNUP_ALLOWED_EMAILS: str({
|
|
40
|
+
desc: 'Comma-separated exact email addresses allowed to create accounts. Required when AUTH_SIGNUP_GATE_ENABLED is true unless AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS is set.',
|
|
41
|
+
default: ''
|
|
42
|
+
}),
|
|
43
|
+
AUTH_SIGNUP_NOT_ALLOWED_MESSAGE: str({
|
|
44
|
+
desc: 'Description returned when the signup gate blocks an account. Optional. The default is This Shipfox deployment does not accept new accounts right now.',
|
|
45
|
+
default: undefined
|
|
46
|
+
}),
|
|
31
47
|
CLIENT_BASE_URL: str({
|
|
32
48
|
desc: 'Base URL of the client app. Used to build links in emails such as password resets.',
|
|
33
49
|
default: 'http://localhost:5173'
|
|
34
50
|
})
|
|
35
51
|
});
|
|
52
|
+
if (config.AUTH_SIGNUP_GATE_ENABLED && !hasSignupAllowlistEntry(config.AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS) && !hasSignupAllowlistEntry(config.AUTH_SIGNUP_ALLOWED_EMAILS)) {
|
|
53
|
+
throw new Error('AUTH_SIGNUP_GATE_ENABLED requires AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS or AUTH_SIGNUP_ALLOWED_EMAILS to be set.');
|
|
54
|
+
}
|
|
55
|
+
function hasSignupAllowlistEntry(value) {
|
|
56
|
+
return value.split(',').some((entry)=>entry.trim().length > 0);
|
|
57
|
+
}
|
|
36
58
|
|
|
37
59
|
//# sourceMappingURL=config.js.map
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {bool, createConfig, num, str} from '@shipfox/config';\n\nexport const config = createConfig({\n AUTH_JWT_EXPIRES_IN: str({\n desc: 'How long an access token stays valid. Accepts a duration string such as 15m, 1h, or 7d.',\n default: '15m',\n }),\n AUTH_JOB_LEASE_TOKEN_EXPIRES_IN: str({\n 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.',\n default: '90m',\n }),\n AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN: str({\n desc: 'How long a runner session token stays valid. A revoked registration token can leave existing sessions usable until this lifetime ends.',\n default: '1h',\n }),\n AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS: num({\n desc: 'How many days a refresh token stays valid before the user must sign in again.',\n default: 14,\n }),\n AUTH_REFRESH_ROTATION_GRACE_SECONDS: num({\n desc: 'Window after a refresh token is rotated during which the now-rotated token is still accepted, so concurrent refreshes from parallel tabs do not log the user out. Reuse past this window is treated as a compromise and revokes the session.',\n default: 30,\n }),\n AUTH_REFRESH_COOKIE_NAME: str({\n desc: 'Name of the browser cookie that stores the refresh token.',\n default: 'shipfox_refresh_token',\n }),\n AUTH_PASSWORD_ENABLED: bool({\n 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.',\n default: true,\n }),\n CLIENT_BASE_URL: str({\n desc: 'Base URL of the client app. Used to build links in emails such as password resets.',\n default: 'http://localhost:5173',\n }),\n});\n"],"names":["bool","createConfig","num","str","config","AUTH_JWT_EXPIRES_IN","desc","default","AUTH_JOB_LEASE_TOKEN_EXPIRES_IN","AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN","AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS","AUTH_REFRESH_ROTATION_GRACE_SECONDS","AUTH_REFRESH_COOKIE_NAME","AUTH_PASSWORD_ENABLED","CLIENT_BASE_URL"],"mappings":"AAAA,SAAQA,IAAI,EAAEC,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AAE7D,OAAO,MAAMC,SAASH,aAAa;IACjCI,qBAAqBF,IAAI;QACvBG,MAAM;QACNC,SAAS;IACX;IACAC,iCAAiCL,IAAI;QACnCG,MAAM;QACNC,SAAS;IACX;IACAE,sCAAsCN,IAAI;QACxCG,MAAM;QACNC,SAAS;IACX;IACAG,oCAAoCR,IAAI;QACtCI,MAAM;QACNC,SAAS;IACX;IACAI,qCAAqCT,IAAI;QACvCI,MAAM;QACNC,SAAS;IACX;IACAK,0BAA0BT,IAAI;QAC5BG,MAAM;QACNC,SAAS;IACX;IACAM,uBAAuBb,KAAK;QAC1BM,MAAM;QACNC,SAAS;IACX;IACAO,
|
|
1
|
+
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {bool, createConfig, num, str} from '@shipfox/config';\n\nexport const config = createConfig({\n AUTH_JWT_EXPIRES_IN: str({\n desc: 'How long an access token stays valid. Accepts a duration string such as 15m, 1h, or 7d.',\n default: '15m',\n }),\n AUTH_JOB_LEASE_TOKEN_EXPIRES_IN: str({\n 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.',\n default: '90m',\n }),\n AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN: str({\n desc: 'How long a runner session token stays valid. A revoked registration token can leave existing sessions usable until this lifetime ends.',\n default: '1h',\n }),\n AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS: num({\n desc: 'How many days a refresh token stays valid before the user must sign in again.',\n default: 14,\n }),\n AUTH_REFRESH_ROTATION_GRACE_SECONDS: num({\n desc: 'Window after a refresh token is rotated during which the now-rotated token is still accepted, so concurrent refreshes from parallel tabs do not log the user out. Reuse past this window is treated as a compromise and revokes the session.',\n default: 30,\n }),\n AUTH_REFRESH_COOKIE_NAME: str({\n desc: 'Name of the browser cookie that stores the refresh token.',\n default: 'shipfox_refresh_token',\n }),\n AUTH_PASSWORD_ENABLED: bool({\n 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.',\n default: true,\n }),\n AUTH_SIGNUP_GATE_ENABLED: bool({\n desc: 'Whether new account creation is restricted to the configured signup email allowlist. Defaults to false, which allows every signup.',\n default: false,\n }),\n AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS: str({\n desc: 'Comma-separated email domains allowed to create accounts, such as shipfox.io,acme.com. Required when AUTH_SIGNUP_GATE_ENABLED is true unless AUTH_SIGNUP_ALLOWED_EMAILS is set.',\n default: '',\n }),\n AUTH_SIGNUP_ALLOWED_EMAILS: str({\n desc: 'Comma-separated exact email addresses allowed to create accounts. Required when AUTH_SIGNUP_GATE_ENABLED is true unless AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS is set.',\n default: '',\n }),\n AUTH_SIGNUP_NOT_ALLOWED_MESSAGE: str({\n desc: 'Description returned when the signup gate blocks an account. Optional. The default is This Shipfox deployment does not accept new accounts right now.',\n default: undefined,\n }),\n CLIENT_BASE_URL: str({\n desc: 'Base URL of the client app. Used to build links in emails such as password resets.',\n default: 'http://localhost:5173',\n }),\n});\n\nif (\n config.AUTH_SIGNUP_GATE_ENABLED &&\n !hasSignupAllowlistEntry(config.AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS) &&\n !hasSignupAllowlistEntry(config.AUTH_SIGNUP_ALLOWED_EMAILS)\n) {\n throw new Error(\n 'AUTH_SIGNUP_GATE_ENABLED requires AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS or AUTH_SIGNUP_ALLOWED_EMAILS to be set.',\n );\n}\n\nfunction hasSignupAllowlistEntry(value: string): boolean {\n return value.split(',').some((entry) => entry.trim().length > 0);\n}\n"],"names":["bool","createConfig","num","str","config","AUTH_JWT_EXPIRES_IN","desc","default","AUTH_JOB_LEASE_TOKEN_EXPIRES_IN","AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN","AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS","AUTH_REFRESH_ROTATION_GRACE_SECONDS","AUTH_REFRESH_COOKIE_NAME","AUTH_PASSWORD_ENABLED","AUTH_SIGNUP_GATE_ENABLED","AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS","AUTH_SIGNUP_ALLOWED_EMAILS","AUTH_SIGNUP_NOT_ALLOWED_MESSAGE","undefined","CLIENT_BASE_URL","hasSignupAllowlistEntry","Error","value","split","some","entry","trim","length"],"mappings":"AAAA,SAAQA,IAAI,EAAEC,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AAE7D,OAAO,MAAMC,SAASH,aAAa;IACjCI,qBAAqBF,IAAI;QACvBG,MAAM;QACNC,SAAS;IACX;IACAC,iCAAiCL,IAAI;QACnCG,MAAM;QACNC,SAAS;IACX;IACAE,sCAAsCN,IAAI;QACxCG,MAAM;QACNC,SAAS;IACX;IACAG,oCAAoCR,IAAI;QACtCI,MAAM;QACNC,SAAS;IACX;IACAI,qCAAqCT,IAAI;QACvCI,MAAM;QACNC,SAAS;IACX;IACAK,0BAA0BT,IAAI;QAC5BG,MAAM;QACNC,SAAS;IACX;IACAM,uBAAuBb,KAAK;QAC1BM,MAAM;QACNC,SAAS;IACX;IACAO,0BAA0Bd,KAAK;QAC7BM,MAAM;QACNC,SAAS;IACX;IACAQ,mCAAmCZ,IAAI;QACrCG,MAAM;QACNC,SAAS;IACX;IACAS,4BAA4Bb,IAAI;QAC9BG,MAAM;QACNC,SAAS;IACX;IACAU,iCAAiCd,IAAI;QACnCG,MAAM;QACNC,SAASW;IACX;IACAC,iBAAiBhB,IAAI;QACnBG,MAAM;QACNC,SAAS;IACX;AACF,GAAG;AAEH,IACEH,OAAOU,wBAAwB,IAC/B,CAACM,wBAAwBhB,OAAOW,iCAAiC,KACjE,CAACK,wBAAwBhB,OAAOY,0BAA0B,GAC1D;IACA,MAAM,IAAIK,MACR;AAEJ;AAEA,SAASD,wBAAwBE,KAAa;IAC5C,OAAOA,MAAMC,KAAK,CAAC,KAAKC,IAAI,CAAC,CAACC,QAAUA,MAAMC,IAAI,GAAGC,MAAM,GAAG;AAChE"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AdminRole } from '@shipfox/api-auth-dto';
|
|
2
|
+
export declare const ADMIN_ROLES: readonly AdminRole[];
|
|
3
|
+
export declare function hasMinimumAdminRole(role: AdminRole, minimumRole: AdminRole): boolean;
|
|
4
|
+
export declare function highestAdminRole(roles: readonly AdminRole[]): AdminRole | null;
|
|
5
|
+
//# sourceMappingURL=admin-role-model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-role-model.d.ts","sourceRoot":"","sources":["../../src/core/admin-role-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAErD,eAAO,MAAM,WAAW,EAAE,SAAS,SAAS,EAI3C,CAAC;AAQF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,GAAG,OAAO,CAEpF;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,GAAG,IAAI,CAM9E"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const ADMIN_ROLES = [
|
|
2
|
+
'admin-observer',
|
|
3
|
+
'admin-operator',
|
|
4
|
+
'admin-owner'
|
|
5
|
+
];
|
|
6
|
+
const ADMIN_ROLE_RANK = {
|
|
7
|
+
'admin-observer': 1,
|
|
8
|
+
'admin-operator': 2,
|
|
9
|
+
'admin-owner': 3
|
|
10
|
+
};
|
|
11
|
+
export function hasMinimumAdminRole(role, minimumRole) {
|
|
12
|
+
return ADMIN_ROLE_RANK[role] >= ADMIN_ROLE_RANK[minimumRole];
|
|
13
|
+
}
|
|
14
|
+
export function highestAdminRole(roles) {
|
|
15
|
+
return roles.reduce((highest, role)=>highest === null || ADMIN_ROLE_RANK[role] > ADMIN_ROLE_RANK[highest] ? role : highest, null);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=admin-role-model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/admin-role-model.ts"],"sourcesContent":["import type {AdminRole} from '@shipfox/api-auth-dto';\n\nexport const ADMIN_ROLES: readonly AdminRole[] = [\n 'admin-observer',\n 'admin-operator',\n 'admin-owner',\n];\n\nconst ADMIN_ROLE_RANK: Record<AdminRole, number> = {\n 'admin-observer': 1,\n 'admin-operator': 2,\n 'admin-owner': 3,\n};\n\nexport function hasMinimumAdminRole(role: AdminRole, minimumRole: AdminRole): boolean {\n return ADMIN_ROLE_RANK[role] >= ADMIN_ROLE_RANK[minimumRole];\n}\n\nexport function highestAdminRole(roles: readonly AdminRole[]): AdminRole | null {\n return roles.reduce<AdminRole | null>(\n (highest, role) =>\n highest === null || ADMIN_ROLE_RANK[role] > ADMIN_ROLE_RANK[highest] ? role : highest,\n null,\n );\n}\n"],"names":["ADMIN_ROLES","ADMIN_ROLE_RANK","hasMinimumAdminRole","role","minimumRole","highestAdminRole","roles","reduce","highest"],"mappings":"AAEA,OAAO,MAAMA,cAAoC;IAC/C;IACA;IACA;CACD,CAAC;AAEF,MAAMC,kBAA6C;IACjD,kBAAkB;IAClB,kBAAkB;IAClB,eAAe;AACjB;AAEA,OAAO,SAASC,oBAAoBC,IAAe,EAAEC,WAAsB;IACzE,OAAOH,eAAe,CAACE,KAAK,IAAIF,eAAe,CAACG,YAAY;AAC9D;AAEA,OAAO,SAASC,iBAAiBC,KAA2B;IAC1D,OAAOA,MAAMC,MAAM,CACjB,CAACC,SAASL,OACRK,YAAY,QAAQP,eAAe,CAACE,KAAK,GAAGF,eAAe,CAACO,QAAQ,GAAGL,OAAOK,SAChF;AAEJ"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AdminRole } from '@shipfox/api-auth-dto';
|
|
2
|
+
export { ADMIN_ROLES, hasMinimumAdminRole, highestAdminRole } from './admin-role-model.js';
|
|
3
|
+
export declare function getCurrentAdminRole(params: {
|
|
4
|
+
userId: string;
|
|
5
|
+
}): Promise<AdminRole | null>;
|
|
6
|
+
export declare function requireAdminRole(params: {
|
|
7
|
+
userId: string;
|
|
8
|
+
minimumRole: AdminRole;
|
|
9
|
+
}): Promise<AdminRole>;
|
|
10
|
+
export declare function revokeAdminGrant(params: {
|
|
11
|
+
grantId: string;
|
|
12
|
+
}): Promise<import("./entities/admin-grant.js").AdminGrant | undefined>;
|
|
13
|
+
//# sourceMappingURL=admin-role.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-role.d.ts","sourceRoot":"","sources":["../../src/core/admin-role.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAKrD,OAAO,EAAC,WAAW,EAAE,mBAAmB,EAAE,gBAAgB,EAAC,MAAM,uBAAuB,CAAC;AAEzF,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAE7F;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,SAAS,CAAC;CACxB,GAAG,OAAO,CAAC,SAAS,CAAC,CAMrB;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,uEAE/D"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { findCurrentAdminRole, revokeAdminGrant as revokeAdminGrantInDb } from '#db/admin-grants.js';
|
|
2
|
+
import { hasMinimumAdminRole } from './admin-role-model.js';
|
|
3
|
+
import { AdminRoleRequiredError } from './errors.js';
|
|
4
|
+
export { ADMIN_ROLES, hasMinimumAdminRole, highestAdminRole } from './admin-role-model.js';
|
|
5
|
+
export async function getCurrentAdminRole(params) {
|
|
6
|
+
return await findCurrentAdminRole(params);
|
|
7
|
+
}
|
|
8
|
+
export async function requireAdminRole(params) {
|
|
9
|
+
const role = await getCurrentAdminRole({
|
|
10
|
+
userId: params.userId
|
|
11
|
+
});
|
|
12
|
+
if (!role || !hasMinimumAdminRole(role, params.minimumRole)) {
|
|
13
|
+
throw new AdminRoleRequiredError(params.minimumRole);
|
|
14
|
+
}
|
|
15
|
+
return role;
|
|
16
|
+
}
|
|
17
|
+
export async function revokeAdminGrant(params) {
|
|
18
|
+
return await revokeAdminGrantInDb(params);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=admin-role.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/admin-role.ts"],"sourcesContent":["import type {AdminRole} from '@shipfox/api-auth-dto';\nimport {findCurrentAdminRole, revokeAdminGrant as revokeAdminGrantInDb} from '#db/admin-grants.js';\nimport {hasMinimumAdminRole} from './admin-role-model.js';\nimport {AdminRoleRequiredError} from './errors.js';\n\nexport {ADMIN_ROLES, hasMinimumAdminRole, highestAdminRole} from './admin-role-model.js';\n\nexport async function getCurrentAdminRole(params: {userId: string}): Promise<AdminRole | null> {\n return await findCurrentAdminRole(params);\n}\n\nexport async function requireAdminRole(params: {\n userId: string;\n minimumRole: AdminRole;\n}): Promise<AdminRole> {\n const role = await getCurrentAdminRole({userId: params.userId});\n if (!role || !hasMinimumAdminRole(role, params.minimumRole)) {\n throw new AdminRoleRequiredError(params.minimumRole);\n }\n return role;\n}\n\nexport async function revokeAdminGrant(params: {grantId: string}) {\n return await revokeAdminGrantInDb(params);\n}\n"],"names":["findCurrentAdminRole","revokeAdminGrant","revokeAdminGrantInDb","hasMinimumAdminRole","AdminRoleRequiredError","ADMIN_ROLES","highestAdminRole","getCurrentAdminRole","params","requireAdminRole","role","userId","minimumRole"],"mappings":"AACA,SAAQA,oBAAoB,EAAEC,oBAAoBC,oBAAoB,QAAO,sBAAsB;AACnG,SAAQC,mBAAmB,QAAO,wBAAwB;AAC1D,SAAQC,sBAAsB,QAAO,cAAc;AAEnD,SAAQC,WAAW,EAAEF,mBAAmB,EAAEG,gBAAgB,QAAO,wBAAwB;AAEzF,OAAO,eAAeC,oBAAoBC,MAAwB;IAChE,OAAO,MAAMR,qBAAqBQ;AACpC;AAEA,OAAO,eAAeC,iBAAiBD,MAGtC;IACC,MAAME,OAAO,MAAMH,oBAAoB;QAACI,QAAQH,OAAOG,MAAM;IAAA;IAC7D,IAAI,CAACD,QAAQ,CAACP,oBAAoBO,MAAMF,OAAOI,WAAW,GAAG;QAC3D,MAAM,IAAIR,uBAAuBI,OAAOI,WAAW;IACrD;IACA,OAAOF;AACT;AAEA,OAAO,eAAeT,iBAAiBO,MAAyB;IAC9D,OAAO,MAAMN,qBAAqBM;AACpC"}
|
package/dist/core/auth.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import { type AdminRole } from '@shipfox/api-auth-dto';
|
|
1
2
|
import { type WorkspacesInterModuleClient } from '@shipfox/api-workspaces-dto/inter-module';
|
|
2
3
|
import type { User } from './entities/user.js';
|
|
3
4
|
import { AuthDependencyUnavailableError, EmailNotVerifiedError, InvalidCredentialsError, UserNotFoundError } from './errors.js';
|
|
5
|
+
import type { SignupPolicy } from './ports.js';
|
|
4
6
|
export interface SignupParams {
|
|
5
7
|
email: string;
|
|
6
8
|
password: string;
|
|
7
9
|
name?: string | undefined;
|
|
10
|
+
signupPolicy?: SignupPolicy | undefined;
|
|
8
11
|
}
|
|
9
12
|
export interface ProvisionUserParams {
|
|
10
13
|
email: string;
|
|
11
14
|
name?: string | null | undefined;
|
|
15
|
+
signupPolicy?: SignupPolicy | undefined;
|
|
12
16
|
}
|
|
13
17
|
/**
|
|
14
18
|
* Creates a verified, password-less user for an external identity provider.
|
|
@@ -53,6 +57,7 @@ export interface LoginResult {
|
|
|
53
57
|
token: string;
|
|
54
58
|
refreshToken: string;
|
|
55
59
|
user: User;
|
|
60
|
+
adminRole: AdminRole | null;
|
|
56
61
|
}
|
|
57
62
|
export declare function login(params: LoginParams): Promise<LoginResult>;
|
|
58
63
|
export interface CreateSessionForUserParams {
|
|
@@ -64,6 +69,7 @@ export interface CreateSessionForUserResult {
|
|
|
64
69
|
token: string;
|
|
65
70
|
refreshToken: string;
|
|
66
71
|
user: User;
|
|
72
|
+
adminRole: AdminRole | null;
|
|
67
73
|
}
|
|
68
74
|
export type CreateSessionForUserError = AuthDependencyUnavailableError | EmailNotVerifiedError | InvalidCredentialsError | UserNotFoundError;
|
|
69
75
|
export declare function createSessionForUser(params: CreateSessionForUserParams): Promise<CreateSessionForUserResult>;
|
|
@@ -72,6 +78,7 @@ export interface RefreshAccessTokenResult {
|
|
|
72
78
|
/** Undefined on a grace-window hit: keep the existing cookie instead of rotating it. */
|
|
73
79
|
refreshToken: string | undefined;
|
|
74
80
|
user: User;
|
|
81
|
+
adminRole: AdminRole | null;
|
|
75
82
|
}
|
|
76
83
|
export declare function refreshAccessToken(params: {
|
|
77
84
|
refreshToken: string;
|
|
@@ -81,6 +88,7 @@ export interface ConfirmEmailVerificationResult {
|
|
|
81
88
|
token: string;
|
|
82
89
|
refreshToken: string;
|
|
83
90
|
user: User;
|
|
91
|
+
adminRole: AdminRole | null;
|
|
84
92
|
}
|
|
85
93
|
export interface ResendEmailVerificationResult {
|
|
86
94
|
nextResendAvailableAt: Date;
|
|
@@ -103,6 +111,7 @@ export interface ConfirmPasswordResetResult {
|
|
|
103
111
|
token: string;
|
|
104
112
|
refreshToken: string;
|
|
105
113
|
user: User;
|
|
114
|
+
adminRole: AdminRole | null;
|
|
106
115
|
}
|
|
107
116
|
export declare function confirmPasswordReset(params: {
|
|
108
117
|
token: string;
|
package/dist/core/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/core/auth.js
CHANGED
|
@@ -9,11 +9,28 @@ import { consumePasswordReset, createPasswordReset } from '#db/password-resets.j
|
|
|
9
9
|
import { createRefreshToken, findActiveRefreshTokenByHash, findRefreshTokenByHash, revokeRefreshSession, revokeRefreshTokensForUser, rotateRefreshToken } from '#db/refresh-tokens.js';
|
|
10
10
|
import { createUser as createDbUser, findUserByEmail, findUserById, markEmailVerified, provisionUser as provisionDbUser, updateUserPassword } from '#db/users.js';
|
|
11
11
|
import { recordTokenRefreshed } from '#metrics/index.js';
|
|
12
|
-
import {
|
|
12
|
+
import { getCurrentAdminRole } from './admin-role.js';
|
|
13
|
+
import { AuthDependencyUnavailableError, EmailNotVerifiedError, EmailTakenError, InvalidCredentialsError, InvitationEmailMismatchError, SignupNotAllowedError, TokenAlreadyUsedError, TokenExpiredError, TokenInvalidError, UserNotFoundError } from './errors.js';
|
|
13
14
|
import { signUserToken } from './jwt.js';
|
|
14
15
|
import { hashPassword, verifyPassword } from './password.js';
|
|
15
16
|
const RESET_TTL_HOURS = 1;
|
|
16
17
|
const PASSWORD_VERIFICATION_PURPOSE = 'password-verification';
|
|
18
|
+
const DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE = 'This Shipfox deployment does not accept new accounts right now.';
|
|
19
|
+
const defaultSignupPolicy = {
|
|
20
|
+
isSignupAllowed: async ()=>({
|
|
21
|
+
allowed: true
|
|
22
|
+
})
|
|
23
|
+
};
|
|
24
|
+
async function assertSignupAllowed(params) {
|
|
25
|
+
const result = await (params.signupPolicy ?? defaultSignupPolicy).isSignupAllowed({
|
|
26
|
+
email: params.email,
|
|
27
|
+
emailVerified: params.emailVerified,
|
|
28
|
+
source: params.source
|
|
29
|
+
});
|
|
30
|
+
if (!result.allowed) {
|
|
31
|
+
throw new SignupNotAllowedError(result.message ?? DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
17
34
|
let dummyHashCache;
|
|
18
35
|
async function getDummyHash() {
|
|
19
36
|
if (!dummyHashCache) {
|
|
@@ -69,10 +86,14 @@ async function createRefreshSession(user, refreshSessionId) {
|
|
|
69
86
|
async function createSessionTokens(user, workspaces) {
|
|
70
87
|
const refreshSessionId = crypto.randomUUID();
|
|
71
88
|
const token = await signAccessToken(user, workspaces, refreshSessionId);
|
|
89
|
+
const adminRole = await getCurrentAdminRole({
|
|
90
|
+
userId: user.id
|
|
91
|
+
});
|
|
72
92
|
const { refreshToken } = await createRefreshSession(user, refreshSessionId);
|
|
73
93
|
return {
|
|
74
94
|
token,
|
|
75
|
-
refreshToken
|
|
95
|
+
refreshToken,
|
|
96
|
+
adminRole
|
|
76
97
|
};
|
|
77
98
|
}
|
|
78
99
|
function passwordResetLink(rawToken) {
|
|
@@ -96,14 +117,26 @@ function invitationErrorMessage(code) {
|
|
|
96
117
|
* Creates a verified, password-less user for an external identity provider.
|
|
97
118
|
* Existing users are returned unchanged, including their password and profile.
|
|
98
119
|
*/ export async function provisionUser(params) {
|
|
120
|
+
const email = emailSchema.parse(params.email);
|
|
121
|
+
const existing = await findUserByEmail({
|
|
122
|
+
email
|
|
123
|
+
});
|
|
124
|
+
if (existing) return existing;
|
|
125
|
+
await assertSignupAllowed({
|
|
126
|
+
signupPolicy: params.signupPolicy,
|
|
127
|
+
email,
|
|
128
|
+
emailVerified: true,
|
|
129
|
+
source: 'external-identity'
|
|
130
|
+
});
|
|
99
131
|
return await provisionDbUser({
|
|
100
|
-
email
|
|
132
|
+
email,
|
|
101
133
|
name: params.name ?? null
|
|
102
134
|
});
|
|
103
135
|
}
|
|
104
136
|
export async function signup(params) {
|
|
137
|
+
const email = emailSchema.parse(params.email);
|
|
105
138
|
const existing = await findUserByEmail({
|
|
106
|
-
email
|
|
139
|
+
email
|
|
107
140
|
});
|
|
108
141
|
if (existing) {
|
|
109
142
|
const canResumeVerification = existing.status === 'active' && existing.emailVerifiedAt === null && existing.hashedPassword !== null && await verifyPassword({
|
|
@@ -123,13 +156,19 @@ export async function signup(params) {
|
|
|
123
156
|
emailChallenge
|
|
124
157
|
};
|
|
125
158
|
}
|
|
126
|
-
throw new EmailTakenError(
|
|
159
|
+
throw new EmailTakenError(email);
|
|
127
160
|
}
|
|
161
|
+
await assertSignupAllowed({
|
|
162
|
+
signupPolicy: params.signupPolicy,
|
|
163
|
+
email,
|
|
164
|
+
emailVerified: false,
|
|
165
|
+
source: 'password'
|
|
166
|
+
});
|
|
128
167
|
const hashedPassword = await hashPassword({
|
|
129
168
|
password: params.password
|
|
130
169
|
});
|
|
131
170
|
const user = await createDbUser({
|
|
132
|
-
email
|
|
171
|
+
email,
|
|
133
172
|
hashedPassword,
|
|
134
173
|
name: params.name ?? null,
|
|
135
174
|
signedUp: {
|
|
@@ -213,21 +252,23 @@ export async function signupWithInvitation(params) {
|
|
|
213
252
|
}
|
|
214
253
|
// Step 4: Issue session. signAccessToken reads memberships through the
|
|
215
254
|
// workspaces module API, so a successful accept is reflected in the JWT.
|
|
216
|
-
const { token, refreshToken } = await createSessionTokens(user, params.workspaces);
|
|
255
|
+
const { token, refreshToken, adminRole } = await createSessionTokens(user, params.workspaces);
|
|
217
256
|
if (acceptError) {
|
|
218
257
|
return {
|
|
219
258
|
token,
|
|
220
259
|
refreshToken,
|
|
221
260
|
user,
|
|
222
261
|
membership,
|
|
223
|
-
acceptError
|
|
262
|
+
acceptError,
|
|
263
|
+
adminRole
|
|
224
264
|
};
|
|
225
265
|
}
|
|
226
266
|
return {
|
|
227
267
|
token,
|
|
228
268
|
refreshToken,
|
|
229
269
|
user,
|
|
230
|
-
membership
|
|
270
|
+
membership,
|
|
271
|
+
adminRole
|
|
231
272
|
};
|
|
232
273
|
}
|
|
233
274
|
export async function createUser(params) {
|
|
@@ -270,11 +311,12 @@ export async function login(params) {
|
|
|
270
311
|
if (user.emailVerifiedAt === null) {
|
|
271
312
|
throw new EmailNotVerifiedError();
|
|
272
313
|
}
|
|
273
|
-
const { token, refreshToken } = await createSessionTokens(user, params.workspaces);
|
|
314
|
+
const { token, refreshToken, adminRole } = await createSessionTokens(user, params.workspaces);
|
|
274
315
|
return {
|
|
275
316
|
token,
|
|
276
317
|
refreshToken,
|
|
277
|
-
user
|
|
318
|
+
user,
|
|
319
|
+
adminRole
|
|
278
320
|
};
|
|
279
321
|
}
|
|
280
322
|
export async function createSessionForUser(params) {
|
|
@@ -292,11 +334,12 @@ export async function createSessionForUser(params) {
|
|
|
292
334
|
if (user.status !== 'active') {
|
|
293
335
|
throw new InvalidCredentialsError();
|
|
294
336
|
}
|
|
295
|
-
const { token, refreshToken } = await createSessionTokens(user, params.workspaces);
|
|
337
|
+
const { token, refreshToken, adminRole } = await createSessionTokens(user, params.workspaces);
|
|
296
338
|
return {
|
|
297
339
|
token,
|
|
298
340
|
refreshToken,
|
|
299
|
-
user
|
|
341
|
+
user,
|
|
342
|
+
adminRole
|
|
300
343
|
};
|
|
301
344
|
}
|
|
302
345
|
export async function refreshAccessToken(params) {
|
|
@@ -319,6 +362,9 @@ export async function refreshAccessToken(params) {
|
|
|
319
362
|
recordRefreshOutcome('rejected');
|
|
320
363
|
throw new TokenInvalidError('Refresh token is invalid or expired');
|
|
321
364
|
}
|
|
365
|
+
const adminRole = await getCurrentAdminRole({
|
|
366
|
+
userId: user.id
|
|
367
|
+
});
|
|
322
368
|
// Within the grace window a rotated token means a concurrent refresh (e.g. a
|
|
323
369
|
// second tab); past it, reuse of a retired token means a compromised session.
|
|
324
370
|
if (current.rotatedAt) {
|
|
@@ -328,7 +374,8 @@ export async function refreshAccessToken(params) {
|
|
|
328
374
|
return {
|
|
329
375
|
token,
|
|
330
376
|
refreshToken: undefined,
|
|
331
|
-
user
|
|
377
|
+
user,
|
|
378
|
+
adminRole
|
|
332
379
|
};
|
|
333
380
|
}
|
|
334
381
|
await revokeRefreshTokensForUser({
|
|
@@ -359,7 +406,8 @@ export async function refreshAccessToken(params) {
|
|
|
359
406
|
return {
|
|
360
407
|
token,
|
|
361
408
|
refreshToken: undefined,
|
|
362
|
-
user
|
|
409
|
+
user,
|
|
410
|
+
adminRole
|
|
363
411
|
};
|
|
364
412
|
}
|
|
365
413
|
const token = await signAccessToken(user, params.workspaces, current.sessionId);
|
|
@@ -367,7 +415,8 @@ export async function refreshAccessToken(params) {
|
|
|
367
415
|
return {
|
|
368
416
|
token,
|
|
369
417
|
refreshToken: nextRefreshToken,
|
|
370
|
-
user
|
|
418
|
+
user,
|
|
419
|
+
adminRole
|
|
371
420
|
};
|
|
372
421
|
}
|
|
373
422
|
export async function confirmEmailVerification(params) {
|
|
@@ -391,11 +440,12 @@ export async function confirmEmailVerification(params) {
|
|
|
391
440
|
if (verifiedUser?.status !== 'active') {
|
|
392
441
|
throw new TokenInvalidError('Verification code is invalid or expired');
|
|
393
442
|
}
|
|
394
|
-
const { token, refreshToken } = await createSessionTokens(verifiedUser, params.workspaces);
|
|
443
|
+
const { token, refreshToken, adminRole } = await createSessionTokens(verifiedUser, params.workspaces);
|
|
395
444
|
return {
|
|
396
445
|
token,
|
|
397
446
|
refreshToken,
|
|
398
|
-
user: verifiedUser
|
|
447
|
+
user: verifiedUser,
|
|
448
|
+
adminRole
|
|
399
449
|
};
|
|
400
450
|
}
|
|
401
451
|
export async function resendEmailVerification(params) {
|
|
@@ -461,11 +511,12 @@ export async function confirmPasswordReset(params) {
|
|
|
461
511
|
await revokeRefreshTokensForUser({
|
|
462
512
|
userId: consumed.userId
|
|
463
513
|
});
|
|
464
|
-
const { token, refreshToken } = await createSessionTokens(user, params.workspaces);
|
|
514
|
+
const { token, refreshToken, adminRole } = await createSessionTokens(user, params.workspaces);
|
|
465
515
|
return {
|
|
466
516
|
token,
|
|
467
517
|
refreshToken,
|
|
468
|
-
user
|
|
518
|
+
user,
|
|
519
|
+
adminRole
|
|
469
520
|
};
|
|
470
521
|
}
|
|
471
522
|
export async function changePassword(params) {
|