feat(auth): access 24ч, jti против гонок ротации, транзакционная ротация, отзыв сессий по sid

This commit is contained in:
2026-09-21 18:13:46 +03:00
parent 2f9d049718
commit c957a70260
8 changed files with 221 additions and 31 deletions

View File

@@ -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: 'Внутренняя ошибка сервера' });
}
}
);
}