cynx-lib 1.3.84 → 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.
- 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 +15 -2
- package/utils/utils/session.js +67 -58
- 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
package/utils/utils/session.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
69
|
+
extractToken,
|
|
70
|
+
formatToken,
|
|
71
|
+
genSessionToken,
|
|
72
|
+
getSessionId,
|
|
73
|
+
parseToken
|
|
61
74
|
});
|
package/utils/utils/session.js
CHANGED
|
@@ -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
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
});
|