webspresso 0.0.99 → 0.0.100
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/.agents/AGENTS.md +2 -2
- package/.agents/AUTH.md +246 -0
- package/.agents/CLI.md +47 -0
- package/.agents/ERRORS.md +59 -0
- package/.agents/ORM.md +90 -0
- package/.agents/PLUGINS.md +44 -7
- package/.agents/QUEUE.md +18 -10
- package/.agents/ROUTING.md +33 -0
- package/.agents/STREAMING.md +35 -0
- package/AGENTS.md +1 -0
- package/README.md +145 -93
- package/bin/commands/add-docker.js +467 -0
- package/bin/commands/deploy-check.js +339 -0
- package/bin/webspresso.js +4 -0
- package/core/auth/index.js +63 -1
- package/core/auth/magic-link.js +170 -0
- package/core/auth/manager.js +343 -3
- package/core/auth/middleware.js +30 -5
- package/core/auth/session-manager.js +255 -0
- package/core/auth/sessions.js +351 -0
- package/core/auth/tokens.js +1 -0
- package/core/auth/totp.js +4 -1
- package/core/auth/user-agent.js +161 -0
- package/core/orm/index.js +2 -1
- package/core/orm/model.js +51 -1
- package/core/orm/repository.js +283 -13
- package/core/orm/scopes.js +3 -2
- package/core/orm/types.js +6 -1
- package/core/queue/cron.js +252 -39
- package/core/queue/manager.js +20 -2
- package/core/queue/scheduler.js +115 -59
- package/core/ssr/stream.js +62 -2
- package/index.d.ts +274 -14
- package/index.js +4 -0
- package/package.json +7 -5
- package/plugins/admin-panel/modules/admin-users.js +1 -1
- package/plugins/audit-log/admin-component.js +54 -54
- package/plugins/audit-log/index.js +2 -2
- package/plugins/content/client/inline-edit.js +2 -46
- package/plugins/cors.js +13 -2
- package/plugins/email/auth-bridge.js +66 -0
- package/plugins/email/bundled-templates.js +15 -0
- package/plugins/email/templates/auth-magic-link.mjml +13 -0
- package/plugins/file-manager/admin-component.js +49 -49
- package/plugins/file-manager/index.js +4 -4
- package/plugins/index.js +2 -0
- package/plugins/webhooks/admin-component.js +608 -0
- package/plugins/webhooks/api-handlers.js +237 -0
- package/plugins/webhooks/crypto.js +144 -0
- package/plugins/webhooks/dispatcher.js +708 -0
- package/plugins/webhooks/index.js +263 -0
- package/plugins/webhooks/matcher.js +113 -0
- package/plugins/webhooks/migration-template.js +75 -0
- package/plugins/webhooks/models.js +98 -0
- package/plugins/webhooks/sender.js +176 -0
- package/src/bundle/index.js +2 -1
- package/src/file-router.js +173 -4
- package/src/helpers.js +7 -1
- package/src/njk-frontmatter.js +3 -0
- package/src/routing/cascading.js +112 -0
package/core/auth/manager.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @module core/auth/manager
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const EventEmitter = require('events');
|
|
7
8
|
const { generateToken, hashToken } = require('./hash');
|
|
8
9
|
const { PolicyManager } = require('./policy');
|
|
9
10
|
const { signJwt, verifyJwt, decodeJwt } = require('./jwt');
|
|
@@ -67,7 +68,7 @@ const DEFAULT_CONFIG = {
|
|
|
67
68
|
/**
|
|
68
69
|
* AuthManager - Handles authentication operations
|
|
69
70
|
*/
|
|
70
|
-
class AuthManager {
|
|
71
|
+
class AuthManager extends EventEmitter {
|
|
71
72
|
/**
|
|
72
73
|
* Create AuthManager instance
|
|
73
74
|
* @param {Object} config - Configuration
|
|
@@ -83,6 +84,7 @@ class AuthManager {
|
|
|
83
84
|
* @param {Object} [config.routes] - Route configuration
|
|
84
85
|
*/
|
|
85
86
|
constructor(config) {
|
|
87
|
+
super();
|
|
86
88
|
this.validateConfig(config);
|
|
87
89
|
|
|
88
90
|
this.config = {
|
|
@@ -106,6 +108,7 @@ class AuthManager {
|
|
|
106
108
|
passwordReset: config.notifications?.passwordReset || null,
|
|
107
109
|
emailVerification: config.notifications?.emailVerification || null,
|
|
108
110
|
welcome: config.notifications?.welcome || null,
|
|
111
|
+
magicLink: config.notifications?.magicLink || null,
|
|
109
112
|
};
|
|
110
113
|
|
|
111
114
|
// TOTP & 2FA initialization
|
|
@@ -138,6 +141,30 @@ class AuthManager {
|
|
|
138
141
|
|
|
139
142
|
// Policy manager instance
|
|
140
143
|
this.policies = new PolicyManager();
|
|
144
|
+
|
|
145
|
+
// Session tracking and revocation manager
|
|
146
|
+
const { SessionManager } = require('./session-manager');
|
|
147
|
+
const { createKnexSessionTracker, createMemorySessionTracker } = require('./sessions');
|
|
148
|
+
|
|
149
|
+
let tracker = config.sessionTracker || null;
|
|
150
|
+
if (!tracker && (config.sessionTracking || config.trackSessions)) {
|
|
151
|
+
if (config.db || this.config.db) {
|
|
152
|
+
tracker = createKnexSessionTracker(config.db || this.config.db);
|
|
153
|
+
} else {
|
|
154
|
+
tracker = createMemorySessionTracker();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (tracker || config.sessionTracking || config.trackSessions) {
|
|
159
|
+
this.sessionManager = new SessionManager({
|
|
160
|
+
tracker: tracker || createMemorySessionTracker(),
|
|
161
|
+
sessionStore: config.sessionStore || this.config.session?.store,
|
|
162
|
+
touchThrottleMs: config.sessionTouchThrottleMs,
|
|
163
|
+
emitter: this,
|
|
164
|
+
});
|
|
165
|
+
} else {
|
|
166
|
+
this.sessionManager = null;
|
|
167
|
+
}
|
|
141
168
|
}
|
|
142
169
|
|
|
143
170
|
/**
|
|
@@ -285,7 +312,7 @@ class AuthManager {
|
|
|
285
312
|
|
|
286
313
|
// Set req.user
|
|
287
314
|
req.user = user;
|
|
288
|
-
|
|
315
|
+
|
|
289
316
|
// Handle remember me
|
|
290
317
|
if (options.remember && self.rememberTokens && self.config.rememberMe.enabled) {
|
|
291
318
|
await self.createRememberToken(user.id, res);
|
|
@@ -294,18 +321,34 @@ class AuthManager {
|
|
|
294
321
|
// Regenerate session ID for security if supported
|
|
295
322
|
if (req.session && typeof req.session.regenerate === 'function') {
|
|
296
323
|
return new Promise((resolve, reject) => {
|
|
297
|
-
req.session.regenerate((err) => {
|
|
324
|
+
req.session.regenerate(async (err) => {
|
|
298
325
|
if (err) {
|
|
299
326
|
reject(new AuthenticationError('Session regeneration failed', 'SESSION_ERROR'));
|
|
300
327
|
} else {
|
|
301
328
|
// Restore userId after regenerate
|
|
302
329
|
req.session.userId = user.id;
|
|
303
330
|
req.session.loggedInAt = Date.now();
|
|
331
|
+
if (req.sessionID && self.sessionManager) {
|
|
332
|
+
try {
|
|
333
|
+
await self.sessionManager.track(req.sessionID, user.id, req);
|
|
334
|
+
} catch (e) {
|
|
335
|
+
// Non-blocking
|
|
336
|
+
}
|
|
337
|
+
}
|
|
304
338
|
resolve();
|
|
305
339
|
}
|
|
306
340
|
});
|
|
307
341
|
});
|
|
308
342
|
}
|
|
343
|
+
|
|
344
|
+
// Track active session if sessionManager is active and not regenerated
|
|
345
|
+
if (req.sessionID && self.sessionManager) {
|
|
346
|
+
try {
|
|
347
|
+
await self.sessionManager.track(req.sessionID, user.id, req);
|
|
348
|
+
} catch (e) {
|
|
349
|
+
// Non-blocking
|
|
350
|
+
}
|
|
351
|
+
}
|
|
309
352
|
},
|
|
310
353
|
|
|
311
354
|
/**
|
|
@@ -337,6 +380,19 @@ class AuthManager {
|
|
|
337
380
|
// Clear 2FA session state
|
|
338
381
|
this.clear2Fa();
|
|
339
382
|
|
|
383
|
+
// Revoke session if sessionManager is active
|
|
384
|
+
if (req.sessionID && self.sessionManager) {
|
|
385
|
+
try {
|
|
386
|
+
if (options.everywhere && userId) {
|
|
387
|
+
await self.sessionManager.revokeAllSessions(userId);
|
|
388
|
+
} else {
|
|
389
|
+
await self.sessionManager.revokeSession(req.sessionID, { userId });
|
|
390
|
+
}
|
|
391
|
+
} catch (e) {
|
|
392
|
+
// Non-blocking
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
340
396
|
// Clear user
|
|
341
397
|
req.user = null;
|
|
342
398
|
|
|
@@ -530,6 +586,91 @@ class AuthManager {
|
|
|
530
586
|
authorize(action, policy = null, resource = null) {
|
|
531
587
|
return self.policies.authorize(req.user, action, policy, resource);
|
|
532
588
|
},
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Request a magic link for passwordless login
|
|
592
|
+
* @param {string} identifier - User email or username
|
|
593
|
+
* @param {Object} [options]
|
|
594
|
+
* @returns {Promise<{ sent: boolean, expiresAt?: Date, token?: string, magicLinkUrl?: string, userFound: boolean }>}
|
|
595
|
+
*/
|
|
596
|
+
requestMagicLink(identifier, options = {}) {
|
|
597
|
+
return self.requestMagicLink(identifier, options);
|
|
598
|
+
},
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Verify magic link token
|
|
602
|
+
* @param {string} rawToken
|
|
603
|
+
* @param {Object} [options]
|
|
604
|
+
* @returns {Promise<{ ok: true, user: Object, userId: * }>}
|
|
605
|
+
*/
|
|
606
|
+
verifyMagicLink(rawToken, options = {}) {
|
|
607
|
+
return self.verifyMagicLink(rawToken, options);
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Verify magic link token and log user into current request session
|
|
612
|
+
* @param {string} rawToken
|
|
613
|
+
* @param {Object} [options]
|
|
614
|
+
* @returns {Promise<{ ok: true, user: Object, userId: *, accessToken?: string, refreshToken?: string }>}
|
|
615
|
+
*/
|
|
616
|
+
async loginWithMagicLink(rawToken, options = {}) {
|
|
617
|
+
const result = await self.verifyMagicLink(rawToken, options);
|
|
618
|
+
await this.login(result.user, options.remember);
|
|
619
|
+
|
|
620
|
+
const response = { ...result };
|
|
621
|
+
if (options.jwt) {
|
|
622
|
+
response.accessToken = self.generateUserToken(result.user, options.jwtAccessOptions);
|
|
623
|
+
response.refreshToken = self.generateRefreshToken(result.user, options.jwtRefreshOptions);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return response;
|
|
627
|
+
},
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* List active sessions/devices for current user
|
|
631
|
+
* @returns {Promise<Array<Object>>}
|
|
632
|
+
*/
|
|
633
|
+
sessions() {
|
|
634
|
+
if (!req.user?.id) return Promise.resolve([]);
|
|
635
|
+
return self.listSessions(req.user.id, req.sessionID);
|
|
636
|
+
},
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Revoke a specific session
|
|
640
|
+
* @param {string} sessionId
|
|
641
|
+
* @returns {Promise<boolean>}
|
|
642
|
+
*/
|
|
643
|
+
async revokeSession(sessionId) {
|
|
644
|
+
if (sessionId === req.sessionID) {
|
|
645
|
+
await this.logout();
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
return await self.revokeSession(sessionId, { userId: req.user?.id });
|
|
649
|
+
},
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Revoke all other active sessions except current
|
|
653
|
+
* @returns {Promise<number>}
|
|
654
|
+
*/
|
|
655
|
+
revokeOtherSessions() {
|
|
656
|
+
if (!req.user?.id) return Promise.resolve(0);
|
|
657
|
+
return self.revokeOtherSessions(req.user.id, req.sessionID);
|
|
658
|
+
},
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Revoke all active sessions for user (force logout from all devices)
|
|
662
|
+
* @param {string|number} [userId]
|
|
663
|
+
* @returns {Promise<number>}
|
|
664
|
+
*/
|
|
665
|
+
async revokeAllSessions(userId = null) {
|
|
666
|
+
const targetUserId = userId || req.user?.id;
|
|
667
|
+
if (!targetUserId) return 0;
|
|
668
|
+
const count = await self.revokeAllSessions(targetUserId);
|
|
669
|
+
if (targetUserId === req.user?.id) {
|
|
670
|
+
await this.logout();
|
|
671
|
+
}
|
|
672
|
+
return count;
|
|
673
|
+
},
|
|
533
674
|
};
|
|
534
675
|
}
|
|
535
676
|
|
|
@@ -787,6 +928,122 @@ class AuthManager {
|
|
|
787
928
|
return { ok: true, userId: verified.userId };
|
|
788
929
|
}
|
|
789
930
|
|
|
931
|
+
/**
|
|
932
|
+
* Request magic link passwordless login — safe against user enumeration
|
|
933
|
+
* @param {string} identifier - Email or username
|
|
934
|
+
* @param {Object} [options]
|
|
935
|
+
* @param {number} [options.tokenTtlMs=900000] - Default 15 minutes (900,000ms)
|
|
936
|
+
* @param {Function} [options.buildMagicLinkUrl] - (rawToken) => string
|
|
937
|
+
* @param {string} [options.path='/auth/magic-link/verify']
|
|
938
|
+
* @param {boolean} [options.hideUserNotFound=true]
|
|
939
|
+
* @returns {Promise<{ sent: boolean, expiresAt?: Date, token?: string, magicLinkUrl?: string, userFound: boolean }>}
|
|
940
|
+
*/
|
|
941
|
+
async requestMagicLink(identifier, options = {}) {
|
|
942
|
+
if (!this.authTokens) {
|
|
943
|
+
throw new Error('authTokens adapter is required for magic link authentication');
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const user = await this.resolveUserByIdentifier(identifier);
|
|
947
|
+
if (!user) {
|
|
948
|
+
return {
|
|
949
|
+
sent: options.hideUserNotFound !== false,
|
|
950
|
+
userFound: false,
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const tokenTtlMs = options.tokenTtlMs ?? 15 * 60 * 1000;
|
|
955
|
+
const { rawToken, expiresAt } = await createAuthToken(
|
|
956
|
+
this.authTokens,
|
|
957
|
+
TOKEN_TYPES.MAGIC_LINK,
|
|
958
|
+
user.id,
|
|
959
|
+
tokenTtlMs
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
let baseUrl = String(options.baseUrl || this.config.baseUrl || process.env.BASE_URL || 'http://localhost:3000');
|
|
963
|
+
while (baseUrl.endsWith('/')) {
|
|
964
|
+
baseUrl = baseUrl.slice(0, -1);
|
|
965
|
+
}
|
|
966
|
+
const pathSegment = options.path || '/auth/magic-link/verify';
|
|
967
|
+
const cleanPath = pathSegment.startsWith('/') ? pathSegment : `/${pathSegment}`;
|
|
968
|
+
const sep = cleanPath.includes('?') ? '&' : '?';
|
|
969
|
+
|
|
970
|
+
const hasCustomUrl = typeof options.buildMagicLinkUrl === 'function' || Boolean(options.path) || Boolean(options.baseUrl);
|
|
971
|
+
const customMagicLinkUrl = typeof options.buildMagicLinkUrl === 'function'
|
|
972
|
+
? options.buildMagicLinkUrl(rawToken)
|
|
973
|
+
: hasCustomUrl
|
|
974
|
+
? `${baseUrl}${cleanPath}${sep}token=${encodeURIComponent(rawToken)}`
|
|
975
|
+
: null;
|
|
976
|
+
|
|
977
|
+
const defaultMagicLinkUrl = `${baseUrl}${cleanPath}${sep}token=${encodeURIComponent(rawToken)}`;
|
|
978
|
+
|
|
979
|
+
if (typeof this.notifications.magicLink === 'function') {
|
|
980
|
+
await this.notifications.magicLink(user, {
|
|
981
|
+
token: rawToken,
|
|
982
|
+
expiresAt,
|
|
983
|
+
magicLinkUrl: customMagicLinkUrl,
|
|
984
|
+
expiresMinutes: Math.round(tokenTtlMs / 60000),
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
this.emit('magicLink:requested', { userId: user.id, email: user.email, expiresAt });
|
|
989
|
+
|
|
990
|
+
return {
|
|
991
|
+
sent: true,
|
|
992
|
+
userFound: true,
|
|
993
|
+
expiresAt,
|
|
994
|
+
token: rawToken,
|
|
995
|
+
magicLinkUrl: customMagicLinkUrl || defaultMagicLinkUrl,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Verify magic link token and resolve user
|
|
1001
|
+
* @param {string} rawToken
|
|
1002
|
+
* @param {Object} [options]
|
|
1003
|
+
* @param {boolean} [options.markEmailVerified=true]
|
|
1004
|
+
* @param {boolean} [options.consumeAllForUser=true]
|
|
1005
|
+
* @returns {Promise<{ ok: true, user: Object, userId: * }>}
|
|
1006
|
+
*/
|
|
1007
|
+
async verifyMagicLink(rawToken, options = {}) {
|
|
1008
|
+
if (!this.authTokens) {
|
|
1009
|
+
throw new Error('authTokens adapter is required for magic link authentication');
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const verified = await verifyAuthToken(
|
|
1013
|
+
this.authTokens,
|
|
1014
|
+
TOKEN_TYPES.MAGIC_LINK,
|
|
1015
|
+
rawToken
|
|
1016
|
+
);
|
|
1017
|
+
if (!verified) {
|
|
1018
|
+
throw new AuthenticationError('Invalid or expired magic link', 'INVALID_MAGIC_LINK');
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const user = await this.findUserById(verified.userId);
|
|
1022
|
+
if (!user) {
|
|
1023
|
+
throw new AuthenticationError('User not found for magic link', 'USER_NOT_FOUND');
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
await consumeAuthToken(this.authTokens, verified.tokenHash);
|
|
1027
|
+
|
|
1028
|
+
if (options.consumeAllForUser !== false) {
|
|
1029
|
+
await this.authTokens.deleteAllForUser(verified.userId, TOKEN_TYPES.MAGIC_LINK);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Auto mark email verified if user has not verified it yet
|
|
1033
|
+
if (options.markEmailVerified !== false && this.updateUser && this.verifiedField && !user[this.verifiedField]) {
|
|
1034
|
+
try {
|
|
1035
|
+
await this.updateUser(user.id, { [this.verifiedField]: new Date() });
|
|
1036
|
+
user[this.verifiedField] = new Date();
|
|
1037
|
+
} catch (e) {
|
|
1038
|
+
// Continue even if update fails
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
this.emit('magicLink:verified', { userId: user.id });
|
|
1043
|
+
|
|
1044
|
+
return { ok: true, user, userId: verified.userId };
|
|
1045
|
+
}
|
|
1046
|
+
|
|
790
1047
|
/**
|
|
791
1048
|
* Send welcome notification if configured
|
|
792
1049
|
* @param {Object} user
|
|
@@ -935,6 +1192,89 @@ class AuthManager {
|
|
|
935
1192
|
}
|
|
936
1193
|
return this._authMiddleware.require2Fa(options);
|
|
937
1194
|
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* Track session for a request
|
|
1198
|
+
* @param {string} sessionId
|
|
1199
|
+
* @param {string|number} userId
|
|
1200
|
+
* @param {Object} req
|
|
1201
|
+
*/
|
|
1202
|
+
async trackSession(sessionId, userId, req) {
|
|
1203
|
+
if (this.sessionManager) {
|
|
1204
|
+
return await this.sessionManager.track(sessionId, userId, req);
|
|
1205
|
+
}
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Touch session to update activity timestamp
|
|
1211
|
+
* @param {string} sessionId
|
|
1212
|
+
*/
|
|
1213
|
+
async touchSession(sessionId) {
|
|
1214
|
+
if (this.sessionManager) {
|
|
1215
|
+
return await this.sessionManager.touch(sessionId);
|
|
1216
|
+
}
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* List active sessions for user
|
|
1222
|
+
* @param {string|number} userId
|
|
1223
|
+
* @param {string} [currentSessionId]
|
|
1224
|
+
*/
|
|
1225
|
+
async listSessions(userId, currentSessionId = null) {
|
|
1226
|
+
if (this.sessionManager) {
|
|
1227
|
+
return await this.sessionManager.listSessions(userId, currentSessionId);
|
|
1228
|
+
}
|
|
1229
|
+
return [];
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Check if session is valid in store
|
|
1234
|
+
* @param {string} sessionId
|
|
1235
|
+
* @param {string|number} [userId]
|
|
1236
|
+
*/
|
|
1237
|
+
async isSessionValid(sessionId, userId = null) {
|
|
1238
|
+
if (this.sessionManager) {
|
|
1239
|
+
return await this.sessionManager.isSessionValid(sessionId, userId);
|
|
1240
|
+
}
|
|
1241
|
+
return true;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* Revoke a specific session
|
|
1246
|
+
* @param {string} sessionId
|
|
1247
|
+
* @param {Object} [options]
|
|
1248
|
+
*/
|
|
1249
|
+
async revokeSession(sessionId, options = {}) {
|
|
1250
|
+
if (this.sessionManager) {
|
|
1251
|
+
return await this.sessionManager.revokeSession(sessionId, options);
|
|
1252
|
+
}
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* Revoke all other sessions for user
|
|
1258
|
+
* @param {string|number} userId
|
|
1259
|
+
* @param {string} currentSessionId
|
|
1260
|
+
*/
|
|
1261
|
+
async revokeOtherSessions(userId, currentSessionId) {
|
|
1262
|
+
if (this.sessionManager) {
|
|
1263
|
+
return await this.sessionManager.revokeOtherSessions(userId, currentSessionId);
|
|
1264
|
+
}
|
|
1265
|
+
return 0;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Revoke all sessions for user
|
|
1270
|
+
* @param {string|number} userId
|
|
1271
|
+
*/
|
|
1272
|
+
async revokeAllSessions(userId) {
|
|
1273
|
+
if (this.sessionManager) {
|
|
1274
|
+
return await this.sessionManager.revokeAllSessions(userId);
|
|
1275
|
+
}
|
|
1276
|
+
return 0;
|
|
1277
|
+
}
|
|
938
1278
|
}
|
|
939
1279
|
|
|
940
1280
|
module.exports = {
|
package/core/auth/middleware.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
const session = require('express-session');
|
|
8
8
|
const cookieParser = require('cookie-parser');
|
|
9
|
+
const { createListSessionsHandler, createRevokeSessionHandler } = require('./session-manager');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Create authentication middleware stack
|
|
@@ -79,12 +80,29 @@ function createAuthMiddleware(authManager) {
|
|
|
79
80
|
|
|
80
81
|
// Try to load user from session if not authenticated by JWT
|
|
81
82
|
if (!req.user && req.session?.userId) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
// If session tracking is enabled, verify session has not been revoked
|
|
84
|
+
let isSessionValid = true;
|
|
85
|
+
if (authManager.sessionManager && req.sessionID) {
|
|
86
|
+
isSessionValid = await authManager.isSessionValid(req.sessionID, req.session.userId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!isSessionValid) {
|
|
90
|
+
// Session was revoked remotely
|
|
87
91
|
delete req.session.userId;
|
|
92
|
+
if (typeof req.session.destroy === 'function') {
|
|
93
|
+
req.session.destroy(() => {});
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
const user = await authManager.findUserById(req.session.userId);
|
|
97
|
+
if (user) {
|
|
98
|
+
req.user = user;
|
|
99
|
+
if (req.sessionID && authManager.sessionManager) {
|
|
100
|
+
authManager.touchSession(req.sessionID).catch(() => {});
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
// User no longer exists, clear session
|
|
104
|
+
delete req.session.userId;
|
|
105
|
+
}
|
|
88
106
|
}
|
|
89
107
|
}
|
|
90
108
|
|
|
@@ -95,6 +113,9 @@ function createAuthMiddleware(authManager) {
|
|
|
95
113
|
req.user = user;
|
|
96
114
|
req.session.userId = user.id;
|
|
97
115
|
req.session.loggedInAt = Date.now();
|
|
116
|
+
if (req.sessionID && authManager.sessionManager) {
|
|
117
|
+
authManager.trackSession(req.sessionID, user.id, req).catch(() => {});
|
|
118
|
+
}
|
|
98
119
|
}
|
|
99
120
|
}
|
|
100
121
|
|
|
@@ -389,6 +410,10 @@ function createAuthMiddleware(authManager) {
|
|
|
389
410
|
// Utility
|
|
390
411
|
parseMiddlewareString,
|
|
391
412
|
|
|
413
|
+
// Session handlers
|
|
414
|
+
listSessions: createListSessionsHandler(authManager),
|
|
415
|
+
revokeSession: createRevokeSessionHandler(authManager),
|
|
416
|
+
|
|
392
417
|
// Aliases for route config (factories — file-router calls requireAuth(opts) / requireGuest(opts))
|
|
393
418
|
auth: requireAuth,
|
|
394
419
|
jwt: requireJwt,
|