mbkauthe 4.9.0 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +98 -112
  2. package/docs/README.md +33 -0
  3. package/docs/STYLE.md +40 -0
  4. package/docs/diagrams/auth-processes.mmd +120 -0
  5. package/docs/diagrams/c.md +122 -0
  6. package/docs/{env.md → guides/configuration.md} +2 -1
  7. package/docs/{db.md → guides/database.md} +4 -2
  8. package/docs/images/auth-process.svg +102 -1
  9. package/docs/images/auth-processes.svg +1 -0
  10. package/docs/reference/api/authentication.md +105 -0
  11. package/docs/{api.md → reference/api/endpoints.md} +31 -861
  12. package/docs/reference/api/examples.md +251 -0
  13. package/docs/reference/api/middleware.md +239 -0
  14. package/docs/reference/api/operations.md +52 -0
  15. package/docs/reference/api.md +19 -0
  16. package/docs/{error-messages.md → reference/error-codes.md} +2 -0
  17. package/docs/schema/db.sql +328 -0
  18. package/index.js +5 -3
  19. package/lib/config/cookies.js +84 -18
  20. package/lib/config/index.js +3 -1
  21. package/lib/config/tokenScopes.js +1 -1
  22. package/lib/createTable.js +95 -8
  23. package/lib/db/AuthRepository.js +57 -16
  24. package/lib/db/BaseRepository.js +9 -1
  25. package/lib/db/dialects/postgres.js +1 -1
  26. package/lib/main.js +5 -5
  27. package/lib/middleware/auth.js +201 -218
  28. package/lib/middleware/index.js +13 -14
  29. package/lib/middleware/scopeValidator.js +8 -3
  30. package/lib/pool.js +5 -6
  31. package/lib/routes/auth.js +42 -47
  32. package/lib/routes/dbLogs.js +247 -29
  33. package/lib/routes/misc.js +6 -4
  34. package/lib/routes/oauth.js +19 -23
  35. package/lib/utils/dbQueryLogger.js +485 -80
  36. package/lib/utils/errors.js +1 -1
  37. package/lib/utils/logger.js +12 -0
  38. package/lib/utils/timingSafeToken.js +1 -1
  39. package/package.json +4 -3
  40. package/public/main.css +1 -1
  41. package/test.spec.js +515 -48
  42. package/views/pages/dbLogs.handlebars +618 -420
  43. package/docs/auth-processes.mmd +0 -71
  44. package/docs/db.sql +0 -276
  45. /package/docs/{auth-flows.mmd → diagrams/auth-flows.mmd} +0 -0
@@ -14,6 +14,28 @@ const OAUTH_PROVIDERS = {
14
14
  };
15
15
 
