- 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>
107 lines
3.6 KiB
TypeScript
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");
|
|
});
|
|
}
|