cynx-lib 1.3.83 → 1.3.85

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 (42) hide show
  1. package/gen.js +77 -77
  2. package/package.json +5 -1
  3. package/ui/components/Drawer.jsx +208 -208
  4. package/ui/components/Layout.jsx +37 -0
  5. package/ui/components/Pagination.jsx +145 -145
  6. package/ui/components/PrivacyBlur.jsx +603 -603
  7. package/ui/components/Sidebar.jsx +300 -134
  8. package/utils/index.cjs +5 -0
  9. package/utils/index.js +22 -20
  10. package/utils/utils/duration.js +139 -139
  11. package/utils/utils/oidc.js +214 -214
  12. package/utils/utils/realtime.js +1634 -1634
  13. package/utils/utils/session.cjs +15 -2
  14. package/utils/utils/session.js +67 -58
  15. package/utils/utils/session_manager.cjs +490 -0
  16. package/utils/utils/session_manager.js +549 -0
  17. package/utils/utils/storage.js +393 -393
  18. package/utils/utils/ui.js +111 -111
  19. package/vue/components/Badge.vue +79 -79
  20. package/vue/components/Button.vue +209 -209
  21. package/vue/components/Checkbox.vue +103 -103
  22. package/vue/components/CircularProgress.vue +383 -383
  23. package/vue/components/Divider.vue +94 -94
  24. package/vue/components/Drawer.vue +243 -243
  25. package/vue/components/InfoBadge.vue +223 -223
  26. package/vue/components/Input.vue +341 -341
  27. package/vue/components/LinearProgress.vue +268 -268
  28. package/vue/components/Modal.vue +226 -226
  29. package/vue/components/Pagination.vue +199 -199
  30. package/vue/components/Select.vue +713 -713
  31. package/vue/components/Sidebar.vue +955 -955
  32. package/vue/components/Skeleton.vue +91 -91
  33. package/vue/components/Spinner.vue +263 -263
  34. package/vue/components/StatusTag.vue +89 -89
  35. package/vue/components/TabBar.vue +176 -176
  36. package/vue/components/Table.vue +388 -388
  37. package/vue/components/TimePicker.vue +399 -399
  38. package/vue/components/Toggle.vue +115 -115
  39. package/vue/components/XTable.vue +119 -119
  40. package/vue/composables/useAutoSkeletonRows.ts +71 -71
  41. package/vue/index.d.ts +67 -67
  42. package/vue/index.js +86 -86