16
16
  export class AuthRepository extends BaseRepository {
17
+ buildSessionUserSelect({ includeProfile = false, includeTwoFA = false } = {}) {
18
+ const fields = [
19
+ `s.id as sid`,
20
+ `s.expires_at`,
21
+ `u.id as uid`,
22
+ `u."UserName"`,
23
+ `u."Active"`,
24
+ `u."Role"`,
25
+ `u."AllowedApps"`
26
+ ];
27
+
28
+ if (includeProfile) {
29
+ fields.push(`u."FullName"`, `u."Image"`);
30
+ }
31
+
32
+ if (includeTwoFA) {
33
+ fields.push(`tfa."TwoFAStatus"`);
34
+ }
35
+
36
+ return fields.join(", ");
37
+ }
38
+
17
39
  resolveOAuthProvider(provider) {
18
40
  const key = String(provider || "").toLowerCase();
19
41
  const config = OAUTH_PROVIDERS[key];
@@ -24,7 +46,7 @@ export class AuthRepository extends BaseRepository {
24
46
  }
25
47
 
26
48
  async fetchActiveSession(sessionId) {
27
- const query = `SELECT s.id as sid, s.expires_at, u.id as uid, u."UserName", u."Active", u."Role", u."AllowedApps"
49
+ const query = `SELECT ${this.buildSessionUserSelect({ includeProfile: true })}
28
50
  FROM "Sessions" s
29
51
  JOIN "Users" u ON s."UserName" = u."UserName"
30
52
  WHERE s.id = $1 LIMIT 1`;
@@ -42,6 +64,17 @@ export class AuthRepository extends BaseRepository {
42
64
  return this.executeRaw({ name: queryName, text: query, values: [sessionId] });
43
65
  }
44
66
 
67
+ async getSessionsWithUsersByIds(sessionIds, queryName = "multi-session-fetch-many") {
68
+ if (!Array.isArray(sessionIds) || sessionIds.length === 0) return [];
69
+
70
+ const query = `SELECT ${this.buildSessionUserSelect({ includeProfile: true })}
71
+ FROM "Sessions" s
72
+ JOIN "Users" u ON s."UserName" = u."UserName"
73
+ WHERE s.id = ANY($1)`;
74
+ const result = await this.executeRaw({ name: queryName, text: query, values: [sessionIds] });
75
+ return result.rows || [];
76
+ }
77
+
45
78
  async touchTrustedDevice(deviceTokenHash, username) {
46
79
  const query = `
47
80
  UPDATE "TrustedDevices" td
@@ -164,7 +197,7 @@ export class AuthRepository extends BaseRepository {
164
197
  async getUserWithTwoFA(username, queryName = "login-get-user") {
165
198
  const query = `
166
199
  SELECT u.id, u."UserName", u."PasswordEnc", u."Active", u."Role", u."AllowedApps",
167
- tfa."TwoFAStatus"
200
+ tfa."TwoFAStatus", u."FullName", u."Image"
168
201
  FROM "Users" u
169
202
  LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
170
203
  WHERE u."UserName" = $1
@@ -189,7 +222,11 @@ export class AuthRepository extends BaseRepository {
189
222
 
190
223
  async getOAuthUserByProviderId(provider, providerId) {
191
224
  const { table, idColumn, queryName } = this.resolveOAuthProvider(provider);
192
- const query = `SELECT ug.*, u."UserName", u."Role", u."Active", u."AllowedApps", u."id" FROM ${table} ug JOIN "Users" u ON ug.user_name = u."UserName" WHERE ug.${idColumn} = $1`;
225
+ const query = `SELECT ug.*, u."UserName", u."Role", u."Active", u."AllowedApps", u."id", tfa."TwoFAStatus"
226
+ FROM ${table} ug
227
+ JOIN "Users" u ON ug.user_name = u."UserName"
228
+ LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
229
+ WHERE ug.${idColumn} = $1`;
193
230
  const result = await this.executeRaw({ name: queryName, text: query, values: [providerId] });
194
231
  return result.rows?.[0] || null;
195
232
  }
@@ -207,17 +244,26 @@ export class AuthRepository extends BaseRepository {
207
244
  return result.rows?.[0] || null;
208
245
  }
209
246
 
210
- async updateApiTokenLastUsed(tokenId, queryName = null) {
211
- const query = `UPDATE "ApiTokens" SET "LastUsed" = NOW() WHERE id = $1`;
247
+ async updateApiTokenLastUsed(tokenId, queryName = null, minIntervalMinutes = 15) {
248
+ const query = `
249
+ UPDATE "ApiTokens"
250
+ SET "LastUsed" = NOW()
251
+ WHERE id = $1
252
+ AND (
253
+ "LastUsed" IS NULL
254
+ OR "LastUsed" < NOW() - ($2::int * INTERVAL '1 minute')
255
+ )
256
+ `;
257
+ const values = [tokenId, minIntervalMinutes];
212
258
  if (queryName) {
213
- return this.executeRaw({ name: queryName, text: query, values: [tokenId] });
259
+ return this.executeRaw({ name: queryName, text: query, values });
214
260
  }
215
- return this.executeRaw({ text: query, values: [tokenId] });
261
+ return this.executeRaw({ text: query, values });
216
262
  }
217
263
 
218
264
  async getSessionAuthData(sessionId, queryName = "validate-app-session") {
219
265
  const query = `
220
- SELECT s.expires_at, u."Active", u."Role"
266
+ SELECT s.expires_at, u."Active", u."Role", u."AllowedApps", u."UserName"
221
267
  FROM "Sessions" s
222
268
  JOIN "Users" u ON s."UserName" = u."UserName"
223
269
  WHERE s.id = $1
@@ -229,7 +275,7 @@ export class AuthRepository extends BaseRepository {
229
275
  }
230
276
 
231
277
  async getSessionWithUserById(sessionId, queryName = "restore-user-session") {
232
- const query = `SELECT u.id, u."UserName", u."Active", u."Role", u."AllowedApps", s.expires_at
278
+ const query = `SELECT ${this.buildSessionUserSelect({ includeProfile: true })}
233
279
  FROM "Sessions" s
234
280
  JOIN "Users" u ON s."UserName" = u."UserName"
235
281
  WHERE s.id = $1 LIMIT 1`;
@@ -238,12 +284,7 @@ export class AuthRepository extends BaseRepository {
238
284
  }
239
285
 
240
286
  async getSessionWithUserForReload(sessionId, queryName = "reload-session-user") {
241
- const query = `SELECT s.id as sid, s.expires_at, u.id as uid, u."UserName", u."Active", u."Role", u."AllowedApps"
242
- FROM "Sessions" s
243
- JOIN "Users" u ON s."UserName" = u."UserName"
244
- WHERE s.id = $1 LIMIT 1`;
245
- const result = await this.executeRaw({ name: queryName, text: query, values: [sessionId] });
246
- return result.rows?.[0] || null;
287
+ return this.getSessionWithUserById(sessionId, queryName);
247
288
  }
248
289
 
249
290
  async getSessionValidationRow(sessionId, queryName = "check-session-validity-by-id") {
@@ -292,4 +333,4 @@ export class AuthRepository extends BaseRepository {
292
333
  const query = `DELETE FROM "session" WHERE expire > NOW()`;
293
334
  return this.executeRaw({ name: queryName, text: query, values: [] });
294
335
  }
295
- }
336
+ }
@@ -182,4 +182,12 @@ export class BaseRepository {
182
182
  values: []
183
183
  });
184
184
  }
185
- }
185
+
186
+ async advisoryTransactionLock(lockKey, queryName = "advisory-transaction-lock") {
187
+ return this.executeRaw({
188
+ name: queryName,
189
+ text: "SELECT pg_advisory_xact_lock(hashtext($1))",
190
+ values: [String(lockKey ?? "")]
191
+ });
192
+ }
193
+ }
@@ -15,4 +15,4 @@ export const postgresDialect = {
15
15
  return parts.length ? ` ${parts.join(" ")}` : "";
16
16
  },
17
17
  lockTable: (tableSql, mode = "ROW EXCLUSIVE") => `LOCK TABLE ${tableSql} IN ${mode} MODE`
18
- };
18
+ };
package/lib/main.js CHANGED
@@ -41,17 +41,17 @@ router.use(cookieParser());
41
41
  // CORS and security headers
42
42
  router.use(corsMiddleware);
43
43
 
44
+ // Attach request context as early as possible so session-store queries are tied to the request.
45
+ if (process.env.env === 'dev') {
46
+ router.use(requestContextMiddleware);
47
+ }
48
+
44
49
  // Session configuration
45
50
  router.use(session(sessionConfig));
46
51
 
47
52
  // Session restoration
48
53
  router.use(sessionRestorationMiddleware);
49
54
 
50
- // Attach request context for DB query logging (dev only)
51
- if (process.env.env === 'dev') {
52
- router.use(requestContextMiddleware);
53
- }
54
-
55
55
  // Initialize passport
56
56
  router.use(passport.initialize());
57
57
  router.use(passport.session());