mbkauthe 4.8.4 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,60 @@ const getEncryptionKey = () => {
14
14
  return crypto.createHash('sha256').update(COOKIE_ENCRYPTION_KEY).digest();
15
15
  };
16
16
 
17
+ const getSigningKey = () => {
18
+ return crypto.createHash('sha256').update(`${COOKIE_ENCRYPTION_KEY}:cookie-signing`).digest();
19
+ };
20
+
21
+ const encodePayload = (data) => {
22
+ return Buffer.from(JSON.stringify(data), 'utf8').toString('base64url');
23
+ };
24
+
25
+ const decodePayload = (encoded) => {
26
+ return JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
27
+ };
28
+
29
+ const signCookiePayload = (encodedPayload) => {
30
+ return crypto.createHmac('sha256', getSigningKey()).update(encodedPayload).digest('hex');
31
+ };
32
+
33
+ const verifyCookieSignature = (encodedPayload, signature) => {
34
+ if (!encodedPayload || !signature || typeof encodedPayload !== 'string' || typeof signature !== 'string') {
35
+ return false;
36
+ }
37
+
38
+ const expected = signCookiePayload(encodedPayload);
39
+ const expectedBuffer = Buffer.from(expected, 'hex');
40
+ const actualBuffer = Buffer.from(signature, 'hex');
41
+
42
+ return expectedBuffer.length === actualBuffer.length && crypto.timingSafeEqual(expectedBuffer, actualBuffer);
43
+ };
44
+
45
+ const createSignedCookiePayload = (data) => {
46
+ try {
47
+ const payload = encodePayload(data);
48
+ return {
49
+ payload,
50
+ signature: signCookiePayload(payload)
51
+ };
52
+ } catch (error) {
53
+ console.error(`[mbkauthe] Cookie signing error:`, error);
54
+ return null;
55
+ }
56
+ };
57
+
58
+ const parseSignedCookiePayload = (signedPayload) => {
59
+ try {
60
+ if (!signedPayload || !verifyCookieSignature(signedPayload.payload, signedPayload.signature)) {
61
+ return null;
62
+ }
63
+
64
+ return decodePayload(signedPayload.payload);
65
+ } catch (error) {
66
+ console.error(`[mbkauthe] Cookie signature verification error:`, error);
67
+ return null;
68
+ }
69
+ };
70
+
17
71
  // Encrypt and sign cookie payload
