Files
app/src/routes/admin/profile.ts
BOHA cd6d3daf43 v1.6.5: fix attendance unique constraint, schema sync, seed script
- Change attendance idx_attendance_user_date from unique to index (allow multiple shifts per day)
- Reset migrations to single baseline init migration
- Add seed script with admin user (admin/admin)
- Update CLAUDE.md with migration workflow documentation
- Various frontend fixes (queries, pages, hooks)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 16:21:34 +02:00

107 lines
3.6 KiB
TypeScript

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<void> {
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;
if (body.new_password && !body.current_password) {
return error(reply, "Pro změnu hesla zadejte aktuální heslo", 400);
}
if (body.current_password && !body.new_password) {
return error(reply, "Pro změnu hesla zadejte nové heslo", 400);
}
const data: Record<string, unknown> = {};
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");
});
}