@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/README.md +877 -0
- package/dist/index.cjs +2632 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +930 -0
- package/dist/index.d.ts +930 -0
- package/dist/index.js +2555 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
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
|