feat(auth): access 24ч, jti против гонок ротации, транзакционная ротация, отзыв сессий по sid
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
createUserCustomFieldSchema, updateUserCustomFieldSchema,
|
||||
setUserCustomValueSchema,
|
||||
} from "@shared/schema";
|
||||
import { logAudit, getClientIp } from "../utils/audit";
|
||||
|
||||
export function registerUserManagementRoutes(router: Router): void {
|
||||
// === User Custom Fields routes ===
|
||||
@@ -648,4 +649,79 @@ export function registerUserManagementRoutes(router: Router): void {
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Активные сессии пользователя (устройства) — админ или сам пользователь
|
||||
router.get('/api/users/:userId/sessions',
|
||||
authenticateToken,
|
||||
tenantIsolation,
|
||||
async (req: AuthenticatedRequest, res) => {
|
||||
try {
|
||||
const userId = parseInt(req.params.userId);
|
||||
if (isNaN(userId)) {
|
||||
return res.status(400).json({ success: false, error: 'Неверный ID пользователя' });
|
||||
}
|
||||
const targetUser = await storage.getUser(userId);
|
||||
if (!targetUser || targetUser.organizationId !== req.organizationId) {
|
||||
return res.status(404).json({ success: false, error: 'Пользователь не найден' });
|
||||
}
|
||||
const appRole = req.user?.appRole;
|
||||
if (appRole !== 'admin' && req.user?.id !== userId) {
|
||||
return res.status(403).json({ success: false, error: 'Недостаточно прав' });
|
||||
}
|
||||
const sessions = await storage.listUserSessions(userId);
|
||||
const currentSid = (req as any).sessionId ?? null;
|
||||
res.json({
|
||||
success: true,
|
||||
sessions: sessions.map(s => ({
|
||||
id: s.id,
|
||||
createdAt: s.createdAt,
|
||||
expiresAt: s.expiresAt,
|
||||
ipAddress: s.ipAddress,
|
||||
userAgent: s.userAgent,
|
||||
isCurrent: s.id === currentSid,
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('List user sessions error:', error);
|
||||
res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Отзыв сессии (устройства) — своя или любая (админ)
|
||||
router.delete('/api/auth/sessions/:id',
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({ success: false, error: 'Неверный ID сессии' });
|
||||
}
|
||||
const session = await storage.getUserSessionById(id);
|
||||
if (!session) {
|
||||
return res.status(404).json({ success: false, error: 'Сессия не найдена' });
|
||||
}
|
||||
const targetUser = await storage.getUser(session.userId);
|
||||
if (!targetUser || targetUser.organizationId !== req.organizationId) {
|
||||
return res.status(404).json({ success: false, error: 'Сессия не найдена' });
|
||||
}
|
||||
const appRole = req.user?.appRole;
|
||||
if (appRole !== 'admin' && req.user?.id !== session.userId) {
|
||||
return res.status(403).json({ success: false, error: 'Недостаточно прав' });
|
||||
}
|
||||
await storage.revokeSessionById(id);
|
||||
logAudit({
|
||||
action: 'auth.session.revoked',
|
||||
userId: req.user!.id,
|
||||
organizationId: req.organizationId,
|
||||
details: { sessionId: id, targetUserId: session.userId },
|
||||
ip: getClientIp(req),
|
||||
});
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Revoke session error:', error);
|
||||
res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user