18
72
  const encryptCookiePayload = (data) => {
19
73
  try {
@@ -160,33 +214,46 @@ export { getCookieOptions, getClearCookieOptions };
160
214
  const parseAccountList = (raw, req) => {
161
215
  if (!raw) return [];
162
216
  try {
163
- // First, decrypt the cookie payload
164
217
  const parsed = JSON.parse(raw);
165
- const decrypted = decryptCookiePayload(parsed);
218
+ let data = parseSignedCookiePayload(parsed);
219
+ let isLegacyEncryptedCookie = false;
220
+
221
+ // Backward compatibility for previously encrypted account-list cookies.
222
+ if (!data && parsed.iv && parsed.authTag && parsed.data) {
223
+ data = decryptCookiePayload(parsed);
224
+ isLegacyEncryptedCookie = true;
225
+ }
166
226
 
167
- if (!decrypted || !decrypted.accounts || !decrypted.fingerprint) {
227
+ if (!data || !data.accounts || !data.fingerprint) {
168
228
  return [];
169
229
  }
170
230
 
171
231
  // Verify fingerprint matches current request
172
232
  const currentFingerprint = generateFingerprint(req);
173
- if (decrypted.fingerprint !== currentFingerprint) {
233
+ if (data.fingerprint !== currentFingerprint) {
174
234
  console.warn(`[mbkauthe] Cookie fingerprint mismatch - possible cookie theft attempt`);
175
235
  return [];
176
236
  }
177
237
 
178
- const accounts = decrypted.accounts;
238
+ const accounts = data.accounts;
179
239
  if (!Array.isArray(accounts)) return [];
180
240
 
181
241
  // Accept only minimal safe fields
182
242
  return accounts
183
243
  .filter(item => item && typeof item === 'object')
184
- .map(item => ({
185
- sessionId: typeof item.sessionId === 'string' ? item.sessionId : null,
186
- username: typeof item.username === 'string' ? item.username : null,
187
- fullName: typeof item.fullName === 'string' ? item.fullName : null,
188
- image: typeof item.image === 'string' ? item.image : null
189
- }))
244
+ .map(item => {
245
+ const rawSessionId = typeof item.sessionId === 'string' ? item.sessionId : null;
246
+ const sessionId = isLegacyEncryptedCookie
247
+ ? rawSessionId
248
+ : decryptSessionId(rawSessionId);
249
+
250
+ return {
251
+ sessionId,
252
+ username: typeof item.username === 'string' ? item.username : null,
253
+ fullName: typeof item.fullName === 'string' ? item.fullName : null,
254
+ image: typeof item.image === 'string' ? item.image : null
255
+ };
256
+ })
190
257
  .filter(item => item.sessionId && item.username)
191
258
  .slice(0, MAX_REMEMBERED_ACCOUNTS);
192
259
  } catch (error) {
@@ -200,7 +267,7 @@ const writeAccountList = (res, list, req) => {
200
267
 
201
268
  // Clean and limit fields to safe values (limit image URL length)
202
269
  const cleaned = sanitized.map(item => ({
203
- sessionId: item && item.sessionId ? item.sessionId : null,
270
+ sessionId: item && item.sessionId ? encryptSessionId(item.sessionId) : null,
204
271
  username: item && item.username ? item.username : null,
205
272
  fullName: item && item.fullName ? item.fullName : null,
206
273
  image: (item && typeof item.image === 'string' && item.image.length <= 2048) ? item.image : null
@@ -212,14 +279,13 @@ const writeAccountList = (res, list, req) => {
212
279
  fingerprint: generateFingerprint(req)
213
280
  };
214
281
 
215
- // Encrypt the payload
216
- const encrypted = encryptCookiePayload(payload);
217
- if (!encrypted) {
218
- console.error(`[mbkauthe] Failed to encrypt account list cookie`);
282
+ const signed = createSignedCookiePayload(payload);
283
+ if (!signed) {
284
+ console.error(`[mbkauthe] Failed to sign account list cookie`);
219
285
  return;
220
286
  }
221
287
 
222
- res.cookie(ACCOUNT_LIST_COOKIE, JSON.stringify(encrypted), cachedCookieOptions);
288
+ res.cookie(ACCOUNT_LIST_COOKIE, JSON.stringify(signed), cachedCookieOptions);
223
289
  };
224
290
 
225
291
  export const readAccountListFromCookie = (req) => {
@@ -243,4 +309,4 @@ export const removeAccountFromCookie = (req, res, sessionId) => {
243
309
 
244
310
  export const clearAccountListCookie = (res) => {
245
311
  res.clearCookie(ACCOUNT_LIST_COOKIE, cachedClearCookieOptions);
246
- };
312
+ };
@@ -1,7 +1,9 @@
1
1
  import dotenv from "dotenv";
2
2
  import { createRequire } from "module";
3
+ import { createLogger } from "../utils/logger.js";
3
4
 
4
5
  dotenv.config();
6
+ const logConfig = createLogger("config");
5
7
 
6
8
  // Comprehensive validation function
7
9
  function validateConfiguration() {
@@ -215,7 +217,7 @@ function validateConfiguration() {
215
217
  configParts.push(`defaults: ${usedDefaults.length} keys`);
216
218
  }
217
219
  const configSummary = configParts.length > 0 ? ` (${configParts.join(', ')})` : '';
218
- console.log(`[mbkauthe] Configuration loaded${configSummary}`);
220
+ logConfig(`Configuration loaded${configSummary}`);
219
221
  return mbkautheVar;
220
222
  }
221
223
 
@@ -56,4 +56,4 @@ export function getAvailableScopes() {
56
56
  name: value.name,
57
57
  description: value.description
58
58
  }));
59
- }
59
+ }
@@ -2,33 +2,120 @@
2
2
  import { readFile } from "fs/promises";
3
3
  import path from "path";
4
4
  import { fileURLToPath } from "url";
5
+ import { performance } from "perf_hooks";
5
6
 
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = path.dirname(__filename);
8
9
 
9
10
  async function main() {
11
+ const startTime = performance.now();
12
+
13
+ console.log("[mbkauthe] Starting schema creation...");
14
+
10
15
  const schemaPath = path.resolve(__dirname, "../docs/db.sql");
16
+
17
+ console.log(`[mbkauthe] Schema file: ${schemaPath}`);
18
+
11
19
  const schemaSql = await readFile(schemaPath, "utf8");
12
20
 
21
+ console.log(
22
+ `[mbkauthe] Schema loaded (${Buffer.byteLength(schemaSql, "utf8")} bytes)`
23
+ );
24
+
25
+ const statementCount = schemaSql
26
+ .split(";")
27
+ .map(s => s.trim())
28
+ .filter(Boolean).length;
29
+
30
+ console.log(
31
+ `[mbkauthe] Detected approximately ${statementCount} SQL statements`
32
+ );
33
+
13
34
  try {
35
+ console.log("[mbkauthe] Testing database connection...");
36
+
37
+ const ping = await dblogin.query("SELECT version()");
38
+
39
+ console.log(
40
+ `[mbkauthe] Connected to PostgreSQL`
41
+ );
42
+
43
+ console.log(
44
+ `[mbkauthe] PostgreSQL version: ${ping.rows[0].version}`
45
+ );
46
+
47
+ console.log("[mbkauthe] Applying schema...");
48
+
49
+ const queryStart = performance.now();
50
+
14
51
  const res = await dblogin.query(schemaSql);
15
- console.log(`[mbkauthe] Schema applied successfully.`);
52
+
53
+ const queryDuration = (
54
+ performance.now() - queryStart
55
+ ).toFixed(2);
56
+
57
+ console.log(
58
+ `[mbkauthe] Schema applied successfully in ${queryDuration} ms`
59
+ );
60
+
61
+ console.log(
62
+ `[mbkauthe] Command: ${res.command ?? "MULTI"}`
63
+ );
64
+
65
+ console.log(
66
+ `[mbkauthe] Row count: ${res.rowCount ?? 0}`
67
+ );
68
+
16
69
  if (res?.rows?.length) {
17
- console.log(`[mbkauthe] Rows returned by schema query:`, res.rows);
18
- } else {
19
- console.log(`[mbkauthe] No rows returned by schema query (expected). If you want to verify table creation, query the database separately.`);
70
+ console.log(
71
+ `[mbkauthe] Returned rows: ${res.rows.length}`
72
+ );
20
73
  }
74
+
21
75
  } catch (err) {
22
76
  const IGNORE_CODES = ["42710", "42P07"];
23
- if (err && typeof err.code === "string" && IGNORE_CODES.includes(err.code)) {
24
- console.warn(`[mbkauthe] Schema already exists (ignored):`, err.message);
77
+
78
+ if (
79
+ err &&
80
+ typeof err.code === "string" &&
81
+ IGNORE_CODES.includes(err.code)
82
+ ) {
83
+ console.warn(
84
+ `[mbkauthe] Schema object already exists (ignored)`
85
+ );
86
+
87
+ console.warn(`Code: ${err.code}`);
88
+ console.warn(`Message: ${err.message}`);
25
89
  } else {
26
- console.error(`[mbkauthe] Failed to apply schema:`, err);
90
+ console.error(
91
+ `[mbkauthe] Failed to apply schema`
92
+ );
93
+
94
+ console.error("Code:", err.code);
95
+ console.error("Severity:", err.severity);
96
+ console.error("Position:", err.position);
97
+ console.error("Table:", err.table);
98
+ console.error("Column:", err.column);
99
+ console.error("Constraint:", err.constraint);
100
+ console.error("Detail:", err.detail);
101
+ console.error("Hint:", err.hint);
102
+ console.error("Message:", err.message);
103
+
27
104
  process.exitCode = 1;
28
105
  }
29
106
  } finally {
107
+ console.log("[mbkauthe] Closing database connection...");
108
+
30
109
  await dblogin.end();
110
+
111
+ const totalDuration = (
112
+ performance.now() - startTime
113
+ ).toFixed(2);
114
+
115
+ console.log(
116
+ `[mbkauthe] Finished in ${totalDuration} ms`
117
+ );
31
118
  }
32
119
  }
33
120
 
34
- main();
121
+ main();
@@ -0,0 +1,336 @@
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
+ 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
+
39
+ resolveOAuthProvider(provider) {
40
+ const key = String(provider || "").toLowerCase();
41
+ const config = OAUTH_PROVIDERS[key];
42
+ if (!config) {
43
+ throw new Error(`Unsupported OAuth provider: ${provider}`);
44
+ }
45
+ return config;
46
+ }
47
+
48
+ async fetchActiveSession(sessionId) {
49
+ const query = `SELECT ${this.buildSessionUserSelect({ includeProfile: true })}
50
+ FROM "Sessions" s
51
+ JOIN "Users" u ON s."UserName" = u."UserName"
52
+ WHERE s.id = $1 LIMIT 1`;
53
+ const result = await this.executeRaw({ name: "multi-session-fetch", text: query, values: [sessionId] });
54
+ return result.rows?.[0] || null;
55
+ }
56
+
57
+ async deleteAppSessionById(sessionId, queryName = "invalidate-app-session") {
58
+ const query = `DELETE FROM "Sessions" WHERE id = $1`;
59
+ return this.executeRaw({ name: queryName, text: query, values: [sessionId] });
60
+ }
61
+
62
+ async deleteSessionBySid(sessionId, queryName = "login-delete-old-session-before-regen") {
63
+ const query = `DELETE FROM "session" WHERE sid = $1`;
64
+ return this.executeRaw({ name: queryName, text: query, values: [sessionId] });
65
+ }
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
+
78
+ async touchTrustedDevice(deviceTokenHash, username) {
79
+ const query = `
80
+ UPDATE "TrustedDevices" td
81
+ SET "LastUsed" = NOW()
82
+ FROM "Users" u
83
+ WHERE td."DeviceToken" = $1
84
+ AND td."UserName" = $2
85
+ AND td."ExpiresAt" > NOW()
86
+ AND u."UserName" = td."UserName"
87
+ AND u."Active" = TRUE
88
+ RETURNING td."UserName", td."ExpiresAt", u."id" as id, u."Active", u."Role", u."AllowedApps"
89
+ `;
90
+
91
+ const result = await this.executeRaw({
92
+ name: "check-trusted-device",
93
+ text: query,
94
+ values: [deviceTokenHash, username]
95
+ });
96
+ return result.rows?.[0] || null;
97
+ }
98
+
99
+ async cleanupAndCountUserSessions(username, queryName = "cleanup-and-count-user-sessions") {
100
+ const query = `
101
+ WITH deleted AS (
102
+ DELETE FROM "Sessions"
103
+ WHERE "UserName" = $1
104
+ AND expires_at IS NOT NULL
105
+ AND expires_at <= NOW()
106
+ )
107
+ SELECT COUNT(*)::int AS count
108
+ FROM "Sessions"
109
+ WHERE "UserName" = $1
110
+ `;
111
+
112
+ const result = await this.executeRaw({ name: queryName, text: query, values: [username] });
113
+ return Number(result.rows?.[0]?.count ?? 0);
114
+ }
115
+
116
+ async deleteOldestSessionsForUser(username, limit, queryName = "prune-oldest-user-session") {
117
+ if (!Number.isFinite(limit) || limit <= 0) return 0;
118
+ const query = `DELETE FROM "Sessions" WHERE id IN (SELECT id FROM "Sessions" WHERE "UserName" = $1 ORDER BY created_at ASC LIMIT $2)`;
119
+ const result = await this.executeRaw({ name: queryName, text: query, values: [username, limit] });
120
+ return result.rowCount || 0;
121
+ }
122
+
123
+ async deleteExpiredSessionsForUser(username) {
124
+ const query = this.sql`
125
+ DELETE FROM ${this.table("Sessions")}
126
+ WHERE ${this.ident("UserName")} = ${this.value(username)}
127
+ AND ${this.ident("expires_at")} IS NOT NULL
128
+ AND ${this.ident("expires_at")} <= ${this.now()}
129
+ `;
130
+
131
+ const result = await this.execute("cleanup-expired-user-sessions", query);
132
+ return result.rowCount || 0;
133
+ }
134
+
135
+ async countActiveSessionsForUser(username) {
136
+ const columns = this.columns([`COUNT(*) AS ${this.quoteIdentifier("count")}`]);
137
+ const query = this.sql`
138
+ SELECT ${columns}
139
+ FROM ${this.table("Sessions")}
140
+ WHERE ${this.ident("UserName")} = ${this.value(username)}
141
+ `;
142
+ const result = await this.execute("count-user-sessions", query);
143
+ return Number(result.rows?.[0]?.count ?? 0);
144
+ }
145
+
146
+ async getOldestSessionIds(username, limit) {
147
+ if (!Number.isFinite(limit) || limit <= 0) return [];
148
+
149
+ const query = this.sql`
150
+ SELECT ${this.column("id")}
151
+ FROM ${this.table("Sessions")}
152
+ WHERE ${this.ident("UserName")} = ${this.value(username)}
153
+ ORDER BY ${this.ident("created_at")} ASC
154
+ ${this.limit(limit)}
155
+ `;
156
+
157
+ const result = await this.execute("oldest-user-sessions", query);
158
+ return (result.rows || []).map((row) => row.id).filter(Boolean);
159
+ }
160
+
161
+ async insertAppSession(username, expiresAt, meta) {
162
+ const query = `INSERT INTO "Sessions" ("UserName", expires_at, meta) VALUES ($1, $2, $3) RETURNING id`;
163
+ const result = await this.executeRaw({
164
+ name: "insert-app-session",
165
+ text: query,
166
+ values: [username, expiresAt, meta]
167
+ });
168
+ return result.rows?.[0] || null;
169
+ }
170
+
171
+ async updateLastLoginReturnProfile(userId) {
172
+ const query = `UPDATE "Users" SET "last_login" = NOW() WHERE "id" = $1 RETURNING "FullName", "Image"`;
173
+ const result = await this.executeRaw({
174
+ name: "login-update-last-login-return-profile",
175
+ text: query,
176
+ values: [userId]
177
+ });
178
+ return result.rows?.[0] || null;
179
+ }
180
+
181
+ async getUserProfileByUsername(username, queryName = "login-get-fullname-and-image") {
182
+ const query = `SELECT "FullName", "Image" FROM "Users" WHERE "UserName" = $1 LIMIT 1`;
183
+ const result = await this.executeRaw({ name: queryName, text: query, values: [username] });
184
+ return result.rows?.[0] || null;
185
+ }
186
+
187
+ async insertTrustedDevice({ username, deviceTokenHash, deviceName, userAgent, ipAddress, expiresAt }) {
188
+ const query = `INSERT INTO "TrustedDevices" ("UserName", "DeviceToken", "DeviceName", "UserAgent", "IpAddress", "ExpiresAt")
189
+ VALUES ($1, $2, $3, $4, $5, $6)`;
190
+ return this.executeRaw({
191
+ name: "insert-trusted-device",
192
+ text: query,
193
+ values: [username, deviceTokenHash, deviceName, userAgent, ipAddress, expiresAt]
194
+ });
195
+ }
196
+
197
+ async getUserWithTwoFA(username, queryName = "login-get-user") {
198
+ const query = `
199
+ SELECT u.id, u."UserName", u."PasswordEnc", u."Active", u."Role", u."AllowedApps",
200
+ tfa."TwoFAStatus", u."FullName", u."Image"
201
+ FROM "Users" u
202
+ LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
203
+ WHERE u."UserName" = $1
204
+ `;
205
+
206
+ const result = await this.executeRaw({ name: queryName, text: query, values: [username] });
207
+ return result.rows?.[0] || null;
208
+ }
209
+
210
+ async getTwoFASecret(username) {
211
+ const query = `SELECT tfa."TwoFASecret" FROM "TwoFA" tfa WHERE tfa."UserName" = $1`;
212
+ const result = await this.executeRaw({ name: "verify-2fa-secret", text: query, values: [username] });
213
+ return result.rows?.[0] || null;
214
+ }
215
+
216
+ async deleteSessionsByIds(ids, queryName = "delete-sessions-by-ids") {
217
+ if (!Array.isArray(ids) || ids.length === 0) return 0;
218
+ const query = `DELETE FROM "Sessions" WHERE id = ANY($1)`;
219
+ const result = await this.executeRaw({ name: queryName, text: query, values: [ids] });
220
+ return result.rowCount || 0;
221
+ }
222
+
223
+ async getOAuthUserByProviderId(provider, providerId) {
224
+ const { table, idColumn, queryName } = this.resolveOAuthProvider(provider);
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`;
230
+ const result = await this.executeRaw({ name: queryName, text: query, values: [providerId] });
231
+ return result.rows?.[0] || null;
232
+ }
233
+
234
+ async getApiTokenByHash(tokenHash, queryName = "validate-api-token") {
235
+ const query = `
236
+ SELECT t.id, t."UserName", t."ExpiresAt", t."Permissions",
237
+ u.id as uid, u."Active", u."Role", u."AllowedApps" as user_allowed_apps, u."FullName"
238
+ FROM "ApiTokens" t
239
+ JOIN "Users" u ON t."UserName" = u."UserName"
240
+ WHERE t."TokenHash" = $1 LIMIT 1
241
+ `;
242
+
243
+ const result = await this.executeRaw({ name: queryName, text: query, values: [tokenHash] });
244
+ return result.rows?.[0] || null;
245
+ }
246
+
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];
258
+ if (queryName) {
259
+ return this.executeRaw({ name: queryName, text: query, values });
260
+ }
261
+ return this.executeRaw({ text: query, values });
262
+ }
263
+
264
+ async getSessionAuthData(sessionId, queryName = "validate-app-session") {
265
+ const query = `
266
+ SELECT s.expires_at, u."Active", u."Role", u."AllowedApps", u."UserName"
267
+ FROM "Sessions" s
268
+ JOIN "Users" u ON s."UserName" = u."UserName"
269
+ WHERE s.id = $1
270
+ LIMIT 1
271
+ `;
272
+
273
+ const result = await this.executeRaw({ name: queryName, text: query, values: [sessionId] });
274
+ return result.rows?.[0] || null;
275
+ }
276
+
277
+ async getSessionWithUserById(sessionId, queryName = "restore-user-session") {
278
+ const query = `SELECT ${this.buildSessionUserSelect({ includeProfile: true })}
279
+ FROM "Sessions" s
280
+ JOIN "Users" u ON s."UserName" = u."UserName"
281
+ WHERE s.id = $1 LIMIT 1`;
282
+ const result = await this.executeRaw({ name: queryName, text: query, values: [sessionId] });
283
+ return result.rows?.[0] || null;
284
+ }
285
+
286
+ async getSessionWithUserForReload(sessionId, queryName = "reload-session-user") {
287
+ return this.getSessionWithUserById(sessionId, queryName);
288
+ }
289
+
290
+ async getSessionValidationRow(sessionId, queryName = "check-session-validity-by-id") {
291
+ 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`;
292
+ const result = await this.executeRaw({ name: queryName, text: query, values: [sessionId] });
293
+ return result.rows?.[0] || null;
294
+ }
295
+
296
+ async getUserFullNameByUsername(username, queryName = "get-fullname-by-username") {
297
+ const query = `SELECT "FullName" FROM "Users" WHERE "UserName" = $1 LIMIT 1`;
298
+ const result = await this.executeRaw({ name: queryName, text: query, values: [username] });
299
+ return result.rows?.[0] || null;
300
+ }
301
+
302
+ async getUserImageByUsername(username, queryName = "get-user-profile-pic") {
303
+ const query = `SELECT "Image" FROM "Users" WHERE "UserName" = $1 LIMIT 1`;
304
+ const result = await this.executeRaw({ name: queryName, text: query, values: [username] });
305
+ return result.rows?.[0] || null;
306
+ }
307
+
308
+ async getSessionValidity(sessionId, sessionStoreSid, queryName = "check-session-validity") {
309
+ const query = `
310
+ SELECT
311
+ s.expires_at,
312
+ u."Active",
313
+ CASE
314
+ WHEN s.expires_at IS NULL THEN (SELECT expire FROM "session" WHERE sid = $2)
315
+ ELSE NULL
316
+ END AS connect_expire
317
+ FROM "Sessions" s
318
+ JOIN "Users" u ON s."UserName" = u."UserName"
319
+ WHERE s.id = $1
320
+ LIMIT 1
321
+ `;
322
+
323
+ const result = await this.executeRaw({ name: queryName, text: query, values: [sessionId, sessionStoreSid] });
324
+ return result.rows?.[0] || null;
325
+ }
326
+
327
+ async deleteAllAppSessions(queryName = "delete-all-app-sessions") {
328
+ const query = `DELETE FROM "Sessions"`;
329
+ return this.executeRaw({ name: queryName, text: query, values: [] });
330
+ }
331
+
332
+ async deleteActiveSessionStoreRows(queryName = "delete-active-session-store-rows") {
333
+ const query = `DELETE FROM "session" WHERE expire > NOW()`;
334
+ return this.executeRaw({ name: queryName, text: query, values: [] });
335
+ }
336
+ }