security: fix all Critical and High findings from FLAWS_REPORT audit
- Auth: pessimistic locking on login tokens and refresh token rotation, backup code attempt counter, rate limiting verification - Schema: unique constraints on business numbers, FK relations, unsigned/signed alignment, attendance duplicate prevention - Invoices/PDFs: DOMPurify sanitization, bounded queries in stats and alerts, VAT rounding, Puppeteer error handling - Orders/Offers: transactional parent+child creation, Zod NaN refinement, status enums, uniqueness checks - Projects/Files: path traversal protection, streamed uploads, permission guards, query param validation - Attendance/HR: duplicate checks, ownership validation, GPS restrictions, trip distance validation - Frontend: modal lock reference counting, XSS escaping in print HTML, ref mutation fixes, accessibility attributes Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -91,7 +91,10 @@ export async function getUser(id: number) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function createUser(data: CreateUserData) {
|
||||
export async function createUser(
|
||||
data: CreateUserData,
|
||||
callerRoleName?: string,
|
||||
) {
|
||||
const username = data.username.trim();
|
||||
const email = data.email.trim();
|
||||
const firstName = data.first_name.trim();
|
||||
@@ -113,6 +116,15 @@ export async function createUser(data: CreateUserData) {
|
||||
return { error: "E-mail již existuje", status: 409 } as const;
|
||||
}
|
||||
|
||||
if (data.role_id) {
|
||||
const targetRole = await prisma.roles.findUnique({
|
||||
where: { id: Number(data.role_id) },
|
||||
});
|
||||
if (targetRole?.name === "admin" && callerRoleName !== "admin") {
|
||||
return { error: "Nelze přiřadit roli admin", status: 403 } as const;
|
||||
}
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(
|
||||
data.password,
|
||||
config.security.bcryptCost,
|
||||
@@ -133,12 +145,71 @@ export async function createUser(data: CreateUserData) {
|
||||
return { user } as const;
|
||||
}
|
||||
|
||||
export async function updateUser(id: number, body: UpdateUserData) {
|
||||
export async function updateUser(
|
||||
id: number,
|
||||
body: UpdateUserData,
|
||||
callerRoleName?: string,
|
||||
) {
|
||||
const existing = await prisma.users.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return { error: "Uživatel nenalezen", status: 404 } as const;
|
||||
}
|
||||
|
||||
if (body.role_id !== undefined) {
|
||||
if (body.role_id) {
|
||||
const targetRole = await prisma.roles.findUnique({
|
||||
where: { id: Number(body.role_id) },
|
||||
});
|
||||
if (targetRole?.name === "admin" && callerRoleName !== "admin") {
|
||||
return { error: "Nelze přiřadit roli admin", status: 403 } as const;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
existing.role_id !== null &&
|
||||
Number(body.role_id) !== existing.role_id
|
||||
) {
|
||||
const currentRole = await prisma.roles.findUnique({
|
||||
where: { id: existing.role_id },
|
||||
});
|
||||
if (currentRole?.name === "admin") {
|
||||
const adminRole = await prisma.roles.findFirst({
|
||||
where: { name: "admin" },
|
||||
});
|
||||
if (adminRole) {
|
||||
const activeAdminCount = await prisma.users.count({
|
||||
where: { role_id: adminRole.id, is_active: true },
|
||||
});
|
||||
if (activeAdminCount <= 1) {
|
||||
return {
|
||||
error: "Nelze odebrat roli poslednímu aktivnímu administrátorovi",
|
||||
status: 400,
|
||||
} as const;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (body.is_active !== undefined && !body.is_active) {
|
||||
if (existing.role_id !== null && existing.is_active) {
|
||||
const adminRole = await prisma.roles.findFirst({
|
||||
where: { name: "admin" },
|
||||
});
|
||||
if (adminRole && existing.role_id === adminRole.id) {
|
||||
const activeAdminCount = await prisma.users.count({
|
||||
where: { role_id: adminRole.id, is_active: true },
|
||||
});
|
||||
if (activeAdminCount <= 1) {
|
||||
return {
|
||||
error: "Nelze deaktivovat posledního aktivního administrátora",
|
||||
status: 400,
|
||||
} as const;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const data: Record<string, unknown> = {};
|
||||
|
||||
if (body.username !== undefined) {
|
||||
|
||||
Reference in New Issue
Block a user