@prsm/auth 1.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/README.md +226 -0
- package/index.d.ts +19 -0
- package/package.json +76 -0
- package/src/__tests__/auth.test.js +1171 -0
- package/src/__tests__/impersonation-test-setup.js +208 -0
- package/src/__tests__/impersonation.test.js +473 -0
- package/src/__tests__/oauth-test-setup.js +136 -0
- package/src/__tests__/oauth.test.js +400 -0
- package/src/__tests__/prsm.test.js +215 -0
- package/src/__tests__/test-setup.js +385 -0
- package/src/__tests__/totp.test.js +158 -0
- package/src/__tests__/two-factor-test-setup.js +331 -0
- package/src/__tests__/two-factor.test.js +396 -0
- package/src/activity-logger.js +228 -0
- package/src/auth-context.js +120 -0
- package/src/auth-functions.js +520 -0
- package/src/auth-manager.js +1371 -0
- package/src/errors.js +173 -0
- package/src/hooks.js +41 -0
- package/src/index.js +23 -0
- package/src/invalidation.js +166 -0
- package/src/middleware.js +33 -0
- package/src/providers/azure-provider.js +114 -0
- package/src/providers/base-provider.js +152 -0
- package/src/providers/github-provider.js +86 -0
- package/src/providers/google-provider.js +76 -0
- package/src/providers/index.js +4 -0
- package/src/queries.js +543 -0
- package/src/schema.js +261 -0
- package/src/totp.js +221 -0
- package/src/two-factor/index.js +3 -0
- package/src/two-factor/otp-provider.js +128 -0
- package/src/two-factor/totp-provider.js +98 -0
- package/src/two-factor/two-factor-manager.js +676 -0
- package/src/types.js +399 -0
- package/src/user-roles.js +128 -0
- package/src/util.js +32 -0
- package/types/activity-logger.d.ts +73 -0
- package/types/auth-context.d.ts +88 -0
- package/types/auth-functions.d.ts +151 -0
- package/types/auth-manager.d.ts +365 -0
- package/types/errors.d.ts +108 -0
- package/types/hooks.d.ts +30 -0
- package/types/index.d.ts +13 -0
- package/types/invalidation.d.ts +40 -0
- package/types/middleware.d.ts +11 -0
- package/types/providers/azure-provider.d.ts +35 -0
- package/types/providers/base-provider.d.ts +52 -0
- package/types/providers/github-provider.d.ts +29 -0
- package/types/providers/google-provider.d.ts +29 -0
- package/types/providers/index.d.ts +4 -0
- package/types/queries.d.ts +287 -0
- package/types/schema.d.ts +37 -0
- package/types/totp.d.ts +72 -0
- package/types/two-factor/index.d.ts +3 -0
- package/types/two-factor/otp-provider.d.ts +57 -0
- package/types/two-factor/totp-provider.d.ts +58 -0
- package/types/two-factor/two-factor-manager.d.ts +191 -0
- package/types/types.d.ts +688 -0
- package/types/user-roles.d.ts +47 -0
- package/types/util.d.ts +3 -0
package/src/types.js
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// shared types and runtime constants for @prsm/auth
|
|
2
|
+
// the interfaces live here as jsdoc typedefs; tsc emits them as exported types
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import("pg").Pool} Pool
|
|
6
|
+
* @typedef {import("express").Request} ExpressRequest
|
|
7
|
+
* @typedef {import("express").Response} ExpressResponse
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} OAuthProviderConfig
|
|
12
|
+
* @property {string} clientId
|
|
13
|
+
* @property {string} clientSecret
|
|
14
|
+
* @property {string} redirectUri
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {OAuthProviderConfig} GitHubProviderConfig
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {OAuthProviderConfig} GoogleProviderConfig
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {OAuthProviderConfig & { tenantId: string }} AzureProviderConfig
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* optional tracer, duck-typed against @prsm/trace - any object with a span/startSpan
|
|
31
|
+
* method works. auth never imports @prsm/trace; it only calls what's provided
|
|
32
|
+
* @typedef {object} Tracer
|
|
33
|
+
* @property {(name: string, fn: (span?: any) => any, attrs?: Record<string, any>) => any} [span]
|
|
34
|
+
* @property {(name: string, attrs?: Record<string, any>) => any} [startSpan]
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* optional limiter, duck-typed against @prsm/limit. auth consumes one unit
|
|
39
|
+
* before login when a limiter is provided. any @prsm/limit algorithm works -
|
|
40
|
+
* tokenBucket (take), slidingWindow (hit), leakyBucket (drip) - all return
|
|
41
|
+
* { allowed, retryAfter }
|
|
42
|
+
* @typedef {object} Limiter
|
|
43
|
+
* @property {(key: string) => Promise<{ allowed: boolean, retryAfter?: number }>} [take]
|
|
44
|
+
* @property {(key: string) => Promise<{ allowed: boolean, retryAfter?: number }>} [hit]
|
|
45
|
+
* @property {(key: string) => Promise<{ allowed: boolean, retryAfter?: number }>} [drip]
|
|
46
|
+
* @property {(key: string) => Promise<{ allowed: boolean, retryAfter?: number }>} [consume]
|
|
47
|
+
* @property {(key: string) => Promise<{ allowed: boolean, retryAfter?: number }>} [check]
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {object} InvalidationConfig
|
|
52
|
+
* @property {boolean} [listen] when true, the manager opens a dedicated postgres
|
|
53
|
+
* LISTEN connection and drops cached sessions the instant another instance
|
|
54
|
+
* issues a force-logout/role/status change. falls back to poll-based resync
|
|
55
|
+
* when the connection or NOTIFY is unavailable (e.g. pgbouncer txn pooling)
|
|
56
|
+
* @property {string} [channel] notify channel name, defaults to "prsm_auth_invalidate"
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {object} AuthConfig
|
|
61
|
+
* @property {Pool} db required postgres pool (pg-compatible, exposes query())
|
|
62
|
+
* @property {(userData: OAuthUserData) => string | number | Promise<string | number>} [createUser]
|
|
63
|
+
* called for new OAuth users to create your app user record and return its id;
|
|
64
|
+
* when omitted, OAuth users get an auto-generated uuid for user_id
|
|
65
|
+
* @property {string} [tablePrefix] defaults to "user_"
|
|
66
|
+
* @property {Record<string, number>} [roles] custom roles from defineRoles(), defaults to AuthRole
|
|
67
|
+
* @property {number} [minPasswordLength] defaults to 8
|
|
68
|
+
* @property {number} [maxPasswordLength] defaults to 64
|
|
69
|
+
* @property {string} [rememberDuration] defaults to "30d", parsed by @prsm/ms
|
|
70
|
+
* @property {string} [rememberCookieName] defaults to "remember_token"
|
|
71
|
+
* @property {{ domain?: string, secure?: boolean, sameSite?: "strict" | "lax" | "none" }} [cookie]
|
|
72
|
+
* @property {string} [resyncInterval] defaults to "30s"
|
|
73
|
+
* @property {{ enabled?: boolean, maxEntries?: number, actions?: AuthActivityActionType[] }} [activityLog]
|
|
74
|
+
* @property {{ github?: GitHubProviderConfig, google?: GoogleProviderConfig, azure?: AzureProviderConfig }} [providers]
|
|
75
|
+
* @property {string} [githubUserAgent] defaults to "prsm-auth"
|
|
76
|
+
* @property {{ enabled?: boolean, requireForOAuth?: boolean, issuer?: string, codeLength?: number, tokenExpiry?: string, totpWindow?: number, backupCodesCount?: number }} [twoFactor]
|
|
77
|
+
* @property {{ enabled?: boolean, defaultTtl?: string | null, maxTtl?: string, canImpersonate?: (actor: AuthAccount, target: AuthAccount) => boolean | Promise<boolean> }} [impersonation]
|
|
78
|
+
* @property {Tracer} [tracer] optional @prsm/trace tracer, duck-typed
|
|
79
|
+
* @property {Limiter} [limiter] optional @prsm/limit limiter, duck-typed
|
|
80
|
+
* @property {InvalidationConfig} [invalidation] optional cross-instance invalidation
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @typedef {object} AuthAccount
|
|
85
|
+
* @property {number} id
|
|
86
|
+
* @property {string} user_id
|
|
87
|
+
* @property {string} email
|
|
88
|
+
* @property {string | null} password
|
|
89
|
+
* @property {boolean} verified
|
|
90
|
+
* @property {number} status
|
|
91
|
+
* @property {number} rolemask
|
|
92
|
+
* @property {Date | null} last_login
|
|
93
|
+
* @property {number} force_logout
|
|
94
|
+
* @property {boolean} resettable
|
|
95
|
+
* @property {Date} registered
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @typedef {object} AuthProvider
|
|
100
|
+
* @property {number} id
|
|
101
|
+
* @property {number} account_id
|
|
102
|
+
* @property {string} provider
|
|
103
|
+
* @property {string} provider_id
|
|
104
|
+
* @property {string | null} provider_email
|
|
105
|
+
* @property {string | null} provider_username
|
|
106
|
+
* @property {string | null} provider_name
|
|
107
|
+
* @property {string | null} provider_avatar
|
|
108
|
+
* @property {Date} created_at
|
|
109
|
+
* @property {Date} updated_at
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @typedef {object} OAuthUserData
|
|
114
|
+
* @property {string} id
|
|
115
|
+
* @property {string} email
|
|
116
|
+
* @property {string} [username]
|
|
117
|
+
* @property {string} [name]
|
|
118
|
+
* @property {string} [avatar]
|
|
119
|
+
*/
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @typedef {object} OAuthCallbackResult
|
|
123
|
+
* @property {boolean} isNewUser
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @typedef {object} OAuthProvider
|
|
128
|
+
* @property {(state?: string, scopes?: string[]) => string} getAuthUrl
|
|
129
|
+
* @property {(req: ExpressRequest) => Promise<OAuthCallbackResult>} handleCallback
|
|
130
|
+
* @property {(req: ExpressRequest) => Promise<OAuthUserData>} getUserData
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @typedef {object} AuthConfirmation
|
|
135
|
+
* @property {number} id
|
|
136
|
+
* @property {number} account_id
|
|
137
|
+
* @property {string} token
|
|
138
|
+
* @property {string} email
|
|
139
|
+
* @property {Date} expires
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @typedef {object} AuthRemember
|
|
144
|
+
* @property {number} id
|
|
145
|
+
* @property {number} account_id
|
|
146
|
+
* @property {string} token
|
|
147
|
+
* @property {Date} expires
|
|
148
|
+
*/
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @typedef {object} AuthenticateRequestResult
|
|
152
|
+
* @property {AuthAccount | null} account
|
|
153
|
+
* @property {"session" | "remember" | null} source
|
|
154
|
+
*/
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @typedef {object} AuthReset
|
|
158
|
+
* @property {number} id
|
|
159
|
+
* @property {number} account_id
|
|
160
|
+
* @property {string} token
|
|
161
|
+
* @property {Date} expires
|
|
162
|
+
*/
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @typedef {object} AuthActivity
|
|
166
|
+
* @property {number} id
|
|
167
|
+
* @property {number | null} account_id
|
|
168
|
+
* @property {number | null} actor_account_id
|
|
169
|
+
* @property {string} action
|
|
170
|
+
* @property {string | null} ip_address
|
|
171
|
+
* @property {string | null} user_agent
|
|
172
|
+
* @property {string | null} browser
|
|
173
|
+
* @property {string | null} os
|
|
174
|
+
* @property {string | null} device
|
|
175
|
+
* @property {boolean} success
|
|
176
|
+
* @property {Record<string, any> | null} metadata
|
|
177
|
+
* @property {Date} created_at
|
|
178
|
+
*/
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @typedef {object} ImpersonationActor
|
|
182
|
+
* @property {number} accountId
|
|
183
|
+
* @property {string} userId
|
|
184
|
+
* @property {string} email
|
|
185
|
+
* @property {number} rolemask
|
|
186
|
+
* @property {number} forceLogout
|
|
187
|
+
* @property {Date} startedAt
|
|
188
|
+
* @property {Date} [expiresAt]
|
|
189
|
+
* @property {string} [reason]
|
|
190
|
+
*/
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @typedef {object} AwaitingTwoFactor
|
|
194
|
+
* @property {number} accountId
|
|
195
|
+
* @property {Date} expiresAt
|
|
196
|
+
* @property {boolean} remember
|
|
197
|
+
* @property {number[]} availableMechanisms
|
|
198
|
+
* @property {number[]} attemptedMechanisms
|
|
199
|
+
* @property {string} originalEmail
|
|
200
|
+
* @property {{ email?: string, sms?: string }} [selectors]
|
|
201
|
+
*/
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @typedef {object} AuthSession
|
|
205
|
+
* @property {boolean} loggedIn
|
|
206
|
+
* @property {number} accountId
|
|
207
|
+
* @property {string} userId
|
|
208
|
+
* @property {string} email
|
|
209
|
+
* @property {number} status
|
|
210
|
+
* @property {number} rolemask
|
|
211
|
+
* @property {boolean} remembered
|
|
212
|
+
* @property {Date} lastResync
|
|
213
|
+
* @property {Date} lastRememberCheck
|
|
214
|
+
* @property {number} forceLogout
|
|
215
|
+
* @property {boolean} verified
|
|
216
|
+
* @property {boolean} hasPassword
|
|
217
|
+
* @property {boolean} [shouldForceLogout]
|
|
218
|
+
* @property {ImpersonationActor} [actor]
|
|
219
|
+
* @property {AwaitingTwoFactor} [awaitingTwoFactor]
|
|
220
|
+
*/
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @typedef {object} ImpersonationInfo
|
|
224
|
+
* @property {{ accountId: number, userId: string, email: string, rolemask: number }} actor
|
|
225
|
+
* @property {{ accountId: number, userId: string, email: string, rolemask: number }} target
|
|
226
|
+
* @property {Date} startedAt
|
|
227
|
+
* @property {Date} [expiresAt]
|
|
228
|
+
* @property {string} [reason]
|
|
229
|
+
*/
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @typedef {object} StartImpersonationOptions
|
|
233
|
+
* @property {string} [reason]
|
|
234
|
+
* @property {string | number} [ttl]
|
|
235
|
+
*/
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @typedef {object} TwoFactorMethod
|
|
239
|
+
* @property {number} id
|
|
240
|
+
* @property {number} account_id
|
|
241
|
+
* @property {number} mechanism
|
|
242
|
+
* @property {string | null} secret
|
|
243
|
+
* @property {string[] | null} backup_codes
|
|
244
|
+
* @property {boolean} verified
|
|
245
|
+
* @property {Date} created_at
|
|
246
|
+
* @property {Date | null} last_used_at
|
|
247
|
+
*/
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @typedef {object} TwoFactorToken
|
|
251
|
+
* @property {number} id
|
|
252
|
+
* @property {number} account_id
|
|
253
|
+
* @property {number} mechanism
|
|
254
|
+
* @property {string} selector
|
|
255
|
+
* @property {string} token_hash
|
|
256
|
+
* @property {Date} expires_at
|
|
257
|
+
* @property {Date} created_at
|
|
258
|
+
*/
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* @typedef {object} TwoFactorSetupResult
|
|
262
|
+
* @property {string} secret
|
|
263
|
+
* @property {string} qrCode
|
|
264
|
+
* @property {string[]} [backupCodes]
|
|
265
|
+
*/
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* @typedef {object} TwoFactorChallenge
|
|
269
|
+
* @property {boolean} [totp]
|
|
270
|
+
* @property {{ otpValue: string, maskedContact: string }} [email]
|
|
271
|
+
* @property {{ otpValue: string, maskedContact: string }} [sms]
|
|
272
|
+
* @property {{ email?: string, sms?: string }} [selectors]
|
|
273
|
+
*/
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @typedef {(token: string) => void} TokenCallback
|
|
277
|
+
*/
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @typedef {{ accountId?: number, email?: string, userId?: string }} UserIdentifier
|
|
281
|
+
*/
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* the public interface attached to req.auth by createAuthMiddleware
|
|
285
|
+
* @typedef {object} AuthManager
|
|
286
|
+
* @property {() => boolean} isLoggedIn
|
|
287
|
+
* @property {(email: string, password: string, remember?: boolean) => Promise<void>} login
|
|
288
|
+
* @property {() => Promise<void>} completeTwoFactorLogin
|
|
289
|
+
* @property {() => Promise<void>} logout
|
|
290
|
+
* @property {(email: string, password: string, userId?: string | number, callback?: TokenCallback) => Promise<AuthAccount>} register
|
|
291
|
+
* @property {(force?: boolean) => Promise<void>} resyncSession
|
|
292
|
+
* @property {() => number | null} getId
|
|
293
|
+
* @property {() => string | null} getEmail
|
|
294
|
+
* @property {() => number | null} getStatus
|
|
295
|
+
* @property {() => boolean | null} getVerified
|
|
296
|
+
* @property {() => boolean | null} hasPassword
|
|
297
|
+
* @property {(rolemask?: number) => string[]} getRoleNames
|
|
298
|
+
* @property {() => string | null} getStatusName
|
|
299
|
+
* @property {(role: number) => Promise<boolean>} hasRole
|
|
300
|
+
* @property {() => Promise<boolean>} isAdmin
|
|
301
|
+
* @property {() => boolean} isRemembered
|
|
302
|
+
* @property {(newEmail: string, callback: TokenCallback) => Promise<void>} changeEmail
|
|
303
|
+
* @property {(token: string) => Promise<string>} confirmEmail
|
|
304
|
+
* @property {(token: string, remember?: boolean) => Promise<void>} confirmEmailAndLogin
|
|
305
|
+
* @property {(email: string, expiresAfter?: string | number | null, maxOpenRequests?: number | null, callback?: TokenCallback) => Promise<void>} resetPassword
|
|
306
|
+
* @property {(token: string, password: string, logout?: boolean) => Promise<void>} confirmResetPassword
|
|
307
|
+
* @property {(password: string) => Promise<boolean>} verifyPassword
|
|
308
|
+
* @property {() => Promise<void>} logoutEverywhere
|
|
309
|
+
* @property {() => Promise<void>} logoutEverywhereElse
|
|
310
|
+
* @property {(credentials: { email: string, password: string }, userId?: string | number, callback?: TokenCallback) => Promise<AuthAccount>} createUser
|
|
311
|
+
* @property {(identifier: UserIdentifier) => Promise<void>} deleteUserBy
|
|
312
|
+
* @property {(identifier: UserIdentifier, role: number) => Promise<void>} addRoleForUserBy
|
|
313
|
+
* @property {(identifier: UserIdentifier, role: number) => Promise<void>} removeRoleForUserBy
|
|
314
|
+
* @property {(identifier: UserIdentifier, role: number) => Promise<boolean>} hasRoleForUserBy
|
|
315
|
+
* @property {(identifier: UserIdentifier, password: string) => Promise<void>} changePasswordForUserBy
|
|
316
|
+
* @property {(identifier: UserIdentifier, status: number) => Promise<void>} setStatusForUserBy
|
|
317
|
+
* @property {(identifier: UserIdentifier, expiresAfter?: string | number | null, callback?: TokenCallback) => Promise<void>} initiatePasswordResetForUserBy
|
|
318
|
+
* @property {(email: string) => Promise<boolean>} userExistsByEmail
|
|
319
|
+
* @property {(identifier: UserIdentifier) => Promise<void>} forceLogoutForUserBy
|
|
320
|
+
* @property {(identifier: UserIdentifier) => Promise<void>} loginAsUserBy
|
|
321
|
+
* @property {(identifier: UserIdentifier, options?: StartImpersonationOptions) => Promise<void>} startImpersonation
|
|
322
|
+
* @property {() => Promise<void>} stopImpersonation
|
|
323
|
+
* @property {() => boolean} isImpersonating
|
|
324
|
+
* @property {() => number | null} getActorId
|
|
325
|
+
* @property {() => string | null} getActorEmail
|
|
326
|
+
* @property {() => ImpersonationInfo | null} getImpersonationInfo
|
|
327
|
+
* @property {{ github?: OAuthProvider, google?: OAuthProvider, azure?: OAuthProvider }} providers
|
|
328
|
+
* @property {import("./two-factor/two-factor-manager.js").TwoFactorManager} twoFactor
|
|
329
|
+
*/
|
|
330
|
+
|
|
331
|
+
export const AuthStatus = Object.freeze({
|
|
332
|
+
Normal: 0,
|
|
333
|
+
Archived: 1,
|
|
334
|
+
Banned: 2,
|
|
335
|
+
Locked: 3,
|
|
336
|
+
PendingReview: 4,
|
|
337
|
+
Suspended: 5,
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
export const AuthRole = Object.freeze({
|
|
341
|
+
Admin: 1,
|
|
342
|
+
Author: 2,
|
|
343
|
+
Collaborator: 4,
|
|
344
|
+
Consultant: 8,
|
|
345
|
+
Consumer: 16,
|
|
346
|
+
Contributor: 32,
|
|
347
|
+
Owner: 64,
|
|
348
|
+
Creator: 128,
|
|
349
|
+
Developer: 256,
|
|
350
|
+
Director: 512,
|
|
351
|
+
Editor: 1024,
|
|
352
|
+
Employee: 2048,
|
|
353
|
+
Member: 4096,
|
|
354
|
+
Manager: 8192,
|
|
355
|
+
Moderator: 16384,
|
|
356
|
+
Publisher: 32768,
|
|
357
|
+
Reviewer: 65536,
|
|
358
|
+
Subscriber: 131072,
|
|
359
|
+
SuperAdmin: 262144,
|
|
360
|
+
SuperEditor: 524288,
|
|
361
|
+
SuperModerator: 1048576,
|
|
362
|
+
Translator: 2097152,
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
export const AuthActivityAction = Object.freeze({
|
|
366
|
+
Login: "login",
|
|
367
|
+
Logout: "logout",
|
|
368
|
+
FailedLogin: "failed_login",
|
|
369
|
+
Register: "register",
|
|
370
|
+
EmailConfirmed: "email_confirmed",
|
|
371
|
+
PasswordResetRequested: "password_reset_requested",
|
|
372
|
+
PasswordResetCompleted: "password_reset_completed",
|
|
373
|
+
PasswordChanged: "password_changed",
|
|
374
|
+
EmailChanged: "email_changed",
|
|
375
|
+
RoleChanged: "role_changed",
|
|
376
|
+
StatusChanged: "status_changed",
|
|
377
|
+
ForceLogout: "force_logout",
|
|
378
|
+
OAuthConnected: "oauth_connected",
|
|
379
|
+
RememberTokenCreated: "remember_token_created",
|
|
380
|
+
TwoFactorSetup: "two_factor_setup",
|
|
381
|
+
TwoFactorVerified: "two_factor_verified",
|
|
382
|
+
TwoFactorFailed: "two_factor_failed",
|
|
383
|
+
TwoFactorDisabled: "two_factor_disabled",
|
|
384
|
+
BackupCodeUsed: "backup_code_used",
|
|
385
|
+
ImpersonationStarted: "impersonation_started",
|
|
386
|
+
ImpersonationStopped: "impersonation_stopped",
|
|
387
|
+
ImpersonationExpired: "impersonation_expired",
|
|
388
|
+
ImpersonationRejected: "impersonation_rejected",
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* @typedef {(typeof AuthActivityAction)[keyof typeof AuthActivityAction]} AuthActivityActionType
|
|
393
|
+
*/
|
|
394
|
+
|
|
395
|
+
export const TwoFactorMechanism = Object.freeze({
|
|
396
|
+
TOTP: 1,
|
|
397
|
+
EMAIL: 2,
|
|
398
|
+
SMS: 3,
|
|
399
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { AuthQueries } from "./queries.js"
|
|
2
|
+
import { UserNotFoundError } from "./errors.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import("./types.js").AuthConfig} AuthConfig
|
|
6
|
+
* @typedef {import("./types.js").AuthAccount} AuthAccount
|
|
7
|
+
* @typedef {import("./types.js").UserIdentifier} UserIdentifier
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const MAX_ROLES = 31
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Define a set of named roles as a frozen bitmask map. Each role gets the next
|
|
14
|
+
* power-of-two bit. Capped at 31 because postgres INTEGER is 32-bit signed.
|
|
15
|
+
*
|
|
16
|
+
* @param {...string} names
|
|
17
|
+
* @returns {Readonly<Record<string, number>>}
|
|
18
|
+
*/
|
|
19
|
+
export function defineRoles(...names) {
|
|
20
|
+
if (names.length > MAX_ROLES) {
|
|
21
|
+
throw new Error(`Cannot define more than ${MAX_ROLES} roles (postgres INTEGER is 32-bit signed)`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (names.length === 0) {
|
|
25
|
+
throw new Error("At least one role name is required")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const seen = new Set()
|
|
29
|
+
/** @type {Record<string, number>} */
|
|
30
|
+
const roles = {}
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < names.length; i++) {
|
|
33
|
+
const name = names[i]
|
|
34
|
+
if (seen.has(name)) {
|
|
35
|
+
throw new Error(`Duplicate role name: ${name}`)
|
|
36
|
+
}
|
|
37
|
+
seen.add(name)
|
|
38
|
+
roles[name] = 1 << i
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return Object.freeze(roles)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {AuthQueries} queries
|
|
46
|
+
* @param {UserIdentifier} identifier
|
|
47
|
+
* @returns {Promise<AuthAccount>}
|
|
48
|
+
*/
|
|
49
|
+
async function findAccountByIdentifier(queries, identifier) {
|
|
50
|
+
/** @type {AuthAccount | null} */
|
|
51
|
+
let account = null
|
|
52
|
+
|
|
53
|
+
if (identifier.accountId !== undefined) {
|
|
54
|
+
account = await queries.findAccountById(identifier.accountId)
|
|
55
|
+
} else if (identifier.email !== undefined) {
|
|
56
|
+
account = await queries.findAccountByEmail(identifier.email)
|
|
57
|
+
} else if (identifier.userId !== undefined) {
|
|
58
|
+
account = await queries.findAccountByUserId(identifier.userId)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!account) {
|
|
62
|
+
throw new UserNotFoundError()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return account
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Add a role to a user's account using bitwise OR.
|
|
70
|
+
*
|
|
71
|
+
* @param {AuthConfig} config
|
|
72
|
+
* @param {UserIdentifier} identifier
|
|
73
|
+
* @param {number} role
|
|
74
|
+
* @throws {UserNotFoundError}
|
|
75
|
+
*/
|
|
76
|
+
export async function addRoleToUser(config, identifier, role) {
|
|
77
|
+
const queries = new AuthQueries(config)
|
|
78
|
+
const account = await findAccountByIdentifier(queries, identifier)
|
|
79
|
+
|
|
80
|
+
const rolemask = account.rolemask | role
|
|
81
|
+
await queries.updateAccount(account.id, { rolemask })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Remove a role from a user's account using bitwise operations.
|
|
86
|
+
*
|
|
87
|
+
* @param {AuthConfig} config
|
|
88
|
+
* @param {UserIdentifier} identifier
|
|
89
|
+
* @param {number} role
|
|
90
|
+
* @throws {UserNotFoundError}
|
|
91
|
+
*/
|
|
92
|
+
export async function removeRoleFromUser(config, identifier, role) {
|
|
93
|
+
const queries = new AuthQueries(config)
|
|
94
|
+
const account = await findAccountByIdentifier(queries, identifier)
|
|
95
|
+
|
|
96
|
+
const rolemask = account.rolemask & ~role
|
|
97
|
+
await queries.updateAccount(account.id, { rolemask })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Set a user's complete role mask, replacing any existing roles.
|
|
102
|
+
*
|
|
103
|
+
* @param {AuthConfig} config
|
|
104
|
+
* @param {UserIdentifier} identifier
|
|
105
|
+
* @param {number} rolemask
|
|
106
|
+
* @throws {UserNotFoundError}
|
|
107
|
+
*/
|
|
108
|
+
export async function setUserRoles(config, identifier, rolemask) {
|
|
109
|
+
const queries = new AuthQueries(config)
|
|
110
|
+
const account = await findAccountByIdentifier(queries, identifier)
|
|
111
|
+
|
|
112
|
+
await queries.updateAccount(account.id, { rolemask })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get a user's current role mask.
|
|
117
|
+
*
|
|
118
|
+
* @param {AuthConfig} config
|
|
119
|
+
* @param {UserIdentifier} identifier
|
|
120
|
+
* @returns {Promise<number>}
|
|
121
|
+
* @throws {UserNotFoundError}
|
|
122
|
+
*/
|
|
123
|
+
export async function getUserRoles(config, identifier) {
|
|
124
|
+
const queries = new AuthQueries(config)
|
|
125
|
+
const account = await findAccountByIdentifier(queries, identifier)
|
|
126
|
+
|
|
127
|
+
return account.rolemask
|
|
128
|
+
}
|
package/src/util.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { InvalidEmailError } from "./errors.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} email
|
|
5
|
+
* @returns {boolean}
|
|
6
|
+
*/
|
|
7
|
+
export const isValidEmail = (email) => {
|
|
8
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
9
|
+
return emailRegex.test(email)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} email
|
|
14
|
+
* @throws {InvalidEmailError}
|
|
15
|
+
*/
|
|
16
|
+
export const validateEmail = (email) => {
|
|
17
|
+
if (typeof email !== "string") {
|
|
18
|
+
throw new InvalidEmailError()
|
|
19
|
+
}
|
|
20
|
+
if (!email.trim()) {
|
|
21
|
+
throw new InvalidEmailError()
|
|
22
|
+
}
|
|
23
|
+
if (!isValidEmail(email)) {
|
|
24
|
+
throw new InvalidEmailError()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {Record<string, number>} enumObj
|
|
30
|
+
* @returns {Record<number, string>}
|
|
31
|
+
*/
|
|
32
|
+
export const createMapFromEnum = (enumObj) => Object.fromEntries(Object.entries(enumObj).map(([key, value]) => [value, key]))
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("./types.js").AuthConfig} AuthConfig
|
|
3
|
+
* @typedef {import("./types.js").AuthActivity} AuthActivity
|
|
4
|
+
* @typedef {import("./types.js").AuthActivityActionType} AuthActivityActionType
|
|
5
|
+
* @typedef {import("express").Request} Request
|
|
6
|
+
*/
|
|
7
|
+
export class ActivityLogger {
|
|
8
|
+
/**
|
|
9
|
+
* @param {AuthConfig} config
|
|
10
|
+
*/
|
|
11
|
+
constructor(config: AuthConfig);
|
|
12
|
+
config: import("./types.js").AuthConfig;
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
maxEntries: any;
|
|
15
|
+
allowedActions: any;
|
|
16
|
+
tablePrefix: string;
|
|
17
|
+
get activityTable(): string;
|
|
18
|
+
/**
|
|
19
|
+
* @param {string | null} userAgent
|
|
20
|
+
* @returns {{ browser: string | null, os: string | null, device: string | null }}
|
|
21
|
+
*/
|
|
22
|
+
parseUserAgent(userAgent: string | null): {
|
|
23
|
+
browser: string | null;
|
|
24
|
+
os: string | null;
|
|
25
|
+
device: string | null;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} userAgent
|
|
29
|
+
* @returns {{ browser: string | null, os: string | null, device: string | null }}
|
|
30
|
+
*/
|
|
31
|
+
parseUserAgentSimple(userAgent: string): {
|
|
32
|
+
browser: string | null;
|
|
33
|
+
os: string | null;
|
|
34
|
+
device: string | null;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* @param {Request} req
|
|
38
|
+
* @returns {string | null}
|
|
39
|
+
*/
|
|
40
|
+
getIpAddress(req: Request): string | null;
|
|
41
|
+
/**
|
|
42
|
+
* @param {number | null} accountId
|
|
43
|
+
* @param {AuthActivityActionType} action
|
|
44
|
+
* @param {Request} req
|
|
45
|
+
* @param {boolean} [success]
|
|
46
|
+
* @param {Record<string, any>} [metadata]
|
|
47
|
+
* @returns {Promise<void>}
|
|
48
|
+
*/
|
|
49
|
+
logActivity(accountId: number | null, action: AuthActivityActionType, req: Request, success?: boolean, metadata?: Record<string, any>): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* @returns {Promise<void>}
|
|
52
|
+
*/
|
|
53
|
+
cleanup(): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* @param {number} [limit]
|
|
56
|
+
* @param {number} [accountId]
|
|
57
|
+
* @returns {Promise<AuthActivity[]>}
|
|
58
|
+
*/
|
|
59
|
+
getRecentActivity(limit?: number, accountId?: number): Promise<AuthActivity[]>;
|
|
60
|
+
/**
|
|
61
|
+
* @returns {Promise<{ totalEntries: number, uniqueUsers: number, recentLogins: number, failedAttempts: number }>}
|
|
62
|
+
*/
|
|
63
|
+
getActivityStats(): Promise<{
|
|
64
|
+
totalEntries: number;
|
|
65
|
+
uniqueUsers: number;
|
|
66
|
+
recentLogins: number;
|
|
67
|
+
failedAttempts: number;
|
|
68
|
+
}>;
|
|
69
|
+
}
|
|
70
|
+
export type AuthConfig = import("./types.js").AuthConfig;
|
|
71
|
+
export type AuthActivity = import("./types.js").AuthActivity;
|
|
72
|
+
export type AuthActivityActionType = import("./types.js").AuthActivityActionType;
|
|
73
|
+
export type Request = import("express").Request;
|