@eaccess/auth 0.1.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/dist/index.js ADDED
@@ -0,0 +1,2555 @@
1
+ // src/auth-admin-manager.ts
2
+ import { hash } from "@prsm/hash";
3
+ import ms from "@prsm/ms";
4
+
5
+ // src/types.ts
6
+ import "express-session";
7
+ var TwoFactorMechanism = /* @__PURE__ */ ((TwoFactorMechanism2) => {
8
+ TwoFactorMechanism2[TwoFactorMechanism2["TOTP"] = 1] = "TOTP";
9
+ TwoFactorMechanism2[TwoFactorMechanism2["EMAIL"] = 2] = "EMAIL";
10
+ TwoFactorMechanism2[TwoFactorMechanism2["SMS"] = 3] = "SMS";
11
+ return TwoFactorMechanism2;
12
+ })(TwoFactorMechanism || {});
13
+ var AuthStatus = {
14
+ Normal: 0,
15
+ Archived: 1,
16
+ Banned: 2,
17
+ Locked: 3,
18
+ PendingReview: 4,
19
+ Suspended: 5
20
+ };
21
+ var AuthRole = {
22
+ Admin: 1,
23
+ Author: 2,
24
+ Collaborator: 4,
25
+ Consultant: 8,
26
+ Consumer: 16,
27
+ Contributor: 32,
28
+ Coordinator: 64,
29
+ Creator: 128,
30
+ Developer: 256,
31
+ Director: 512,
32
+ Editor: 1024,
33
+ Employee: 2048,
34
+ Maintainer: 4096,
35
+ Manager: 8192,
36
+ Moderator: 16384,
37
+ Publisher: 32768,
38
+ Reviewer: 65536,
39
+ Subscriber: 131072,
40
+ SuperAdmin: 262144,
41
+ SuperEditor: 524288,
42
+ SuperModerator: 1048576,
43
+ Translator: 2097152
44
+ };
45
+ var AuthActivityAction = {
46
+ Login: "login",
47
+ Logout: "logout",
48
+ FailedLogin: "failed_login",
49
+ Register: "register",
50
+ EmailConfirmed: "email_confirmed",
51
+ PasswordResetRequested: "password_reset_requested",
52
+ PasswordResetCompleted: "password_reset_completed",
53
+ PasswordChanged: "password_changed",
54
+ EmailChanged: "email_changed",
55
+ RoleChanged: "role_changed",
56
+ StatusChanged: "status_changed",
57
+ ForceLogout: "force_logout",
58
+ OAuthConnected: "oauth_connected",
59
+ RememberTokenCreated: "remember_token_created",
60
+ TwoFactorSetup: "two_factor_setup",
61
+ TwoFactorVerified: "two_factor_verified",
62
+ TwoFactorFailed: "two_factor_failed",
63
+ TwoFactorDisabled: "two_factor_disabled",
64
+ BackupCodeUsed: "backup_code_used"
65
+ };
66
+
67
+ // src/queries.ts
68
+ var AuthQueries = class {
69
+ constructor(config) {
70
+ this.db = config.db;
71
+ this.tablePrefix = config.tablePrefix || "user_";
72
+ }
73
+ get accountsTable() {
74
+ return `${this.tablePrefix}accounts`;
75
+ }
76
+ get confirmationsTable() {
77
+ return `${this.tablePrefix}confirmations`;
78
+ }
79
+ get remembersTable() {
80
+ return `${this.tablePrefix}remembers`;
81
+ }
82
+ get resetsTable() {
83
+ return `${this.tablePrefix}resets`;
84
+ }
85
+ get providersTable() {
86
+ return `${this.tablePrefix}providers`;
87
+ }
88
+ get twoFactorMethodsTable() {
89
+ return `${this.tablePrefix}2fa_methods`;
90
+ }
91
+ get twoFactorTokensTable() {
92
+ return `${this.tablePrefix}2fa_tokens`;
93
+ }
94
+ async findAccountById(id) {
95
+ const sql = `SELECT * FROM ${this.accountsTable} WHERE id = $1`;
96
+ const result = await this.db.query(sql, [id]);
97
+ return result.rows[0] || null;
98
+ }
99
+ async findAccountByUserId(userId) {
100
+ const sql = `SELECT * FROM ${this.accountsTable} WHERE user_id = $1`;
101
+ const result = await this.db.query(sql, [userId]);
102
+ return result.rows[0] || null;
103
+ }
104
+ async findAccountByEmail(email) {
105
+ const sql = `SELECT * FROM ${this.accountsTable} WHERE email = $1`;
106
+ const result = await this.db.query(sql, [email]);
107
+ return result.rows[0] || null;
108
+ }
109
+ async createAccount(data) {
110
+ const sql = `
111
+ INSERT INTO ${this.accountsTable} (
112
+ user_id, email, password, verified, status, rolemask,
113
+ force_logout, resettable, registered
114
+ )
115
+ VALUES ($1, $2, $3, $4, $5, $6, 0, true, NOW())
116
+ RETURNING *
117
+ `;
118
+ const result = await this.db.query(sql, [data.userId, data.email, data.password, data.verified, data.status, data.rolemask]);
119
+ return result.rows[0];
120
+ }
121
+ async updateAccount(id, updates) {
122
+ const fields = [];
123
+ const values = [];
124
+ let paramIndex = 1;
125
+ for (const [key, value] of Object.entries(updates)) {
126
+ if (key === "id") continue;
127
+ fields.push(`${key} = $${paramIndex++}`);
128
+ values.push(value);
129
+ }
130
+ if (fields.length === 0) return;
131
+ values.push(id);
132
+ const sql = `UPDATE ${this.accountsTable} SET ${fields.join(", ")} WHERE id = $${paramIndex}`;
133
+ await this.db.query(sql, values);
134
+ }
135
+ async updateAccountLastLogin(id) {
136
+ const sql = `UPDATE ${this.accountsTable} SET last_login = NOW() WHERE id = $1`;
137
+ await this.db.query(sql, [id]);
138
+ }
139
+ async incrementForceLogout(id) {
140
+ const sql = `UPDATE ${this.accountsTable} SET force_logout = force_logout + 1 WHERE id = $1`;
141
+ await this.db.query(sql, [id]);
142
+ }
143
+ async deleteAccount(id) {
144
+ await this.db.query(`DELETE FROM ${this.twoFactorTokensTable} WHERE account_id = $1`, [id]);
145
+ await this.db.query(`DELETE FROM ${this.twoFactorMethodsTable} WHERE account_id = $1`, [id]);
146
+ await this.db.query(`DELETE FROM ${this.providersTable} WHERE account_id = $1`, [id]);
147
+ await this.db.query(`DELETE FROM ${this.confirmationsTable} WHERE account_id = $1`, [id]);
148
+ await this.db.query(`DELETE FROM ${this.remembersTable} WHERE account_id = $1`, [id]);
149
+ await this.db.query(`DELETE FROM ${this.resetsTable} WHERE account_id = $1`, [id]);
150
+ await this.db.query(`DELETE FROM ${this.accountsTable} WHERE id = $1`, [id]);
151
+ }
152
+ async createConfirmation(data) {
153
+ await this.db.query(`DELETE FROM ${this.confirmationsTable} WHERE account_id = $1`, [data.accountId]);
154
+ const sql = `
155
+ INSERT INTO ${this.confirmationsTable} (account_id, token, email, expires)
156
+ VALUES ($1, $2, $3, $4)
157
+ `;
158
+ await this.db.query(sql, [data.accountId, data.token, data.email, data.expires]);
159
+ }
160
+ async findConfirmation(token) {
161
+ const sql = `SELECT * FROM ${this.confirmationsTable} WHERE token = $1`;
162
+ const result = await this.db.query(sql, [token]);
163
+ return result.rows[0] || null;
164
+ }
165
+ async findLatestConfirmationForAccount(accountId) {
166
+ const sql = `
167
+ SELECT * FROM ${this.confirmationsTable}
168
+ WHERE account_id = $1
169
+ ORDER BY expires DESC
170
+ LIMIT 1
171
+ `;
172
+ const result = await this.db.query(sql, [accountId]);
173
+ return result.rows[0] || null;
174
+ }
175
+ async deleteConfirmation(token) {
176
+ await this.db.query(`DELETE FROM ${this.confirmationsTable} WHERE token = $1`, [token]);
177
+ }
178
+ async createRememberToken(data) {
179
+ await this.db.query(`DELETE FROM ${this.remembersTable} WHERE account_id = $1`, [data.accountId]);
180
+ const sql = `
181
+ INSERT INTO ${this.remembersTable} (account_id, token, expires)
182
+ VALUES ($1, $2, $3)
183
+ `;
184
+ await this.db.query(sql, [data.accountId, data.token, data.expires]);
185
+ }
186
+ async findRememberToken(token) {
187
+ const sql = `SELECT * FROM ${this.remembersTable} WHERE token = $1`;
188
+ const result = await this.db.query(sql, [token]);
189
+ return result.rows[0] || null;
190
+ }
191
+ async deleteRememberToken(token) {
192
+ await this.db.query(`DELETE FROM ${this.remembersTable} WHERE token = $1`, [token]);
193
+ }
194
+ async deleteRememberTokensForAccount(accountId) {
195
+ await this.db.query(`DELETE FROM ${this.remembersTable} WHERE account_id = $1`, [accountId]);
196
+ }
197
+ async deleteExpiredRememberTokensForAccount(accountId) {
198
+ await this.db.query(`DELETE FROM ${this.remembersTable} WHERE account_id = $1 AND expires <= NOW()`, [accountId]);
199
+ }
200
+ async createResetToken(data) {
201
+ const sql = `
202
+ INSERT INTO ${this.resetsTable} (account_id, token, expires)
203
+ VALUES ($1, $2, $3)
204
+ `;
205
+ await this.db.query(sql, [data.accountId, data.token, data.expires]);
206
+ }
207
+ async findResetToken(token) {
208
+ const sql = `
209
+ SELECT * FROM ${this.resetsTable}
210
+ WHERE token = $1
211
+ ORDER BY expires DESC
212
+ LIMIT 1
213
+ `;
214
+ const result = await this.db.query(sql, [token]);
215
+ return result.rows[0] || null;
216
+ }
217
+ async countActiveResetTokensForAccount(accountId) {
218
+ const sql = `
219
+ SELECT COUNT(*) as count FROM ${this.resetsTable}
220
+ WHERE account_id = $1 AND expires >= NOW()
221
+ `;
222
+ const result = await this.db.query(sql, [accountId]);
223
+ return parseInt(result.rows[0]?.count || "0");
224
+ }
225
+ async deleteResetToken(token) {
226
+ await this.db.query(`DELETE FROM ${this.resetsTable} WHERE token = $1`, [token]);
227
+ }
228
+ async deleteResetTokensForAccount(accountId) {
229
+ await this.db.query(`DELETE FROM ${this.resetsTable} WHERE account_id = $1`, [accountId]);
230
+ }
231
+ async createProvider(data) {
232
+ const sql = `
233
+ INSERT INTO ${this.providersTable} (
234
+ account_id, provider, provider_id, provider_email,
235
+ provider_username, provider_name, provider_avatar
236
+ )
237
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
238
+ RETURNING *
239
+ `;
240
+ const result = await this.db.query(sql, [data.accountId, data.provider, data.providerId, data.providerEmail, data.providerUsername, data.providerName, data.providerAvatar]);
241
+ return result.rows[0];
242
+ }
243
+ async findProviderByProviderIdAndType(providerId, provider) {
244
+ const sql = `SELECT * FROM ${this.providersTable} WHERE provider_id = $1 AND provider = $2`;
245
+ const result = await this.db.query(sql, [providerId, provider]);
246
+ return result.rows[0] || null;
247
+ }
248
+ async findProvidersByAccountId(accountId) {
249
+ const sql = `SELECT * FROM ${this.providersTable} WHERE account_id = $1 ORDER BY created_at DESC`;
250
+ const result = await this.db.query(sql, [accountId]);
251
+ return result.rows;
252
+ }
253
+ async deleteProvider(id) {
254
+ await this.db.query(`DELETE FROM ${this.providersTable} WHERE id = $1`, [id]);
255
+ }
256
+ async deleteProvidersByAccountId(accountId) {
257
+ await this.db.query(`DELETE FROM ${this.providersTable} WHERE account_id = $1`, [accountId]);
258
+ }
259
+ // two-factor authentication methods
260
+ async findTwoFactorMethodsByAccountId(accountId) {
261
+ const sql = `SELECT * FROM ${this.twoFactorMethodsTable} WHERE account_id = $1 ORDER BY created_at DESC`;
262
+ const result = await this.db.query(sql, [accountId]);
263
+ return result.rows;
264
+ }
265
+ async findTwoFactorMethodByAccountAndMechanism(accountId, mechanism) {
266
+ const sql = `SELECT * FROM ${this.twoFactorMethodsTable} WHERE account_id = $1 AND mechanism = $2`;
267
+ const result = await this.db.query(sql, [accountId, mechanism]);
268
+ return result.rows[0] || null;
269
+ }
270
+ async createTwoFactorMethod(data) {
271
+ const sql = `
272
+ INSERT INTO ${this.twoFactorMethodsTable} (
273
+ account_id, mechanism, secret, backup_codes, verified
274
+ )
275
+ VALUES ($1, $2, $3, $4, $5)
276
+ RETURNING *
277
+ `;
278
+ const result = await this.db.query(sql, [data.accountId, data.mechanism, data.secret || null, data.backupCodes || null, data.verified || false]);
279
+ return result.rows[0];
280
+ }
281
+ async updateTwoFactorMethod(id, updates) {
282
+ const fields = [];
283
+ const values = [];
284
+ let paramIndex = 1;
285
+ for (const [key, value] of Object.entries(updates)) {
286
+ if (key === "id") continue;
287
+ fields.push(`${key} = $${paramIndex++}`);
288
+ values.push(value);
289
+ }
290
+ if (fields.length === 0) return;
291
+ values.push(id);
292
+ const sql = `UPDATE ${this.twoFactorMethodsTable} SET ${fields.join(", ")} WHERE id = $${paramIndex}`;
293
+ await this.db.query(sql, values);
294
+ }
295
+ async deleteTwoFactorMethod(id) {
296
+ await this.db.query(`DELETE FROM ${this.twoFactorMethodsTable} WHERE id = $1`, [id]);
297
+ }
298
+ async deleteTwoFactorMethodsByAccountId(accountId) {
299
+ await this.db.query(`DELETE FROM ${this.twoFactorMethodsTable} WHERE account_id = $1`, [accountId]);
300
+ }
301
+ // two-factor authentication tokens
302
+ async createTwoFactorToken(data) {
303
+ const sql = `
304
+ INSERT INTO ${this.twoFactorTokensTable} (
305
+ account_id, mechanism, selector, token_hash, expires_at
306
+ )
307
+ VALUES ($1, $2, $3, $4, $5)
308
+ RETURNING *
309
+ `;
310
+ const result = await this.db.query(sql, [data.accountId, data.mechanism, data.selector, data.tokenHash, data.expiresAt]);
311
+ return result.rows[0];
312
+ }
313
+ async findTwoFactorTokenBySelector(selector) {
314
+ const sql = `SELECT * FROM ${this.twoFactorTokensTable} WHERE selector = $1 AND expires_at > NOW()`;
315
+ const result = await this.db.query(sql, [selector]);
316
+ return result.rows[0] || null;
317
+ }
318
+ async deleteTwoFactorToken(id) {
319
+ await this.db.query(`DELETE FROM ${this.twoFactorTokensTable} WHERE id = $1`, [id]);
320
+ }
321
+ async deleteTwoFactorTokensByAccountId(accountId) {
322
+ await this.db.query(`DELETE FROM ${this.twoFactorTokensTable} WHERE account_id = $1`, [accountId]);
323
+ }
324
+ async deleteTwoFactorTokensByAccountAndMechanism(accountId, mechanism) {
325
+ await this.db.query(`DELETE FROM ${this.twoFactorTokensTable} WHERE account_id = $1 AND mechanism = $2`, [accountId, mechanism]);
326
+ }
327
+ };
328
+
329
+ // src/errors.ts
330
+ var AuthError = class extends Error {
331
+ constructor(message) {
332
+ super(message);
333
+ this.name = this.constructor.name;
334
+ }
335
+ };
336
+ var ConfirmationExpiredError = class extends AuthError {
337
+ constructor() {
338
+ super("Confirmation token has expired");
339
+ }
340
+ };
341
+ var ConfirmationNotFoundError = class extends AuthError {
342
+ constructor() {
343
+ super("Confirmation token not found");
344
+ }
345
+ };
346
+ var EmailNotVerifiedError = class extends AuthError {
347
+ constructor() {
348
+ super("Email address has not been verified");
349
+ }
350
+ };
351
+ var EmailTakenError = class extends AuthError {
352
+ constructor() {
353
+ super("Email address is already in use");
354
+ }
355
+ };
356
+ var InvalidEmailError = class extends AuthError {
357
+ constructor() {
358
+ super("Invalid email address format");
359
+ }
360
+ };
361
+ var InvalidPasswordError = class extends AuthError {
362
+ constructor() {
363
+ super("Invalid password");
364
+ }
365
+ };
366
+ var InvalidTokenError = class extends AuthError {
367
+ constructor() {
368
+ super("Invalid token");
369
+ }
370
+ };
371
+ var ResetDisabledError = class extends AuthError {
372
+ constructor() {
373
+ super("Password reset is disabled for this account");
374
+ }
375
+ };
376
+ var ResetExpiredError = class extends AuthError {
377
+ constructor() {
378
+ super("Password reset token has expired");
379
+ }
380
+ };
381
+ var ResetNotFoundError = class extends AuthError {
382
+ constructor() {
383
+ super("Password reset token not found");
384
+ }
385
+ };
386
+ var TooManyResetsError = class extends AuthError {
387
+ constructor() {
388
+ super("Too many password reset requests");
389
+ }
390
+ };
391
+ var UserInactiveError = class extends AuthError {
392
+ constructor() {
393
+ super("User account is inactive");
394
+ }
395
+ };
396
+ var UserNotFoundError = class extends AuthError {
397
+ constructor() {
398
+ super("User not found");
399
+ }
400
+ };
401
+ var UserNotLoggedInError = class extends AuthError {
402
+ constructor() {
403
+ super("User is not logged in");
404
+ }
405
+ };
406
+ var SecondFactorRequiredError = class extends AuthError {
407
+ constructor(availableMethods) {
408
+ super("Second factor authentication required");
409
+ this.availableMethods = availableMethods;
410
+ }
411
+ };
412
+ var InvalidTwoFactorCodeError = class extends AuthError {
413
+ constructor() {
414
+ super("Invalid two-factor authentication code");
415
+ }
416
+ };
417
+ var TwoFactorExpiredError = class extends AuthError {
418
+ constructor() {
419
+ super("Two-factor authentication session has expired");
420
+ }
421
+ };
422
+ var TwoFactorNotSetupError = class extends AuthError {
423
+ constructor() {
424
+ super("Two-factor authentication is not set up for this account");
425
+ }
426
+ };
427
+ var TwoFactorAlreadyEnabledError = class extends AuthError {
428
+ constructor() {
429
+ super("Two-factor authentication is already enabled for this mechanism");
430
+ }
431
+ };
432
+ var InvalidBackupCodeError = class extends AuthError {
433
+ constructor() {
434
+ super("Invalid backup code");
435
+ }
436
+ };
437
+ var TwoFactorSetupIncompleteError = class extends AuthError {
438
+ constructor() {
439
+ super("Two-factor authentication setup is not complete");
440
+ }
441
+ };
442
+
443
+ // src/util.ts
444
+ var isValidEmail = (email) => {
445
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
446
+ return emailRegex.test(email);
447
+ };
448
+ var validateEmail = (email) => {
449
+ if (typeof email !== "string") {
450
+ throw new InvalidEmailError();
451
+ }
452
+ if (!email.trim()) {
453
+ throw new InvalidEmailError();
454
+ }
455
+ if (!isValidEmail(email)) {
456
+ throw new InvalidEmailError();
457
+ }
458
+ };
459
+ var createMapFromEnum = (enumObj) => Object.fromEntries(Object.entries(enumObj).map(([key, value]) => [value, key]));
460
+
461
+ // src/auth-admin-manager.ts
462
+ var AuthAdminManager = class {
463
+ constructor(req, res, config, auth) {
464
+ this.req = req;
465
+ this.res = res;
466
+ this.config = config;
467
+ this.queries = new AuthQueries(config);
468
+ this.auth = auth;
469
+ }
470
+ validatePassword(password) {
471
+ const minLength = this.config.minPasswordLength || 8;
472
+ const maxLength = this.config.maxPasswordLength || 64;
473
+ if (typeof password !== "string") {
474
+ throw new InvalidPasswordError();
475
+ }
476
+ if (password.length < minLength) {
477
+ throw new InvalidPasswordError();
478
+ }
479
+ if (password.length > maxLength) {
480
+ throw new InvalidPasswordError();
481
+ }
482
+ }
483
+ generateAutoUserId() {
484
+ return crypto.randomUUID();
485
+ }
486
+ async findAccountByIdentifier(identifier) {
487
+ if (identifier.accountId !== void 0) {
488
+ return await this.queries.findAccountById(identifier.accountId);
489
+ } else if (identifier.email !== void 0) {
490
+ return await this.queries.findAccountByEmail(identifier.email);
491
+ } else if (identifier.userId !== void 0) {
492
+ return await this.queries.findAccountByUserId(identifier.userId);
493
+ }
494
+ return null;
495
+ }
496
+ async createConfirmationToken(account, email, callback) {
497
+ const token = hash.encode(email);
498
+ const expires = new Date(Date.now() + 1e3 * 60 * 60 * 24 * 7);
499
+ await this.queries.createConfirmation({
500
+ accountId: account.id,
501
+ token,
502
+ email,
503
+ expires
504
+ });
505
+ if (callback) {
506
+ callback(token);
507
+ }
508
+ }
509
+ /**
510
+ * Create a new user account (admin function).
511
+ *
512
+ * @param credentials - Email and password for new account
513
+ * @param userId - Optional user ID to link this auth account to. If not provided, a UUID will be generated automatically.
514
+ * @param callback - If provided, account is created unverified and callback receives confirmation token. Create a URL like /confirm/{token} and call confirmEmail() in that handler. If omitted, account is immediately verified.
515
+ * @returns The created account record
516
+ * @throws {EmailTakenError} Email is already registered
517
+ * @throws {InvalidPasswordError} Password doesn't meet length requirements
518
+ */
519
+ async createUser(credentials, userId, callback) {
520
+ validateEmail(credentials.email);
521
+ this.validatePassword(credentials.password);
522
+ const existing = await this.queries.findAccountByEmail(credentials.email);
523
+ if (existing) {
524
+ throw new EmailTakenError();
525
+ }
526
+ const finalUserId = userId || this.generateAutoUserId();
527
+ const hashedPassword = hash.encode(credentials.password);
528
+ const verified = typeof callback !== "function";
529
+ const account = await this.queries.createAccount({
530
+ userId: finalUserId,
531
+ email: credentials.email,
532
+ password: hashedPassword,
533
+ verified,
534
+ status: AuthStatus.Normal,
535
+ rolemask: 0
536
+ });
537
+ if (!verified && callback) {
538
+ await this.createConfirmationToken(account, credentials.email, callback);
539
+ }
540
+ return account;
541
+ }
542
+ /**
543
+ * Log in as another user (admin function).
544
+ * Creates a new session as the target user without requiring their password.
545
+ *
546
+ * @param identifier - Find user by accountId, email, or userId
547
+ * @throws {UserNotFoundError} No account matches the identifier
548
+ */
549
+ async loginAsUserBy(identifier) {
550
+ const account = await this.findAccountByIdentifier(identifier);
551
+ if (!account) {
552
+ throw new UserNotFoundError();
553
+ }
554
+ await this.auth.onLoginSuccessful(account, false);
555
+ }
556
+ /**
557
+ * Delete a user account and all associated data (admin function).
558
+ * Removes account, confirmations, remember tokens, and reset tokens.
559
+ *
560
+ * @param identifier - Find user by accountId, email, or userId
561
+ * @throws {UserNotFoundError} No account matches the identifier
562
+ */
563
+ async deleteUserBy(identifier) {
564
+ const account = await this.findAccountByIdentifier(identifier);
565
+ if (!account) {
566
+ throw new UserNotFoundError();
567
+ }
568
+ await this.queries.deleteAccount(account.id);
569
+ }
570
+ /**
571
+ * Add a role to a user's account (admin function).
572
+ * Uses bitwise OR to add role to existing rolemask.
573
+ *
574
+ * @param identifier - Find user by accountId, email, or userId
575
+ * @param role - Role bitmask to add (e.g., AuthRole.Admin)
576
+ * @throws {UserNotFoundError} No account matches the identifier
577
+ */
578
+ async addRoleForUserBy(identifier, role) {
579
+ const account = await this.findAccountByIdentifier(identifier);
580
+ if (!account) {
581
+ throw new UserNotFoundError();
582
+ }
583
+ const rolemask = account.rolemask | role;
584
+ await this.queries.updateAccount(account.id, { rolemask });
585
+ }
586
+ /**
587
+ * Remove a role from a user's account (admin function).
588
+ * Uses bitwise operations to remove role from rolemask.
589
+ *
590
+ * @param identifier - Find user by accountId, email, or userId
591
+ * @param role - Role bitmask to remove (e.g., AuthRole.Admin)
592
+ * @throws {UserNotFoundError} No account matches the identifier
593
+ */
594
+ async removeRoleForUserBy(identifier, role) {
595
+ const account = await this.findAccountByIdentifier(identifier);
596
+ if (!account) {
597
+ throw new UserNotFoundError();
598
+ }
599
+ const rolemask = account.rolemask & ~role;
600
+ await this.queries.updateAccount(account.id, { rolemask });
601
+ }
602
+ /**
603
+ * Check if a user has a specific role (admin function).
604
+ *
605
+ * @param identifier - Find user by accountId, email, or userId
606
+ * @param role - Role bitmask to check (e.g., AuthRole.Admin)
607
+ * @returns true if user has the role, false otherwise
608
+ * @throws {UserNotFoundError} No account matches the identifier
609
+ */
610
+ async hasRoleForUserBy(identifier, role) {
611
+ const account = await this.findAccountByIdentifier(identifier);
612
+ if (!account) {
613
+ throw new UserNotFoundError();
614
+ }
615
+ return (account.rolemask & role) === role;
616
+ }
617
+ /**
618
+ * Change a user's password (admin function).
619
+ * Does not require knowing the current password.
620
+ *
621
+ * @param identifier - Find user by accountId, email, or userId
622
+ * @param password - New password (will be hashed)
623
+ * @throws {UserNotFoundError} No account matches the identifier
624
+ * @throws {InvalidPasswordError} New password doesn't meet requirements
625
+ */
626
+ async changePasswordForUserBy(identifier, password) {
627
+ this.validatePassword(password);
628
+ const account = await this.findAccountByIdentifier(identifier);
629
+ if (!account) {
630
+ throw new UserNotFoundError();
631
+ }
632
+ await this.queries.updateAccount(account.id, {
633
+ password: hash.encode(password)
634
+ });
635
+ }
636
+ /**
637
+ * Change a user's account status (admin function).
638
+ *
639
+ * @param identifier - Find user by accountId, email, or userId
640
+ * @param status - New status (0=Normal, 1=Archived, 2=Banned, 3=Locked, etc.)
641
+ * @throws {UserNotFoundError} No account matches the identifier
642
+ */
643
+ async setStatusForUserBy(identifier, status) {
644
+ const account = await this.findAccountByIdentifier(identifier);
645
+ if (!account) {
646
+ throw new UserNotFoundError();
647
+ }
648
+ await this.queries.updateAccount(account.id, { status });
649
+ }
650
+ /**
651
+ * Initiate password reset for a user (admin function).
652
+ * Creates a reset token without rate limiting.
653
+ *
654
+ * @param identifier - Find user by accountId, email, or userId
655
+ * @param expiresAfter - Token expiration (default: 6h). Accepts ms format like '1h', '30m'
656
+ * @param callback - Called with reset token. Create a URL like /reset/{token} and call confirmResetPassword() in that handler
657
+ * @throws {UserNotFoundError} No account matches the identifier
658
+ * @throws {EmailNotVerifiedError} Account exists but email is not verified
659
+ */
660
+ async initiatePasswordResetForUserBy(identifier, expiresAfter = null, callback) {
661
+ const account = await this.findAccountByIdentifier(identifier);
662
+ if (!account) {
663
+ throw new UserNotFoundError();
664
+ }
665
+ if (!account.verified) {
666
+ throw new EmailNotVerifiedError();
667
+ }
668
+ const expiry = !expiresAfter ? ms("6h") : ms(expiresAfter);
669
+ const token = hash.encode(account.email);
670
+ const expires = new Date(Date.now() + expiry);
671
+ await this.queries.createResetToken({
672
+ accountId: account.id,
673
+ token,
674
+ expires
675
+ });
676
+ if (callback) {
677
+ callback(token);
678
+ }
679
+ }
680
+ /**
681
+ * Force logout all sessions for a specific user (admin function).
682
+ * Increments force_logout counter and deletes all remember tokens.
683
+ * If target user is currently logged in, marks their session for logout.
684
+ *
685
+ * @param identifier - Find user by accountId, email, or userId
686
+ * @throws {UserNotFoundError} No account matches the identifier
687
+ */
688
+ async forceLogoutForUserBy(identifier) {
689
+ const account = await this.findAccountByIdentifier(identifier);
690
+ if (!account) {
691
+ throw new UserNotFoundError();
692
+ }
693
+ await this.queries.incrementForceLogout(account.id);
694
+ if (this.auth.getId() === account.id) {
695
+ this.req.session.auth.shouldForceLogout = true;
696
+ }
697
+ }
698
+ };
699
+
700
+ // src/auth-manager.ts
701
+ import { hash as hash4 } from "@prsm/hash";
702
+ import ms3 from "@prsm/ms";
703
+
704
+ // src/activity-logger.ts
705
+ import Bowser from "bowser";
706
+ var ActivityLogger = class {
707
+ constructor(config) {
708
+ this.config = config;
709
+ this.enabled = config.activityLog?.enabled !== false;
710
+ this.maxEntries = config.activityLog?.maxEntries || 1e4;
711
+ this.allowedActions = config.activityLog?.actions || null;
712
+ this.tablePrefix = config.tablePrefix || "user_";
713
+ }
714
+ get activityTable() {
715
+ return `${this.tablePrefix}activity_log`;
716
+ }
717
+ parseUserAgent(userAgent) {
718
+ if (!userAgent) {
719
+ return { browser: null, os: null, device: null };
720
+ }
721
+ try {
722
+ const browser = Bowser.getParser(userAgent);
723
+ const result = browser.getResult();
724
+ return {
725
+ browser: result.browser.name || null,
726
+ os: result.os.name || null,
727
+ device: result.platform.type || "desktop"
728
+ };
729
+ } catch (error) {
730
+ return this.parseUserAgentSimple(userAgent);
731
+ }
732
+ }
733
+ parseUserAgentSimple(userAgent) {
734
+ let browser = null;
735
+ if (userAgent.includes("Chrome")) browser = "Chrome";
736
+ else if (userAgent.includes("Firefox")) browser = "Firefox";
737
+ else if (userAgent.includes("Safari")) browser = "Safari";
738
+ else if (userAgent.includes("Edge")) browser = "Edge";
739
+ let os = null;
740
+ if (userAgent.includes("Windows")) os = "Windows";
741
+ else if (userAgent.includes("Mac OS")) os = "macOS";
742
+ else if (userAgent.includes("Linux")) os = "Linux";
743
+ else if (userAgent.includes("Android")) os = "Android";
744
+ else if (userAgent.includes("iOS")) os = "iOS";
745
+ let device = "desktop";
746
+ if (userAgent.includes("Mobile")) device = "mobile";
747
+ else if (userAgent.includes("Tablet")) device = "tablet";
748
+ return { browser, os, device };
749
+ }
750
+ getIpAddress(req) {
751
+ return req.ip || req.connection?.remoteAddress || req.socket?.remoteAddress || req.connection?.socket?.remoteAddress || null;
752
+ }
753
+ async logActivity(accountId, action, req, success = true, metadata = {}) {
754
+ if (!this.enabled) return;
755
+ if (this.allowedActions && !this.allowedActions.includes(action)) {
756
+ return;
757
+ }
758
+ const userAgent = (typeof req.get === "function" ? req.get("User-Agent") : req.headers?.["user-agent"]) || null;
759
+ const ip = this.getIpAddress(req);
760
+ const parsed = this.parseUserAgent(userAgent);
761
+ try {
762
+ await this.config.db.query(
763
+ `
764
+ INSERT INTO ${this.activityTable}
765
+ (account_id, action, ip_address, user_agent, browser, os, device, success, metadata)
766
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
767
+ `,
768
+ [accountId, action, ip, userAgent, parsed.browser, parsed.os, parsed.device, success, Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null]
769
+ );
770
+ if (Math.random() < 0.01) {
771
+ await this.cleanup();
772
+ }
773
+ } catch (error) {
774
+ console.error("ActivityLogger: Failed to log activity:", error);
775
+ }
776
+ }
777
+ async cleanup() {
778
+ if (!this.enabled) return;
779
+ try {
780
+ await this.config.db.query(
781
+ `
782
+ DELETE FROM ${this.activityTable}
783
+ WHERE id NOT IN (
784
+ SELECT id FROM ${this.activityTable}
785
+ ORDER BY created_at DESC
786
+ LIMIT $1
787
+ )
788
+ `,
789
+ [this.maxEntries]
790
+ );
791
+ } catch (error) {
792
+ console.error("ActivityLogger: Failed to cleanup old entries:", error);
793
+ }
794
+ }
795
+ async getRecentActivity(limit = 100, accountId) {
796
+ if (!this.enabled) return [];
797
+ try {
798
+ let sql = `
799
+ SELECT
800
+ al.*,
801
+ a.email
802
+ FROM ${this.activityTable} al
803
+ LEFT JOIN ${this.tablePrefix}accounts a ON al.account_id = a.id
804
+ `;
805
+ const params = [];
806
+ if (accountId !== void 0) {
807
+ sql += " WHERE al.account_id = $1";
808
+ params.push(accountId);
809
+ }
810
+ sql += ` ORDER BY al.created_at DESC LIMIT $${params.length + 1}`;
811
+ params.push(Math.min(limit, 1e3));
812
+ const result = await this.config.db.query(sql, params);
813
+ return result.rows.map((row) => ({
814
+ ...row,
815
+ metadata: row.metadata ? JSON.parse(row.metadata) : null
816
+ }));
817
+ } catch (error) {
818
+ console.error("ActivityLogger: Failed to get recent activity:", error);
819
+ return [];
820
+ }
821
+ }
822
+ async getActivityStats() {
823
+ if (!this.enabled) {
824
+ return {
825
+ totalEntries: 0,
826
+ uniqueUsers: 0,
827
+ recentLogins: 0,
828
+ failedAttempts: 0
829
+ };
830
+ }
831
+ try {
832
+ const [total, unique, recent, failed] = await Promise.all([
833
+ this.config.db.query(`SELECT COUNT(*) as count FROM ${this.activityTable}`),
834
+ this.config.db.query(`SELECT COUNT(DISTINCT account_id) as count FROM ${this.activityTable} WHERE account_id IS NOT NULL`),
835
+ this.config.db.query(`SELECT COUNT(*) as count FROM ${this.activityTable} WHERE action = 'login' AND created_at > NOW() - INTERVAL '24 hours'`),
836
+ this.config.db.query(`SELECT COUNT(*) as count FROM ${this.activityTable} WHERE success = false AND created_at > NOW() - INTERVAL '24 hours'`)
837
+ ]);
838
+ return {
839
+ totalEntries: parseInt(total.rows[0]?.count || "0"),
840
+ uniqueUsers: parseInt(unique.rows[0]?.count || "0"),
841
+ recentLogins: parseInt(recent.rows[0]?.count || "0"),
842
+ failedAttempts: parseInt(failed.rows[0]?.count || "0")
843
+ };
844
+ } catch (error) {
845
+ console.error("ActivityLogger: Failed to get activity stats:", error);
846
+ return {
847
+ totalEntries: 0,
848
+ uniqueUsers: 0,
849
+ recentLogins: 0,
850
+ failedAttempts: 0
851
+ };
852
+ }
853
+ }
854
+ };
855
+
856
+ // src/providers/base-provider.ts
857
+ var BaseOAuthProvider = class {
858
+ constructor(config, authConfig, authManager) {
859
+ this.config = config;
860
+ this.authConfig = authConfig;
861
+ this.authManager = authManager;
862
+ }
863
+ async handleCallback(req) {
864
+ const userData = await this.getUserData(req);
865
+ await this.processOAuthLogin(userData, req);
866
+ }
867
+ async processOAuthLogin(userData, req) {
868
+ const { queries } = this.authManager;
869
+ const providerName = this.getProviderName();
870
+ const existingProvider = await queries.findProviderByProviderIdAndType(userData.id, providerName);
871
+ if (existingProvider) {
872
+ const account2 = await queries.findAccountById(existingProvider.account_id);
873
+ if (account2) {
874
+ await this.authManager.onLoginSuccessful(account2, false);
875
+ return;
876
+ }
877
+ }
878
+ if (userData.email) {
879
+ const existingAccount = await queries.findAccountByEmail(userData.email);
880
+ if (existingAccount) {
881
+ throw new Error("You already have an account associated with this email address.");
882
+ }
883
+ }
884
+ let userId;
885
+ if (this.authConfig.createUser) {
886
+ userId = await this.authConfig.createUser(userData);
887
+ } else {
888
+ userId = crypto.randomUUID();
889
+ }
890
+ const account = await queries.createAccount({
891
+ userId,
892
+ email: userData.email,
893
+ password: null,
894
+ verified: true,
895
+ // OAuth providers are pre-verified
896
+ status: 0,
897
+ // AuthStatus.Normal
898
+ rolemask: 0
899
+ });
900
+ await queries.createProvider({
901
+ accountId: account.id,
902
+ provider: providerName,
903
+ providerId: userData.id,
904
+ providerEmail: userData.email,
905
+ providerUsername: userData.username || null,
906
+ providerName: userData.name || null,
907
+ providerAvatar: userData.avatar || null
908
+ });
909
+ await this.authManager.onLoginSuccessful(account, false);
910
+ }
911
+ async exchangeCodeForToken(code, tokenUrl) {
912
+ const response = await fetch(tokenUrl, {
913
+ method: "POST",
914
+ headers: {
915
+ Accept: "application/json",
916
+ "Content-Type": "application/x-www-form-urlencoded"
917
+ },
918
+ body: new URLSearchParams({
919
+ client_id: this.config.clientId,
920
+ client_secret: this.config.clientSecret,
921
+ code,
922
+ redirect_uri: this.config.redirectUri,
923
+ grant_type: "authorization_code"
924
+ })
925
+ });
926
+ if (!response.ok) {
927
+ throw new Error(`OAuth token exchange failed: ${response.status} ${response.statusText}`);
928
+ }
929
+ const data = await response.json();
930
+ if (!data.access_token) {
931
+ throw new Error("No access token received from OAuth provider");
932
+ }
933
+ return data.access_token;
934
+ }
935
+ async fetchUserFromAPI(accessToken, apiUrl) {
936
+ const response = await fetch(apiUrl, {
937
+ headers: {
938
+ Authorization: `Bearer ${accessToken}`,
939
+ Accept: "application/json"
940
+ }
941
+ });
942
+ if (!response.ok) {
943
+ throw new Error(`Failed to fetch user data: ${response.status} ${response.statusText}`);
944
+ }
945
+ return response.json();
946
+ }
947
+ };
948
+
949
+ // src/providers/github-provider.ts
950
+ var GitHubProvider = class extends BaseOAuthProvider {
951
+ constructor(config, authConfig, authManager) {
952
+ super(config, authConfig, authManager);
953
+ }
954
+ getAuthUrl(state, scopes) {
955
+ const params = new URLSearchParams({
956
+ client_id: this.config.clientId,
957
+ redirect_uri: this.config.redirectUri,
958
+ scope: scopes?.join(" ") || "user:email",
959
+ state: state || Math.random().toString(36).substring(2),
960
+ response_type: "code"
961
+ });
962
+ return `https://github.com/login/oauth/authorize?${params}`;
963
+ }
964
+ async getUserData(req) {
965
+ const code = req.query.code;
966
+ if (!code) {
967
+ throw new Error("No authorization code provided");
968
+ }
969
+ const accessToken = await this.exchangeCodeForToken(code, "https://github.com/login/oauth/access_token");
970
+ const [user, emails] = await Promise.all([this.fetchUserFromAPI(accessToken, "https://api.github.com/user"), this.fetchUserFromAPI(accessToken, "https://api.github.com/user/emails")]);
971
+ const primaryEmail = Array.isArray(emails) ? emails.find((email) => email.primary)?.email : null;
972
+ if (!primaryEmail) {
973
+ throw new Error("No primary email found in GitHub account");
974
+ }
975
+ return {
976
+ id: user.id.toString(),
977
+ email: primaryEmail,
978
+ username: user.login,
979
+ name: user.name || user.login,
980
+ avatar: user.avatar_url
981
+ };
982
+ }
983
+ getProviderName() {
984
+ return "github";
985
+ }
986
+ };
987
+
988
+ // src/providers/google-provider.ts
989
+ var GoogleProvider = class extends BaseOAuthProvider {
990
+ constructor(config, authConfig, authManager) {
991
+ super(config, authConfig, authManager);
992
+ }
993
+ getAuthUrl(state, scopes) {
994
+ const params = new URLSearchParams({
995
+ client_id: this.config.clientId,
996
+ redirect_uri: this.config.redirectUri,
997
+ scope: scopes?.join(" ") || "openid profile email",
998
+ state: state || Math.random().toString(36).substring(2),
999
+ response_type: "code",
1000
+ access_type: "offline",
1001
+ prompt: "consent"
1002
+ });
1003
+ return `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
1004
+ }
1005
+ async getUserData(req) {
1006
+ const code = req.query.code;
1007
+ if (!code) {
1008
+ throw new Error("No authorization code provided");
1009
+ }
1010
+ const accessToken = await this.exchangeCodeForToken(code, "https://oauth2.googleapis.com/token");
1011
+ const user = await this.fetchUserFromAPI(accessToken, "https://www.googleapis.com/oauth2/v2/userinfo");
1012
+ if (!user.email) {
1013
+ throw new Error("No email found in Google account");
1014
+ }
1015
+ return {
1016
+ id: user.id,
1017
+ email: user.email,
1018
+ username: user.email.split("@")[0],
1019
+ // use email prefix as username
1020
+ name: user.name,
1021
+ avatar: user.picture
1022
+ };
1023
+ }
1024
+ getProviderName() {
1025
+ return "google";
1026
+ }
1027
+ };
1028
+
1029
+ // src/providers/azure-provider.ts
1030
+ var AzureProvider = class extends BaseOAuthProvider {
1031
+ constructor(config, authConfig, authManager) {
1032
+ super(config, authConfig, authManager);
1033
+ }
1034
+ getAuthUrl(state, scopes) {
1035
+ const azureConfig = this.config;
1036
+ const params = new URLSearchParams({
1037
+ client_id: azureConfig.clientId,
1038
+ redirect_uri: azureConfig.redirectUri,
1039
+ scope: scopes?.join(" ") || "openid profile email User.Read",
1040
+ state: state || Math.random().toString(36).substring(2),
1041
+ response_type: "code",
1042
+ response_mode: "query"
1043
+ });
1044
+ return `https://login.microsoftonline.com/${azureConfig.tenantId}/oauth2/v2.0/authorize?${params}`;
1045
+ }
1046
+ async getUserData(req) {
1047
+ const code = req.query.code;
1048
+ if (!code) {
1049
+ throw new Error("No authorization code provided");
1050
+ }
1051
+ const azureConfig = this.config;
1052
+ const accessToken = await this.exchangeCodeForToken(code, `https://login.microsoftonline.com/${azureConfig.tenantId}/oauth2/v2.0/token`);
1053
+ const user = await this.fetchUserFromAPI(accessToken, "https://graph.microsoft.com/v1.0/me");
1054
+ if (!user.mail && !user.userPrincipalName) {
1055
+ throw new Error("No email found in Azure account");
1056
+ }
1057
+ return {
1058
+ id: user.id,
1059
+ email: user.mail || user.userPrincipalName,
1060
+ username: user.mailNickname || user.userPrincipalName?.split("@")[0],
1061
+ name: user.displayName,
1062
+ avatar: void 0
1063
+ // Azure doesn't provide avatar in basic profile
1064
+ };
1065
+ }
1066
+ getProviderName() {
1067
+ return "azure";
1068
+ }
1069
+ async exchangeCodeForToken(code, tokenUrl) {
1070
+ const azureConfig = this.config;
1071
+ const response = await fetch(tokenUrl, {
1072
+ method: "POST",
1073
+ headers: {
1074
+ Accept: "application/json",
1075
+ "Content-Type": "application/x-www-form-urlencoded"
1076
+ },
1077
+ body: new URLSearchParams({
1078
+ client_id: azureConfig.clientId,
1079
+ client_secret: azureConfig.clientSecret,
1080
+ code,
1081
+ redirect_uri: azureConfig.redirectUri,
1082
+ grant_type: "authorization_code",
1083
+ scope: "openid profile email User.Read"
1084
+ })
1085
+ });
1086
+ if (!response.ok) {
1087
+ throw new Error(`OAuth token exchange failed: ${response.status} ${response.statusText}`);
1088
+ }
1089
+ const data = await response.json();
1090
+ if (!data.access_token) {
1091
+ throw new Error("No access token received from Azure");
1092
+ }
1093
+ return data.access_token;
1094
+ }
1095
+ };
1096
+
1097
+ // src/two-factor/totp-provider.ts
1098
+ import Otp from "@eaccess/totp";
1099
+ import { hash as hash2 } from "@prsm/hash";
1100
+ var TotpProvider = class {
1101
+ constructor(config) {
1102
+ this.config = config;
1103
+ }
1104
+ generateSecret() {
1105
+ return Otp.createSecret();
1106
+ }
1107
+ generateQRCode(email, secret) {
1108
+ const issuer = this.config.twoFactor?.issuer || "EasyAccess";
1109
+ return Otp.createTotpKeyUriForQrCode(issuer, email, secret);
1110
+ }
1111
+ verify(secret, code) {
1112
+ const window = this.config.twoFactor?.totpWindow || 1;
1113
+ return Otp.verifyTotp(secret, code, window);
1114
+ }
1115
+ generateBackupCodes(count = 10) {
1116
+ const codes = [];
1117
+ for (let i = 0; i < count; i++) {
1118
+ const chars = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
1119
+ let code = "";
1120
+ for (let j = 0; j < 8; j++) {
1121
+ code += chars.charAt(Math.floor(Math.random() * chars.length));
1122
+ }
1123
+ codes.push(code);
1124
+ }
1125
+ return codes;
1126
+ }
1127
+ hashBackupCodes(codes) {
1128
+ return codes.map((code) => hash2.encode(code));
1129
+ }
1130
+ verifyBackupCode(hashedCodes, inputCode) {
1131
+ for (let i = 0; i < hashedCodes.length; i++) {
1132
+ if (hash2.verify(hashedCodes[i], inputCode.toUpperCase())) {
1133
+ return { isValid: true, index: i };
1134
+ }
1135
+ }
1136
+ return { isValid: false, index: -1 };
1137
+ }
1138
+ maskEmail(email) {
1139
+ const [username, domain] = email.split("@");
1140
+ if (username.length <= 2) {
1141
+ return `${username[0]}***@${domain}`;
1142
+ }
1143
+ return `${username[0]}${"*".repeat(username.length - 2)}${username[username.length - 1]}@${domain}`;
1144
+ }
1145
+ };
1146
+
1147
+ // src/two-factor/otp-provider.ts
1148
+ import ms2 from "@prsm/ms";
1149
+ import { hash as hash3 } from "@prsm/hash";
1150
+ var OtpProvider = class {
1151
+ constructor(config) {
1152
+ this.config = config;
1153
+ this.queries = new AuthQueries(config);
1154
+ }
1155
+ generateOTP() {
1156
+ const length = this.config.twoFactor?.codeLength || 6;
1157
+ let otp = "";
1158
+ for (let i = 0; i < length; i++) {
1159
+ otp += Math.floor(Math.random() * 10).toString();
1160
+ }
1161
+ return otp;
1162
+ }
1163
+ generateSelector() {
1164
+ const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
1165
+ let selector = "";
1166
+ for (let i = 0; i < 32; i++) {
1167
+ selector += chars.charAt(Math.floor(Math.random() * chars.length));
1168
+ }
1169
+ return selector;
1170
+ }
1171
+ async createAndStoreOTP(accountId, mechanism) {
1172
+ const otp = this.generateOTP();
1173
+ const selector = this.generateSelector();
1174
+ const tokenHash = hash3.encode(otp);
1175
+ const expiryDuration = this.config.twoFactor?.tokenExpiry || "5m";
1176
+ const expiresAt = new Date(Date.now() + ms2(expiryDuration));
1177
+ await this.queries.deleteTwoFactorTokensByAccountAndMechanism(accountId, mechanism);
1178
+ await this.queries.createTwoFactorToken({
1179
+ accountId,
1180
+ mechanism,
1181
+ selector,
1182
+ tokenHash,
1183
+ expiresAt
1184
+ });
1185
+ return { otp, selector };
1186
+ }
1187
+ async verifyOTP(selector, inputCode) {
1188
+ const token = await this.queries.findTwoFactorTokenBySelector(selector);
1189
+ if (!token) {
1190
+ return { isValid: false };
1191
+ }
1192
+ if (token.expires_at <= /* @__PURE__ */ new Date()) {
1193
+ await this.queries.deleteTwoFactorToken(token.id);
1194
+ return { isValid: false };
1195
+ }
1196
+ const isValid = hash3.verify(token.token_hash, inputCode);
1197
+ if (isValid) {
1198
+ await this.queries.deleteTwoFactorToken(token.id);
1199
+ return { isValid: true, token };
1200
+ }
1201
+ return { isValid: false };
1202
+ }
1203
+ maskPhone(phone) {
1204
+ if (phone.length < 4) {
1205
+ return phone.replace(/./g, "*");
1206
+ }
1207
+ if (phone.startsWith("+")) {
1208
+ return phone[0] + phone[1] + "*".repeat(phone.length - 3) + phone.slice(-2);
1209
+ }
1210
+ return phone[0] + "*".repeat(phone.length - 3) + phone.slice(-2);
1211
+ }
1212
+ maskEmail(email) {
1213
+ const [username, domain] = email.split("@");
1214
+ if (username.length <= 2) {
1215
+ return `${username[0]}***@${domain}`;
1216
+ }
1217
+ return `${username[0]}${"*".repeat(username.length - 2)}${username[username.length - 1]}@${domain}`;
1218
+ }
1219
+ };
1220
+
1221
+ // src/two-factor/two-factor-manager.ts
1222
+ var TwoFactorManager = class {
1223
+ constructor(req, res, config) {
1224
+ // setup & management
1225
+ this.setup = {
1226
+ totp: async (requireVerification = false) => {
1227
+ const accountId = this.getAccountId();
1228
+ const email = this.getEmail();
1229
+ if (!accountId || !email) {
1230
+ throw new UserNotLoggedInError();
1231
+ }
1232
+ const existingMethod = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, 1 /* TOTP */);
1233
+ if (existingMethod?.verified) {
1234
+ throw new TwoFactorAlreadyEnabledError();
1235
+ }
1236
+ const secret = this.totpProvider.generateSecret();
1237
+ const qrCode = this.totpProvider.generateQRCode(email, secret);
1238
+ let backupCodes;
1239
+ if (!requireVerification) {
1240
+ const backupCodesCount = this.config.twoFactor?.backupCodesCount || 10;
1241
+ backupCodes = this.totpProvider.generateBackupCodes(backupCodesCount);
1242
+ }
1243
+ const hashedBackupCodes = backupCodes ? this.totpProvider.hashBackupCodes(backupCodes) : void 0;
1244
+ const verified = !requireVerification;
1245
+ if (existingMethod) {
1246
+ await this.queries.updateTwoFactorMethod(existingMethod.id, {
1247
+ secret,
1248
+ backup_codes: hashedBackupCodes || null,
1249
+ verified
1250
+ });
1251
+ } else {
1252
+ await this.queries.createTwoFactorMethod({
1253
+ accountId,
1254
+ mechanism: 1 /* TOTP */,
1255
+ secret,
1256
+ backupCodes: hashedBackupCodes,
1257
+ verified
1258
+ });
1259
+ }
1260
+ if (verified) {
1261
+ await this.activityLogger.logActivity(accountId, AuthActivityAction.TwoFactorSetup, this.req, true, { mechanism: "totp" });
1262
+ }
1263
+ return { secret, qrCode, backupCodes };
1264
+ },
1265
+ email: async (email, requireVerification = false) => {
1266
+ const accountId = this.getAccountId();
1267
+ const userEmail = email || this.getEmail();
1268
+ if (!accountId || !userEmail) {
1269
+ throw new UserNotLoggedInError();
1270
+ }
1271
+ const existingMethod = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, 2 /* EMAIL */);
1272
+ if (existingMethod?.verified) {
1273
+ throw new TwoFactorAlreadyEnabledError();
1274
+ }
1275
+ const verified = !requireVerification;
1276
+ if (existingMethod) {
1277
+ await this.queries.updateTwoFactorMethod(existingMethod.id, {
1278
+ secret: userEmail,
1279
+ verified
1280
+ });
1281
+ } else {
1282
+ await this.queries.createTwoFactorMethod({
1283
+ accountId,
1284
+ mechanism: 2 /* EMAIL */,
1285
+ secret: userEmail,
1286
+ verified
1287
+ });
1288
+ }
1289
+ if (verified) {
1290
+ await this.activityLogger.logActivity(accountId, AuthActivityAction.TwoFactorSetup, this.req, true, { mechanism: "email" });
1291
+ }
1292
+ },
1293
+ sms: async (phone, requireVerification = true) => {
1294
+ const accountId = this.getAccountId();
1295
+ if (!accountId) {
1296
+ throw new UserNotLoggedInError();
1297
+ }
1298
+ const existingMethod = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, 3 /* SMS */);
1299
+ if (existingMethod?.verified) {
1300
+ throw new TwoFactorAlreadyEnabledError();
1301
+ }
1302
+ const verified = !requireVerification;
1303
+ if (existingMethod) {
1304
+ await this.queries.updateTwoFactorMethod(existingMethod.id, {
1305
+ secret: phone,
1306
+ verified
1307
+ });
1308
+ } else {
1309
+ await this.queries.createTwoFactorMethod({
1310
+ accountId,
1311
+ mechanism: 3 /* SMS */,
1312
+ secret: phone,
1313
+ verified
1314
+ });
1315
+ }
1316
+ if (verified) {
1317
+ await this.activityLogger.logActivity(accountId, AuthActivityAction.TwoFactorSetup, this.req, true, { mechanism: "sms" });
1318
+ }
1319
+ }
1320
+ };
1321
+ this.complete = {
1322
+ totp: async (code) => {
1323
+ const accountId = this.getAccountId();
1324
+ if (!accountId) {
1325
+ throw new UserNotLoggedInError();
1326
+ }
1327
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, 1 /* TOTP */);
1328
+ if (!method || !method.secret) {
1329
+ throw new TwoFactorNotSetupError();
1330
+ }
1331
+ if (method.verified) {
1332
+ throw new TwoFactorAlreadyEnabledError();
1333
+ }
1334
+ const isValid = this.totpProvider.verify(method.secret, code);
1335
+ if (!isValid) {
1336
+ await this.activityLogger.logActivity(accountId, AuthActivityAction.TwoFactorFailed, this.req, false, { mechanism: "totp", reason: "invalid_code" });
1337
+ throw new InvalidTwoFactorCodeError();
1338
+ }
1339
+ const backupCodesCount = this.config.twoFactor?.backupCodesCount || 10;
1340
+ const backupCodes = this.totpProvider.generateBackupCodes(backupCodesCount);
1341
+ const hashedBackupCodes = this.totpProvider.hashBackupCodes(backupCodes);
1342
+ await this.queries.updateTwoFactorMethod(method.id, {
1343
+ verified: true,
1344
+ backup_codes: hashedBackupCodes,
1345
+ last_used_at: /* @__PURE__ */ new Date()
1346
+ });
1347
+ await this.activityLogger.logActivity(accountId, AuthActivityAction.TwoFactorSetup, this.req, true, { mechanism: "totp" });
1348
+ return backupCodes;
1349
+ },
1350
+ email: async (code) => {
1351
+ await this.completeOtpSetup(2 /* EMAIL */, code);
1352
+ },
1353
+ sms: async (code) => {
1354
+ await this.completeOtpSetup(3 /* SMS */, code);
1355
+ }
1356
+ };
1357
+ // verification during login flow
1358
+ this.verify = {
1359
+ totp: async (code) => {
1360
+ const twoFactorState = this.req.session?.auth?.awaitingTwoFactor;
1361
+ if (!twoFactorState) {
1362
+ throw new UserNotLoggedInError();
1363
+ }
1364
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(twoFactorState.accountId, 1 /* TOTP */);
1365
+ if (!method || !method.verified || !method.secret) {
1366
+ throw new TwoFactorNotSetupError();
1367
+ }
1368
+ const isValid = this.totpProvider.verify(method.secret, code);
1369
+ if (!isValid) {
1370
+ await this.activityLogger.logActivity(twoFactorState.accountId, AuthActivityAction.TwoFactorFailed, this.req, false, { mechanism: "totp", reason: "invalid_code" });
1371
+ throw new InvalidTwoFactorCodeError();
1372
+ }
1373
+ await this.queries.updateTwoFactorMethod(method.id, {
1374
+ last_used_at: /* @__PURE__ */ new Date()
1375
+ });
1376
+ await this.activityLogger.logActivity(twoFactorState.accountId, AuthActivityAction.TwoFactorVerified, this.req, true, { mechanism: "totp" });
1377
+ },
1378
+ email: async (code) => {
1379
+ await this.verifyOtp(2 /* EMAIL */, code);
1380
+ },
1381
+ sms: async (code) => {
1382
+ await this.verifyOtp(3 /* SMS */, code);
1383
+ },
1384
+ backupCode: async (code) => {
1385
+ const twoFactorState = this.req.session?.auth?.awaitingTwoFactor;
1386
+ if (!twoFactorState) {
1387
+ throw new UserNotLoggedInError();
1388
+ }
1389
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(twoFactorState.accountId, 1 /* TOTP */);
1390
+ if (!method || !method.verified || !method.backup_codes) {
1391
+ throw new TwoFactorNotSetupError();
1392
+ }
1393
+ const { isValid, index } = this.totpProvider.verifyBackupCode(method.backup_codes, code);
1394
+ if (!isValid) {
1395
+ await this.activityLogger.logActivity(twoFactorState.accountId, AuthActivityAction.TwoFactorFailed, this.req, false, { mechanism: "backup_code", reason: "invalid_code" });
1396
+ throw new InvalidBackupCodeError();
1397
+ }
1398
+ const updatedBackupCodes = [...method.backup_codes];
1399
+ updatedBackupCodes.splice(index, 1);
1400
+ await this.queries.updateTwoFactorMethod(method.id, {
1401
+ backup_codes: updatedBackupCodes,
1402
+ last_used_at: /* @__PURE__ */ new Date()
1403
+ });
1404
+ await this.activityLogger.logActivity(twoFactorState.accountId, AuthActivityAction.BackupCodeUsed, this.req, true, { remaining_codes: updatedBackupCodes.length });
1405
+ },
1406
+ otp: async (code) => {
1407
+ const twoFactorState = this.req.session?.auth?.awaitingTwoFactor;
1408
+ if (!twoFactorState) {
1409
+ throw new UserNotLoggedInError();
1410
+ }
1411
+ const availableMechanisms = twoFactorState.availableMechanisms.filter((m) => m === 2 /* EMAIL */ || m === 3 /* SMS */);
1412
+ if (availableMechanisms.length === 0) {
1413
+ throw new TwoFactorNotSetupError();
1414
+ }
1415
+ for (const mechanism of availableMechanisms) {
1416
+ try {
1417
+ await this.verifyOtp(mechanism, code);
1418
+ return;
1419
+ } catch (error) {
1420
+ continue;
1421
+ }
1422
+ }
1423
+ await this.activityLogger.logActivity(twoFactorState.accountId, AuthActivityAction.TwoFactorFailed, this.req, false, { mechanism: "otp", reason: "invalid_code" });
1424
+ throw new InvalidTwoFactorCodeError();
1425
+ }
1426
+ };
1427
+ this.req = req;
1428
+ this.res = res;
1429
+ this.config = config;
1430
+ this.queries = new AuthQueries(config);
1431
+ this.activityLogger = new ActivityLogger(config);
1432
+ this.totpProvider = new TotpProvider(config);
1433
+ this.otpProvider = new OtpProvider(config);
1434
+ }
1435
+ getAccountId() {
1436
+ return this.req.session?.auth?.accountId || null;
1437
+ }
1438
+ getEmail() {
1439
+ return this.req.session?.auth?.email || null;
1440
+ }
1441
+ // status queries
1442
+ async isEnabled() {
1443
+ const accountId = this.getAccountId();
1444
+ if (!accountId) return false;
1445
+ const methods = await this.queries.findTwoFactorMethodsByAccountId(accountId);
1446
+ return methods.some((method) => method.verified);
1447
+ }
1448
+ async totpEnabled() {
1449
+ const accountId = this.getAccountId();
1450
+ if (!accountId) return false;
1451
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, 1 /* TOTP */);
1452
+ return method?.verified || false;
1453
+ }
1454
+ async emailEnabled() {
1455
+ const accountId = this.getAccountId();
1456
+ if (!accountId) return false;
1457
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, 2 /* EMAIL */);
1458
+ return method?.verified || false;
1459
+ }
1460
+ async smsEnabled() {
1461
+ const accountId = this.getAccountId();
1462
+ if (!accountId) return false;
1463
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, 3 /* SMS */);
1464
+ return method?.verified || false;
1465
+ }
1466
+ async getEnabledMethods() {
1467
+ const accountId = this.getAccountId();
1468
+ if (!accountId) return [];
1469
+ const methods = await this.queries.findTwoFactorMethodsByAccountId(accountId);
1470
+ return methods.filter((method) => method.verified).map((method) => method.mechanism);
1471
+ }
1472
+ async completeOtpSetup(mechanism, code) {
1473
+ const accountId = this.getAccountId();
1474
+ if (!accountId) {
1475
+ throw new UserNotLoggedInError();
1476
+ }
1477
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, mechanism);
1478
+ if (!method) {
1479
+ throw new TwoFactorNotSetupError();
1480
+ }
1481
+ if (method.verified) {
1482
+ throw new TwoFactorAlreadyEnabledError();
1483
+ }
1484
+ await this.queries.updateTwoFactorMethod(method.id, {
1485
+ verified: true,
1486
+ last_used_at: /* @__PURE__ */ new Date()
1487
+ });
1488
+ await this.activityLogger.logActivity(accountId, AuthActivityAction.TwoFactorSetup, this.req, true, { mechanism: mechanism === 2 /* EMAIL */ ? "email" : "sms" });
1489
+ }
1490
+ async verifyOtp(mechanism, code) {
1491
+ const twoFactorState = this.req.session?.auth?.awaitingTwoFactor;
1492
+ if (!twoFactorState) {
1493
+ throw new UserNotLoggedInError();
1494
+ }
1495
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(twoFactorState.accountId, mechanism);
1496
+ if (!method || !method.verified) {
1497
+ throw new TwoFactorNotSetupError();
1498
+ }
1499
+ const selector = mechanism === 2 /* EMAIL */ ? this.req.session?.auth?.awaitingTwoFactor?.selectors?.email : this.req.session?.auth?.awaitingTwoFactor?.selectors?.sms;
1500
+ if (!selector) {
1501
+ throw new InvalidTwoFactorCodeError();
1502
+ }
1503
+ const { isValid } = await this.otpProvider.verifyOTP(selector, code);
1504
+ if (!isValid) {
1505
+ await this.activityLogger.logActivity(twoFactorState.accountId, AuthActivityAction.TwoFactorFailed, this.req, false, {
1506
+ mechanism: mechanism === 2 /* EMAIL */ ? "email" : "sms",
1507
+ reason: "invalid_code"
1508
+ });
1509
+ throw new InvalidTwoFactorCodeError();
1510
+ }
1511
+ await this.queries.updateTwoFactorMethod(method.id, {
1512
+ last_used_at: /* @__PURE__ */ new Date()
1513
+ });
1514
+ await this.activityLogger.logActivity(twoFactorState.accountId, AuthActivityAction.TwoFactorVerified, this.req, true, { mechanism: mechanism === 2 /* EMAIL */ ? "email" : "sms" });
1515
+ }
1516
+ // management
1517
+ async disable(mechanism) {
1518
+ const accountId = this.getAccountId();
1519
+ if (!accountId) {
1520
+ throw new UserNotLoggedInError();
1521
+ }
1522
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, mechanism);
1523
+ if (!method) {
1524
+ throw new TwoFactorNotSetupError();
1525
+ }
1526
+ await this.queries.deleteTwoFactorMethod(method.id);
1527
+ await this.activityLogger.logActivity(accountId, AuthActivityAction.TwoFactorDisabled, this.req, true, {
1528
+ mechanism: mechanism === 1 /* TOTP */ ? "totp" : mechanism === 2 /* EMAIL */ ? "email" : "sms"
1529
+ });
1530
+ }
1531
+ async generateNewBackupCodes() {
1532
+ const accountId = this.getAccountId();
1533
+ if (!accountId) {
1534
+ throw new UserNotLoggedInError();
1535
+ }
1536
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, 1 /* TOTP */);
1537
+ if (!method || !method.verified) {
1538
+ throw new TwoFactorNotSetupError();
1539
+ }
1540
+ const backupCodesCount = this.config.twoFactor?.backupCodesCount || 10;
1541
+ const backupCodes = this.totpProvider.generateBackupCodes(backupCodesCount);
1542
+ const hashedBackupCodes = this.totpProvider.hashBackupCodes(backupCodes);
1543
+ await this.queries.updateTwoFactorMethod(method.id, {
1544
+ backup_codes: hashedBackupCodes
1545
+ });
1546
+ return backupCodes;
1547
+ }
1548
+ async getContact(mechanism) {
1549
+ const accountId = this.getAccountId();
1550
+ if (!accountId) {
1551
+ return null;
1552
+ }
1553
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, mechanism);
1554
+ return method?.secret || null;
1555
+ }
1556
+ // challenge creation (used during login)
1557
+ async createChallenge(accountId) {
1558
+ const methods = await this.queries.findTwoFactorMethodsByAccountId(accountId);
1559
+ const verifiedMethods = methods.filter((method) => method.verified);
1560
+ const challenge = {
1561
+ selectors: {}
1562
+ };
1563
+ for (const method of verifiedMethods) {
1564
+ switch (method.mechanism) {
1565
+ case 1 /* TOTP */:
1566
+ challenge.totp = true;
1567
+ break;
1568
+ case 2 /* EMAIL */:
1569
+ if (method.secret) {
1570
+ const { otp, selector } = await this.otpProvider.createAndStoreOTP(accountId, method.mechanism);
1571
+ challenge.email = {
1572
+ otpValue: otp,
1573
+ maskedContact: this.otpProvider.maskEmail(method.secret)
1574
+ };
1575
+ challenge.selectors.email = selector;
1576
+ }
1577
+ break;
1578
+ case 3 /* SMS */:
1579
+ if (method.secret) {
1580
+ const { otp, selector } = await this.otpProvider.createAndStoreOTP(accountId, method.mechanism);
1581
+ challenge.sms = {
1582
+ otpValue: otp,
1583
+ maskedContact: this.otpProvider.maskPhone(method.secret)
1584
+ };
1585
+ challenge.selectors.sms = selector;
1586
+ }
1587
+ break;
1588
+ }
1589
+ }
1590
+ return challenge;
1591
+ }
1592
+ };
1593
+
1594
+ // src/auth-manager.ts
1595
+ var AuthManager = class {
1596
+ constructor(req, res, config) {
1597
+ this.req = req;
1598
+ this.res = res;
1599
+ this.config = config;
1600
+ this.queries = new AuthQueries(config);
1601
+ this.activityLogger = new ActivityLogger(config);
1602
+ this.providers = this.initializeProviders();
1603
+ this.twoFactor = new TwoFactorManager(req, res, config);
1604
+ }
1605
+ initializeProviders() {
1606
+ const providers = {};
1607
+ if (this.config.providers?.github) {
1608
+ providers.github = new GitHubProvider(this.config.providers.github, this.config, this);
1609
+ }
1610
+ if (this.config.providers?.google) {
1611
+ providers.google = new GoogleProvider(this.config.providers.google, this.config, this);
1612
+ }
1613
+ if (this.config.providers?.azure) {
1614
+ providers.azure = new AzureProvider(this.config.providers.azure, this.config, this);
1615
+ }
1616
+ return providers;
1617
+ }
1618
+ generateAutoUserId() {
1619
+ return crypto.randomUUID();
1620
+ }
1621
+ async shouldRequire2FA(account) {
1622
+ const providers = await this.queries.findProvidersByAccountId(account.id);
1623
+ const hasOAuthProviders = providers.length > 0;
1624
+ if (hasOAuthProviders && !this.config.twoFactor?.requireForOAuth) {
1625
+ return false;
1626
+ }
1627
+ return true;
1628
+ }
1629
+ validatePassword(password) {
1630
+ const minLength = this.config.minPasswordLength || 8;
1631
+ const maxLength = this.config.maxPasswordLength || 64;
1632
+ if (typeof password !== "string") {
1633
+ throw new InvalidPasswordError();
1634
+ }
1635
+ if (password.length < minLength) {
1636
+ throw new InvalidPasswordError();
1637
+ }
1638
+ if (password.length > maxLength) {
1639
+ throw new InvalidPasswordError();
1640
+ }
1641
+ }
1642
+ getRoleMap() {
1643
+ return createMapFromEnum(AuthRole);
1644
+ }
1645
+ getStatusMap() {
1646
+ return createMapFromEnum(AuthStatus);
1647
+ }
1648
+ async getAuthAccount() {
1649
+ if (!this.req.session?.auth?.accountId) {
1650
+ return null;
1651
+ }
1652
+ return await this.queries.findAccountById(this.req.session.auth.accountId);
1653
+ }
1654
+ setRememberCookie(token, expires) {
1655
+ const cookieName = this.config.rememberCookieName || "remember_token";
1656
+ if (token === null) {
1657
+ this.res.clearCookie(cookieName);
1658
+ } else {
1659
+ this.res.cookie(cookieName, token, {
1660
+ expires,
1661
+ httpOnly: true,
1662
+ secure: this.req.secure
1663
+ });
1664
+ }
1665
+ }
1666
+ getRememberToken() {
1667
+ const { cookies } = this.req;
1668
+ if (!cookies) {
1669
+ return { token: null };
1670
+ }
1671
+ const cookieName = this.config.rememberCookieName || "remember_token";
1672
+ const token = cookies[cookieName];
1673
+ return { token: token || null };
1674
+ }
1675
+ async regenerateSession() {
1676
+ const { auth } = this.req.session;
1677
+ return new Promise((resolve, reject) => {
1678
+ this.req.session.regenerate((err) => {
1679
+ if (err) {
1680
+ reject(err);
1681
+ return;
1682
+ }
1683
+ this.req.session.auth = auth;
1684
+ resolve();
1685
+ });
1686
+ });
1687
+ }
1688
+ async resyncSession(force = false) {
1689
+ if (!this.isLoggedIn()) {
1690
+ return;
1691
+ }
1692
+ if (this.req.session.auth.shouldForceLogout) {
1693
+ await this.logout();
1694
+ return;
1695
+ }
1696
+ const interval = ms3(this.config.resyncInterval || "30s");
1697
+ const lastResync = new Date(this.req.session.auth.lastResync);
1698
+ if (!force && lastResync && lastResync.getTime() > Date.now() - interval) {
1699
+ return;
1700
+ }
1701
+ const account = await this.getAuthAccount();
1702
+ if (!account) {
1703
+ await this.logout();
1704
+ return;
1705
+ }
1706
+ if (account.force_logout > this.req.session.auth.forceLogout) {
1707
+ await this.logout();
1708
+ return;
1709
+ }
1710
+ this.req.session.auth.shouldForceLogout = false;
1711
+ this.req.session.auth.email = account.email;
1712
+ this.req.session.auth.status = account.status;
1713
+ this.req.session.auth.rolemask = account.rolemask;
1714
+ this.req.session.auth.verified = account.verified;
1715
+ this.req.session.auth.lastResync = /* @__PURE__ */ new Date();
1716
+ }
1717
+ async processRememberDirective() {
1718
+ if (this.isLoggedIn()) {
1719
+ return;
1720
+ }
1721
+ const { token } = this.getRememberToken();
1722
+ if (!token) {
1723
+ return;
1724
+ }
1725
+ const remember = await this.queries.findRememberToken(token);
1726
+ if (!remember) {
1727
+ this.setRememberCookie(null, /* @__PURE__ */ new Date(0));
1728
+ return;
1729
+ }
1730
+ if (/* @__PURE__ */ new Date() > remember.expires) {
1731
+ await this.queries.deleteRememberToken(token);
1732
+ this.setRememberCookie(null, /* @__PURE__ */ new Date(0));
1733
+ return;
1734
+ }
1735
+ await this.queries.deleteExpiredRememberTokensForAccount(remember.account_id);
1736
+ const account = await this.queries.findAccountById(remember.account_id);
1737
+ if (!account) {
1738
+ await this.queries.deleteRememberToken(token);
1739
+ this.setRememberCookie(null, /* @__PURE__ */ new Date(0));
1740
+ return;
1741
+ }
1742
+ await this.onLoginSuccessful(account, true);
1743
+ }
1744
+ async onLoginSuccessful(account, remember = false) {
1745
+ await this.queries.updateAccountLastLogin(account.id);
1746
+ return new Promise((resolve, reject) => {
1747
+ if (!this.req.session?.regenerate) {
1748
+ resolve();
1749
+ return;
1750
+ }
1751
+ this.req.session.regenerate(async (err) => {
1752
+ if (err) {
1753
+ reject(err);
1754
+ return;
1755
+ }
1756
+ const session = {
1757
+ loggedIn: true,
1758
+ accountId: account.id,
1759
+ userId: account.user_id,
1760
+ email: account.email,
1761
+ status: account.status,
1762
+ rolemask: account.rolemask,
1763
+ remembered: remember,
1764
+ lastResync: /* @__PURE__ */ new Date(),
1765
+ lastRememberCheck: /* @__PURE__ */ new Date(),
1766
+ forceLogout: account.force_logout,
1767
+ verified: account.verified,
1768
+ shouldForceLogout: false
1769
+ };
1770
+ this.req.session.auth = session;
1771
+ if (remember) {
1772
+ await this.createRememberDirective(account);
1773
+ }
1774
+ this.req.session.save((err2) => {
1775
+ if (err2) {
1776
+ reject(err2);
1777
+ return;
1778
+ }
1779
+ resolve();
1780
+ });
1781
+ });
1782
+ });
1783
+ }
1784
+ async createRememberDirective(account) {
1785
+ const token = hash4.encode(account.email);
1786
+ const duration = this.config.rememberDuration || "30d";
1787
+ const expires = new Date(Date.now() + ms3(duration));
1788
+ await this.queries.createRememberToken({
1789
+ accountId: account.id,
1790
+ token,
1791
+ expires
1792
+ });
1793
+ this.setRememberCookie(token, expires);
1794
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.RememberTokenCreated, this.req, true, { email: account.email, duration });
1795
+ return token;
1796
+ }
1797
+ /**
1798
+ * Check if the current user is logged in.
1799
+ * @returns true if user has an active authenticated session
1800
+ */
1801
+ isLoggedIn() {
1802
+ return this.req.session?.auth?.loggedIn ?? false;
1803
+ }
1804
+ /**
1805
+ * Authenticate user with email and password.
1806
+ * Creates a new session and optionally sets a remember token for persistent login.
1807
+ *
1808
+ * @param email - User's email address
1809
+ * @param password - Plain text password
1810
+ * @param remember - If true, sets a persistent cookie for auto-login on future visits
1811
+ * @throws {UserNotFoundError} Account with this email doesn't exist
1812
+ * @throws {InvalidPasswordError} Password is incorrect
1813
+ * @throws {EmailNotVerifiedError} Account exists but email is not verified
1814
+ * @throws {UserInactiveError} Account is banned, locked, or otherwise inactive
1815
+ */
1816
+ async login(email, password, remember = false) {
1817
+ try {
1818
+ const account = await this.queries.findAccountByEmail(email);
1819
+ if (!account) {
1820
+ await this.activityLogger.logActivity(null, AuthActivityAction.FailedLogin, this.req, false, { email, reason: "account_not_found" });
1821
+ throw new UserNotFoundError();
1822
+ }
1823
+ if (!account.password || !hash4.verify(account.password, password)) {
1824
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.FailedLogin, this.req, false, { email, reason: "invalid_password" });
1825
+ throw new InvalidPasswordError();
1826
+ }
1827
+ if (!account.verified) {
1828
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.FailedLogin, this.req, false, { email, reason: "email_not_verified" });
1829
+ throw new EmailNotVerifiedError();
1830
+ }
1831
+ if (account.status !== AuthStatus.Normal) {
1832
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.FailedLogin, this.req, false, { email, reason: "account_inactive", status: account.status });
1833
+ throw new UserInactiveError();
1834
+ }
1835
+ if (this.config.twoFactor?.enabled && await this.shouldRequire2FA(account)) {
1836
+ const twoFactorMethods = await this.queries.findTwoFactorMethodsByAccountId(account.id);
1837
+ const enabledMethods = twoFactorMethods.filter((method) => method.verified);
1838
+ if (enabledMethods.length > 0) {
1839
+ const challenge = await this.twoFactor.createChallenge(account.id);
1840
+ const expiryDuration = this.config.twoFactor?.tokenExpiry || "5m";
1841
+ const expiresAt = new Date(Date.now() + ms3(expiryDuration));
1842
+ this.req.session.auth = {
1843
+ loggedIn: false,
1844
+ accountId: 0,
1845
+ userId: "",
1846
+ email: "",
1847
+ status: 0,
1848
+ rolemask: 0,
1849
+ remembered: false,
1850
+ lastResync: /* @__PURE__ */ new Date(),
1851
+ lastRememberCheck: /* @__PURE__ */ new Date(),
1852
+ forceLogout: 0,
1853
+ verified: false,
1854
+ awaitingTwoFactor: {
1855
+ accountId: account.id,
1856
+ expiresAt,
1857
+ remember,
1858
+ availableMechanisms: enabledMethods.map((m) => m.mechanism),
1859
+ attemptedMechanisms: [],
1860
+ originalEmail: account.email,
1861
+ selectors: challenge.selectors
1862
+ }
1863
+ };
1864
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.TwoFactorFailed, this.req, true, { prompt: true, mechanisms: enabledMethods.map((m) => m.mechanism) });
1865
+ throw new SecondFactorRequiredError(challenge);
1866
+ }
1867
+ }
1868
+ await this.onLoginSuccessful(account, remember);
1869
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.Login, this.req, true, { email, remember });
1870
+ } catch (error) {
1871
+ throw error;
1872
+ }
1873
+ }
1874
+ /**
1875
+ * Complete two-factor authentication and log in the user.
1876
+ * This should be called after receiving a SecondFactorRequiredError.
1877
+ */
1878
+ async completeTwoFactorLogin() {
1879
+ const twoFactorState = this.req.session?.auth?.awaitingTwoFactor;
1880
+ if (!twoFactorState) {
1881
+ throw new TwoFactorExpiredError();
1882
+ }
1883
+ if (twoFactorState.expiresAt <= /* @__PURE__ */ new Date()) {
1884
+ delete this.req.session.auth.awaitingTwoFactor;
1885
+ throw new TwoFactorExpiredError();
1886
+ }
1887
+ const account = await this.queries.findAccountById(twoFactorState.accountId);
1888
+ if (!account) {
1889
+ delete this.req.session.auth.awaitingTwoFactor;
1890
+ throw new UserNotFoundError();
1891
+ }
1892
+ await this.onLoginSuccessful(account, twoFactorState.remember);
1893
+ delete this.req.session.auth.awaitingTwoFactor;
1894
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.Login, this.req, true, { email: account.email, remember: twoFactorState.remember, twoFactorCompleted: true });
1895
+ }
1896
+ /**
1897
+ * Log out the current user.
1898
+ * Clears the session and removes any remember tokens.
1899
+ */
1900
+ async logout() {
1901
+ if (!this.isLoggedIn()) {
1902
+ return;
1903
+ }
1904
+ const accountId = this.getId();
1905
+ const email = this.getEmail();
1906
+ const { token } = this.getRememberToken();
1907
+ if (token) {
1908
+ await this.queries.deleteRememberToken(token);
1909
+ this.setRememberCookie(null, /* @__PURE__ */ new Date(0));
1910
+ }
1911
+ this.req.session.auth = void 0;
1912
+ if (accountId && email) {
1913
+ await this.activityLogger.logActivity(accountId, AuthActivityAction.Logout, this.req, true, { email });
1914
+ }
1915
+ }
1916
+ /**
1917
+ * Register a new account.
1918
+ *
1919
+ * @param email - Email address for the new account
1920
+ * @param password - Plain text password (will be hashed)
1921
+ * @param userId - Optional user ID to link this auth account to. If not provided, a UUID will be generated automatically.
1922
+ * @param callback - If provided, account is created unverified and callback receives confirmation token. Create a URL like /confirm/{token} and call confirmEmail() in that handler. If omitted, account is immediately verified.
1923
+ * @returns The created account record
1924
+ * @throws {EmailTakenError} Email is already registered
1925
+ * @throws {InvalidPasswordError} Password doesn't meet length requirements
1926
+ */
1927
+ async register(email, password, userId, callback) {
1928
+ validateEmail(email);
1929
+ this.validatePassword(password);
1930
+ const existing = await this.queries.findAccountByEmail(email);
1931
+ if (existing) {
1932
+ throw new EmailTakenError();
1933
+ }
1934
+ const finalUserId = userId || this.generateAutoUserId();
1935
+ const hashedPassword = hash4.encode(password);
1936
+ const verified = typeof callback !== "function";
1937
+ const account = await this.queries.createAccount({
1938
+ userId: finalUserId,
1939
+ email,
1940
+ password: hashedPassword,
1941
+ verified,
1942
+ status: AuthStatus.Normal,
1943
+ rolemask: 0
1944
+ });
1945
+ if (!verified && callback) {
1946
+ await this.createConfirmationToken(account, email, callback);
1947
+ }
1948
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.Register, this.req, true, { email, verified, userId: finalUserId });
1949
+ return account;
1950
+ }
1951
+ async createConfirmationToken(account, email, callback) {
1952
+ const token = hash4.encode(email);
1953
+ const expires = new Date(Date.now() + 1e3 * 60 * 60 * 24 * 7);
1954
+ await this.queries.createConfirmation({
1955
+ accountId: account.id,
1956
+ token,
1957
+ email,
1958
+ expires
1959
+ });
1960
+ if (callback) {
1961
+ callback(token);
1962
+ }
1963
+ }
1964
+ /**
1965
+ * Get the current user's account ID.
1966
+ * @returns Account ID if logged in, null otherwise
1967
+ */
1968
+ getId() {
1969
+ return this.req.session?.auth?.accountId || null;
1970
+ }
1971
+ /**
1972
+ * Get the current user's email address.
1973
+ * @returns Email if logged in, null otherwise
1974
+ */
1975
+ getEmail() {
1976
+ return this.req.session?.auth?.email || null;
1977
+ }
1978
+ /**
1979
+ * Get the current user's account status.
1980
+ * @returns Status number (0=Normal, 1=Archived, 2=Banned, etc.) if logged in, null otherwise
1981
+ */
1982
+ getStatus() {
1983
+ return this.req.session?.auth?.status ?? null;
1984
+ }
1985
+ /**
1986
+ * Check if the current user's email is verified.
1987
+ * @returns true if verified, false if unverified, null if not logged in
1988
+ */
1989
+ getVerified() {
1990
+ return this.req.session?.auth?.verified ?? null;
1991
+ }
1992
+ /**
1993
+ * Get human-readable role names for the current user or a specific rolemask.
1994
+ * @param rolemask - Optional specific rolemask to check. If omitted, uses current user's roles
1995
+ * @returns Array of role names (e.g., ['Admin', 'Editor'])
1996
+ */
1997
+ getRoleNames(rolemask) {
1998
+ const mask = rolemask !== void 0 ? rolemask : this.req.session?.auth?.rolemask ?? 0;
1999
+ if (!mask && mask !== 0) {
2000
+ return [];
2001
+ }
2002
+ return Object.entries(this.getRoleMap()).filter(([key]) => mask & parseInt(key)).map(([, value]) => value);
2003
+ }
2004
+ /**
2005
+ * Get human-readable status name for the current user.
2006
+ * @returns Status name (e.g., 'Normal', 'Banned', 'Locked') if logged in, null otherwise
2007
+ */
2008
+ getStatusName() {
2009
+ const status = this.getStatus();
2010
+ if (status === null) return null;
2011
+ return this.getStatusMap()[status] || null;
2012
+ }
2013
+ /**
2014
+ * Check if the current user has a specific role.
2015
+ * @param role - Role bitmask to check (e.g., AuthRole.Admin)
2016
+ * @returns true if user has the role, false otherwise
2017
+ */
2018
+ async hasRole(role) {
2019
+ if (this.req.session?.auth) {
2020
+ return (this.req.session.auth.rolemask & role) === role;
2021
+ }
2022
+ const account = await this.getAuthAccount();
2023
+ return account ? (account.rolemask & role) === role : false;
2024
+ }
2025
+ /**
2026
+ * Check if the current user has admin privileges.
2027
+ * @returns true if user has Admin role, false otherwise
2028
+ */
2029
+ async isAdmin() {
2030
+ return this.hasRole(AuthRole.Admin);
2031
+ }
2032
+ /**
2033
+ * Check if the current user was automatically logged in via remember token.
2034
+ * @returns true if auto-logged in from persistent cookie, false if manual login or not logged in
2035
+ */
2036
+ isRemembered() {
2037
+ return this.req.session?.auth?.remembered ?? false;
2038
+ }
2039
+ /**
2040
+ * Request an email change for the current user.
2041
+ * Sends a confirmation token to verify the new email before changing it.
2042
+ *
2043
+ * @param newEmail - New email address
2044
+ * @param callback - Called with confirmation token. Create a URL like /confirm/{token} and call confirmEmail() in that handler
2045
+ * @throws {UserNotLoggedInError} User is not logged in
2046
+ * @throws {EmailTakenError} New email is already registered
2047
+ * @throws {UserNotFoundError} Current user account not found
2048
+ * @throws {EmailNotVerifiedError} Current account's email is not verified
2049
+ */
2050
+ async changeEmail(newEmail, callback) {
2051
+ if (!this.isLoggedIn()) {
2052
+ throw new UserNotLoggedInError();
2053
+ }
2054
+ validateEmail(newEmail);
2055
+ const existing = await this.queries.findAccountByEmail(newEmail);
2056
+ if (existing) {
2057
+ throw new EmailTakenError();
2058
+ }
2059
+ const account = await this.getAuthAccount();
2060
+ if (!account) {
2061
+ throw new UserNotFoundError();
2062
+ }
2063
+ if (!account.verified) {
2064
+ throw new EmailNotVerifiedError();
2065
+ }
2066
+ await this.createConfirmationToken(account, newEmail, callback);
2067
+ }
2068
+ /**
2069
+ * Confirm an email address using a token from registration or email change.
2070
+ * Updates the account to verified status and changes email if this was from changeEmail.
2071
+ *
2072
+ * @param token - Confirmation token from registration or email change
2073
+ * @returns The confirmed email address
2074
+ * @throws {ConfirmationNotFoundError} Token is invalid or doesn't exist
2075
+ * @throws {ConfirmationExpiredError} Token has expired
2076
+ * @throws {InvalidTokenError} Token format is invalid
2077
+ */
2078
+ async confirmEmail(token) {
2079
+ const confirmation = await this.queries.findConfirmation(token);
2080
+ if (!confirmation) {
2081
+ throw new ConfirmationNotFoundError();
2082
+ }
2083
+ if (new Date(confirmation.expires) < /* @__PURE__ */ new Date()) {
2084
+ throw new ConfirmationExpiredError();
2085
+ }
2086
+ if (!hash4.verify(token, confirmation.email)) {
2087
+ throw new InvalidTokenError();
2088
+ }
2089
+ await this.queries.updateAccount(confirmation.account_id, {
2090
+ verified: true,
2091
+ email: confirmation.email
2092
+ });
2093
+ if (this.isLoggedIn() && this.req.session?.auth?.accountId === confirmation.account_id) {
2094
+ this.req.session.auth.verified = true;
2095
+ this.req.session.auth.email = confirmation.email;
2096
+ }
2097
+ await this.queries.deleteConfirmation(token);
2098
+ await this.activityLogger.logActivity(confirmation.account_id, AuthActivityAction.EmailConfirmed, this.req, true, { email: confirmation.email });
2099
+ return confirmation.email;
2100
+ }
2101
+ /**
2102
+ * Confirm email and automatically log in the user.
2103
+ * Useful for "click to verify and login" flows.
2104
+ *
2105
+ * @param token - Confirmation token from registration
2106
+ * @param remember - Whether to set persistent login cookie
2107
+ * @throws {ConfirmationNotFoundError} Token is invalid or doesn't exist
2108
+ * @throws {ConfirmationExpiredError} Token has expired
2109
+ * @throws {InvalidTokenError} Token format is invalid
2110
+ * @throws {UserNotFoundError} Associated account no longer exists
2111
+ */
2112
+ async confirmEmailAndLogin(token, remember = false) {
2113
+ const email = await this.confirmEmail(token);
2114
+ if (this.isLoggedIn()) {
2115
+ return;
2116
+ }
2117
+ const account = await this.queries.findAccountByEmail(email);
2118
+ if (!account) {
2119
+ throw new UserNotFoundError();
2120
+ }
2121
+ if (this.config.twoFactor?.enabled && await this.shouldRequire2FA(account)) {
2122
+ const twoFactorMethods = await this.queries.findTwoFactorMethodsByAccountId(account.id);
2123
+ const enabledMethods = twoFactorMethods.filter((method) => method.verified);
2124
+ if (enabledMethods.length > 0) {
2125
+ const challenge = await this.twoFactor.createChallenge(account.id);
2126
+ const expiryDuration = this.config.twoFactor?.tokenExpiry || "5m";
2127
+ const expiresAt = new Date(Date.now() + ms3(expiryDuration));
2128
+ this.req.session.auth = {
2129
+ loggedIn: false,
2130
+ accountId: 0,
2131
+ userId: "",
2132
+ email: "",
2133
+ status: 0,
2134
+ rolemask: 0,
2135
+ remembered: false,
2136
+ lastResync: /* @__PURE__ */ new Date(),
2137
+ lastRememberCheck: /* @__PURE__ */ new Date(),
2138
+ forceLogout: 0,
2139
+ verified: false,
2140
+ awaitingTwoFactor: {
2141
+ accountId: account.id,
2142
+ expiresAt,
2143
+ remember,
2144
+ availableMechanisms: enabledMethods.map((m) => m.mechanism),
2145
+ attemptedMechanisms: [],
2146
+ originalEmail: account.email,
2147
+ selectors: challenge.selectors
2148
+ }
2149
+ };
2150
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.TwoFactorFailed, this.req, true, { prompt: true, mechanisms: enabledMethods.map((m) => m.mechanism) });
2151
+ throw new SecondFactorRequiredError(challenge);
2152
+ }
2153
+ }
2154
+ await this.onLoginSuccessful(account, remember);
2155
+ }
2156
+ /**
2157
+ * Initiate a password reset for a user.
2158
+ * Creates a reset token and calls the callback to send reset email.
2159
+ *
2160
+ * @param email - Email address of account to reset
2161
+ * @param expiresAfter - Token expiration (default: 6h). Accepts ms format like '1h', '30m'
2162
+ * @param maxOpenRequests - Maximum concurrent reset tokens (default: 2)
2163
+ * @param callback - Called with reset token. Create a URL like /reset/{token} and call confirmResetPassword() in that handler
2164
+ * @throws {EmailNotVerifiedError} Account doesn't exist or email not verified
2165
+ * @throws {ResetDisabledError} Account has password reset disabled
2166
+ * @throws {TooManyResetsError} Too many active reset requests
2167
+ */
2168
+ async resetPassword(email, expiresAfter = null, maxOpenRequests = null, callback) {
2169
+ validateEmail(email);
2170
+ const expiry = !expiresAfter ? ms3("6h") : ms3(expiresAfter);
2171
+ const maxRequests = maxOpenRequests === null ? 2 : Math.max(1, maxOpenRequests);
2172
+ const account = await this.queries.findAccountByEmail(email);
2173
+ if (!account || !account.verified) {
2174
+ throw new EmailNotVerifiedError();
2175
+ }
2176
+ if (!account.resettable) {
2177
+ throw new ResetDisabledError();
2178
+ }
2179
+ const openRequests = await this.queries.countActiveResetTokensForAccount(account.id);
2180
+ if (openRequests >= maxRequests) {
2181
+ throw new TooManyResetsError();
2182
+ }
2183
+ const token = hash4.encode(email);
2184
+ const expires = new Date(Date.now() + expiry);
2185
+ await this.queries.createResetToken({
2186
+ accountId: account.id,
2187
+ token,
2188
+ expires
2189
+ });
2190
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.PasswordResetRequested, this.req, true, { email });
2191
+ if (callback) {
2192
+ callback(token);
2193
+ }
2194
+ }
2195
+ /**
2196
+ * Complete a password reset using a reset token.
2197
+ * Changes the password and optionally logs out all sessions.
2198
+ *
2199
+ * @param token - Reset token from resetPassword callback
2200
+ * @param password - New password (will be hashed)
2201
+ * @param logout - Whether to force logout all sessions (default: true)
2202
+ * @throws {ResetNotFoundError} Token is invalid or doesn't exist
2203
+ * @throws {ResetExpiredError} Token has expired
2204
+ * @throws {UserNotFoundError} Associated account no longer exists
2205
+ * @throws {ResetDisabledError} Account has password reset disabled
2206
+ * @throws {InvalidPasswordError} New password doesn't meet requirements
2207
+ * @throws {InvalidTokenError} Token format is invalid
2208
+ */
2209
+ async confirmResetPassword(token, password, logout = true) {
2210
+ const reset = await this.queries.findResetToken(token);
2211
+ if (!reset) {
2212
+ throw new ResetNotFoundError();
2213
+ }
2214
+ if (new Date(reset.expires) < /* @__PURE__ */ new Date()) {
2215
+ throw new ResetExpiredError();
2216
+ }
2217
+ const account = await this.queries.findAccountById(reset.account_id);
2218
+ if (!account) {
2219
+ throw new UserNotFoundError();
2220
+ }
2221
+ if (!account.resettable) {
2222
+ throw new ResetDisabledError();
2223
+ }
2224
+ this.validatePassword(password);
2225
+ if (!hash4.verify(token, account.email)) {
2226
+ throw new InvalidTokenError();
2227
+ }
2228
+ await this.queries.updateAccount(account.id, {
2229
+ password: hash4.encode(password)
2230
+ });
2231
+ if (logout) {
2232
+ await this.forceLogoutForAccountById(account.id);
2233
+ }
2234
+ await this.queries.deleteResetToken(token);
2235
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.PasswordResetCompleted, this.req, true, { email: account.email });
2236
+ }
2237
+ /**
2238
+ * Verify if a password matches the current user's password.
2239
+ * Useful for "confirm current password" flows before sensitive operations.
2240
+ *
2241
+ * @param password - Password to verify
2242
+ * @returns true if password matches, false otherwise
2243
+ * @throws {UserNotLoggedInError} User is not logged in
2244
+ * @throws {UserNotFoundError} Current user account not found
2245
+ */
2246
+ async verifyPassword(password) {
2247
+ if (!this.isLoggedIn()) {
2248
+ throw new UserNotLoggedInError();
2249
+ }
2250
+ const account = await this.getAuthAccount();
2251
+ if (!account) {
2252
+ throw new UserNotFoundError();
2253
+ }
2254
+ if (!account.password) {
2255
+ return false;
2256
+ }
2257
+ return hash4.verify(account.password, password);
2258
+ }
2259
+ async forceLogoutForAccountById(accountId) {
2260
+ await this.queries.deleteRememberTokensForAccount(accountId);
2261
+ await this.queries.incrementForceLogout(accountId);
2262
+ }
2263
+ /**
2264
+ * Force logout all OTHER sessions while keeping current session active.
2265
+ * Useful for "logout other devices" functionality.
2266
+ */
2267
+ async logoutEverywhereElse() {
2268
+ if (!this.isLoggedIn()) {
2269
+ return;
2270
+ }
2271
+ const accountId = this.getId();
2272
+ if (!accountId) {
2273
+ return;
2274
+ }
2275
+ const account = await this.queries.findAccountById(accountId);
2276
+ if (!account) {
2277
+ await this.logout();
2278
+ return;
2279
+ }
2280
+ await this.forceLogoutForAccountById(accountId);
2281
+ this.req.session.auth.forceLogout += 1;
2282
+ await this.regenerateSession();
2283
+ }
2284
+ /**
2285
+ * Force logout ALL sessions including the current one.
2286
+ * Logs out everywhere else, then logs out current session.
2287
+ */
2288
+ async logoutEverywhere() {
2289
+ if (!this.isLoggedIn()) {
2290
+ return;
2291
+ }
2292
+ await this.logoutEverywhereElse();
2293
+ await this.logout();
2294
+ }
2295
+ };
2296
+
2297
+ // src/middleware.ts
2298
+ function createAuthMiddleware(config) {
2299
+ return async (req, res, next) => {
2300
+ try {
2301
+ const authManager = new AuthManager(req, res, config);
2302
+ const authAdminManager = new AuthAdminManager(req, res, config, authManager);
2303
+ req.auth = authManager;
2304
+ req.authAdmin = authAdminManager;
2305
+ await authManager.resyncSession();
2306
+ await authManager.processRememberDirective();
2307
+ next();
2308
+ } catch (error) {
2309
+ next(error);
2310
+ }
2311
+ };
2312
+ }
2313
+
2314
+ // src/schema.ts
2315
+ async function createAuthTables(config) {
2316
+ const prefix = config.tablePrefix || "user_";
2317
+ const { db } = config;
2318
+ const accountsTable = `${prefix}accounts`;
2319
+ await db.query(`
2320
+ CREATE TABLE IF NOT EXISTS ${accountsTable} (
2321
+ id SERIAL PRIMARY KEY,
2322
+ user_id VARCHAR(255) NOT NULL,
2323
+ email VARCHAR(255) NOT NULL UNIQUE,
2324
+ password VARCHAR(255),
2325
+ verified BOOLEAN DEFAULT FALSE,
2326
+ status INTEGER DEFAULT 0,
2327
+ rolemask INTEGER DEFAULT 0,
2328
+ last_login TIMESTAMPTZ,
2329
+ force_logout INTEGER DEFAULT 0,
2330
+ resettable BOOLEAN DEFAULT TRUE,
2331
+ registered TIMESTAMPTZ DEFAULT NOW(),
2332
+ CONSTRAINT ${prefix}unique_user_id_per_account UNIQUE(user_id)
2333
+ )
2334
+ `);
2335
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}accounts_user_id ON ${accountsTable}(user_id)`);
2336
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}accounts_email ON ${accountsTable}(email)`);
2337
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}accounts_status ON ${accountsTable}(status)`);
2338
+ const confirmationsTable = `${prefix}confirmations`;
2339
+ await db.query(`
2340
+ CREATE TABLE IF NOT EXISTS ${confirmationsTable} (
2341
+ id SERIAL PRIMARY KEY,
2342
+ account_id INTEGER NOT NULL,
2343
+ token VARCHAR(255) NOT NULL,
2344
+ email VARCHAR(255) NOT NULL,
2345
+ expires TIMESTAMPTZ NOT NULL,
2346
+ CONSTRAINT fk_${prefix}confirmations_account
2347
+ FOREIGN KEY (account_id) REFERENCES ${accountsTable}(id) ON DELETE CASCADE
2348
+ )
2349
+ `);
2350
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}confirmations_token ON ${confirmationsTable}(token)`);
2351
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}confirmations_email ON ${confirmationsTable}(email)`);
2352
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}confirmations_account_id ON ${confirmationsTable}(account_id)`);
2353
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}confirmations_expires ON ${confirmationsTable}(expires)`);
2354
+ const remembersTable = `${prefix}remembers`;
2355
+ await db.query(`
2356
+ CREATE TABLE IF NOT EXISTS ${remembersTable} (
2357
+ id SERIAL PRIMARY KEY,
2358
+ account_id INTEGER NOT NULL,
2359
+ token VARCHAR(255) NOT NULL,
2360
+ expires TIMESTAMPTZ NOT NULL,
2361
+ CONSTRAINT fk_${prefix}remembers_account
2362
+ FOREIGN KEY (account_id) REFERENCES ${accountsTable}(id) ON DELETE CASCADE
2363
+ )
2364
+ `);
2365
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}remembers_token ON ${remembersTable}(token)`);
2366
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}remembers_account_id ON ${remembersTable}(account_id)`);
2367
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}remembers_expires ON ${remembersTable}(expires)`);
2368
+ const resetsTable = `${prefix}resets`;
2369
+ await db.query(`
2370
+ CREATE TABLE IF NOT EXISTS ${resetsTable} (
2371
+ id SERIAL PRIMARY KEY,
2372
+ account_id INTEGER NOT NULL,
2373
+ token VARCHAR(255) NOT NULL,
2374
+ expires TIMESTAMPTZ NOT NULL,
2375
+ CONSTRAINT fk_${prefix}resets_account
2376
+ FOREIGN KEY (account_id) REFERENCES ${accountsTable}(id) ON DELETE CASCADE
2377
+ )
2378
+ `);
2379
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}resets_token ON ${resetsTable}(token)`);
2380
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}resets_account_id ON ${resetsTable}(account_id)`);
2381
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}resets_expires ON ${resetsTable}(expires)`);
2382
+ const providersTable = `${prefix}providers`;
2383
+ await db.query(`
2384
+ CREATE TABLE IF NOT EXISTS ${providersTable} (
2385
+ id SERIAL PRIMARY KEY,
2386
+ account_id INTEGER NOT NULL,
2387
+ provider VARCHAR(50) NOT NULL,
2388
+ provider_id VARCHAR(255) NOT NULL,
2389
+ provider_email VARCHAR(255),
2390
+ provider_username VARCHAR(255),
2391
+ provider_name VARCHAR(255),
2392
+ provider_avatar VARCHAR(500),
2393
+ created_at TIMESTAMPTZ DEFAULT NOW(),
2394
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
2395
+ CONSTRAINT fk_${prefix}providers_account
2396
+ FOREIGN KEY (account_id) REFERENCES ${accountsTable}(id) ON DELETE CASCADE,
2397
+ CONSTRAINT ${prefix}unique_provider_identity
2398
+ UNIQUE(provider, provider_id)
2399
+ )
2400
+ `);
2401
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}providers_account_id ON ${providersTable}(account_id)`);
2402
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}providers_provider ON ${providersTable}(provider)`);
2403
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}providers_provider_id ON ${providersTable}(provider_id)`);
2404
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}providers_email ON ${providersTable}(provider_email)`);
2405
+ const activityTable = `${prefix}activity_log`;
2406
+ await db.query(`
2407
+ CREATE TABLE IF NOT EXISTS ${activityTable} (
2408
+ id SERIAL PRIMARY KEY,
2409
+ account_id INTEGER,
2410
+ action VARCHAR(255) NOT NULL,
2411
+ ip_address INET,
2412
+ user_agent TEXT,
2413
+ browser VARCHAR(255),
2414
+ os VARCHAR(255),
2415
+ device VARCHAR(255),
2416
+ success BOOLEAN DEFAULT TRUE,
2417
+ metadata JSONB,
2418
+ created_at TIMESTAMPTZ DEFAULT NOW(),
2419
+ CONSTRAINT fk_${prefix}activity_log_account
2420
+ FOREIGN KEY (account_id) REFERENCES ${accountsTable}(id) ON DELETE CASCADE
2421
+ )
2422
+ `);
2423
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}activity_log_created_at ON ${activityTable}(created_at DESC)`);
2424
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}activity_log_account_id ON ${activityTable}(account_id)`);
2425
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}activity_log_action ON ${activityTable}(action)`);
2426
+ const twoFactorMethodsTable = `${prefix}2fa_methods`;
2427
+ await db.query(`
2428
+ CREATE TABLE IF NOT EXISTS ${twoFactorMethodsTable} (
2429
+ id SERIAL PRIMARY KEY,
2430
+ account_id INTEGER NOT NULL,
2431
+ mechanism INTEGER NOT NULL,
2432
+ secret VARCHAR(255),
2433
+ backup_codes TEXT[],
2434
+ verified BOOLEAN DEFAULT FALSE,
2435
+ created_at TIMESTAMPTZ DEFAULT NOW(),
2436
+ last_used_at TIMESTAMPTZ,
2437
+ CONSTRAINT fk_${prefix}2fa_methods_account
2438
+ FOREIGN KEY (account_id) REFERENCES ${accountsTable}(id) ON DELETE CASCADE,
2439
+ CONSTRAINT ${prefix}unique_account_mechanism
2440
+ UNIQUE(account_id, mechanism)
2441
+ )
2442
+ `);
2443
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}2fa_methods_account_id ON ${twoFactorMethodsTable}(account_id)`);
2444
+ const twoFactorTokensTable = `${prefix}2fa_tokens`;
2445
+ await db.query(`
2446
+ CREATE TABLE IF NOT EXISTS ${twoFactorTokensTable} (
2447
+ id SERIAL PRIMARY KEY,
2448
+ account_id INTEGER NOT NULL,
2449
+ mechanism INTEGER NOT NULL,
2450
+ selector VARCHAR(32) NOT NULL,
2451
+ token_hash VARCHAR(255) NOT NULL,
2452
+ expires_at TIMESTAMPTZ NOT NULL,
2453
+ created_at TIMESTAMPTZ DEFAULT NOW(),
2454
+ CONSTRAINT fk_${prefix}2fa_tokens_account
2455
+ FOREIGN KEY (account_id) REFERENCES ${accountsTable}(id) ON DELETE CASCADE
2456
+ )
2457
+ `);
2458
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}2fa_tokens_selector ON ${twoFactorTokensTable}(selector)`);
2459
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}2fa_tokens_account_id ON ${twoFactorTokensTable}(account_id)`);
2460
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}2fa_tokens_expires ON ${twoFactorTokensTable}(expires_at)`);
2461
+ }
2462
+ async function dropAuthTables(config) {
2463
+ const prefix = config.tablePrefix || "user_";
2464
+ const { db } = config;
2465
+ await db.query(`DROP TABLE IF EXISTS ${prefix}2fa_tokens CASCADE`);
2466
+ await db.query(`DROP TABLE IF EXISTS ${prefix}2fa_methods CASCADE`);
2467
+ await db.query(`DROP TABLE IF EXISTS ${prefix}activity_log CASCADE`);
2468
+ await db.query(`DROP TABLE IF EXISTS ${prefix}providers CASCADE`);
2469
+ await db.query(`DROP TABLE IF EXISTS ${prefix}resets CASCADE`);
2470
+ await db.query(`DROP TABLE IF EXISTS ${prefix}remembers CASCADE`);
2471
+ await db.query(`DROP TABLE IF EXISTS ${prefix}confirmations CASCADE`);
2472
+ await db.query(`DROP TABLE IF EXISTS ${prefix}accounts CASCADE`);
2473
+ }
2474
+ async function cleanupExpiredTokens(config) {
2475
+ const prefix = config.tablePrefix || "user_";
2476
+ const { db } = config;
2477
+ await db.query(`DELETE FROM ${prefix}confirmations WHERE expires < NOW()`);
2478
+ await db.query(`DELETE FROM ${prefix}remembers WHERE expires < NOW()`);
2479
+ await db.query(`DELETE FROM ${prefix}resets WHERE expires < NOW()`);
2480
+ await db.query(`DELETE FROM ${prefix}2fa_tokens WHERE expires_at < NOW()`);
2481
+ }
2482
+ async function getAuthTableStats(config) {
2483
+ const prefix = config.tablePrefix || "user_";
2484
+ const { db } = config;
2485
+ const [accountsResult, providersResult, confirmationsResult, remembersResult, resetsResult, twoFactorMethodsResult, twoFactorTokensResult, expiredConfirmationsResult, expiredRemembersResult, expiredResetsResult, expiredTwoFactorTokensResult] = await Promise.all([
2486
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}accounts`),
2487
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}providers`),
2488
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}confirmations`),
2489
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}remembers`),
2490
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}resets`),
2491
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}2fa_methods`),
2492
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}2fa_tokens`),
2493
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}confirmations WHERE expires < NOW()`),
2494
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}remembers WHERE expires < NOW()`),
2495
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}resets WHERE expires < NOW()`),
2496
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}2fa_tokens WHERE expires_at < NOW()`)
2497
+ ]);
2498
+ return {
2499
+ accounts: parseInt(accountsResult.rows[0]?.count || "0"),
2500
+ providers: parseInt(providersResult.rows[0]?.count || "0"),
2501
+ confirmations: parseInt(confirmationsResult.rows[0]?.count || "0"),
2502
+ remembers: parseInt(remembersResult.rows[0]?.count || "0"),
2503
+ resets: parseInt(resetsResult.rows[0]?.count || "0"),
2504
+ twoFactorMethods: parseInt(twoFactorMethodsResult.rows[0]?.count || "0"),
2505
+ twoFactorTokens: parseInt(twoFactorTokensResult.rows[0]?.count || "0"),
2506
+ expiredConfirmations: parseInt(expiredConfirmationsResult.rows[0]?.count || "0"),
2507
+ expiredRemembers: parseInt(expiredRemembersResult.rows[0]?.count || "0"),
2508
+ expiredResets: parseInt(expiredResetsResult.rows[0]?.count || "0"),
2509
+ expiredTwoFactorTokens: parseInt(expiredTwoFactorTokensResult.rows[0]?.count || "0")
2510
+ };
2511
+ }
2512
+ export {
2513
+ ActivityLogger,
2514
+ AuthActivityAction,
2515
+ AuthError,
2516
+ AuthRole,
2517
+ AuthStatus,
2518
+ AzureProvider,
2519
+ BaseOAuthProvider,
2520
+ ConfirmationExpiredError,
2521
+ ConfirmationNotFoundError,
2522
+ EmailNotVerifiedError,
2523
+ EmailTakenError,
2524
+ GitHubProvider,
2525
+ GoogleProvider,
2526
+ InvalidBackupCodeError,
2527
+ InvalidEmailError,
2528
+ InvalidPasswordError,
2529
+ InvalidTokenError,
2530
+ InvalidTwoFactorCodeError,
2531
+ OtpProvider,
2532
+ ResetDisabledError,
2533
+ ResetExpiredError,
2534
+ ResetNotFoundError,
2535
+ SecondFactorRequiredError,
2536
+ TooManyResetsError,
2537
+ TotpProvider,
2538
+ TwoFactorAlreadyEnabledError,
2539
+ TwoFactorExpiredError,
2540
+ TwoFactorManager,
2541
+ TwoFactorMechanism,
2542
+ TwoFactorNotSetupError,
2543
+ TwoFactorSetupIncompleteError,
2544
+ UserInactiveError,
2545
+ UserNotFoundError,
2546
+ UserNotLoggedInError,
2547
+ cleanupExpiredTokens,
2548
+ createAuthMiddleware,
2549
+ createAuthTables,
2550
+ dropAuthTables,
2551
+ getAuthTableStats,
2552
+ isValidEmail,
2553
+ validateEmail
2554
+ };
2555
+ //# sourceMappingURL=index.js.map