cynx-lib 1.3.84 → 1.3.86
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/gen.js +77 -77
- package/package.json +5 -1
- package/ui/components/Drawer.jsx +208 -208
- package/ui/components/Pagination.jsx +145 -145
- package/ui/components/PrivacyBlur.jsx +603 -603
- package/utils/index.cjs +5 -0
- package/utils/index.js +22 -20
- package/utils/utils/duration.js +139 -139
- package/utils/utils/oidc.js +214 -214
- package/utils/utils/realtime.js +1634 -1634
- package/utils/utils/session.cjs +87 -9
- package/utils/utils/session.js +81 -8
- package/utils/utils/session_manager.cjs +490 -0
- package/utils/utils/session_manager.js +549 -0
- package/utils/utils/storage.js +393 -393
- package/utils/utils/ui.js +111 -111
- package/vue/components/Badge.vue +79 -79
- package/vue/components/Button.vue +209 -209
- package/vue/components/Checkbox.vue +103 -103
- package/vue/components/CircularProgress.vue +383 -383
- package/vue/components/Divider.vue +94 -94
- package/vue/components/Drawer.vue +243 -243
- package/vue/components/InfoBadge.vue +223 -223
- package/vue/components/Input.vue +341 -341
- package/vue/components/LinearProgress.vue +268 -268
- package/vue/components/Modal.vue +226 -226
- package/vue/components/Pagination.vue +199 -199
- package/vue/components/Select.vue +713 -713
- package/vue/components/Sidebar.vue +955 -955
- package/vue/components/Skeleton.vue +91 -91
- package/vue/components/Spinner.vue +263 -263
- package/vue/components/StatusTag.vue +89 -89
- package/vue/components/TabBar.vue +176 -176
- package/vue/components/Table.vue +388 -388
- package/vue/components/TimePicker.vue +399 -399
- package/vue/components/Toggle.vue +115 -115
- package/vue/components/XTable.vue +119 -119
- package/vue/composables/useAutoSkeletonRows.ts +71 -71
- package/vue/index.d.ts +67 -67
- package/vue/index.js +86 -86
|
@@ -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
|
+
});
|