@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
@@ -0,0 +1,120 @@
1
+ import * as authFunctions from "./auth-functions.js"
2
+ import { AuthQueries } from "./queries.js"
3
+ import { ActivityLogger } from "./activity-logger.js"
4
+ import { AuthRole, AuthStatus, TwoFactorMechanism } from "./types.js"
5
+ import { UserNotFoundError } from "./errors.js"
6
+
7
+ /**
8
+ * @typedef {import("./types.js").AuthConfig} AuthConfig
9
+ * @typedef {import("./types.js").TokenCallback} TokenCallback
10
+ * @typedef {import("./types.js").AuthAccount} AuthAccount
11
+ * @typedef {import("./types.js").UserIdentifier} UserIdentifier
12
+ */
13
+
14
+ /**
15
+ * @param {AuthQueries} queries
16
+ * @param {UserIdentifier} identifier
17
+ * @returns {Promise<AuthAccount | null>}
18
+ */
19
+ async function resolveAccount(queries, identifier) {
20
+ if (identifier.accountId !== undefined) return queries.findAccountById(identifier.accountId)
21
+ if (identifier.email !== undefined) return queries.findAccountByEmail(identifier.email)
22
+ if (identifier.userId !== undefined) return queries.findAccountByUserId(identifier.userId)
23
+ return null
24
+ }
25
+
26
+ /**
27
+ * Create a requestless auth context for scripts, workers, cron jobs, and admin
28
+ * tasks. The same object doubles as the binding surface for the @prsm/devtools
29
+ * admin panel: it exposes read methods (listAccounts, getAccount, getStats,
30
+ * recent activity, roles) and control actions (role/status/force-logout/etc),
31
+ * all duck-typed so devtools needs no @prsm/auth dependency.
32
+ * @param {AuthConfig} config
33
+ */
34
+ export function createAuthContext(config) {
35
+ const queries = new AuthQueries(config)
36
+ const activityLogger = new ActivityLogger(config)
37
+
38
+ return {
39
+ // user management (requestless equivalents of the req.auth admin methods)
40
+ createUser: (credentials, userId, callback) => authFunctions.createUser(config, credentials, userId, callback),
41
+ register: (email, password, userId, callback) => authFunctions.register(config, email, password, userId, callback),
42
+ deleteUserBy: (identifier) => authFunctions.deleteUserBy(config, identifier),
43
+ addRoleForUserBy: (identifier, role) => authFunctions.addRoleForUserBy(config, identifier, role),
44
+ removeRoleForUserBy: (identifier, role) => authFunctions.removeRoleForUserBy(config, identifier, role),
45
+ hasRoleForUserBy: (identifier, role) => authFunctions.hasRoleForUserBy(config, identifier, role),
46
+ changePasswordForUserBy: (identifier, password) => authFunctions.changePasswordForUserBy(config, identifier, password),
47
+ setStatusForUserBy: (identifier, status) => authFunctions.setStatusForUserBy(config, identifier, status),
48
+ initiatePasswordResetForUserBy: (identifier, expiresAfter, callback) => authFunctions.initiatePasswordResetForUserBy(config, identifier, expiresAfter, callback),
49
+ resetPassword: (email, expiresAfter, maxOpenRequests, callback) => authFunctions.resetPassword(config, email, expiresAfter, maxOpenRequests, callback),
50
+ confirmResetPassword: (token, password) => authFunctions.confirmResetPassword(config, token, password),
51
+ userExistsByEmail: (email) => authFunctions.userExistsByEmail(config, email),
52
+ forceLogoutForUserBy: (identifier) => authFunctions.forceLogoutForUserBy(config, identifier),
53
+
54
+ // introspection surface for @prsm/devtools
55
+
56
+ /**
57
+ * @param {{ limit?: number, offset?: number, search?: string }} [opts]
58
+ * @returns {Promise<{ accounts: AuthAccount[], total: number }>}
59
+ */
60
+ async listAccounts(opts = {}) {
61
+ const [accounts, total] = await Promise.all([queries.listAccounts(opts), queries.countAccounts(opts.search)])
62
+ return { accounts, total }
63
+ },
64
+
65
+ /**
66
+ * @param {UserIdentifier} identifier
67
+ * @returns {Promise<AuthAccount>}
68
+ */
69
+ async getAccount(identifier) {
70
+ const account = await resolveAccount(queries, identifier)
71
+ if (!account) throw new UserNotFoundError()
72
+ return account
73
+ },
74
+
75
+ /**
76
+ * @param {number} accountId
77
+ */
78
+ getProvidersForAccount: (accountId) => queries.findProvidersByAccountId(accountId),
79
+
80
+ /**
81
+ * @param {number} accountId
82
+ */
83
+ getTwoFactorMethods: (accountId) => queries.findTwoFactorMethodsByAccountId(accountId),
84
+
85
+ /**
86
+ * The role name -> bit map devtools renders, defaulting to AuthRole.
87
+ * @returns {Record<string, number>}
88
+ */
89
+ getRoles: () => config.roles || AuthRole,
90
+
91
+ /**
92
+ * The status code -> name map devtools renders for account status.
93
+ * @returns {Record<string, number>}
94
+ */
95
+ getStatuses: () => AuthStatus,
96
+
97
+ /**
98
+ * The 2FA mechanism code -> name map devtools renders.
99
+ * @returns {Record<string, number>}
100
+ */
101
+ getMechanisms: () => TwoFactorMechanism,
102
+
103
+ /**
104
+ * @returns {Promise<ReturnType<typeof import("./schema.js").getAuthTableStats>>}
105
+ */
106
+ getStats: () => import("./schema.js").then((m) => m.getAuthTableStats(config)),
107
+
108
+ /**
109
+ * @param {number} [limit]
110
+ * @param {number} [accountId]
111
+ */
112
+ getRecentActivity: (limit, accountId) => activityLogger.getRecentActivity(limit, accountId),
113
+
114
+ getActivityStats: () => activityLogger.getActivityStats(),
115
+ }
116
+ }
117
+
118
+ /**
119
+ * @typedef {ReturnType<typeof createAuthContext>} AuthContext
120
+ */
@@ -0,0 +1,520 @@
1
+ import hash from "@prsm/hash"
2
+ import ms from "@prsm/ms"
3
+ import { AuthQueries } from "./queries.js"
4
+ import { validateEmail } from "./util.js"
5
+ import { EmailTakenError, InvalidPasswordError, UserNotFoundError, EmailNotVerifiedError, ResetDisabledError, TooManyResetsError, ResetNotFoundError, ResetExpiredError, InvalidTokenError } from "./errors.js"
6
+ import { AuthStatus } from "./types.js"
7
+
8
+ /**
9
+ * @typedef {import("./types.js").AuthConfig} AuthConfig
10
+ * @typedef {import("./types.js").AuthAccount} AuthAccount
11
+ * @typedef {import("./types.js").TokenCallback} TokenCallback
12
+ * @typedef {import("./types.js").UserIdentifier} UserIdentifier
13
+ * @typedef {import("./types.js").AuthenticateRequestResult} AuthenticateRequestResult
14
+ */
15
+
16
+ /**
17
+ * @param {string} cookieHeader
18
+ * @returns {Record<string, string>}
19
+ */
20
+ function parseCookies(cookieHeader) {
21
+ /** @type {Record<string, string>} */
22
+ const cookies = {}
23
+ if (!cookieHeader) return cookies
24
+
25
+ for (const pair of cookieHeader.split(";")) {
26
+ const idx = pair.indexOf("=")
27
+ if (idx === -1) continue
28
+ const key = pair.slice(0, idx).trim()
29
+ const value = pair.slice(idx + 1).trim()
30
+ if (key) cookies[key] = decodeURIComponent(value)
31
+ }
32
+
33
+ return cookies
34
+ }
35
+
36
+ /**
37
+ * Resolve the account for an incoming request via session or remember-me cookie.
38
+ * @param {AuthConfig} config
39
+ * @param {import("http").IncomingMessage} req
40
+ * @param {(req: any, res: any, next: () => void) => void} [sessionMiddleware]
41
+ * @returns {Promise<AuthenticateRequestResult>}
42
+ */
43
+ export async function authenticateRequest(config, req, sessionMiddleware) {
44
+ const queries = new AuthQueries(config)
45
+
46
+ if (sessionMiddleware) {
47
+ await new Promise(resolve => {
48
+ sessionMiddleware(req, {}, resolve)
49
+ })
50
+ }
51
+
52
+ const session = req.session
53
+ if (session?.auth?.loggedIn && session.auth.accountId) {
54
+ const account = await queries.findAccountById(session.auth.accountId)
55
+ if (account && account.status === AuthStatus.Normal) {
56
+ return { account, source: "session" }
57
+ }
58
+ }
59
+
60
+ const cookies = parseCookies(req.headers.cookie || "")
61
+ const cookieName = config.rememberCookieName || "remember_token"
62
+ const token = cookies[cookieName]
63
+
64
+ if (!token) {
65
+ return { account: null, source: null }
66
+ }
67
+
68
+ const remember = await queries.findRememberToken(token)
69
+ if (!remember || new Date() > remember.expires) {
70
+ return { account: null, source: null }
71
+ }
72
+
73
+ const account = await queries.findAccountById(remember.account_id)
74
+ if (!account || account.status !== AuthStatus.Normal) {
75
+ return { account: null, source: null }
76
+ }
77
+
78
+ return { account, source: "remember" }
79
+ }
80
+
81
+ /**
82
+ * @param {string} password
83
+ * @param {AuthConfig} config
84
+ * @throws {InvalidPasswordError}
85
+ */
86
+ function validatePassword(password, config) {
87
+ const minLength = config.minPasswordLength || 8
88
+ const maxLength = config.maxPasswordLength || 64
89
+
90
+ if (typeof password !== "string") {
91
+ throw new InvalidPasswordError()
92
+ }
93
+
94
+ if (password.length < minLength) {
95
+ throw new InvalidPasswordError()
96
+ }
97
+
98
+ if (password.length > maxLength) {
99
+ throw new InvalidPasswordError()
100
+ }
101
+ }
102
+
103
+ /**
104
+ * @returns {string}
105
+ */
106
+ function generateAutoUserId() {
107
+ return crypto.randomUUID()
108
+ }
109
+
110
+ /**
111
+ * @param {AuthQueries} queries
112
+ * @param {UserIdentifier} identifier
113
+ * @returns {Promise<AuthAccount | null>}
114
+ */
115
+ async function findAccountByIdentifier(queries, identifier) {
116
+ if (identifier.accountId !== undefined) {
117
+ return await queries.findAccountById(identifier.accountId)
118
+ } else if (identifier.email !== undefined) {
119
+ return await queries.findAccountByEmail(identifier.email)
120
+ } else if (identifier.userId !== undefined) {
121
+ return await queries.findAccountByUserId(identifier.userId)
122
+ }
123
+ return null
124
+ }
125
+
126
+ /**
127
+ * @param {AuthQueries} queries
128
+ * @param {AuthAccount} account
129
+ * @param {string} email
130
+ * @param {TokenCallback} callback
131
+ * @returns {Promise<void>}
132
+ */
133
+ async function createConfirmationToken(queries, account, email, callback) {
134
+ const token = await hash.encode(email)
135
+ const expires = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7) // 1 week
136
+
137
+ await queries.createConfirmation({
138
+ accountId: account.id,
139
+ token,
140
+ email,
141
+ expires,
142
+ })
143
+
144
+ if (callback) {
145
+ callback(token)
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Create a new local account. When a callback is provided the account starts
151
+ * unverified and a confirmation token is generated.
152
+ * @param {AuthConfig} config
153
+ * @param {{ email: string, password: string }} credentials
154
+ * @param {string | number} [userId]
155
+ * @param {TokenCallback} [callback]
156
+ * @returns {Promise<AuthAccount>}
157
+ * @throws {EmailTakenError}
158
+ */
159
+ export async function createUser(config, credentials, userId, callback) {
160
+ validateEmail(credentials.email)
161
+ validatePassword(credentials.password, config)
162
+
163
+ const queries = new AuthQueries(config)
164
+
165
+ const existing = await queries.findAccountByEmail(credentials.email)
166
+ if (existing) {
167
+ throw new EmailTakenError()
168
+ }
169
+
170
+ const finalUserId = userId || generateAutoUserId()
171
+ const hashedPassword = await hash.encode(credentials.password)
172
+ const verified = typeof callback !== "function"
173
+
174
+ const account = await queries.createAccount({
175
+ userId: finalUserId,
176
+ email: credentials.email,
177
+ password: hashedPassword,
178
+ verified,
179
+ status: AuthStatus.Normal,
180
+ rolemask: 0,
181
+ })
182
+
183
+ if (!verified && callback) {
184
+ await createConfirmationToken(queries, account, credentials.email, callback)
185
+ }
186
+
187
+ return account
188
+ }
189
+
190
+ /**
191
+ * Register a new local account. When a callback is provided the account starts
192
+ * unverified and a confirmation token is generated.
193
+ * @param {AuthConfig} config
194
+ * @param {string} email
195
+ * @param {string} password
196
+ * @param {string | number} [userId]
197
+ * @param {TokenCallback} [callback]
198
+ * @returns {Promise<AuthAccount>}
199
+ * @throws {EmailTakenError}
200
+ */
201
+ export async function register(config, email, password, userId, callback) {
202
+ validateEmail(email)
203
+ validatePassword(password, config)
204
+
205
+ const queries = new AuthQueries(config)
206
+
207
+ const existing = await queries.findAccountByEmail(email)
208
+ if (existing) {
209
+ throw new EmailTakenError()
210
+ }
211
+
212
+ const finalUserId = userId || generateAutoUserId()
213
+ const hashedPassword = await hash.encode(password)
214
+ const verified = typeof callback !== "function"
215
+
216
+ const account = await queries.createAccount({
217
+ userId: finalUserId,
218
+ email,
219
+ password: hashedPassword,
220
+ verified,
221
+ status: AuthStatus.Normal,
222
+ rolemask: 0,
223
+ })
224
+
225
+ if (!verified && callback) {
226
+ await createConfirmationToken(queries, account, email, callback)
227
+ }
228
+
229
+ return account
230
+ }
231
+
232
+ /**
233
+ * Delete the account matched by the identifier.
234
+ * @param {AuthConfig} config
235
+ * @param {UserIdentifier} identifier
236
+ * @returns {Promise<void>}
237
+ * @throws {UserNotFoundError}
238
+ */
239
+ export async function deleteUserBy(config, identifier) {
240
+ const queries = new AuthQueries(config)
241
+ const account = await findAccountByIdentifier(queries, identifier)
242
+
243
+ if (!account) {
244
+ throw new UserNotFoundError()
245
+ }
246
+
247
+ await queries.deleteAccount(account.id)
248
+ }
249
+
250
+ /**
251
+ * Add a role bit to the account's rolemask.
252
+ * @param {AuthConfig} config
253
+ * @param {UserIdentifier} identifier
254
+ * @param {number} role
255
+ * @returns {Promise<void>}
256
+ * @throws {UserNotFoundError}
257
+ */
258
+ export async function addRoleForUserBy(config, identifier, role) {
259
+ const queries = new AuthQueries(config)
260
+ const account = await findAccountByIdentifier(queries, identifier)
261
+
262
+ if (!account) {
263
+ throw new UserNotFoundError()
264
+ }
265
+
266
+ const rolemask = account.rolemask | role
267
+ await queries.updateAccount(account.id, { rolemask })
268
+ }
269
+
270
+ /**
271
+ * Remove a role bit from the account's rolemask.
272
+ * @param {AuthConfig} config
273
+ * @param {UserIdentifier} identifier
274
+ * @param {number} role
275
+ * @returns {Promise<void>}
276
+ * @throws {UserNotFoundError}
277
+ */
278
+ export async function removeRoleForUserBy(config, identifier, role) {
279
+ const queries = new AuthQueries(config)
280
+ const account = await findAccountByIdentifier(queries, identifier)
281
+
282
+ if (!account) {
283
+ throw new UserNotFoundError()
284
+ }
285
+
286
+ const rolemask = account.rolemask & ~role
287
+ await queries.updateAccount(account.id, { rolemask })
288
+ }
289
+
290
+ /**
291
+ * Check whether the account has every bit in the given role mask.
292
+ * @param {AuthConfig} config
293
+ * @param {UserIdentifier} identifier
294
+ * @param {number} role
295
+ * @returns {Promise<boolean>}
296
+ * @throws {UserNotFoundError}
297
+ */
298
+ export async function hasRoleForUserBy(config, identifier, role) {
299
+ const queries = new AuthQueries(config)
300
+ const account = await findAccountByIdentifier(queries, identifier)
301
+
302
+ if (!account) {
303
+ throw new UserNotFoundError()
304
+ }
305
+
306
+ return (account.rolemask & role) === role
307
+ }
308
+
309
+ /**
310
+ * Change the password for the account matched by the identifier.
311
+ * @param {AuthConfig} config
312
+ * @param {UserIdentifier} identifier
313
+ * @param {string} password
314
+ * @returns {Promise<void>}
315
+ * @throws {UserNotFoundError}
316
+ * @throws {InvalidPasswordError}
317
+ */
318
+ export async function changePasswordForUserBy(config, identifier, password) {
319
+ validatePassword(password, config)
320
+
321
+ const queries = new AuthQueries(config)
322
+ const account = await findAccountByIdentifier(queries, identifier)
323
+
324
+ if (!account) {
325
+ throw new UserNotFoundError()
326
+ }
327
+
328
+ await queries.updateAccount(account.id, {
329
+ password: await hash.encode(password),
330
+ })
331
+ }
332
+
333
+ /**
334
+ * Set the status code for the account matched by the identifier.
335
+ * @param {AuthConfig} config
336
+ * @param {UserIdentifier} identifier
337
+ * @param {number} status
338
+ * @returns {Promise<void>}
339
+ * @throws {UserNotFoundError}
340
+ */
341
+ export async function setStatusForUserBy(config, identifier, status) {
342
+ const queries = new AuthQueries(config)
343
+ const account = await findAccountByIdentifier(queries, identifier)
344
+
345
+ if (!account) {
346
+ throw new UserNotFoundError()
347
+ }
348
+
349
+ await queries.updateAccount(account.id, { status })
350
+ }
351
+
352
+ /**
353
+ * Create a password reset token for the account matched by the identifier.
354
+ * @param {AuthConfig} config
355
+ * @param {UserIdentifier} identifier
356
+ * @param {string | number | null} [expiresAfter]
357
+ * @param {TokenCallback} [callback]
358
+ * @returns {Promise<void>}
359
+ * @throws {UserNotFoundError}
360
+ * @throws {EmailNotVerifiedError}
361
+ */
362
+ export async function initiatePasswordResetForUserBy(config, identifier, expiresAfter = null, callback) {
363
+ const queries = new AuthQueries(config)
364
+ const account = await findAccountByIdentifier(queries, identifier)
365
+
366
+ if (!account) {
367
+ throw new UserNotFoundError()
368
+ }
369
+
370
+ if (!account.verified) {
371
+ throw new EmailNotVerifiedError()
372
+ }
373
+
374
+ const expiry = !expiresAfter ? ms("6h") : ms(expiresAfter)
375
+ const token = await hash.encode(account.email)
376
+ const expires = new Date(Date.now() + expiry)
377
+
378
+ await queries.createResetToken({
379
+ accountId: account.id,
380
+ token,
381
+ expires,
382
+ })
383
+
384
+ if (callback) {
385
+ callback(token)
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Request a password reset by email, subject to the open-request limit.
391
+ * @param {AuthConfig} config
392
+ * @param {string} email
393
+ * @param {string | number | null} [expiresAfter]
394
+ * @param {number | null} [maxOpenRequests]
395
+ * @param {TokenCallback} [callback]
396
+ * @returns {Promise<void>}
397
+ * @throws {EmailNotVerifiedError}
398
+ * @throws {ResetDisabledError}
399
+ * @throws {TooManyResetsError}
400
+ */
401
+ export async function resetPassword(config, email, expiresAfter = null, maxOpenRequests = null, callback) {
402
+ validateEmail(email)
403
+
404
+ const expiry = !expiresAfter ? ms("6h") : ms(expiresAfter)
405
+ const maxRequests = maxOpenRequests === null ? 2 : Math.max(1, maxOpenRequests)
406
+
407
+ const queries = new AuthQueries(config)
408
+ const account = await queries.findAccountByEmail(email)
409
+
410
+ if (!account || !account.verified) {
411
+ throw new EmailNotVerifiedError()
412
+ }
413
+
414
+ if (!account.resettable) {
415
+ throw new ResetDisabledError()
416
+ }
417
+
418
+ const openRequests = await queries.countActiveResetTokensForAccount(account.id)
419
+
420
+ if (openRequests >= maxRequests) {
421
+ throw new TooManyResetsError()
422
+ }
423
+
424
+ const token = await hash.encode(email)
425
+ const expires = new Date(Date.now() + expiry)
426
+
427
+ await queries.createResetToken({
428
+ accountId: account.id,
429
+ token,
430
+ expires,
431
+ })
432
+
433
+ if (callback) {
434
+ callback(token)
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Confirm a password reset token and apply the new password.
440
+ * @param {AuthConfig} config
441
+ * @param {string} token
442
+ * @param {string} password
443
+ * @returns {Promise<{ accountId: number, email: string }>}
444
+ * @throws {ResetNotFoundError}
445
+ * @throws {ResetExpiredError}
446
+ * @throws {UserNotFoundError}
447
+ * @throws {ResetDisabledError}
448
+ * @throws {InvalidPasswordError}
449
+ * @throws {InvalidTokenError}
450
+ */
451
+ export async function confirmResetPassword(config, token, password) {
452
+ const queries = new AuthQueries(config)
453
+ const reset = await queries.findResetToken(token)
454
+
455
+ if (!reset) {
456
+ throw new ResetNotFoundError()
457
+ }
458
+
459
+ if (new Date(reset.expires) < new Date()) {
460
+ throw new ResetExpiredError()
461
+ }
462
+
463
+ const account = await queries.findAccountById(reset.account_id)
464
+ if (!account) {
465
+ throw new UserNotFoundError()
466
+ }
467
+
468
+ if (!account.resettable) {
469
+ throw new ResetDisabledError()
470
+ }
471
+
472
+ validatePassword(password, config)
473
+
474
+ if (!(await hash.verify(token, account.email))) {
475
+ throw new InvalidTokenError()
476
+ }
477
+
478
+ await queries.updateAccount(account.id, {
479
+ password: await hash.encode(password),
480
+ })
481
+
482
+ await queries.deleteResetToken(token)
483
+
484
+ return { accountId: account.id, email: account.email }
485
+ }
486
+
487
+ /**
488
+ * Check whether an account exists for the given email.
489
+ * @param {AuthConfig} config
490
+ * @param {string} email
491
+ * @returns {Promise<boolean>}
492
+ */
493
+ export async function userExistsByEmail(config, email) {
494
+ validateEmail(email)
495
+
496
+ const queries = new AuthQueries(config)
497
+ const account = await queries.findAccountByEmail(email)
498
+
499
+ return account !== null
500
+ }
501
+
502
+ /**
503
+ * Force logout of all sessions for the account matched by the identifier.
504
+ * @param {AuthConfig} config
505
+ * @param {UserIdentifier} identifier
506
+ * @returns {Promise<{ accountId: number }>}
507
+ * @throws {UserNotFoundError}
508
+ */
509
+ export async function forceLogoutForUserBy(config, identifier) {
510
+ const queries = new AuthQueries(config)
511
+ const account = await findAccountByIdentifier(queries, identifier)
512
+
513
+ if (!account) {
514
+ throw new UserNotFoundError()
515
+ }
516
+
517
+ await queries.incrementForceLogout(account.id)
518
+
519
+ return { accountId: account.id }
520
+ }