import { FastifyInstance } from "fastify"; import prisma from "../../config/database"; import { requireAuth } from "../../middleware/auth"; import { success, error } from "../../utils/response"; import bcrypt from "bcryptjs"; import { config } from "../../config/env"; import { logAudit } from "../../services/audit"; import { parseBody } from "../../schemas/common"; import { UpdateProfileSchema } from "../../schemas/profile.schema"; export default async function profileRoutes( fastify: FastifyInstance, ): Promise { fastify.get("/", { preHandler: requireAuth }, async (request, reply) => { const user = await prisma.users.findUnique({ where: { id: request.authData!.userId }, select: { id: true, username: true, email: true, first_name: true, last_name: true, totp_enabled: true, last_login: true, password_changed_at: true, roles: { select: { id: true, name: true, display_name: true } }, }, }); if (!user) return error(reply, "Uživatel nenalezen", 404); return success(reply, user); }); fastify.put("/", { preHandler: requireAuth }, async (request, reply) => { const parsed = parseBody(UpdateProfileSchema, request.body); if ("error" in parsed) return error(reply, parsed.error, 400); const body = parsed.data; const userId = request.authData!.userId; const data: Record = {}; if (body.email) { data.email = String(body.email).trim(); } if (body.first_name) data.first_name = String(body.first_name); if (body.last_name) data.last_name = String(body.last_name); if (body.current_password && body.new_password) { const user = await prisma.users.findUnique({ where: { id: userId } }); if (!user) return error(reply, "Uživatel nenalezen", 404); const valid = await bcrypt.compare( String(body.current_password), user.password_hash, ); if (!valid) return error(reply, "Nesprávné aktuální heslo", 400); data.password_hash = await bcrypt.hash( String(body.new_password), config.security.bcryptCost, ); data.password_changed_at = new Date(); } // Wrap email uniqueness check and update in a transaction to prevent race condition try { await prisma.$transaction(async (tx) => { if (data.email) { const existing = await tx.users.findFirst({ where: { email: String(data.email), id: { not: userId } }, }); if (existing) throw new Error("EMAIL_EXISTS"); } await tx.users.update({ where: { id: userId }, data }); }); } catch (e) { if (e instanceof Error && e.message === "EMAIL_EXISTS") { return error(reply, "E-mail již existuje", 409); } throw e; } await logAudit({ request, authData: request.authData, action: "update", entityType: "user", entityId: userId, description: data.password_hash ? "Změna hesla" : "Aktualizace profilu", }); if (body.current_password && body.new_password) { await prisma.refresh_tokens.updateMany({ where: { user_id: userId, replaced_at: null }, data: { replaced_at: new Date() }, }); } return success(reply, null, 200, "Profil aktualizován"); }); }