mbkauthe 4.8.4 → 4.9.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 CHANGED
@@ -20,6 +20,7 @@
20
20
 
21
21
  ## ✨ Key Features
22
22
 
23
+ - Compatible With Serverless Function (Vercel)
23
24
  - Secure password authentication (PBKDF2)
24
25
  - PostgreSQL session management
25
26
  - Multi-session support (configurable concurrent sessions per user)
package/index.js CHANGED
@@ -98,5 +98,6 @@ export * from "./lib/routes/auth.js";
98
98
  export * from "./lib/utils/errors.js";
99
99
  export * from "./lib/config/cookies.js";
100
100
  export * from "./lib/config/security.js";
101
+ export * from "./lib/db/AuthRepository.js";
101
102
  export { mbkautheVar } from "#config.js";
102
103
  export default app;
@@ -0,0 +1,295 @@
1
+ import { BaseRepository } from "./BaseRepository.js";
2
+
3
+ const OAUTH_PROVIDERS = {
4
+ github: {
5
+ table: "user_github",
6
+ idColumn: "github_id",
7
+ queryName: "github-login-get-user"
8
+ },
9
+ google: {
10
+ table: "user_google",
11
+ idColumn: "google_id",
12
+ queryName: "google-login-get-user"
13
+ }
14
+ };
15
+
16
+ export class AuthRepository extends BaseRepository {
17
+ resolveOAuthProvider(provider) {
18
+ const key = String(provider || "").toLowerCase();
19
+ const config = OAUTH_PROVIDERS[key];
20
+ if (!config) {
21
+ throw new Error(`Unsupported OAuth provider: ${provider}`);
22
+ }
23
+ return config;
24
+ }
25
+
26
+ 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"
28
+ FROM "Sessions" s
29
+ JOIN "Users" u ON s."UserName" = u."UserName"
30
+ WHERE s.id = $1 LIMIT 1`;
31
+ const result = await this.executeRaw({ name: "multi-session-fetch", text: query, values: [sessionId] });
32
+ return result.rows?.[0] || null;
33
+ }
34
+
35
+ async deleteAppSessionById(sessionId, queryName = "invalidate-app-session") {
36
+ const query = `DELETE FROM "Sessions" WHERE id = $1`;
37
+ return this.executeRaw({ name: queryName, text: query, values: [sessionId] });
38
+ }
39
+
40
+ async deleteSessionBySid(sessionId, queryName = "login-delete-old-session-before-regen") {
41
+ const query = `DELETE FROM "session" WHERE sid = $1`;
42
+ return this.executeRaw({ name: queryName, text: query, values: [sessionId] });
43
+ }
44
+
45
+ async touchTrustedDevice(deviceTokenHash, username) {
46
+ const query = `
47
+ UPDATE "TrustedDevices" td
48
+ SET "LastUsed" = NOW()
49
+ FROM "Users" u
50
+ WHERE td."DeviceToken" = $1
51
+ AND td."UserName" = $2
52
+ AND td."ExpiresAt" > NOW()
53
+ AND u."UserName" = td."UserName"
54
+ AND u."Active" = TRUE
55
+ RETURNING td."UserName", td."ExpiresAt", u."id" as id, u."Active", u."Role", u."AllowedApps"
56
+ `;
57
+
58
+ const result = await this.executeRaw({
59
+ name: "check-trusted-device",
60
+ text: query,
61
+ values: [deviceTokenHash, username]
62
+ });
63
+ return result.rows?.[0] || null;
64
+ }
65
+
66
+ async cleanupAndCountUserSessions(username, queryName = "cleanup-and-count-user-sessions") {
67
+ const query = `
68
+ WITH deleted AS (
69
+ DELETE FROM "Sessions"
70
+ WHERE "UserName" = $1
71
+ AND expires_at IS NOT NULL
72
+ AND expires_at <= NOW()
73
+ )
74
+ SELECT COUNT(*)::int AS count
75
+ FROM "Sessions"
76
+ WHERE "UserName" = $1
77
+ `;
78
+
79
+ const result = await this.executeRaw({ name: queryName, text: query, values: [username] });
80
+ return Number(result.rows?.[0]?.count ?? 0);
81
+ }
82
+
83
+ async deleteOldestSessionsForUser(username, limit, queryName = "prune-oldest-user-session") {
84
+ if (!Number.isFinite(limit) || limit <= 0) return 0;
85
+ const query = `DELETE FROM "Sessions" WHERE id IN (SELECT id FROM "Sessions" WHERE "UserName" = $1 ORDER BY created_at ASC LIMIT $2)`;
86
+ const result = await this.executeRaw({ name: queryName, text: query, values: [username, limit] });
87
+ return result.rowCount || 0;
88
+ }
89
+
90
+ async deleteExpiredSessionsForUser(username) {
91
+ const query = this.sql`
92
+ DELETE FROM ${this.table("Sessions")}
93
+ WHERE ${this.ident("UserName")} = ${this.value(username)}
94
+ AND ${this.ident("expires_at")} IS NOT NULL
95
+ AND ${this.ident("expires_at")} <= ${this.now()}
96
+ `;
97
+
98
+ const result = await this.execute("cleanup-expired-user-sessions", query);
99
+ return result.rowCount || 0;
100
+ }
101
+
102
+ async countActiveSessionsForUser(username) {
103
+ const columns = this.columns([`COUNT(*) AS ${this.quoteIdentifier("count")}`]);
104
+ const query = this.sql`
105
+ SELECT ${columns}
106
+ FROM ${this.table("Sessions")}
107
+ WHERE ${this.ident("UserName")} = ${this.value(username)}
108
+ `;
109
+ const result = await this.execute("count-user-sessions", query);
110
+ return Number(result.rows?.[0]?.count ?? 0);
111
+ }
112
+
113
+ async getOldestSessionIds(username, limit) {
114
+ if (!Number.isFinite(limit) || limit <= 0) return [];
115
+
116
+ const query = this.sql`
117
+ SELECT ${this.column("id")}
118
+ FROM ${this.table("Sessions")}
119
+ WHERE ${this.ident("UserName")} = ${this.value(username)}
120
+ ORDER BY ${this.ident("created_at")} ASC
121
+ ${this.limit(limit)}
122
+ `;
123
+
124
+ const result = await this.execute("oldest-user-sessions", query);
125
+ return (result.rows || []).map((row) => row.id).filter(Boolean);
126
+ }
127
+
128
+ async insertAppSession(username, expiresAt, meta) {
129
+ const query = `INSERT INTO "Sessions" ("UserName", expires_at, meta) VALUES ($1, $2, $3) RETURNING id`;
130
+ const result = await this.executeRaw({
131
+ name: "insert-app-session",
132
+ text: query,
133
+ values: [username, expiresAt, meta]
134
+ });
135
+ return result.rows?.[0] || null;
136
+ }
137
+
138
+ async updateLastLoginReturnProfile(userId) {
139
+ const query = `UPDATE "Users" SET "last_login" = NOW() WHERE "id" = $1 RETURNING "FullName", "Image"`;
140
+ const result = await this.executeRaw({
141
+ name: "login-update-last-login-return-profile",
142
+ text: query,
143
+ values: [userId]
144
+ });
145
+ return result.rows?.[0] || null;
146
+ }
147
+
148
+ async getUserProfileByUsername(username, queryName = "login-get-fullname-and-image") {
149
+ const query = `SELECT "FullName", "Image" FROM "Users" WHERE "UserName" = $1 LIMIT 1`;
150
+ const result = await this.executeRaw({ name: queryName, text: query, values: [username] });
151
+ return result.rows?.[0] || null;
152
+ }
153
+
154
+ async insertTrustedDevice({ username, deviceTokenHash, deviceName, userAgent, ipAddress, expiresAt }) {
155
+ const query = `INSERT INTO "TrustedDevices" ("UserName", "DeviceToken", "DeviceName", "UserAgent", "IpAddress", "ExpiresAt")
156
+ VALUES ($1, $2, $3, $4, $5, $6)`;
157
+ return this.executeRaw({
158
+ name: "insert-trusted-device",
159
+ text: query,
160
+ values: [username, deviceTokenHash, deviceName, userAgent, ipAddress, expiresAt]
161
+ });
162
+ }
163
+
164
+ async getUserWithTwoFA(username, queryName = "login-get-user") {
165
+ const query = `
166
+ SELECT u.id, u."UserName", u."PasswordEnc", u."Active", u."Role", u."AllowedApps",
167
+ tfa."TwoFAStatus"
168
+ FROM "Users" u
169
+ LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
170
+ WHERE u."UserName" = $1
171
+ `;
172
+
173
+ const result = await this.executeRaw({ name: queryName, text: query, values: [username] });
174
+ return result.rows?.[0] || null;
175
+ }
176
+
177
+ async getTwoFASecret(username) {
178
+ const query = `SELECT tfa."TwoFASecret" FROM "TwoFA" tfa WHERE tfa."UserName" = $1`;
179
+ const result = await this.executeRaw({ name: "verify-2fa-secret", text: query, values: [username] });
180
+ return result.rows?.[0] || null;
181
+ }
182
+
183
+ async deleteSessionsByIds(ids, queryName = "delete-sessions-by-ids") {
184
+ if (!Array.isArray(ids) || ids.length === 0) return 0;
185
+ const query = `DELETE FROM "Sessions" WHERE id = ANY($1)`;
186
+ const result = await this.executeRaw({ name: queryName, text: query, values: [ids] });
187
+ return result.rowCount || 0;
188
+ }
189
+
190
+ async getOAuthUserByProviderId(provider, providerId) {
191
+ 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`;
193
+ const result = await this.executeRaw({ name: queryName, text: query, values: [providerId] });
194
+ return result.rows?.[0] || null;
195
+ }
196
+
197
+ async getApiTokenByHash(tokenHash, queryName = "validate-api-token") {
198
+ const query = `
199
+ SELECT t.id, t."UserName", t."ExpiresAt", t."Permissions",
200
+ u.id as uid, u."Active", u."Role", u."AllowedApps" as user_allowed_apps, u."FullName"
201
+ FROM "ApiTokens" t
202
+ JOIN "Users" u ON t."UserName" = u."UserName"
203
+ WHERE t."TokenHash" = $1 LIMIT 1
204
+ `;
205
+
206
+ const result = await this.executeRaw({ name: queryName, text: query, values: [tokenHash] });
207
+ return result.rows?.[0] || null;
208
+ }
209
+
210
+ async updateApiTokenLastUsed(tokenId, queryName = null) {
211
+ const query = `UPDATE "ApiTokens" SET "LastUsed" = NOW() WHERE id = $1`;
212
+ if (queryName) {
213
+ return this.executeRaw({ name: queryName, text: query, values: [tokenId] });
214
+ }
215
+ return this.executeRaw({ text: query, values: [tokenId] });
216
+ }
217
+
218
+ async getSessionAuthData(sessionId, queryName = "validate-app-session") {
219
+ const query = `
220
+ SELECT s.expires_at, u."Active", u."Role"
221
+ FROM "Sessions" s
222
+ JOIN "Users" u ON s."UserName" = u."UserName"
223
+ WHERE s.id = $1
224
+ LIMIT 1
225
+ `;
226
+
227
+ const result = await this.executeRaw({ name: queryName, text: query, values: [sessionId] });
228
+ return result.rows?.[0] || null;
229
+ }
230
+
231
+ async getSessionWithUserById(sessionId, queryName = "restore-user-session") {
232
+ const query = `SELECT u.id, u."UserName", u."Active", u."Role", u."AllowedApps", s.expires_at
233
+ FROM "Sessions" s
234
+ JOIN "Users" u ON s."UserName" = u."UserName"
235
+ WHERE s.id = $1 LIMIT 1`;
236
+ const result = await this.executeRaw({ name: queryName, text: query, values: [sessionId] });
237
+ return result.rows?.[0] || null;
238
+ }
239
+
240
+ 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;
247
+ }
248
+
249
+ async getSessionValidationRow(sessionId, queryName = "check-session-validity-by-id") {
250
+ const query = `SELECT s.expires_at, u."Active", u."UserName", u."Role" FROM "Sessions" s JOIN "Users" u ON s."UserName" = u."UserName" WHERE s.id = $1 LIMIT 1`;
251
+ const result = await this.executeRaw({ name: queryName, text: query, values: [sessionId] });
252
+ return result.rows?.[0] || null;
253
+ }
254
+
255
+ async getUserFullNameByUsername(username, queryName = "get-fullname-by-username") {
256
+ const query = `SELECT "FullName" FROM "Users" WHERE "UserName" = $1 LIMIT 1`;
257
+ const result = await this.executeRaw({ name: queryName, text: query, values: [username] });
258
+ return result.rows?.[0] || null;
259
+ }
260
+
261
+ async getUserImageByUsername(username, queryName = "get-user-profile-pic") {
262
+ const query = `SELECT "Image" FROM "Users" WHERE "UserName" = $1 LIMIT 1`;
263
+ const result = await this.executeRaw({ name: queryName, text: query, values: [username] });
264
+ return result.rows?.[0] || null;
265
+ }
266
+
267
+ async getSessionValidity(sessionId, sessionStoreSid, queryName = "check-session-validity") {
268
+ const query = `
269
+ SELECT
270
+ s.expires_at,
271
+ u."Active",
272
+ CASE
273
+ WHEN s.expires_at IS NULL THEN (SELECT expire FROM "session" WHERE sid = $2)
274
+ ELSE NULL
275
+ END AS connect_expire
276
+ FROM "Sessions" s
277
+ JOIN "Users" u ON s."UserName" = u."UserName"
278
+ WHERE s.id = $1
279
+ LIMIT 1
280
+ `;
281
+
282
+ const result = await this.executeRaw({ name: queryName, text: query, values: [sessionId, sessionStoreSid] });
283
+ return result.rows?.[0] || null;
284
+ }
285
+
286
+ async deleteAllAppSessions(queryName = "delete-all-app-sessions") {
287
+ const query = `DELETE FROM "Sessions"`;
288
+ return this.executeRaw({ name: queryName, text: query, values: [] });
289
+ }
290
+
291
+ async deleteActiveSessionStoreRows(queryName = "delete-active-session-store-rows") {
292
+ const query = `DELETE FROM "session" WHERE expire > NOW()`;
293
+ return this.executeRaw({ name: queryName, text: query, values: [] });
294
+ }
295
+ }
@@ -0,0 +1,185 @@
1
+ import { postgresDialect } from "./dialects/postgres.js";
2
+
3
+ const isPlainObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
4
+
5
+ export class BaseRepository {
6
+ constructor({ db, dialect = postgresDialect } = {}) {
7
+ this.db = db;
8
+ this.dialect = dialect;
9
+ }
10
+
11
+ quoteIdentifier(name) {
12
+ if (name === "*") return "*";
13
+ return String(name)
14
+ .split(".")
15
+ .map((part) => (part === "*" ? "*" : this.dialect.quoteIdentifier(part)))
16
+ .join(".");
17
+ }
18
+
19
+ ident(name) {
20
+ return { kind: "ident", name };
21
+ }
22
+
23
+ value(val) {
24
+ return { kind: "param", value: val };
25
+ }
26
+
27
+ raw(text) {
28
+ return { kind: "raw", text: text ?? "" };
29
+ }
30
+
31
+ list(values) {
32
+ return { kind: "list", values };
33
+ }
34
+
35
+ table(name, alias = null) {
36
+ const tableSql = this.quoteIdentifier(name);
37
+ if (alias) {
38
+ return this.raw(`${tableSql} AS ${this.quoteIdentifier(alias)}`);
39
+ }
40
+ return this.raw(tableSql);
41
+ }
42
+
43
+ column(name, alias = null) {
44
+ const columnSql = this.quoteIdentifier(name);
45
+ if (alias) {
46
+ return `${columnSql} AS ${this.quoteIdentifier(alias)}`;
47
+ }
48
+ return columnSql;
49
+ }
50
+
51
+ columns(list) {
52
+ const items = (list || []).filter(Boolean).map((item) => {
53
+ if (isPlainObject(item) && item.kind) {
54
+ if (item.kind === "ident") return this.quoteIdentifier(item.name);
55
+ if (item.kind === "raw") return item.text;
56
+ }
57
+ return String(item);
58
+ });
59
+ return this.raw(items.join(", "));
60
+ }
61
+
62
+ star(alias = null) {
63
+ if (!alias) return this.raw("*");
64
+ return this.raw(`${this.quoteIdentifier(alias)}.*`);
65
+ }
66
+
67
+ now() {
68
+ return this.raw(this.dialect.now());
69
+ }
70
+
71
+ boolean(value) {
72
+ return this.raw(this.dialect.boolean(value));
73
+ }
74
+
75
+ returning(columns) {
76
+ if (!this.dialect.supportsReturning) return this.raw("");
77
+ if (!columns) return this.raw("");
78
+
79
+ let columnSql = "";
80
+ if (isPlainObject(columns) && columns.kind === "raw") {
81
+ columnSql = columns.text;
82
+ } else if (Array.isArray(columns)) {
83
+ columnSql = columns.join(", ");
84
+ } else {
85
+ columnSql = String(columns);
86
+ }
87
+
88
+ if (!columnSql) return this.raw("");
89
+ return this.raw(this.dialect.returningClause(columnSql));
90
+ }
91
+
92
+ limit(limit, offset) {
93
+ return this.raw(this.dialect.limitOffset({ limit, offset }));
94
+ }
95
+
96
+ sql(strings, ...exprs) {
97
+ const values = [];
98
+ let text = strings?.[0] ?? "";
99
+
100
+ for (let i = 0; i < exprs.length; i += 1) {
101
+ text += this.renderToken(exprs[i], values);
102
+ text += strings?.[i + 1] ?? "";
103
+ }
104
+
105
+ return { text, values };
106
+ }
107
+
108
+ renderToken(token, values) {
109
+ if (token == null) return "";
110
+
111
+ if (!isPlainObject(token) || !token.kind) {
112
+ return String(token);
113
+ }
114
+
115
+ switch (token.kind) {
116
+ case "raw":
117
+ return token.text;
118
+ case "ident":
119
+ return this.quoteIdentifier(token.name);
120
+ case "param":
121
+ values.push(token.value);
122
+ return this.dialect.param(values.length);
123
+ case "list":
124
+ if (!Array.isArray(token.values) || token.values.length === 0) {
125
+ return "(NULL)";
126
+ }
127
+ return `(${token.values.map((item) => {
128
+ values.push(item);
129
+ return this.dialect.param(values.length);
130
+ }).join(", ")})`;
131
+ default:
132
+ return "";
133
+ }
134
+ }
135
+
136
+ async execute(name, query) {
137
+ const { text, values } = query;
138
+ if (name) {
139
+ return this.db.query({ name, text, values });
140
+ }
141
+ return this.db.query({ text, values });
142
+ }
143
+
144
+ async executeRaw({ name, text, values }) {
145
+ if (name) {
146
+ return this.db.query({ name, text, values });
147
+ }
148
+ return this.db.query({ text, values });
149
+ }
150
+
151
+ cloneWithDb(db) {
152
+ return new this.constructor({ db, dialect: this.dialect });
153
+ }
154
+
155
+ async withTransaction(fn) {
156
+ if (!this.db || typeof this.db.connect !== "function") {
157
+ return fn(this);
158
+ }
159
+
160
+ const client = await this.db.connect();
161
+ const txRepo = this.cloneWithDb(client);
162
+
163
+ try {
164
+ await client.query("BEGIN");
165
+ const result = await fn(txRepo);
166
+ await client.query("COMMIT");
167
+ return result;
168
+ } catch (err) {
169
+ await client.query("ROLLBACK").catch(() => {});
170
+ throw err;
171
+ } finally {
172
+ client.release();
173
+ }
174
+ }
175
+
176
+ async lockTable(tableName, mode) {
177
+ if (!this.dialect.lockTable) return null;
178
+ const text = this.dialect.lockTable(this.quoteIdentifier(tableName), mode);
179
+ return this.executeRaw({
180
+ name: `lock-${tableName}`,
181
+ text,
182
+ values: []
183
+ });
184
+ }
185
+ }
@@ -0,0 +1,18 @@
1
+ const quoteIdentifier = (name) => `"${String(name).replace(/"/g, '""')}"`;
2
+
3
+ export const postgresDialect = {
4
+ name: "postgres",
5
+ quoteIdentifier,
6
+ param: (index) => `$${index}`,
7
+ now: () => "NOW()",
8
+ boolean: (value) => (value ? "TRUE" : "FALSE"),
9
+ supportsReturning: true,
10
+ returningClause: (columns) => ` RETURNING ${columns}`,
11
+ limitOffset: ({ limit, offset } = {}) => {
12
+ const parts = [];
13
+ if (typeof limit === "number") parts.push(`LIMIT ${limit}`);
14
+ if (typeof offset === "number") parts.push(`OFFSET ${offset}`);
15
+ return parts.length ? ` ${parts.join(" ")}` : "";
16
+ },
17
+ lockTable: (tableSql, mode = "ROW EXCLUSIVE") => `LOCK TABLE ${tableSql} IN ${mode} MODE`
18
+ };
@@ -6,18 +6,12 @@ import { ErrorCodes, createErrorResponse } from "../utils/errors.js";
6
6
  import { hashApiToken } from "#config.js";