@@ -17,12 +17,19 @@ var __copyProps = (to, from, except, desc) => {
17
17
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
18
  var session_exports = {};
19
19
  __export(session_exports, {
20
+ SessionManager: () => import_session_manager.SessionManager,
21
+ createSessionManager: () => import_session_manager.createSessionManager,
20
22
  default: () => session_default,
21
23
  deriveDeviceName: () => deriveDeviceName,
22
- genSessionToken: () => genSessionToken
24
+ extractToken: () => import_session_manager.extractToken,
25
+ formatToken: () => import_session_manager.formatToken,
26
+ genSessionToken: () => genSessionToken,
27
+ getSessionId: () => import_session_manager.getSessionId,
28
+ parseToken: () => import_session_manager.parseToken
23
29
  });
24
30
  module.exports = __toCommonJS(session_exports);
25
31
  var import_uid = require("./uid.cjs");
32
+ var import_session_manager = require("./session_manager.cjs");
26
33
  function genSessionToken(byteLength = 32) {
27
34
  const bytes = (0, import_uid.getRandomBytes)(byteLength);
28
35
  let hex = "";
@@ -56,6 +63,12 @@ var session_default = {
56
63
  };
57
64
  // Annotate the CommonJS export names for ESM import in node:
58
65
  0 && (module.exports = {
66
+ SessionManager,
67
+ createSessionManager,
59
68
  deriveDeviceName,
60
- genSessionToken
69
+ extractToken,
70
+ formatToken,
71
+ genSessionToken,
72
+ getSessionId,
73
+ parseToken
61
74
  });
@@ -1,58 +1,67 @@
1
- /**
2
- * cynx/utils — Session and Device Resolution Utilities (session.js)
3
- *
4
- * Helpers for generating secure session tokens and extracting human-friendly device names
5
- * from request headers and user-agent strings.
6
- */
7
-
8
- import { getRandomBytes } from './uid.js';
9
-
10
- /**
11
- * Generate a cryptographically secure random session token.
12
- * Example: "st_9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d..."
13
- *
14
- * @param {number} [byteLength=32]
15
- * @returns {string}
16
- */
17
- export function genSessionToken(byteLength = 32) {
18
- const bytes = getRandomBytes(byteLength);
19
- let hex = '';
20
- for (let i = 0; i < bytes.length; i++) {
21
- hex += bytes[i].toString(16).padStart(2, '0');
22
- }
23
- return `st_${hex}`;
24
- }
25
-
26
- /**
27
- * Derive human-readable device name from Express/Fastify request or User-Agent string.
28
- *
29
- * @param {object} [req] HTTP request object
30
- * @param {string} [userAgent=''] User-Agent string
31
- * @returns {string} e.g. "Windows PC", "Mac", "iPhone", "Android Device", "Desktop PC"
32
- */
33
- export function deriveDeviceName(req, userAgent = '') {
34
- const headers = req?.headers || {};
35
- const fromHeader = headers['x-device-name'] || headers['x-pc-name'] || headers['x-computer-name'] || '';
36
- if (typeof fromHeader === 'string' && fromHeader.trim()) {
37
- return fromHeader.trim().slice(0, 120);
38
- }
39
-
40
- const fromBody = req?.body?.deviceName || req?.body?.device_name || req?.body?.pcName || req?.body?.pc_name;
41
- if (typeof fromBody === 'string' && fromBody.trim()) {
42
- return fromBody.trim().slice(0, 120);
43
- }
44
-
45
- const ua = userAgent || headers['user-agent'] || '';
46
- if (/windows/i.test(ua)) return 'Windows PC';
47
- if (/macintosh|mac os/i.test(ua)) return 'Mac';
48
- if (/iphone/i.test(ua)) return 'iPhone';
49
- if (/ipad/i.test(ua)) return 'iPad';
50
- if (/android/i.test(ua)) return 'Android Device';
51
- if (/linux/i.test(ua)) return 'Linux PC';
52
- return 'Desktop PC';
53
- }
54
-
55
- export default {
56
- genSessionToken,
57
- deriveDeviceName,
58
- };
1
+ /**
2
+ * cynx/utils — Session and Device Resolution Utilities (session.js)
3
+ *
4
+ * Helpers for generating secure session tokens and extracting human-friendly device names
5
+ * from request headers and user-agent strings.
6
+ */
7
+
8
+ import { getRandomBytes } from './uid.js';
9
+
10
+ /**
11
+ * Generate a cryptographically secure random session token.
12
+ * Example: "st_9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d..."
13
+ *
14
+ * @param {number} [byteLength=32]
15
+ * @returns {string}
16
+ */
17
+ export function genSessionToken(byteLength = 32) {
18
+ const bytes = getRandomBytes(byteLength);
19
+ let hex = '';
20
+ for (let i = 0; i < bytes.length; i++) {
21
+ hex += bytes[i].toString(16).padStart(2, '0');
22
+ }
23
+ return `st_${hex}`;
24
+ }
25
+
26
+ /**
27
+ * Derive human-readable device name from Express/Fastify request or User-Agent string.
28
+ *
29
+ * @param {object} [req] HTTP request object
30
+ * @param {string} [userAgent=''] User-Agent string
31
+ * @returns {string} e.g. "Windows PC", "Mac", "iPhone", "Android Device", "Desktop PC"
32
+ */
33
+ export function deriveDeviceName(req, userAgent = '') {
34
+ const headers = req?.headers || {};
35
+ const fromHeader = headers['x-device-name'] || headers['x-pc-name'] || headers['x-computer-name'] || '';
36
+ if (typeof fromHeader === 'string' && fromHeader.trim()) {
37
+ return fromHeader.trim().slice(0, 120);
38
+ }
39
+
40
+ const fromBody = req?.body?.deviceName || req?.body?.device_name || req?.body?.pcName || req?.body?.pc_name;
41
+ if (typeof fromBody === 'string' && fromBody.trim()) {
42
+ return fromBody.trim().slice(0, 120);
43
+ }
44
+
45
+ const ua = userAgent || headers['user-agent'] || '';
46
+ if (/windows/i.test(ua)) return 'Windows PC';
47
+ if (/macintosh|mac os/i.test(ua)) return 'Mac';
48
+ if (/iphone/i.test(ua)) return 'iPhone';
49
+ if (/ipad/i.test(ua)) return 'iPad';
50
+ if (/android/i.test(ua)) return 'Android Device';
51
+ if (/linux/i.test(ua)) return 'Linux PC';
52
+ return 'Desktop PC';
53
+ }
54
+
55
+ export {
56
+ formatToken,
57
+ parseToken,
58
+ getSessionId,
59
+ extractToken,
60
+ SessionManager,
61
+ createSessionManager,
62
+ } from './session_manager.js';
63
+
64
+ export default {
65
+ genSessionToken,
66
+ deriveDeviceName,
67
+ };
@@ -0,0 +1,490 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+ var session_manager_exports = {};
19
+ __export(session_manager_exports, {
20
+ SessionManager: () => SessionManager,
21
+ createSessionManager: () => createSessionManager,
22
+ default: () => session_manager_default,
23
+ extractToken: () => extractToken,
24
+ formatToken: () => formatToken,
25
+ getSessionId: () => getSessionId,
26
+ parseToken: () => parseToken
27
+ });
28
+ module.exports = __toCommonJS(session_manager_exports);
29
+ var import_uid = require("./uid.cjs");
30
+ var import_duration = require("./duration.cjs");
31
+ var import_session = require("./session.cjs");
32
+ function formatToken(sessionId, secret, prefix = "st_") {
33
+ return `${prefix}${sessionId}$${secret}`;
34
+ }
35
+ function parseToken(token, prefix = "st_") {
36
+ if (!token || typeof token !== "string") {
37
+ return { version: 0, sessionId: null, secret: null, prefix, raw: "", isValid: false };
38
+ }
39
+ const clean = token.replace(/^Bearer\s+/i, "").trim();
40
+ if (!clean) {
41
+ return { version: 0, sessionId: null, secret: null, prefix, raw: "", isValid: false };
42
+ }
43
+ if (clean.includes("$")) {
44
+ const parts = clean.split("$");
45
+ const head = parts[0];
46
+ const secret = parts.slice(1).join("$");
47
+ const sessionId = head.startsWith(prefix) ? head.slice(prefix.length) : head;
48
+ return {
49
+ version: 2,
50
+ sessionId,
51
+ secret,
52
+ prefix,
53
+ raw: clean,
54
+ isValid: Boolean(sessionId && secret)
55
+ };
56
+ }
57
+ return {
58
+ version: 1,
59
+ sessionId: null,
60
+ secret: clean,
61
+ prefix,
62
+ raw: clean,
63
+ isValid: true
64
+ };
65
+ }
66
+ function getSessionId(token, prefix = "st_") {
67
+ const parsed = parseToken(token, prefix);
68
+ return parsed.sessionId;
69
+ }
70
+ function extractToken(req) {
71
+ if (!req) return "";
72
+ const headers = req.headers || {};
73
+ const auth = headers.authorization || headers.Authorization || "";
74
+ if (typeof auth === "string" && auth.startsWith("Bearer ")) {
75
+ return auth.split(" ")[1].trim();
76
+ }
77
+ const custom = headers["x-auth-token"] || headers["x-session-token"] || headers["x-token"] || "";
78
+ if (typeof custom === "string" && custom.trim()) {
79
+ return custom.trim();
80
+ }
81
+ if (req.query && req.query.token) {
82
+ return String(req.query.token).trim();
83
+ }
84
+ return "";
85
+ }
86
+ class SessionManager {
87
+ constructor(options = {}) {
88
+ this.options = {
89
+ query: options.query || null,
90
+ sessionsTable: options.sessionsTable || "user_sessions",
91
+ usersTable: options.usersTable || "users",
92
+ rolesTable: options.rolesTable || "roles",
93
+ settingsTable: options.settingsTable || "system_settings",
94
+ superRole: (options.superRole || "superadmin").toLowerCase(),
95
+ tokenPrefix: options.tokenPrefix || "st_",
96
+ tokenVersion: options.tokenVersion || 2,
97
+ secretBytes: options.secretBytes || 32,
98
+ idLength: options.idLength || 12,
99
+ defaultAbsoluteTimeoutMs: options.defaultAbsoluteTimeoutMs || 30 * 24 * 60 * 60 * 1e3,
100
+ inactivityCacheTtlMs: options.inactivityCacheTtlMs || 3e4,
101
+ jwtSecret: options.jwtSecret || (typeof process !== "undefined" ? process.env.JWT_SECRET : ""),
102
+ ...options
103
+ };
104
+ this._inactTimeoutMs = null;
105
+ this._inactFetchedAt = 0;
106
+ }
107
+ async _query(sql, params = []) {
108
+ if (typeof this.options.query !== "function") {
109
+ throw new Error("[cynx/session] Database query function not configured in SessionManager.");
110
+ }
111
+ return this.options.query(sql, params);
112
+ }
113
+ parseToken(token) {
114
+ return parseToken(token, this.options.tokenPrefix);
115
+ }
116
+ getSessionId(token) {
117
+ return getSessionId(token, this.options.tokenPrefix);
118
+ }
119
+ extractToken(req) {
120
+ return extractToken(req);
121
+ }
122
+ formatToken(sessionId, secret) {
123
+ return formatToken(sessionId, secret, this.options.tokenPrefix);
124
+ }
125
+ async getInactivityTimeoutMs() {
126
+ const now = Date.now();
127
+ if (this._inactTimeoutMs !== null && now - this._inactFetchedAt < this.options.inactivityCacheTtlMs) {
128
+ return this._inactTimeoutMs;
129
+ }
130
+ try {
131
+ const sql = `SELECT value FROM ${this.options.settingsTable} WHERE key = 'session_inactivity_timeout'`;
132
+ const sRes = await this._query(sql).catch(() => ({ rows: [] }));
133
+ if (sRes.rows && sRes.rows.length > 0) {
134
+ let val = sRes.rows[0].value;
135
+ if (typeof val === "string") {
136
+ try {
137
+ val = JSON.parse(val);
138
+ } catch {
139
+ }
140
+ }
141
+ this._inactTimeoutMs = (0, import_duration.parseDurationMs)(val, 0);
142
+ } else {
143
+ this._inactTimeoutMs = 0;
144
+ }
145
+ } catch {
146
+ this._inactTimeoutMs = 0;
147
+ }
148
+ this._inactFetchedAt = now;
149
+ return this._inactTimeoutMs;
150
+ }
151
+ clearSettingsCache() {
152
+ this._inactTimeoutMs = null;
153
+ this._inactFetchedAt = 0;
154
+ }
155
+ /**
156
+ * Create and persist a new session for a user.
157
+ *
158
+ * @param {string|number} userId
159
+ * @param {object} [req] HTTP request object for device and IP detection
160
+ * @param {object} [options]
161
+ * @returns {Promise<string>} Generated session token
162
+ */
163
+ async createSession(userId, req = null, options = {}) {
164
+ const id = (0, import_uid.genUid)(this.options.idLength);
165
+ const userAgent = (req?.headers ? req.headers["user-agent"] || "" : "").slice(0, 500);
166
+ const ipAddress = (req?.headers ? req.headers["x-forwarded-for"] || req.socket?.remoteAddress || "" : "").slice(0, 45);
167
+ const deviceName = (0, import_session.deriveDeviceName)(req, userAgent);
168
+ let token;
169
+ if (this.options.tokenVersion === 2) {
170
+ const bytes = (0, import_uid.getRandomBytes)(this.options.secretBytes);
171
+ let hex = "";
172
+ for (let i = 0; i < bytes.length; i++) {
173
+ hex += bytes[i].toString(16).padStart(2, "0");
174
+ }
175
+ token = formatToken(id, hex, this.options.tokenPrefix);
176
+ } else {
177
+ token = (0, import_session.genSessionToken)(this.options.secretBytes);
178
+ }
179
+ let absoluteMs = this.options.defaultAbsoluteTimeoutMs;
180
+ try {
181
+ const sRes = await this._query(`SELECT value FROM ${this.options.settingsTable} WHERE key = 'session_absolute_timeout'`);
182
+ if (sRes.rows && sRes.rows.length > 0) {
183
+ let val = sRes.rows[0].value;
184
+ if (typeof val === "string") {
185
+ try {
186
+ val = JSON.parse(val);
187
+ } catch {
188
+ }
189
+ }
190
+ const parsed = (0, import_duration.parseDurationMs)(val, absoluteMs);
191
+ if (parsed > 0) absoluteMs = parsed;
192
+ }
193
+ } catch {
194
+ }
195
+ const expiresAt = new Date(Date.now() + absoluteMs);
196
+ const insertSql = `
197
+ INSERT INTO ${this.options.sessionsTable} (id, user_id, token, user_agent, device_name, ip_address, is_active, expires_at)
198
+ VALUES ($1, $2, $3, $4, $5, $6, true, $7)
199
+ `;
200
+ await this._query(insertSql, [id, userId, token, userAgent, deviceName, ipAddress, expiresAt]);
201
+ return token;
202
+ }
203
+ /**
204
+ * Validate a session token against PostgreSQL database and users table.
205
+ * Seamlessly resolves v2 compound tokens (O(1) lookup by primary key id) and v1 tokens.
206
+ *
207
+ * @param {string} token
208
+ * @param {object} [options] Options: { updateActivity: boolean }
209
+ * @returns {Promise<object|null>}
210
+ */
211
+ async validateSession(token, options = {}) {
212
+ if (!token || typeof token !== "string") return null;
213
+ const cleanToken = token.replace(/^Bearer\s+/i, "").trim();
214
+ if (!cleanToken) return null;
215
+ try {
216
+ const parsed = parseToken(cleanToken, this.options.tokenPrefix);
217
+ let sessionRes;
218
+ if (parsed.version === 2 && parsed.sessionId) {
219
+ const sql = `
220
+ SELECT s.id as session_id, s.user_id, s.token, s.is_active as session_active,
221
+ s.expires_at, s.last_activity, s.device_name, s.ip_address,
222
+ u.id as user_id, u.username, u.full_name, u.role, u.is_active as user_active,
223
+ COALESCE(u.is_disabled, false) as user_disabled,
224
+ u.employee_id, u.leader_id,
225
+ u.permissions as user_permissions,
226
+ r.permissions as role_permissions
227
+ FROM ${this.options.sessionsTable} s
228
+ JOIN ${this.options.usersTable} u ON u.id = s.user_id
229
+ LEFT JOIN ${this.options.rolesTable} r ON r.id = u.role OR r.name = u.role OR LOWER(r.name) = LOWER(u.role)
230
+ WHERE s.id = $1
231
+ `;
232
+ sessionRes = await this._query(sql, [parsed.sessionId]).catch(() => ({ rows: [] }));
233
+ } else {
234
+ const sql = `
235
+ SELECT s.id as session_id, s.user_id, s.token, s.is_active as session_active,
236
+ s.expires_at, s.last_activity, s.device_name, s.ip_address,
237
+ u.id as user_id, u.username, u.full_name, u.role, u.is_active as user_active,
238
+ COALESCE(u.is_disabled, false) as user_disabled,
239
+ u.employee_id, u.leader_id,
240
+ u.permissions as user_permissions,
241
+ r.permissions as role_permissions
242
+ FROM ${this.options.sessionsTable} s
243
+ JOIN ${this.options.usersTable} u ON u.id = s.user_id
244
+ LEFT JOIN ${this.options.rolesTable} r ON r.id = u.role OR r.name = u.role OR LOWER(r.name) = LOWER(u.role)
245
+ WHERE s.token = $1
246
+ `;
247
+ sessionRes = await this._query(sql, [cleanToken]).catch(() => ({ rows: [] }));
248
+ }
249
+ if (sessionRes.rows && sessionRes.rows.length > 0) {
250
+ const row = sessionRes.rows[0];
251
+ if (row.token !== cleanToken) {
252
+ return null;
253
+ }
254
+ const now = /* @__PURE__ */ new Date();
255
+ if (!row.session_active || !row.user_active || row.user_disabled || new Date(row.expires_at) < now) {
256
+ return null;
257
+ }
258
+ const inactTimeoutMs = await this.getInactivityTimeoutMs();
259
+ if (inactTimeoutMs > 0 && row.last_activity) {
260
+ const lastAct = new Date(row.last_activity).getTime();
261
+ if (now.getTime() - lastAct > inactTimeoutMs) {
262
+ this._query(`UPDATE ${this.options.sessionsTable} SET is_active = false WHERE id = $1`, [row.session_id]).catch(() => {
263
+ });
264
+ return null;
265
+ }
266
+ }
267
+ if (options.updateActivity !== false) {
268
+ this._query(`UPDATE ${this.options.sessionsTable} SET last_activity = NOW() WHERE id = $1`, [row.session_id]).catch(() => {
269
+ });
270
+ }
271
+ return {
272
+ valid: true,
273
+ userId: row.user_id,
274
+ sessionId: row.session_id,
275
+ token: cleanToken,
276
+ role: row.role,
277
+ employeeId: row.employee_id || null,
278
+ leaderId: row.leader_id || null,
279
+ userPermissions: row.user_permissions || {},
280
+ rolePermissions: row.role_permissions || {},
281
+ user: {
282
+ id: row.user_id,
283
+ username: row.username,
284
+ fullName: row.full_name,
285
+ role: row.role,
286
+ employeeId: row.employee_id,
287
+ leaderId: row.leader_id
288
+ }
289
+ };
290
+ }
291
+ if (this.options.jwtSecret) {
292
+ try {
293
+ const jwtModule = typeof require === "function" ? require("jsonwebtoken") : null;
294
+ if (jwtModule) {
295
+ const decoded = jwtModule.verify(cleanToken, this.options.jwtSecret);
296
+ if (decoded && (decoded.userId || decoded.id)) {
297
+ const uId = decoded.userId || decoded.id;
298
+ const uRes = await this._query(
299
+ `SELECT u.id, u.username, u.full_name, u.role, u.is_active, COALESCE(u.is_disabled, false) as is_disabled,
300
+ u.employee_id, u.leader_id, u.permissions as user_permissions, r.permissions as role_permissions
301
+ FROM ${this.options.usersTable} u
302
+ LEFT JOIN ${this.options.rolesTable} r ON r.id = u.role OR r.name = u.role OR LOWER(r.name) = LOWER(u.role)
303
+ WHERE u.id = $1`,
304
+ [uId]
305
+ ).catch(() => ({ rows: [] }));
306
+ if (uRes.rows && uRes.rows.length > 0 && uRes.rows[0].is_active && !uRes.rows[0].is_disabled) {
307
+ const uRow = uRes.rows[0];
308
+ return {
309
+ valid: true,
310
+ userId: uRow.id,
311
+ sessionId: null,
312
+ token: cleanToken,
313
+ role: uRow.role || decoded.role,
314
+ employeeId: uRow.employee_id || null,
315
+ leaderId: uRow.leader_id || null,
316
+ userPermissions: uRow.user_permissions || {},
317
+ rolePermissions: uRow.role_permissions || {},
318
+ user: {
319
+ id: uRow.id,
320
+ username: uRow.username,
321
+ fullName: uRow.full_name,
322
+ role: uRow.role || decoded.role,
323
+ employeeId: uRow.employee_id,
324
+ leaderId: uRow.leader_id
325
+ }
326
+ };
327
+ }
328
+ }
329
+ }
330
+ } catch {
331
+ }
332
+ }
333
+ return null;
334
+ } catch (err) {
335
+ console.warn("[SessionManager validateSession error]:", err.message);
336
+ return null;
337
+ }
338
+ }
339
+ /**
340
+ * Lock a session screen.
341
+ *
342
+ * @param {string} sessionIdOrToken Session ID or Token
343
+ * @returns {Promise<{ ok: boolean, sessionId: string, userId: string, token: string } | null>}
344
+ */
345
+ async lockSession(sessionIdOrToken) {
346
+ if (!sessionIdOrToken) return null;
347
+ const parsed = parseToken(sessionIdOrToken, this.options.tokenPrefix);
348
+ const idToUse = parsed.sessionId || sessionIdOrToken;
349
+ const sql = `
350
+ UPDATE ${this.options.sessionsTable}
351
+ SET is_locked = true, locked_at = NOW()
352
+ WHERE (id = $1 OR token = $1) AND is_active = true
353
+ RETURNING id, user_id, token
354
+ `;
355
+ const r = await this._query(sql, [idToUse]);
356
+ if (r.rows && r.rows.length > 0) {
357
+ const row = r.rows[0];
358
+ return { ok: true, sessionId: row.id, userId: row.user_id, token: row.token };
359
+ }
360
+ return null;
361
+ }
362
+ /**
363
+ * Unlock a session screen.
364
+ *
365
+ * @param {string} sessionIdOrToken Session ID or Token
366
+ * @returns {Promise<{ ok: boolean, sessionId: string, userId: string, token: string } | null>}
367
+ */
368
+ async unlockSession(sessionIdOrToken) {
369
+ if (!sessionIdOrToken) return null;
370
+ const parsed = parseToken(sessionIdOrToken, this.options.tokenPrefix);
371
+ const idToUse = parsed.sessionId || sessionIdOrToken;
372
+ const sql = `
373
+ UPDATE ${this.options.sessionsTable}
374
+ SET is_locked = false, locked_at = NULL
375
+ WHERE (id = $1 OR token = $1) AND is_active = true
376
+ RETURNING id, user_id, token
377
+ `;
378
+ const r = await this._query(sql, [idToUse]);
379
+ if (r.rows && r.rows.length > 0) {
380
+ const row = r.rows[0];
381
+ return { ok: true, sessionId: row.id, userId: row.user_id, token: row.token };
382
+ }
383
+ return null;
384
+ }
385
+ /**
386
+ * Terminate/revoke a single session.
387
+ *
388
+ * @param {string} sessionIdOrToken Session ID or Token
389
+ * @returns {Promise<{ ok: boolean, sessionId: string, userId: string, token: string } | null>}
390
+ */
391
+ async terminateSession(sessionIdOrToken) {
392
+ if (!sessionIdOrToken) return null;
393
+ const parsed = parseToken(sessionIdOrToken, this.options.tokenPrefix);
394
+ const idToUse = parsed.sessionId || sessionIdOrToken;
395
+ const sql = `
396
+ UPDATE ${this.options.sessionsTable}
397
+ SET is_active = false
398
+ WHERE id = $1 OR token = $1
399
+ RETURNING id, user_id, token
400
+ `;
401
+ const r = await this._query(sql, [idToUse]);
402
+ if (r.rows && r.rows.length > 0) {
403
+ const row = r.rows[0];
404
+ return { ok: true, sessionId: row.id, userId: row.user_id, token: row.token };
405
+ }
406
+ return null;
407
+ }
408
+ /**
409
+ * Terminate all other sessions except the current one.
410
+ *
411
+ * @param {string} currentSessionIdOrToken Current session ID or token to preserve
412
+ * @param {string|number} [currentUserId]
413
+ * @param {boolean} [protectSuperAdmin=true]
414
+ * @returns {Promise<Array<{ sessionId: string, userId: string, token: string }>>}
415
+ */
416
+ async terminateOtherSessions(currentSessionIdOrToken, currentUserId = null, protectSuperAdmin = true) {
417
+ const parsed = parseToken(currentSessionIdOrToken, this.options.tokenPrefix);
418
+ const preserveId = parsed.sessionId || currentSessionIdOrToken;
419
+ let whereClause = `id != $1 AND token != $1`;
420
+ let params = [preserveId];
421
+ if (currentUserId) {
422
+ whereClause += ` AND user_id = $2`;
423
+ params.push(currentUserId);
424
+ } else if (protectSuperAdmin) {
425
+ whereClause += ` AND user_id NOT IN (SELECT id FROM ${this.options.usersTable} WHERE LOWER(COALESCE(role, '')) = '${this.options.superRole}' OR LOWER(username) = '${this.options.superRole}')`;
426
+ }
427
+ const sql = `
428
+ UPDATE ${this.options.sessionsTable}
429
+ SET is_active = false
430
+ WHERE ${whereClause} AND is_active = true
431
+ RETURNING id, user_id, token
432
+ `;
433
+ const r = await this._query(sql, params);
434
+ return (r.rows || []).map((row) => ({
435
+ sessionId: row.id,
436
+ userId: row.user_id,
437
+ token: row.token
438
+ }));
439
+ }
440
+ /**
441
+ * Terminate all active sessions for a specific user.
442
+ *
443
+ * @param {string|number} userId
444
+ * @returns {Promise<Array<{ sessionId: string, userId: string, token: string }>>}
445
+ */
446
+ async terminateUserSessions(userId) {
447
+ if (!userId) return [];
448
+ const sql = `
449
+ UPDATE ${this.options.sessionsTable}
450
+ SET is_active = false
451
+ WHERE user_id = $1 AND is_active = true
452
+ RETURNING id, user_id, token
453
+ `;
454
+ const r = await this._query(sql, [userId]);
455
+ return (r.rows || []).map((row) => ({
456
+ sessionId: row.id,
457
+ userId: row.user_id,
458
+ token: row.token
459
+ }));
460
+ }
461
+ /**
462
+ * Aliases for backward compatibility.
463
+ */
464
+ async revokeSession(tokenOrId) {
465
+ return this.terminateSession(tokenOrId);
466
+ }
467
+ async revokeAllUserSessions(userId) {
468
+ return this.terminateUserSessions(userId);
469
+ }
470
+ }
471
+ function createSessionManager(options) {
472
+ return new SessionManager(options);
473
+ }
474
+ var session_manager_default = {
475
+ SessionManager,
476
+ createSessionManager,
477
+ formatToken,
478
+ parseToken,
479
+ getSessionId,
480
+ extractToken
481
+ };
482
+ // Annotate the CommonJS export names for ESM import in node:
483
+ 0 && (module.exports = {
484
+ SessionManager,
485
+ createSessionManager,
486
+ extractToken,
487
+ formatToken,
488
+ getSessionId,
489
+ parseToken
490
+ });