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>
This commit is contained in:
@@ -100,7 +100,7 @@ export default async function attendanceRoutes(
|
||||
|
||||
// --- action=balances: leave balance overview for all users ---
|
||||
if (action === "balances") {
|
||||
if (!authData.permissions.includes("attendance.admin")) {
|
||||
if (!authData.permissions.includes("attendance.balances")) {
|
||||
return error(reply, "Nedostatečná oprávnění", 403);
|
||||
}
|
||||
const yr = Number(query.year) || new Date().getFullYear();
|
||||
@@ -110,7 +110,7 @@ export default async function attendanceRoutes(
|
||||
|
||||
// --- action=workfund: monthly work fund overview ---
|
||||
if (action === "workfund") {
|
||||
if (!authData.permissions.includes("attendance.admin")) {
|
||||
if (!authData.permissions.includes("attendance.manage")) {
|
||||
return error(reply, "Nedostatečná oprávnění", 403);
|
||||
}
|
||||
const yr = Number(query.year) || new Date().getFullYear();
|
||||
@@ -120,7 +120,7 @@ export default async function attendanceRoutes(
|
||||
|
||||
// --- action=project_report: monthly project hours ---
|
||||
if (action === "project_report") {
|
||||
if (!authData.permissions.includes("attendance.admin")) {
|
||||
if (!authData.permissions.includes("attendance.manage")) {
|
||||
return error(reply, "Nedostatečná oprávnění", 403);
|
||||
}
|
||||
const yr = Number(query.year) || new Date().getFullYear();
|
||||
@@ -130,7 +130,7 @@ export default async function attendanceRoutes(
|
||||
|
||||
// --- action=print: attendance print data for admin ---
|
||||
if (action === "print") {
|
||||
if (!authData.permissions.includes("attendance.admin")) {
|
||||
if (!authData.permissions.includes("attendance.manage")) {
|
||||
return error(reply, "Nedostatečná oprávnění", 403);
|
||||
}
|
||||
|
||||
@@ -145,8 +145,7 @@ export default async function attendanceRoutes(
|
||||
// --- action=attendance_users: users with attendance.record permission ---
|
||||
if (action === "attendance_users") {
|
||||
if (
|
||||
!authData.permissions.includes("attendance.admin") &&
|
||||
!authData.permissions.includes("attendance.view") &&
|
||||
!authData.permissions.includes("attendance.manage") &&
|
||||
!authData.permissions.includes("attendance.record")
|
||||
) {
|
||||
return error(reply, "Nedostatečná oprávnění", 403);
|
||||
@@ -182,10 +181,7 @@ export default async function attendanceRoutes(
|
||||
|
||||
// --- action=project_logs: get project logs for a specific attendance record ---
|
||||
if (action === "project_logs") {
|
||||
if (
|
||||
!authData.permissions.includes("attendance.view") &&
|
||||
!authData.permissions.includes("attendance.record")
|
||||
) {
|
||||
if (!authData.permissions.includes("attendance.record")) {
|
||||
return error(reply, "Nedostatečná oprávnění", 403);
|
||||
}
|
||||
const attendanceId = Number(query.attendance_id);
|
||||
@@ -200,7 +196,7 @@ export default async function attendanceRoutes(
|
||||
if (!id) return error(reply, "Missing id", 400);
|
||||
const record = await attendanceService.getLocationRecord(id);
|
||||
if (!record) return error(reply, "Záznam nenalezen", 404);
|
||||
const isAdmin = authData.permissions.includes("attendance.admin");
|
||||
const isAdmin = authData.permissions.includes("attendance.manage");
|
||||
if (record.user_id !== authData.userId && !isAdmin) {
|
||||
return error(reply, "Nedostatečná oprávnění", 403);
|
||||
}
|
||||
@@ -209,7 +205,7 @@ export default async function attendanceRoutes(
|
||||
|
||||
// --- Default: paginated records list ---
|
||||
const { page, limit, skip, order } = parsePagination(query);
|
||||
const isAdmin = authData.permissions.includes("attendance.admin");
|
||||
const isAdmin = authData.permissions.includes("attendance.manage");
|
||||
const userId = query.user_id ? Number(query.user_id) : undefined;
|
||||
|
||||
const result = await attendanceService.listAttendance({
|
||||
@@ -285,7 +281,7 @@ export default async function attendanceRoutes(
|
||||
|
||||
// --- action=bulk_attendance: bulk fill month ---
|
||||
if (postQuery.action === "bulk_attendance") {
|
||||
if (!authData.permissions.includes("attendance.admin")) {
|
||||
if (!authData.permissions.includes("attendance.manage")) {
|
||||
return error(reply, "Nedostatečná oprávnění", 403);
|
||||
}
|
||||
|
||||
@@ -328,7 +324,7 @@ export default async function attendanceRoutes(
|
||||
if (
|
||||
leaveBody.user_id != null &&
|
||||
leaveBody.user_id !== authData.userId &&
|
||||
!authData.permissions.includes("attendance.admin")
|
||||
!authData.permissions.includes("attendance.manage")
|
||||
) {
|
||||
return error(reply, "Nedostatečná oprávnění", 403);
|
||||
}
|
||||
@@ -384,7 +380,7 @@ export default async function attendanceRoutes(
|
||||
if (
|
||||
body.user_id != null &&
|
||||
body.user_id !== authData.userId &&
|
||||
!authData.permissions.includes("attendance.admin")
|
||||
!authData.permissions.includes("attendance.manage")
|
||||
) {
|
||||
return error(reply, "Nedostatečná oprávnění", 403);
|
||||
}
|
||||
@@ -429,7 +425,7 @@ export default async function attendanceRoutes(
|
||||
// PUT /api/admin/attendance/:id
|
||||
fastify.put<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("attendance.edit") },
|
||||
{ preHandler: requirePermission("attendance.manage") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
@@ -438,7 +434,7 @@ export default async function attendanceRoutes(
|
||||
const body = parsed.data;
|
||||
|
||||
const authData = request.authData!;
|
||||
const isAdmin = authData.permissions.includes("attendance.admin");
|
||||
const isAdmin = authData.permissions.includes("attendance.manage");
|
||||
|
||||
const result = await attendanceService.updateAttendance(
|
||||
id,
|
||||
@@ -481,7 +477,7 @@ export default async function attendanceRoutes(
|
||||
// DELETE /api/admin/attendance/:id
|
||||
fastify.delete<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("attendance.admin") },
|
||||
{ preHandler: requirePermission("attendance.manage") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
|
||||
@@ -14,7 +14,7 @@ export default async function bankAccountsRoutes(
|
||||
): Promise<void> {
|
||||
fastify.get(
|
||||
"/",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.banking") },
|
||||
async (_request, reply) => {
|
||||
const accounts = await prisma.bank_accounts.findMany({
|
||||
orderBy: { position: "asc" },
|
||||
@@ -25,7 +25,7 @@ export default async function bankAccountsRoutes(
|
||||
|
||||
fastify.post(
|
||||
"/",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.banking") },
|
||||
async (request, reply) => {
|
||||
const parsed = parseBody(CreateBankAccountSchema, request.body);
|
||||
if ("error" in parsed) return error(reply, parsed.error, 400);
|
||||
@@ -59,7 +59,7 @@ export default async function bankAccountsRoutes(
|
||||
|
||||
fastify.put<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.banking") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
@@ -126,7 +126,7 @@ export default async function bankAccountsRoutes(
|
||||
|
||||
fastify.delete<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.banking") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
|
||||
@@ -99,7 +99,7 @@ export default async function companySettingsRoutes(
|
||||
// POST /api/admin/company-settings/logo?variant=light|dark
|
||||
fastify.post(
|
||||
"/logo",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.company") },
|
||||
async (request, reply) => {
|
||||
const query = request.query as Record<string, string>;
|
||||
const variant = query.variant === "dark" ? "dark" : "light";
|
||||
@@ -333,7 +333,7 @@ export default async function companySettingsRoutes(
|
||||
// GET /api/admin/company-settings/system-info
|
||||
fastify.get(
|
||||
"/system-info",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.company") },
|
||||
async (request, reply) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const pkg = require("../../../package.json") as { version: string };
|
||||
@@ -404,7 +404,7 @@ export default async function companySettingsRoutes(
|
||||
|
||||
fastify.put(
|
||||
"/",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.company") },
|
||||
async (request, reply) => {
|
||||
const parsed = parseBody(UpdateCompanySettingsSchema, request.body);
|
||||
if ("error" in parsed) return error(reply, parsed.error, 400);
|
||||
|
||||
@@ -126,7 +126,7 @@ export default async function customersRoutes(
|
||||
|
||||
fastify.post(
|
||||
"/",
|
||||
{ preHandler: requirePermission("customers.manage") },
|
||||
{ preHandler: requirePermission("customers.create") },
|
||||
async (request, reply) => {
|
||||
const parsed = parseBody(CreateCustomerSchema, request.body);
|
||||
if ("error" in parsed) return error(reply, parsed.error, 400);
|
||||
@@ -164,7 +164,7 @@ export default async function customersRoutes(
|
||||
|
||||
fastify.put<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("customers.manage") },
|
||||
{ preHandler: requirePermission("customers.edit") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
@@ -239,7 +239,7 @@ export default async function customersRoutes(
|
||||
|
||||
fastify.delete<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("customers.manage") },
|
||||
{ preHandler: requirePermission("customers.delete") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
|
||||
@@ -42,8 +42,8 @@ export default async function dashboardRoutes(
|
||||
result.my_shift = { has_ongoing: myShift !== null };
|
||||
}
|
||||
|
||||
// Attendance admin — only for attendance.admin
|
||||
if (has("attendance.admin")) {
|
||||
// Attendance admin — only for attendance.manage
|
||||
if (has("attendance.manage")) {
|
||||
const [todayAttendance, onLeaveToday, usersCount] = await Promise.all([
|
||||
prisma.attendance.findMany({
|
||||
where: {
|
||||
|
||||
@@ -36,6 +36,13 @@ export default async function profileRoutes(
|
||||
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();
|
||||
|
||||
@@ -12,7 +12,7 @@ export default async function rolesRoutes(
|
||||
// GET /api/admin/roles
|
||||
fastify.get(
|
||||
"/",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.roles") },
|
||||
async (request, reply) => {
|
||||
const roles = await prisma.roles.findMany({
|
||||
include: {
|
||||
@@ -35,7 +35,7 @@ export default async function rolesRoutes(
|
||||
// GET /api/admin/roles/permissions
|
||||
fastify.get(
|
||||
"/permissions",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.roles") },
|
||||
async (_request, reply) => {
|
||||
const permissions = await prisma.permissions.findMany({
|
||||
orderBy: { module: "asc" },
|
||||
@@ -47,7 +47,7 @@ export default async function rolesRoutes(
|
||||
// POST /api/admin/roles
|
||||
fastify.post(
|
||||
"/",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.roles") },
|
||||
async (request, reply) => {
|
||||
const parsed = parseBody(CreateRoleSchema, request.body);
|
||||
if ("error" in parsed) return error(reply, parsed.error, 400);
|
||||
@@ -90,7 +90,7 @@ export default async function rolesRoutes(
|
||||
// PUT /api/admin/roles/:id
|
||||
fastify.put<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.roles") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
@@ -142,7 +142,7 @@ export default async function rolesRoutes(
|
||||
// DELETE /api/admin/roles/:id
|
||||
fastify.delete<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.roles") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
|
||||
@@ -22,7 +22,7 @@ export default async function scopeTemplatesRoutes(
|
||||
// Legacy ?action= dispatcher for item templates
|
||||
fastify.get(
|
||||
"/",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.templates") },
|
||||
async (request, reply) => {
|
||||
const query = request.query as Record<string, unknown>;
|
||||
const action = query.action ? String(query.action) : null;
|
||||
@@ -53,7 +53,7 @@ export default async function scopeTemplatesRoutes(
|
||||
// Item template CRUD via ?action=item
|
||||
fastify.post(
|
||||
"/",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.templates") },
|
||||
async (request, reply) => {
|
||||
const query = request.query as Record<string, unknown>;
|
||||
|
||||
@@ -121,7 +121,7 @@ export default async function scopeTemplatesRoutes(
|
||||
// Item template delete via DELETE ?action=item&id=X
|
||||
fastify.delete(
|
||||
"/",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.templates") },
|
||||
async (request, reply) => {
|
||||
const query = request.query as Record<string, unknown>;
|
||||
|
||||
@@ -140,7 +140,7 @@ export default async function scopeTemplatesRoutes(
|
||||
|
||||
fastify.get<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.templates") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
@@ -161,7 +161,7 @@ export default async function scopeTemplatesRoutes(
|
||||
|
||||
fastify.put<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.templates") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
@@ -210,7 +210,7 @@ export default async function scopeTemplatesRoutes(
|
||||
|
||||
fastify.delete<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("settings.manage") },
|
||||
{ preHandler: requirePermission("settings.templates") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
|
||||
@@ -193,7 +193,7 @@ export default async function totpRoutes(
|
||||
// GET - check if 2FA is required company-wide
|
||||
fastify.get(
|
||||
"/required",
|
||||
{ preHandler: [requireAuth, requirePermission("settings.manage")] },
|
||||
{ preHandler: requireAuth },
|
||||
async (request, reply) => {
|
||||
const settings = await prisma.company_settings.findFirst({
|
||||
select: { require_2fa: true },
|
||||
@@ -207,7 +207,7 @@ export default async function totpRoutes(
|
||||
fastify.post(
|
||||
"/required",
|
||||
{
|
||||
preHandler: [requireAuth, requirePermission("settings.manage")],
|
||||
preHandler: [requireAuth, requirePermission("settings.company")],
|
||||
bodyLimit: 10240,
|
||||
},
|
||||
async (request, reply) => {
|
||||
|
||||
@@ -14,7 +14,12 @@ export default async function tripsRoutes(
|
||||
const query = request.query as Record<string, unknown>;
|
||||
const { page, limit, skip, order } = parsePagination(query);
|
||||
const authData = request.authData!;
|
||||
const isAdmin = authData.permissions.includes("trips.admin");
|
||||
const hasTripsAccess =
|
||||
authData.permissions.includes("trips.manage") ||
|
||||
authData.permissions.includes("trips.record") ||
|
||||
authData.permissions.includes("trips.history");
|
||||
if (!hasTripsAccess) return error(reply, "Nedostatečná oprávnění", 403);
|
||||
const isAdmin = authData.permissions.includes("trips.manage");
|
||||
|
||||
const where: Record<string, unknown> = {};
|
||||
if (!isAdmin) where.user_id = authData.userId;
|
||||
@@ -107,7 +112,7 @@ export default async function tripsRoutes(
|
||||
// GET /api/admin/trips/print — print data for trip report
|
||||
fastify.get(
|
||||
"/print",
|
||||
{ preHandler: requirePermission("trips.admin") },
|
||||
{ preHandler: requirePermission("trips.manage") },
|
||||
async (request, reply) => {
|
||||
const query = request.query as Record<string, unknown>;
|
||||
const filterUserId = query.user_id ? Number(query.user_id) : null;
|
||||
@@ -182,7 +187,7 @@ export default async function tripsRoutes(
|
||||
// Matches PHP: COALESCE(MAX(end_km), vehicle.initial_km, 0)
|
||||
fastify.get<{ Params: { vehicleId: string } }>(
|
||||
"/last-km/:vehicleId",
|
||||
{ preHandler: requirePermission("trips.view") },
|
||||
{ preHandler: requirePermission("trips.manage") },
|
||||
async (request, reply) => {
|
||||
const vehicleId = parseInt(request.params.vehicleId, 10);
|
||||
if (isNaN(vehicleId)) return error(reply, "Neplatné ID vozidla", 400);
|
||||
@@ -213,6 +218,11 @@ export default async function tripsRoutes(
|
||||
const body = parsed.data;
|
||||
const authData = request.authData!;
|
||||
|
||||
const canRecord =
|
||||
authData.permissions.includes("trips.manage") ||
|
||||
authData.permissions.includes("trips.record");
|
||||
if (!canRecord) return error(reply, "Nedostatečná oprávnění", 403);
|
||||
|
||||
if (body.end_km < body.start_km) {
|
||||
return error(reply, "Konečný stav km nesmí být menší než počáteční", 400);
|
||||
}
|
||||
@@ -274,7 +284,7 @@ export default async function tripsRoutes(
|
||||
if (!existing) return error(reply, "Jízda nenalezena", 404);
|
||||
|
||||
// Ownership check — same as DELETE handler
|
||||
const isAdmin = authData.permissions.includes("trips.admin");
|
||||
const isAdmin = authData.permissions.includes("trips.manage");
|
||||
if (existing.user_id !== authData.userId && !isAdmin) {
|
||||
return error(reply, "Nemáte oprávnění upravit tuto jízdu", 403);
|
||||
}
|
||||
@@ -332,7 +342,7 @@ export default async function tripsRoutes(
|
||||
if (!existing) return error(reply, "Jízda nenalezena", 404);
|
||||
|
||||
// Allow users to delete their own trips, admins can delete any
|
||||
const isAdmin = authData.permissions.includes("trips.admin");
|
||||
const isAdmin = authData.permissions.includes("trips.manage");
|
||||
if (existing.user_id !== authData.userId && !isAdmin) {
|
||||
return error(reply, "Nemáte oprávnění smazat tuto jízdu", 403);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export default async function vehiclesRoutes(
|
||||
|
||||
fastify.post(
|
||||
"/",
|
||||
{ preHandler: requirePermission("trips.vehicles") },
|
||||
{ preHandler: requirePermission("vehicles.manage") },
|
||||
async (request, reply) => {
|
||||
const parsed = parseBody(CreateVehicleSchema, request.body);
|
||||
if ("error" in parsed) return error(reply, parsed.error, 400);
|
||||
@@ -75,7 +75,7 @@ export default async function vehiclesRoutes(
|
||||
|
||||
fastify.put<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("trips.vehicles") },
|
||||
{ preHandler: requirePermission("vehicles.manage") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
@@ -124,7 +124,7 @@ export default async function vehiclesRoutes(
|
||||
|
||||
fastify.delete<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("trips.vehicles") },
|
||||
{ preHandler: requirePermission("vehicles.manage") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
|
||||
Reference in New Issue
Block a user