s3db.js 13.4.0 → 13.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -10
- package/dist/{s3db.cjs.js → s3db.cjs} +38801 -32446
- package/dist/s3db.cjs.map +1 -0
- package/dist/s3db.es.js +38653 -32291
- package/dist/s3db.es.js.map +1 -1
- package/package.json +218 -22
- package/src/concerns/id.js +90 -6
- package/src/concerns/index.js +2 -1
- package/src/concerns/password-hashing.js +150 -0
- package/src/database.class.js +6 -2
- package/src/plugins/api/auth/basic-auth.js +40 -10
- package/src/plugins/api/auth/index.js +49 -3
- package/src/plugins/api/auth/oauth2-auth.js +171 -0
- package/src/plugins/api/auth/oidc-auth.js +789 -0
- package/src/plugins/api/auth/oidc-client.js +462 -0
- package/src/plugins/api/auth/path-auth-matcher.js +284 -0
- package/src/plugins/api/concerns/event-emitter.js +134 -0
- package/src/plugins/api/concerns/failban-manager.js +651 -0
- package/src/plugins/api/concerns/guards-helpers.js +402 -0
- package/src/plugins/api/concerns/metrics-collector.js +346 -0
- package/src/plugins/api/index.js +510 -57
- package/src/plugins/api/middlewares/failban.js +305 -0
- package/src/plugins/api/middlewares/rate-limit.js +301 -0
- package/src/plugins/api/middlewares/request-id.js +74 -0
- package/src/plugins/api/middlewares/security-headers.js +120 -0
- package/src/plugins/api/middlewares/session-tracking.js +194 -0
- package/src/plugins/api/routes/auth-routes.js +119 -78
- package/src/plugins/api/routes/resource-routes.js +73 -30
- package/src/plugins/api/server.js +1139 -45
- package/src/plugins/api/utils/custom-routes.js +102 -0
- package/src/plugins/api/utils/guards.js +213 -0
- package/src/plugins/api/utils/mime-types.js +154 -0
- package/src/plugins/api/utils/openapi-generator.js +91 -12
- package/src/plugins/api/utils/path-matcher.js +173 -0
- package/src/plugins/api/utils/static-filesystem.js +262 -0
- package/src/plugins/api/utils/static-s3.js +231 -0
- package/src/plugins/api/utils/template-engine.js +188 -0
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
- package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
- package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
- package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
- package/src/plugins/cloud-inventory/index.js +20 -0
- package/src/plugins/cloud-inventory/registry.js +146 -0
- package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
- package/src/plugins/cloud-inventory.plugin.js +1333 -0
- package/src/plugins/concerns/plugin-dependencies.js +62 -2
- package/src/plugins/eventual-consistency/analytics.js +1 -0
- package/src/plugins/eventual-consistency/consolidation.js +2 -2
- package/src/plugins/eventual-consistency/garbage-collection.js +2 -2
- package/src/plugins/eventual-consistency/install.js +2 -2
- package/src/plugins/identity/README.md +335 -0
- package/src/plugins/identity/concerns/mfa-manager.js +204 -0
- package/src/plugins/identity/concerns/password.js +138 -0
- package/src/plugins/identity/concerns/resource-schemas.js +273 -0
- package/src/plugins/identity/concerns/token-generator.js +172 -0
- package/src/plugins/identity/email-service.js +422 -0
- package/src/plugins/identity/index.js +1052 -0
- package/src/plugins/identity/oauth2-server.js +1033 -0
- package/src/plugins/identity/oidc-discovery.js +285 -0
- package/src/plugins/identity/rsa-keys.js +323 -0
- package/src/plugins/identity/server.js +500 -0
- package/src/plugins/identity/session-manager.js +453 -0
- package/src/plugins/identity/ui/layouts/base.js +251 -0
- package/src/plugins/identity/ui/middleware.js +135 -0
- package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
- package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
- package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
- package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
- package/src/plugins/identity/ui/pages/admin/users.js +263 -0
- package/src/plugins/identity/ui/pages/consent.js +262 -0
- package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
- package/src/plugins/identity/ui/pages/login.js +144 -0
- package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
- package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
- package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
- package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
- package/src/plugins/identity/ui/pages/profile.js +361 -0
- package/src/plugins/identity/ui/pages/register.js +226 -0
- package/src/plugins/identity/ui/pages/reset-password.js +128 -0
- package/src/plugins/identity/ui/pages/verify-email.js +172 -0
- package/src/plugins/identity/ui/routes.js +2541 -0
- package/src/plugins/identity/ui/styles/main.css +465 -0
- package/src/plugins/index.js +4 -1
- package/src/plugins/ml/base-model.class.js +65 -16
- package/src/plugins/ml/classification-model.class.js +1 -1
- package/src/plugins/ml/timeseries-model.class.js +3 -1
- package/src/plugins/ml.plugin.js +584 -31
- package/src/plugins/shared/error-handler.js +147 -0
- package/src/plugins/shared/index.js +9 -0
- package/src/plugins/shared/middlewares/compression.js +117 -0
- package/src/plugins/shared/middlewares/cors.js +49 -0
- package/src/plugins/shared/middlewares/index.js +11 -0
- package/src/plugins/shared/middlewares/logging.js +54 -0
- package/src/plugins/shared/middlewares/rate-limit.js +73 -0
- package/src/plugins/shared/middlewares/security.js +158 -0
- package/src/plugins/shared/response-formatter.js +264 -0
- package/src/plugins/state-machine.plugin.js +57 -2
- package/src/resource.class.js +140 -12
- package/src/schema.class.js +30 -1
- package/src/validator.class.js +57 -6
- package/dist/s3db.cjs.js.map +0 -1
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager - Handles user sessions for Identity Provider
|
|
3
|
+
*
|
|
4
|
+
* Manages session lifecycle using S3DB resource as storage:
|
|
5
|
+
* - Create/validate/destroy sessions
|
|
6
|
+
* - Cookie-based session handling
|
|
7
|
+
* - Automatic session cleanup (expired sessions)
|
|
8
|
+
* - IP address and user agent tracking
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { generateSessionId, calculateExpiration, isExpired } from './concerns/token-generator.js';
|
|
12
|
+
import tryFn from '../../concerns/try-fn.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default session configuration
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_CONFIG = {
|
|
18
|
+
sessionExpiry: '24h', // Default: 24 hours
|
|
19
|
+
cookieName: 's3db_session', // Cookie name
|
|
20
|
+
cookiePath: '/', // Cookie path
|
|
21
|
+
cookieHttpOnly: true, // HTTP-only cookie (no JS access)
|
|
22
|
+
cookieSecure: false, // Secure cookie (HTTPS only) - set to true in production
|
|
23
|
+
cookieSameSite: 'Lax', // SameSite attribute ('Strict', 'Lax', 'None')
|
|
24
|
+
cleanupInterval: 3600000, // Cleanup interval: 1 hour (in ms)
|
|
25
|
+
enableCleanup: true // Enable automatic cleanup
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* SessionManager class
|
|
30
|
+
* @class
|
|
31
|
+
*/
|
|
32
|
+
export class SessionManager {
|
|
33
|
+
/**
|
|
34
|
+
* Create Session Manager
|
|
35
|
+
* @param {Object} options - Configuration options
|
|
36
|
+
* @param {Object} options.sessionResource - S3DB sessions resource
|
|
37
|
+
* @param {Object} [options.config] - Session configuration
|
|
38
|
+
*/
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
this.sessionResource = options.sessionResource;
|
|
41
|
+
this.config = { ...DEFAULT_CONFIG, ...options.config };
|
|
42
|
+
|
|
43
|
+
this.cleanupTimer = null;
|
|
44
|
+
|
|
45
|
+
if (!this.sessionResource) {
|
|
46
|
+
throw new Error('SessionManager requires a sessionResource');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Start automatic cleanup
|
|
50
|
+
if (this.config.enableCleanup) {
|
|
51
|
+
this._startCleanup();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a new session
|
|
57
|
+
* @param {Object} data - Session data
|
|
58
|
+
* @param {string} data.userId - User ID
|
|
59
|
+
* @param {Object} [data.metadata] - Additional session metadata
|
|
60
|
+
* @param {string} [data.ipAddress] - Client IP address
|
|
61
|
+
* @param {string} [data.userAgent] - Client user agent
|
|
62
|
+
* @param {string} [duration] - Session duration (overrides default)
|
|
63
|
+
* @returns {Promise<{sessionId: string, expiresAt: number, session: Object}>}
|
|
64
|
+
*/
|
|
65
|
+
async createSession(data) {
|
|
66
|
+
const { userId, metadata = {}, ipAddress, userAgent, duration } = data;
|
|
67
|
+
|
|
68
|
+
if (!userId) {
|
|
69
|
+
throw new Error('userId is required to create a session');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Generate session ID
|
|
73
|
+
const sessionId = generateSessionId();
|
|
74
|
+
|
|
75
|
+
// Calculate expiration
|
|
76
|
+
const expiresAt = calculateExpiration(duration || this.config.sessionExpiry);
|
|
77
|
+
|
|
78
|
+
// Create session record
|
|
79
|
+
const sessionData = {
|
|
80
|
+
userId,
|
|
81
|
+
expiresAt: new Date(expiresAt).toISOString(),
|
|
82
|
+
ipAddress: ipAddress || null,
|
|
83
|
+
userAgent: userAgent || null,
|
|
84
|
+
metadata,
|
|
85
|
+
createdAt: new Date().toISOString()
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Insert into S3DB
|
|
89
|
+
const [ok, err, session] = await tryFn(() =>
|
|
90
|
+
this.sessionResource.insert(sessionData)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (!ok) {
|
|
94
|
+
throw new Error(`Failed to create session: ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
sessionId: session.id, // S3DB auto-generated ID
|
|
99
|
+
expiresAt,
|
|
100
|
+
session
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate a session
|
|
106
|
+
* @param {string} sessionId - Session ID to validate
|
|
107
|
+
* @returns {Promise<{valid: boolean, session: Object|null, reason: string|null}>}
|
|
108
|
+
*/
|
|
109
|
+
async validateSession(sessionId) {
|
|
110
|
+
if (!sessionId) {
|
|
111
|
+
return { valid: false, session: null, reason: 'No session ID provided' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Fetch session from S3DB
|
|
115
|
+
const [ok, err, session] = await tryFn(() =>
|
|
116
|
+
this.sessionResource.get(sessionId)
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (!ok || !session) {
|
|
120
|
+
return { valid: false, session: null, reason: 'Session not found' };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check if session is expired
|
|
124
|
+
if (isExpired(session.expiresAt)) {
|
|
125
|
+
// Delete expired session
|
|
126
|
+
await this.destroySession(sessionId);
|
|
127
|
+
return { valid: false, session: null, reason: 'Session expired' };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { valid: true, session, reason: null };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get session data without validation
|
|
135
|
+
* @param {string} sessionId - Session ID
|
|
136
|
+
* @returns {Promise<Object|null>} Session object or null
|
|
137
|
+
*/
|
|
138
|
+
async getSession(sessionId) {
|
|
139
|
+
if (!sessionId) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const [ok, , session] = await tryFn(() =>
|
|
144
|
+
this.sessionResource.get(sessionId)
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return ok ? session : null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Update session metadata
|
|
152
|
+
* @param {string} sessionId - Session ID
|
|
153
|
+
* @param {Object} metadata - New metadata to merge
|
|
154
|
+
* @returns {Promise<Object>} Updated session
|
|
155
|
+
*/
|
|
156
|
+
async updateSession(sessionId, metadata) {
|
|
157
|
+
if (!sessionId) {
|
|
158
|
+
throw new Error('sessionId is required');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const session = await this.getSession(sessionId);
|
|
162
|
+
|
|
163
|
+
if (!session) {
|
|
164
|
+
throw new Error('Session not found');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const updatedMetadata = { ...session.metadata, ...metadata };
|
|
168
|
+
|
|
169
|
+
const [ok, err, updated] = await tryFn(() =>
|
|
170
|
+
this.sessionResource.update(sessionId, {
|
|
171
|
+
metadata: updatedMetadata
|
|
172
|
+
})
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (!ok) {
|
|
176
|
+
throw new Error(`Failed to update session: ${err.message}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return updated;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Destroy a session (logout)
|
|
184
|
+
* @param {string} sessionId - Session ID to destroy
|
|
185
|
+
* @returns {Promise<boolean>} True if session was destroyed
|
|
186
|
+
*/
|
|
187
|
+
async destroySession(sessionId) {
|
|
188
|
+
if (!sessionId) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const [ok] = await tryFn(() =>
|
|
193
|
+
this.sessionResource.delete(sessionId)
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
return ok;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Destroy all sessions for a user (logout all devices)
|
|
201
|
+
* @param {string} userId - User ID
|
|
202
|
+
* @returns {Promise<number>} Number of sessions destroyed
|
|
203
|
+
*/
|
|
204
|
+
async destroyUserSessions(userId) {
|
|
205
|
+
if (!userId) {
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Query all sessions for user
|
|
210
|
+
const [ok, , sessions] = await tryFn(() =>
|
|
211
|
+
this.sessionResource.query({ userId })
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (!ok || !sessions || sessions.length === 0) {
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Delete all sessions
|
|
219
|
+
let count = 0;
|
|
220
|
+
for (const session of sessions) {
|
|
221
|
+
const destroyed = await this.destroySession(session.id);
|
|
222
|
+
if (destroyed) count++;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return count;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get all active sessions for a user
|
|
230
|
+
* @param {string} userId - User ID
|
|
231
|
+
* @returns {Promise<Array>} Array of active sessions
|
|
232
|
+
*/
|
|
233
|
+
async getUserSessions(userId) {
|
|
234
|
+
if (!userId) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const [ok, , sessions] = await tryFn(() =>
|
|
239
|
+
this.sessionResource.query({ userId })
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
if (!ok || !sessions) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Filter out expired sessions
|
|
247
|
+
const activeSessions = [];
|
|
248
|
+
for (const session of sessions) {
|
|
249
|
+
if (!isExpired(session.expiresAt)) {
|
|
250
|
+
activeSessions.push(session);
|
|
251
|
+
} else {
|
|
252
|
+
// Clean up expired session
|
|
253
|
+
await this.destroySession(session.id);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return activeSessions;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Set session cookie in HTTP response
|
|
262
|
+
* @param {Object} res - HTTP response object (Express/Hono style)
|
|
263
|
+
* @param {string} sessionId - Session ID
|
|
264
|
+
* @param {number} expiresAt - Expiration timestamp (Unix ms)
|
|
265
|
+
*/
|
|
266
|
+
setSessionCookie(res, sessionId, expiresAt) {
|
|
267
|
+
const expires = new Date(expiresAt);
|
|
268
|
+
|
|
269
|
+
const cookieOptions = [
|
|
270
|
+
`${this.config.cookieName}=${sessionId}`,
|
|
271
|
+
`Path=${this.config.cookiePath}`,
|
|
272
|
+
`Expires=${expires.toUTCString()}`,
|
|
273
|
+
`Max-Age=${Math.floor((expiresAt - Date.now()) / 1000)}`
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
if (this.config.cookieHttpOnly) {
|
|
277
|
+
cookieOptions.push('HttpOnly');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (this.config.cookieSecure) {
|
|
281
|
+
cookieOptions.push('Secure');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (this.config.cookieSameSite) {
|
|
285
|
+
cookieOptions.push(`SameSite=${this.config.cookieSameSite}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const cookieValue = cookieOptions.join('; ');
|
|
289
|
+
|
|
290
|
+
// Set cookie header
|
|
291
|
+
if (typeof res.setHeader === 'function') {
|
|
292
|
+
// Express-style
|
|
293
|
+
res.setHeader('Set-Cookie', cookieValue);
|
|
294
|
+
} else if (typeof res.header === 'function') {
|
|
295
|
+
// Hono-style
|
|
296
|
+
res.header('Set-Cookie', cookieValue);
|
|
297
|
+
} else {
|
|
298
|
+
throw new Error('Unsupported response object');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Clear session cookie in HTTP response
|
|
304
|
+
* @param {Object} res - HTTP response object
|
|
305
|
+
*/
|
|
306
|
+
clearSessionCookie(res) {
|
|
307
|
+
const cookieOptions = [
|
|
308
|
+
`${this.config.cookieName}=`,
|
|
309
|
+
`Path=${this.config.cookiePath}`,
|
|
310
|
+
'Expires=Thu, 01 Jan 1970 00:00:00 GMT',
|
|
311
|
+
'Max-Age=0'
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
if (this.config.cookieHttpOnly) {
|
|
315
|
+
cookieOptions.push('HttpOnly');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (this.config.cookieSecure) {
|
|
319
|
+
cookieOptions.push('Secure');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (this.config.cookieSameSite) {
|
|
323
|
+
cookieOptions.push(`SameSite=${this.config.cookieSameSite}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const cookieValue = cookieOptions.join('; ');
|
|
327
|
+
|
|
328
|
+
if (typeof res.setHeader === 'function') {
|
|
329
|
+
res.setHeader('Set-Cookie', cookieValue);
|
|
330
|
+
} else if (typeof res.header === 'function') {
|
|
331
|
+
res.header('Set-Cookie', cookieValue);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get session ID from HTTP request cookies
|
|
337
|
+
* @param {Object} req - HTTP request object
|
|
338
|
+
* @returns {string|null} Session ID or null
|
|
339
|
+
*/
|
|
340
|
+
getSessionIdFromRequest(req) {
|
|
341
|
+
// Parse cookies from request
|
|
342
|
+
const cookieHeader = req.headers?.cookie || req.header?.('cookie');
|
|
343
|
+
|
|
344
|
+
if (!cookieHeader) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const cookies = cookieHeader.split(';').reduce((acc, cookie) => {
|
|
349
|
+
const [key, value] = cookie.trim().split('=');
|
|
350
|
+
acc[key] = value;
|
|
351
|
+
return acc;
|
|
352
|
+
}, {});
|
|
353
|
+
|
|
354
|
+
return cookies[this.config.cookieName] || null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Cleanup expired sessions
|
|
359
|
+
* @returns {Promise<number>} Number of sessions cleaned up
|
|
360
|
+
*/
|
|
361
|
+
async cleanupExpiredSessions() {
|
|
362
|
+
// List all sessions
|
|
363
|
+
const [ok, , sessions] = await tryFn(() =>
|
|
364
|
+
this.sessionResource.list({ limit: 1000 })
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
if (!ok || !sessions) {
|
|
368
|
+
return 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let count = 0;
|
|
372
|
+
for (const session of sessions) {
|
|
373
|
+
if (isExpired(session.expiresAt)) {
|
|
374
|
+
const destroyed = await this.destroySession(session.id);
|
|
375
|
+
if (destroyed) count++;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return count;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Start automatic cleanup of expired sessions
|
|
384
|
+
* @private
|
|
385
|
+
*/
|
|
386
|
+
_startCleanup() {
|
|
387
|
+
if (this.cleanupTimer) {
|
|
388
|
+
return; // Already running
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
this.cleanupTimer = setInterval(async () => {
|
|
392
|
+
try {
|
|
393
|
+
const count = await this.cleanupExpiredSessions();
|
|
394
|
+
if (count > 0) {
|
|
395
|
+
console.log(`[SessionManager] Cleaned up ${count} expired sessions`);
|
|
396
|
+
}
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.error('[SessionManager] Cleanup error:', error.message);
|
|
399
|
+
}
|
|
400
|
+
}, this.config.cleanupInterval);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Stop automatic cleanup
|
|
405
|
+
*/
|
|
406
|
+
stopCleanup() {
|
|
407
|
+
if (this.cleanupTimer) {
|
|
408
|
+
clearInterval(this.cleanupTimer);
|
|
409
|
+
this.cleanupTimer = null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Get session statistics
|
|
415
|
+
* @returns {Promise<Object>} Session statistics
|
|
416
|
+
*/
|
|
417
|
+
async getStatistics() {
|
|
418
|
+
const [ok, , sessions] = await tryFn(() =>
|
|
419
|
+
this.sessionResource.list({ limit: 10000 })
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
if (!ok || !sessions) {
|
|
423
|
+
return {
|
|
424
|
+
total: 0,
|
|
425
|
+
active: 0,
|
|
426
|
+
expired: 0,
|
|
427
|
+
users: 0
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
let active = 0;
|
|
432
|
+
let expired = 0;
|
|
433
|
+
const uniqueUsers = new Set();
|
|
434
|
+
|
|
435
|
+
for (const session of sessions) {
|
|
436
|
+
if (isExpired(session.expiresAt)) {
|
|
437
|
+
expired++;
|
|
438
|
+
} else {
|
|
439
|
+
active++;
|
|
440
|
+
uniqueUsers.add(session.userId);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
total: sessions.length,
|
|
446
|
+
active,
|
|
447
|
+
expired,
|
|
448
|
+
users: uniqueUsers.size
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export default SessionManager;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base HTML Layout for Identity Provider UI
|
|
3
|
+
* Uses Hono's html helper for server-side rendering
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { html } from 'hono/html';
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { dirname, join } from 'path';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
|
|
14
|
+
// Read CSS file once at module load
|
|
15
|
+
const cssPath = join(__dirname, '../styles/main.css');
|
|
16
|
+
let cachedCSS = null;
|
|
17
|
+
|
|
18
|
+
function getCSS() {
|
|
19
|
+
if (!cachedCSS) {
|
|
20
|
+
cachedCSS = readFileSync(cssPath, 'utf-8');
|
|
21
|
+
}
|
|
22
|
+
return cachedCSS;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert hex colors to rgba for translucent gradients
|
|
27
|
+
* @param {string} hex - Hex color (#fff or #ffffff)
|
|
28
|
+
* @param {number} alpha - Alpha channel value between 0 and 1
|
|
29
|
+
* @returns {string} rgba(...) string
|
|
30
|
+
*/
|
|
31
|
+
function hexToRgba(hex, alpha = 1) {
|
|
32
|
+
if (!hex || typeof hex !== 'string') {
|
|
33
|
+
return `rgba(0, 0, 0, ${alpha})`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const normalized = hex.replace('#', '');
|
|
37
|
+
if (![3, 6].includes(normalized.length)) {
|
|
38
|
+
return `rgba(0, 0, 0, ${alpha})`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const full = normalized.length === 3
|
|
42
|
+
? normalized.split('').map(char => `${char}${char}`).join('')
|
|
43
|
+
: normalized;
|
|
44
|
+
|
|
45
|
+
const r = parseInt(full.slice(0, 2), 16);
|
|
46
|
+
const g = parseInt(full.slice(2, 4), 16);
|
|
47
|
+
const b = parseInt(full.slice(4, 6), 16);
|
|
48
|
+
|
|
49
|
+
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
|
|
50
|
+
return `rgba(0, 0, 0, ${alpha})`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Base layout component
|
|
58
|
+
* @param {Object} props - Layout properties
|
|
59
|
+
* @param {string} props.title - Page title
|
|
60
|
+
* @param {string} props.content - Page content (HTML string)
|
|
61
|
+
* @param {Object} [props.user] - Authenticated user (if logged in)
|
|
62
|
+
* @param {Object} [props.config] - UI configuration (title, logo, etc.)
|
|
63
|
+
* @param {string} [props.error] - Error message to display
|
|
64
|
+
* @param {string} [props.success] - Success message to display
|
|
65
|
+
* @returns {string} HTML string
|
|
66
|
+
*/
|
|
67
|
+
export function BaseLayout(props) {
|
|
68
|
+
const {
|
|
69
|
+
title = 'Identity Provider',
|
|
70
|
+
content = '',
|
|
71
|
+
user = null,
|
|
72
|
+
config = {},
|
|
73
|
+
error = null,
|
|
74
|
+
success = null
|
|
75
|
+
} = props;
|
|
76
|
+
|
|
77
|
+
// Theme configuration with defaults
|
|
78
|
+
const theme = {
|
|
79
|
+
title: config.title || 'S3DB Identity',
|
|
80
|
+
logo: config.logo || null,
|
|
81
|
+
logoUrl: config.logoUrl || null,
|
|
82
|
+
favicon: config.favicon || null,
|
|
83
|
+
registrationEnabled: config.registrationEnabled !== false, // Show register link
|
|
84
|
+
|
|
85
|
+
// Colors
|
|
86
|
+
primaryColor: config.primaryColor || '#007bff',
|
|
87
|
+
secondaryColor: config.secondaryColor || '#6c757d',
|
|
88
|
+
successColor: config.successColor || '#28a745',
|
|
89
|
+
dangerColor: config.dangerColor || '#dc3545',
|
|
90
|
+
warningColor: config.warningColor || '#ffc107',
|
|
91
|
+
infoColor: config.infoColor || '#17a2b8',
|
|
92
|
+
|
|
93
|
+
// Text colors
|
|
94
|
+
textColor: config.textColor || '#212529',
|
|
95
|
+
textMuted: config.textMuted || '#6c757d',
|
|
96
|
+
|
|
97
|
+
// Background colors
|
|
98
|
+
backgroundColor: config.backgroundColor || '#ffffff',
|
|
99
|
+
backgroundLight: config.backgroundLight || '#f8f9fa',
|
|
100
|
+
borderColor: config.borderColor || '#dee2e6',
|
|
101
|
+
|
|
102
|
+
// Typography
|
|
103
|
+
fontFamily: config.fontFamily || '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
104
|
+
fontSize: config.fontSize || '16px',
|
|
105
|
+
|
|
106
|
+
// Layout
|
|
107
|
+
borderRadius: config.borderRadius || '0.375rem',
|
|
108
|
+
boxShadow: config.boxShadow || '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
|
|
109
|
+
|
|
110
|
+
// Company info
|
|
111
|
+
companyName: config.companyName || 'S3DB',
|
|
112
|
+
legalName: config.legalName || config.companyName || 'S3DB Corp',
|
|
113
|
+
tagline: config.tagline || 'Secure Identity & Access Management',
|
|
114
|
+
welcomeMessage: config.welcomeMessage || 'Welcome back!',
|
|
115
|
+
footerText: config.footerText || null,
|
|
116
|
+
supportEmail: config.supportEmail || null,
|
|
117
|
+
privacyUrl: config.privacyUrl || '/privacy',
|
|
118
|
+
termsUrl: config.termsUrl || '/terms',
|
|
119
|
+
|
|
120
|
+
// Social links
|
|
121
|
+
socialLinks: config.socialLinks || null,
|
|
122
|
+
|
|
123
|
+
// Custom CSS
|
|
124
|
+
customCSS: config.customCSS || null
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const primaryGlow = hexToRgba(theme.primaryColor, 0.28);
|
|
128
|
+
const secondaryGlow = hexToRgba(theme.secondaryColor, 0.22);
|
|
129
|
+
const surfaceGlow = hexToRgba(theme.backgroundLight, 0.65);
|
|
130
|
+
|
|
131
|
+
// Build dynamic CSS variables
|
|
132
|
+
const themeCSS = `
|
|
133
|
+
:root {
|
|
134
|
+
--color-primary: ${theme.primaryColor};
|
|
135
|
+
--color-secondary: ${theme.secondaryColor};
|
|
136
|
+
--color-success: ${theme.successColor};
|
|
137
|
+
--color-danger: ${theme.dangerColor};
|
|
138
|
+
--color-warning: ${theme.warningColor};
|
|
139
|
+
--color-info: ${theme.infoColor};
|
|
140
|
+
|
|
141
|
+
--color-text: ${theme.textColor};
|
|
142
|
+
--color-text-muted: ${theme.textMuted};
|
|
143
|
+
|
|
144
|
+
--color-bg: ${theme.backgroundColor};
|
|
145
|
+
--color-light: ${theme.backgroundLight};
|
|
146
|
+
--color-border: ${theme.borderColor};
|
|
147
|
+
--color-card-bg: ${theme.backgroundLight};
|
|
148
|
+
--color-primary-glow: ${primaryGlow};
|
|
149
|
+
--color-secondary-glow: ${secondaryGlow};
|
|
150
|
+
--color-surface-glow: ${surfaceGlow};
|
|
151
|
+
|
|
152
|
+
--font-family: ${theme.fontFamily};
|
|
153
|
+
--font-size-base: ${theme.fontSize};
|
|
154
|
+
|
|
155
|
+
--border-radius: ${theme.borderRadius};
|
|
156
|
+
--box-shadow: ${theme.boxShadow};
|
|
157
|
+
}
|
|
158
|
+
`;
|
|
159
|
+
|
|
160
|
+
const backgroundGradient = `
|
|
161
|
+
radial-gradient(circle at 12% 18%, ${primaryGlow} 0%, transparent 52%),
|
|
162
|
+
radial-gradient(circle at 88% 16%, ${secondaryGlow} 0%, transparent 55%),
|
|
163
|
+
linear-gradient(160deg, ${theme.backgroundColor} 0%, ${theme.backgroundLight} 55%, ${theme.backgroundColor} 100%)
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
const flashContainer = (error || success) ? html`
|
|
167
|
+
<div class="mx-auto mb-8 w-full max-w-3xl space-y-3">
|
|
168
|
+
${error ? html`
|
|
169
|
+
<div class="rounded-2xl border border-red-500/40 bg-red-500/15 px-4 py-3 text-sm leading-6 text-red-100 shadow-lg shadow-red-900/30 backdrop-blur">
|
|
170
|
+
${error}
|
|
171
|
+
</div>
|
|
172
|
+
` : ''}
|
|
173
|
+
${success ? html`
|
|
174
|
+
<div class="rounded-2xl border border-emerald-400/40 bg-emerald-500/15 px-4 py-3 text-sm leading-6 text-emerald-100 shadow-lg shadow-emerald-900/25 backdrop-blur">
|
|
175
|
+
${success}
|
|
176
|
+
</div>
|
|
177
|
+
` : ''}
|
|
178
|
+
</div>
|
|
179
|
+
` : '';
|
|
180
|
+
|
|
181
|
+
return html`<!DOCTYPE html>
|
|
182
|
+
<html lang="en">
|
|
183
|
+
<head>
|
|
184
|
+
<meta charset="utf-8">
|
|
185
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
|
186
|
+
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
|
187
|
+
<meta name="description" content="${theme.tagline}">
|
|
188
|
+
<title>${title} - ${theme.title}</title>
|
|
189
|
+
|
|
190
|
+
${theme.favicon ? html`
|
|
191
|
+
<link rel="shortcut icon" href="${theme.favicon}">
|
|
192
|
+
<link rel="icon" href="${theme.favicon}">
|
|
193
|
+
` : ''}
|
|
194
|
+
|
|
195
|
+
<script>
|
|
196
|
+
window.tailwind = window.tailwind || {};
|
|
197
|
+
window.tailwind.config = {
|
|
198
|
+
darkMode: 'class',
|
|
199
|
+
theme: {
|
|
200
|
+
extend: {
|
|
201
|
+
colors: {
|
|
202
|
+
primary: 'var(--color-primary)',
|
|
203
|
+
secondary: 'var(--color-secondary)',
|
|
204
|
+
surface: 'var(--color-card-bg)'
|
|
205
|
+
},
|
|
206
|
+
fontFamily: {
|
|
207
|
+
display: ['var(--font-family)'],
|
|
208
|
+
body: ['var(--font-family)']
|
|
209
|
+
},
|
|
210
|
+
boxShadow: {
|
|
211
|
+
surface: 'var(--box-shadow)'
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
</script>
|
|
217
|
+
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
218
|
+
|
|
219
|
+
<!-- Custom Styles -->
|
|
220
|
+
<style>${themeCSS}</style>
|
|
221
|
+
<style>${getCSS()}</style>
|
|
222
|
+
${theme.customCSS ? html`<style>${theme.customCSS}</style>` : ''}
|
|
223
|
+
</head>
|
|
224
|
+
<body class="min-h-screen bg-slate-950 antialiased text-white">
|
|
225
|
+
<div
|
|
226
|
+
class="relative flex min-h-screen flex-col overflow-hidden"
|
|
227
|
+
style="
|
|
228
|
+
background-image: ${backgroundGradient};
|
|
229
|
+
background-attachment: fixed;
|
|
230
|
+
background-size: cover;
|
|
231
|
+
color: ${theme.textColor};
|
|
232
|
+
font-family: ${theme.fontFamily};
|
|
233
|
+
font-size: ${theme.fontSize};
|
|
234
|
+
"
|
|
235
|
+
>
|
|
236
|
+
<div class="pointer-events-none absolute inset-0 overflow-hidden">
|
|
237
|
+
<div class="absolute -left-20 top-[10%] h-64 w-64 rounded-full blur-[120px]" style="background: ${primaryGlow}; opacity: 0.85;"></div>
|
|
238
|
+
<div class="absolute right-[-15%] top-[5%] h-72 w-72 rounded-full blur-[120px]" style="background: ${secondaryGlow}; opacity: 0.65;"></div>
|
|
239
|
+
<div class="absolute left-1/2 top-[65%] h-96 w-[36rem] -translate-x-1/2 rounded-[200px] blur-[160px]" style="background: ${surfaceGlow}; opacity: 0.35;"></div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<main class="relative z-10 flex flex-1 items-stretch justify-center">
|
|
243
|
+
${flashContainer}
|
|
244
|
+
${content}
|
|
245
|
+
</main>
|
|
246
|
+
</div>
|
|
247
|
+
</body>
|
|
248
|
+
</html>`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export default BaseLayout;
|