initial commit
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
106
src/routes/admin/sessions.ts
Normal file
106
src/routes/admin/sessions.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
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<void> {
|
||||
// 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<string, unknown>;
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user