import { FastifyInstance } from 'fastify'; import crypto from 'crypto'; import prisma from '../../config/database'; import { requireAuth } from '../../middleware/auth'; import { success, error } from '../../utils/response'; function hashToken(token: string): string { return crypto.createHash('sha256').update(token).digest('hex'); } /** Parse user-agent string into browser, OS, and device icon */ function parseUserAgent(ua: string | null): { browser: string; os: string; icon: string } { if (!ua) return { browser: 'Neznámý prohlížeč', os: 'Neznámý systém', icon: 'monitor' }; // Browser detection let browser = 'Neznámý prohlížeč'; if (ua.includes('Edg/')) browser = 'Edge'; else if (ua.includes('OPR/') || ua.includes('Opera')) browser = 'Opera'; else if (ua.includes('Chrome/')) browser = 'Chrome'; else if (ua.includes('Safari/') && !ua.includes('Chrome')) browser = 'Safari'; else if (ua.includes('Firefox/')) browser = 'Firefox'; // OS detection let os = 'Neznámý systém'; if (ua.includes('Windows')) os = 'Windows'; else if (ua.includes('Mac OS X') || ua.includes('Macintosh')) os = 'macOS'; else if (ua.includes('Linux') && !ua.includes('Android')) os = 'Linux'; else if (ua.includes('Android')) os = 'Android'; else if (ua.includes('iPhone') || ua.includes('iPad')) os = 'iOS'; // Device icon let icon = 'monitor'; if (ua.includes('Mobile') || ua.includes('iPhone') || ua.includes('Android')) { icon = ua.includes('iPad') || ua.includes('Tablet') ? 'tablet' : 'smartphone'; } return { browser, os, icon }; } export default async function sessionsRoutes(fastify: FastifyInstance): Promise { // GET /api/admin/sessions — list active sessions for current user fastify.get('/', { preHandler: requireAuth }, async (request, reply) => { const authData = request.authData!; const currentToken = request.cookies?.refresh_token; const currentHash = currentToken ? hashToken(currentToken) : null; const sessions = await prisma.refresh_tokens.findMany({ where: { user_id: authData.userId, replaced_at: null, expires_at: { gt: new Date() } }, orderBy: { created_at: 'desc' }, }); const enriched = sessions.map(s => { const device_info = parseUserAgent(s.user_agent); return { id: s.id, is_current: currentHash ? s.token_hash === currentHash : false, device_info, ip_address: s.ip_address || '', created_at: s.created_at ? s.created_at.toISOString() : '', }; }); return success(reply, enriched); }); // DELETE /api/admin/sessions/:id — delete specific session fastify.delete<{ Params: { id: string } }>('/:id', { preHandler: requireAuth }, async (request, reply) => { const id = parseInt(request.params.id, 10); const authData = request.authData!; const session = await prisma.refresh_tokens.findFirst({ where: { id, user_id: authData.userId }, }); if (!session) return error(reply, 'Relace nenalezena', 404); await prisma.refresh_tokens.update({ where: { id }, data: { replaced_at: new Date() }, }); return success(reply, null, 200, 'Relace ukončena'); }); // DELETE /api/admin/sessions — delete all sessions except current fastify.delete('/', { preHandler: requireAuth }, async (request, reply) => { const authData = request.authData!; const query = request.query as Record; if (query.action === 'all') { // Get current token from cookie to exclude (hash it to match stored token_hash) const currentToken = request.cookies?.refresh_token; const currentHash = currentToken ? hashToken(currentToken) : null; await prisma.refresh_tokens.updateMany({ where: { user_id: authData.userId, replaced_at: null, ...(currentHash ? { token_hash: { not: currentHash } } : {}), }, data: { replaced_at: new Date() }, }); return success(reply, null, 200, 'Všechny ostatní relace ukončeny'); } return error(reply, 'Neplatná akce', 400); }); }