7
7
  import { canAccessMethod } from "#config.js";
8
8
  import { extractAuthorizationToken, timingSafeTokenMatch } from "../utils/timingSafeToken.js";
9
+ import { AuthRepository } from "../db/AuthRepository.js";
9
10
 
10
11
  const IS_DEV = process.env.env === 'dev' || process.env.test === 'dev' || process.env.NODE_ENV === 'development';
11
12
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12
13
  const isUuid = (val) => typeof val === 'string' && UUID_RE.test(val);
13
-
14
- const SQL_VALIDATE_APP_SESSION = `
15
- SELECT s.expires_at, u."Active", u."Role"
16
- FROM "Sessions" s
17
- JOIN "Users" u ON s."UserName" = u."UserName"
18
- WHERE s.id = $1
19
- LIMIT 1
20
- `;
14
+ const authRepo = new AuthRepository({ db: dblogin });
21
15
 
22
16
  /**
23
17
  * Decide if the incoming request should return JSON errors instead of HTML.
@@ -64,18 +58,9 @@ async function validateTokenAuthentication(req) {
64
58
  // 1. Check for API Token (mbk_)
65
59
  if (token.startsWith('mbk_')) {
66
60
  const tokenHash = hashApiToken(token);
67
- const tokenQuery = `
68
- SELECT t.id, t."UserName", t."ExpiresAt", t."Permissions",
69
- u.id as uid, u."Active", u."Role", u."AllowedApps" as user_allowed_apps, u."FullName"
70
- FROM "ApiTokens" t
71
- JOIN "Users" u ON t."UserName" = u."UserName"
72
- WHERE t."TokenHash" = $1 LIMIT 1
73
- `;
74
- const tokenResult = await dblogin.query({ name: 'validate-api-token', text: tokenQuery, values: [tokenHash] });
75
-
76
- if (tokenResult.rows.length === 0) return { error: 'INVALID_TOKEN' };
77
-
78
- const row = tokenResult.rows[0];
61
+ const row = await authRepo.getApiTokenByHash(tokenHash);
62
+
63
+ if (!row) return { error: 'INVALID_TOKEN' };
79
64
  if (row.ExpiresAt && new Date(row.ExpiresAt) <= new Date()) return { error: 'TOKEN_EXPIRED' };
80
65
 
81
66
  // Parse permissions from JSONB
@@ -90,10 +75,7 @@ async function validateTokenAuthentication(req) {
90
75
  }
91
76
 
92
77
  // Update usage
93
- dblogin.query({
94
- text: 'UPDATE "ApiTokens" SET "LastUsed" = NOW() WHERE id = $1',
95
- values: [row.id]
96
- }).catch(e => console.error(`[mbkauthe] Failed to update token usage:`, e));
78
+ authRepo.updateApiTokenLastUsed(row.id).catch(e => console.error(`[mbkauthe] Failed to update token usage:`, e));
97
79
 
98
80
  return {
99
81
  id: row.uid,
@@ -208,13 +190,12 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
208
190
  if (isJsonRequest(req)) {
209
191
  return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
210
192
  }
211
- return renderError(res, req, {
212
- code: 401,
213
- error: "Not Logged In",
214
- message: "You Are Not Logged In. Please Log In To Continue.",
215
- pagename: "Login",
216
- page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
193
+ // Use OAuth 2.0 style redirect to the login page for browser navigation
194
+ const redirectParams = new URLSearchParams({
195
+ redirect: req.originalUrl,
196
+ reason: 'logged_out'
217
197
  });
198
+ return res.redirect(302, `/mbkauthe/login?${redirectParams.toString()}`);
218
199
  }
219
200
 
220
201
  try {
@@ -238,9 +219,9 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
238
219
  }
239
220
 
240
221
  // Validate session by DB primary key id and join to user
241
- const result = await dblogin.query({ name: 'validate-app-session', text: SQL_VALIDATE_APP_SESSION, values: [sessionId] });
222
+ const sessionRow = await authRepo.getSessionAuthData(sessionId, 'validate-app-session');
242
223
 
243
- if (result.rows.length === 0) {
224
+ if (!sessionRow) {
244
225
  console.log(`[mbkauthe] Session not found for user "${req.session.user.username}"`);
245
226
  req.session.destroy();
246
227
  clearSessionCookies(res);
@@ -256,8 +237,6 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
256
237
  });
257
238
  }
258
239
 
259
- const sessionRow = result.rows[0];
260
-
261
240
  // Check expired
262
241
  if (sessionRow.expires_at) {
263
242
  const expiresMs = sessionRow.expires_at instanceof Date
@@ -349,16 +328,14 @@ async function validateApiSession(req, res, next) {
349
328
  }
350
329
 
351
330
  // Validate session by DB primary key id and join to user
352
- const result = await dblogin.query({ name: 'validate-app-session-for-api', text: SQL_VALIDATE_APP_SESSION, values: [sessionId] });
331
+ const sessionRow = await authRepo.getSessionAuthData(sessionId, 'validate-app-session-for-api');
353
332
 
354
- if (result.rows.length === 0) {
333
+ if (!sessionRow) {
355
334
  req.session.destroy();
356
335
  clearSessionCookies(res);
357
336
  return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_INVALID));
358
337
  }
359
338
 
360
- const sessionRow = result.rows[0];
361
-
362
339
  // Check expired
363
340
  if (sessionRow.expires_at) {
364
341
  const expiresMs = sessionRow.expires_at instanceof Date
@@ -372,7 +349,7 @@ async function validateApiSession(req, res, next) {
372
349
  }
373
350
 
374
351
 
375
- if (!result.rows[0].Active) {
352
+ if (!sessionRow.Active) {
376
353
  console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
377
354
  req.session.destroy();
378
355
  clearSessionCookies(res);
@@ -420,21 +397,15 @@ async function reloadSessionUser(req, res) {
420
397
  }
421
398
 
422
399
  const normalizedSessionId = String(currentSessionId);
423
- const query = `SELECT s.id as sid, s.expires_at, u.id as uid, u."UserName", u."Active", u."Role", u."AllowedApps"
424
- FROM "Sessions" s
425
- JOIN "Users" u ON s."UserName" = u."UserName"
426
- WHERE s.id = $1 LIMIT 1`;
427
- const result = await dblogin.query({ name: 'reload-session-user', text: query, values: [normalizedSessionId] });
400
+ const row = await authRepo.getSessionWithUserForReload(normalizedSessionId, 'reload-session-user');
428
401
 
429
- if (result.rows.length === 0) {
402
+ if (!row) {
430
403
  // Session not found — invalidate session
431
404
  req.session.destroy(() => { });
432
405
  clearSessionCookies(res);
433
406
  return false;
434
407
  }
435
408
 
436
- const row = result.rows[0];
437
-
438
409
  // Check expired
439
410
  if (row.expires_at && new Date(row.expires_at) <= new Date()) {
440
411
  req.session.destroy(() => { });
@@ -470,8 +441,8 @@ async function reloadSessionUser(req, res) {
470
441
  req.session.user.fullname = req.cookies.fullName;
471
442
  } else {
472
443
  try {
473
- const prof = await dblogin.query({ name: 'reload-get-fullname', text: 'SELECT "FullName" FROM "Users" WHERE "UserName" = $1 LIMIT 1', values: [row.UserName] });
474
- if (prof.rows.length > 0 && prof.rows[0].FullName) req.session.user.fullname = prof.rows[0].FullName;
444
+ const prof = await authRepo.getUserFullNameByUsername(row.UserName, 'reload-get-fullname');
445
+ if (prof && prof.FullName) req.session.user.fullname = prof.FullName;
475
446
  } catch (profileErr) {
476
447
  console.error(`[mbkauthe] Error fetching fullname during reload:`, profileErr);
477
448
  }