@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.
Files changed (61) hide show
  1. package/README.md +226 -0
  2. package/index.d.ts +19 -0
  3. package/package.json +76 -0
  4. package/src/__tests__/auth.test.js +1171 -0
  5. package/src/__tests__/impersonation-test-setup.js +208 -0
  6. package/src/__tests__/impersonation.test.js +473 -0
  7. package/src/__tests__/oauth-test-setup.js +136 -0
  8. package/src/__tests__/oauth.test.js +400 -0
  9. package/src/__tests__/prsm.test.js +215 -0
  10. package/src/__tests__/test-setup.js +385 -0
  11. package/src/__tests__/totp.test.js +158 -0
  12. package/src/__tests__/two-factor-test-setup.js +331 -0
  13. package/src/__tests__/two-factor.test.js +396 -0
  14. package/src/activity-logger.js +228 -0
  15. package/src/auth-context.js +120 -0
  16. package/src/auth-functions.js +520 -0
  17. package/src/auth-manager.js +1371 -0
  18. package/src/errors.js +173 -0
  19. package/src/hooks.js +41 -0
  20. package/src/index.js +23 -0
  21. package/src/invalidation.js +166 -0
  22. package/src/middleware.js +33 -0
  23. package/src/providers/azure-provider.js +114 -0
  24. package/src/providers/base-provider.js +152 -0
  25. package/src/providers/github-provider.js +86 -0
  26. package/src/providers/google-provider.js +76 -0
  27. package/src/providers/index.js +4 -0
  28. package/src/queries.js +543 -0
  29. package/src/schema.js +261 -0
  30. package/src/totp.js +221 -0
  31. package/src/two-factor/index.js +3 -0
  32. package/src/two-factor/otp-provider.js +128 -0
  33. package/src/two-factor/totp-provider.js +98 -0
  34. package/src/two-factor/two-factor-manager.js +676 -0
  35. package/src/types.js +399 -0
  36. package/src/user-roles.js +128 -0
  37. package/src/util.js +32 -0
  38. package/types/activity-logger.d.ts +73 -0
  39. package/types/auth-context.d.ts +88 -0
  40. package/types/auth-functions.d.ts +151 -0
  41. package/types/auth-manager.d.ts +365 -0
  42. package/types/errors.d.ts +108 -0
  43. package/types/hooks.d.ts +30 -0
  44. package/types/index.d.ts +13 -0
  45. package/types/invalidation.d.ts +40 -0
  46. package/types/middleware.d.ts +11 -0
  47. package/types/providers/azure-provider.d.ts +35 -0
  48. package/types/providers/base-provider.d.ts +52 -0
  49. package/types/providers/github-provider.d.ts +29 -0
  50. package/types/providers/google-provider.d.ts +29 -0
  51. package/types/providers/index.d.ts +4 -0
  52. package/types/queries.d.ts +287 -0
  53. package/types/schema.d.ts +37 -0
  54. package/types/totp.d.ts +72 -0
  55. package/types/two-factor/index.d.ts +3 -0
  56. package/types/two-factor/otp-provider.d.ts +57 -0
  57. package/types/two-factor/totp-provider.d.ts +58 -0
  58. package/types/two-factor/two-factor-manager.d.ts +191 -0
  59. package/types/types.d.ts +688 -0
  60. package/types/user-roles.d.ts +47 -0
  61. 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;