import { FastifyInstance } from "fastify"; import prisma from "../../config/database"; import { requireAuth } from "../../middleware/auth"; import { success, error, parseId } from "../../utils/response"; import { hashToken } from "../../services/auth"; /** 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", }; 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"; 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"; 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 ?? "", }; }); 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 = parseId((request.params as any).id, reply); if (id === null) return; 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; if (!currentHash) { return error(reply, "Nelze identifikovat aktuální relaci", 400); } await prisma.refresh_tokens.updateMany({ where: { user_id: authData.userId, replaced_at: null, 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); }); }