feat: NAS storage for invoices/offers, code cleanup, date/time fixes

- NAS storage for created invoices (PDF via puppeteer), received invoices,
  and offers with auto-save on create/edit
- Deterministic file paths derived from DB fields (no file_path column needed)
- Separate NAS mount points: NAS_FINANCIALS_PATH, NAS_OFFERS_PATH
- Invoice language field (cs/en) stored per invoice, replaces lang modal
- Invoices list filtered by month/year matching KPI card selection
- Centralized date helpers (src/utils/date.ts) replacing all .toISOString()
  calls that returned UTC instead of local time
- Attendance project switching uses exact time (not rounded)
- Comment cleanup: removed ~100 unnecessary/Czech comments
- Removed as-any casts in orders and attendance
- Prisma migrations: add invoice language, drop received_invoices BLOB columns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-03-26 10:36:39 +01:00
parent 0317ba3168
commit baceb88347
60 changed files with 2475 additions and 563 deletions

View File

@@ -16,6 +16,7 @@ import {
UpdateAttendanceSchema,
} from "../../schemas/attendance.schema";
import * as attendanceService from "../../services/attendance.service";
import { localMonthStr } from "../../utils/date";
export default async function attendanceRoutes(
fastify: FastifyInstance,
@@ -125,7 +126,7 @@ export default async function attendanceRoutes(
const monthStr = query.month
? String(query.month)
: `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`;
: localMonthStr(new Date());
const filterUserId = query.user_id ? Number(query.user_id) : null;
const data = await attendanceService.getPrintData(monthStr, filterUserId);
return reply.send({ success: true, data });

View File

@@ -128,7 +128,6 @@ export default async function authRoutes(
return error(reply, "Neplatný TOTP kód", 401);
}
// Delete used login token
await prisma.totp_login_tokens.delete({ where: { id: storedToken.id } });
// Reset failed attempts and update last login (TOTP verified = successful login)
@@ -149,7 +148,6 @@ export default async function authRoutes(
return error(reply, "Chyba načítání uživatele", 500);
}
// Create tokens manually since password was already verified
const jwt = await import("jsonwebtoken");
const accessToken = jwt.default.sign(
{

View File

@@ -40,8 +40,7 @@ export default async function bankAccountsRoutes(
iban: body.iban ? String(body.iban) : null,
bic: body.bic ? String(body.bic) : null,
currency: body.currency ? String(body.currency) : "CZK",
is_default:
!!body.is_default,
is_default: !!body.is_default,
position: body.position ? Number(body.position) : 0,
},
});
@@ -107,9 +106,7 @@ export default async function bankAccountsRoutes(
currency:
body.currency !== undefined ? String(body.currency) : undefined,
is_default:
body.is_default !== undefined
? !!body.is_default
: undefined,
body.is_default !== undefined ? !!body.is_default : undefined,
position:
body.position !== undefined ? Number(body.position) : undefined,
modified_at: new Date(),

View File

@@ -2,6 +2,7 @@ import { FastifyInstance } from "fastify";
import prisma from "../../config/database";
import { requireAuth } from "../../middleware/auth";
import { success } from "../../utils/response";
import { localTimeStr } from "../../utils/date";
export default async function dashboardRoutes(
fastify: FastifyInstance,
@@ -106,9 +107,7 @@ export default async function dashboardRoutes(
name: `${user.first_name} ${user.last_name}`,
initials: `${firstInitial}${lastInitial}`.toUpperCase(),
status,
arrived_at: a.arrival_time
? `${String(a.arrival_time.getHours()).padStart(2, "0")}:${String(a.arrival_time.getMinutes()).padStart(2, "0")}`
: null,
arrived_at: a.arrival_time ? localTimeStr(a.arrival_time) : null,
});
}
@@ -252,7 +251,7 @@ export default async function dashboardRoutes(
entity_type: log.entity_type ?? "",
description: log.description ?? "",
username: log.username ?? null,
created_at: log.created_at ? log.created_at.toISOString() : "",
created_at: log.created_at ?? "",
}));
}

View File

@@ -2,6 +2,9 @@ import { FastifyInstance } from "fastify";
import QRCode from "qrcode";
import prisma from "../../config/database";
import { requirePermission } from "../../middleware/auth";
import { localDateCzStr } from "../../utils/date";
import { nasFinancialsManager } from "../../services/nas-financials-manager";
import { htmlToPdf } from "../../utils/html-to-pdf";
/* ── Helpers ─────────────────────────────────────────────────────── */
@@ -9,7 +12,7 @@ function formatDate(date: Date | string | null | undefined): string {
if (!date) return "";
const d = new Date(date);
if (isNaN(d.getTime())) return String(date);
return `${String(d.getDate()).padStart(2, "0")}.${String(d.getMonth() + 1).padStart(2, "0")}.${d.getFullYear()}`;
return localDateCzStr(d);
}
function formatNum(n: number, decimals = 2): string {
@@ -278,7 +281,6 @@ export default async function invoicesPdfRoutes(
unknown
> | null;
// Order number lookup
let orderNumber = "";
if (invoice.order_id) {
const orderRow = await prisma.orders.findUnique({
@@ -298,7 +300,6 @@ export default async function invoicesPdfRoutes(
}
}
// Logo
let logoImg = "";
if (settings?.logo_data) {
const buf = Buffer.from(settings.logo_data as Buffer);
@@ -313,7 +314,6 @@ export default async function invoicesPdfRoutes(
const currency = invoice.currency || "CZK";
const applyVat = !!invoice.apply_vat;
// Calculations
const vatSummary: Record<string, { base: number; vat: number }> = {};
let subtotal = 0;
@@ -380,7 +380,6 @@ export default async function invoicesPdfRoutes(
});
}
// Address lines
const supp = buildAddressLines(settings, true, t);
const cust = buildAddressLines(customer, false, t);
@@ -410,7 +409,6 @@ export default async function invoicesPdfRoutes(
const invoiceNumber = escapeHtml(invoice.invoice_number);
// Items HTML
const itemsHtml = items
.map((item, i) => {
const qty = Number(item.quantity);
@@ -434,7 +432,6 @@ export default async function invoicesPdfRoutes(
})
.join("");
// VAT recap rows
const vatRecapHtml = vatRecap
.map(
(vr) => `<tr>
@@ -446,7 +443,6 @@ export default async function invoicesPdfRoutes(
)
.join("");
// VAT detail rows for totals section
let vatDetailHtml = "";
if (applyVat) {
for (const [rate, data] of Object.entries(vatSummary)) {
@@ -460,7 +456,6 @@ export default async function invoicesPdfRoutes(
}
}
// Notes section
const notesRaw = invoice.notes ?? "";
const notesStripped = notesRaw.replace(/<[^>]*>/g, "").trim();
const notesHtml = notesStripped
@@ -1027,6 +1022,31 @@ ${indentCSS}
</body>
</html>`;
// Save PDF to NAS
if (nasFinancialsManager.isConfigured() && invoice.invoice_number) {
const issueDate = invoice.issue_date
? new Date(invoice.issue_date)
: new Date();
const saveMode = query.save === "1";
const pdfPromise = htmlToPdf(html)
.then((pdfBuffer) => {
nasFinancialsManager.saveIssuedInvoicePdf(
invoice.invoice_number!,
issueDate.getFullYear(),
issueDate.getMonth() + 1,
pdfBuffer,
);
})
.catch((err) => {
request.log.error(err, "Failed to save invoice PDF to NAS");
});
if (saveMode) {
await pdfPromise;
return reply.send({ success: true, message: "PDF uloženo" });
}
}
return reply.type("text/html").send(html);
},
);

View File

@@ -1,4 +1,5 @@
import { FastifyInstance } from "fastify";
import prisma from "../../config/database";
import { requirePermission } from "../../middleware/auth";
import { logAudit } from "../../services/audit";
import { success, error, parseId } from "../../utils/response";
@@ -19,6 +20,7 @@ import {
updateInvoice,
deleteInvoice,
} from "../../services/invoices.service";
import { nasFinancialsManager } from "../../services/nas-financials-manager";
export default async function invoicesRoutes(
fastify: FastifyInstance,
@@ -46,6 +48,8 @@ export default async function invoicesRoutes(
search,
status: query.status ? String(query.status) : undefined,
customer_id: query.customer_id ? Number(query.customer_id) : undefined,
month: query.month ? Number(query.month) : undefined,
year: query.year ? Number(query.year) : undefined,
});
return reply.send({
@@ -185,6 +189,13 @@ export default async function invoicesRoutes(
const existing = await deleteInvoice(id);
if (!existing) return error(reply, "Faktura nenalezena", 404);
// Delete PDF from NAS
if (existing.invoice_number && existing.issue_date) {
const d = new Date(existing.issue_date);
const relPath = `Vydané/${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, "0")}/${existing.invoice_number}.pdf`;
nasFinancialsManager.deleteIssuedInvoice(relPath);
}
await logAudit({
request,
authData: request.authData,
@@ -196,4 +207,33 @@ export default async function invoicesRoutes(
return success(reply, null, 200, "Faktura smazána");
},
);
// GET /api/admin/invoices/:id/file — serve PDF from NAS
fastify.get<{ Params: { id: string } }>(
"/:id/file",
{ preHandler: requirePermission("invoices.view") },
async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const invoice = await prisma.invoices.findUnique({
where: { id },
select: { invoice_number: true, issue_date: true },
});
if (!invoice?.invoice_number || !invoice.issue_date)
return error(reply, "Faktura nenalezena", 404);
const d = new Date(invoice.issue_date);
const relPath = `Vydané/${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, "0")}/${invoice.invoice_number}.pdf`;
const file = nasFinancialsManager.readIssuedInvoice(relPath);
if (!file) return error(reply, "PDF soubor nenalezen", 404);
return reply
.type("application/pdf")
.header(
"Content-Disposition",
`inline; filename="${invoice.invoice_number}.pdf"`,
)
.send(file.data);
},
);
}

View File

@@ -205,7 +205,6 @@ export default async function leaveRequestsRoutes(
}
}
// Count business days and create attendance records
let totalBusinessDays = 0;
const current = new Date(dateFrom);
const attendanceCreates: Array<{
@@ -242,7 +241,6 @@ export default async function leaveRequestsRoutes(
const totalHours = totalBusinessDays * 8;
// Run everything in a transaction
await prisma.$transaction(async (tx) => {
// 1. Create attendance records for each business day
if (attendanceCreates.length > 0) {

View File

@@ -1,12 +1,15 @@
import { FastifyInstance } from "fastify";
import prisma from "../../config/database";
import { requirePermission } from "../../middleware/auth";
import { localDateCzStr } from "../../utils/date";
import { nasOffersManager } from "../../services/nas-offers-manager";
import { htmlToPdf } from "../../utils/html-to-pdf";
function formatDate(date: Date | string | null | undefined): string {
if (!date) return "";
const d = new Date(date);
if (isNaN(d.getTime())) return String(date);
return `${String(d.getDate()).padStart(2, "0")}.${String(d.getMonth() + 1).padStart(2, "0")}.${d.getFullYear()}`;
return localDateCzStr(d);
}
/** Format number with comma decimal separator and non-breaking space thousands separator */
@@ -53,7 +56,6 @@ function cleanQuillHtml(html: string | null | undefined): string {
if (!html) return "";
const allowedTags =
"<p><br><strong><em><u><s><ul><ol><li><span><sub><sup><a><h1><h2><h3><h4><blockquote><pre>";
// Simple strip_tags equivalent: remove tags not in allowed list
let s = html;
// Remove dangerous tags with content
s = s.replace(
@@ -95,7 +97,6 @@ function buildAddressLines(
const nameKey = isSupplier ? "company_name" : "name";
const name = String(entity[nameKey] || "");
// Parse custom_fields
let cfData: Array<{ name?: string; value?: string; showLabel?: boolean }> =
[];
let fieldOrder: string[] | null = null;
@@ -201,6 +202,7 @@ export default async function offersPdfRoutes(
{ preHandler: requirePermission("offers.view") },
async (request, reply) => {
const id = parseInt(request.params.id, 10);
const query = request.query as Record<string, string>;
try {
const quotation = await prisma.quotations.findUnique({
@@ -225,7 +227,6 @@ export default async function offersPdfRoutes(
const currency = quotation.currency || "EUR";
const t = (key: string): string => TRANSLATIONS[key]?.[langKey] || key;
// Logo
let logoImg = "";
if (settings?.logo_data) {
const buf = Buffer.from(settings.logo_data);
@@ -236,7 +237,6 @@ export default async function offersPdfRoutes(
logoImg = `<img src="data:${escapeHtml(mime)};base64,${buf.toString("base64")}" class="logo" />`;
}
// Calculations
const items = quotation.quotation_items;
let subtotal = 0;
for (const item of items) {
@@ -251,7 +251,6 @@ export default async function offersPdfRoutes(
const totalToPay = subtotal + vatAmount;
const exchangeRate = Number(quotation.exchange_rate) || 0;
// Scope content check
let hasScopeContent = false;
for (const s of quotation.scope_sections) {
if ((s.content || "").trim() || (s.title || "").trim()) {
@@ -260,7 +259,6 @@ export default async function offersPdfRoutes(
}
}
// Addresses
const cust = buildAddressLines(
quotation.customers as unknown as Record<string, unknown>,
false,
@@ -288,7 +286,6 @@ export default async function offersPdfRoutes(
indentCSS += ` li.ql-indent-${n} { padding-left: ${liPad}em; }\n`;
}
// Items HTML
let itemsHtml = "";
items.forEach((item, i) => {
const lineTotal =
@@ -304,7 +301,6 @@ export default async function offersPdfRoutes(
</tr>`;
});
// Totals HTML
let totalsHtml = "";
if (applyVat) {
totalsHtml += `<div class="detail-rows">
@@ -328,7 +324,6 @@ export default async function offersPdfRoutes(
const quotationNumber = escapeHtml(quotation.quotation_number);
// Scope HTML
let scopeHtml = "";
if (hasScopeContent) {
scopeHtml += '<div class="scope-page">';
@@ -768,6 +763,30 @@ ${indentCSS}
</body>
</html>`;
// Save PDF to NAS
if (nasOffersManager.isConfigured() && quotation.quotation_number) {
const created = quotation.created_at
? new Date(quotation.created_at)
: new Date();
const saveMode = query.save === "1";
const pdfPromise = htmlToPdf(html)
.then((pdfBuffer) => {
nasOffersManager.saveOfferPdf(
quotation.quotation_number!,
created.getFullYear(),
pdfBuffer,
);
})
.catch((err) => {
request.log.error(err, "Failed to save offer PDF to NAS");
});
if (saveMode) {
await pdfPromise;
return reply.send({ success: true, message: "PDF uloženo" });
}
}
return reply.type("text/html").send(html);
} catch (err) {
request.log.error(err, "PDF generation failed");

View File

@@ -208,7 +208,7 @@ export default async function ordersRoutes(
if ("error" in manualParsed) return error(reply, manualParsed.error, 400);
const body = manualParsed.data;
const result = await createOrder(body as any);
const result = await createOrder(body);
await logAudit({
request,
@@ -236,7 +236,7 @@ export default async function ordersRoutes(
const parsed = parseBody(UpdateOrderSchema, request.body);
if ("error" in parsed) return error(reply, parsed.error, 400);
const result = await updateOrder(id, parsed.data as any);
const result = await updateOrder(id, parsed.data);
if ("error" in result) return error(reply, result.error!, result.status!);
await logAudit({

View File

@@ -60,7 +60,6 @@ export default async function projectFilesRoutes(
.send(stream);
}
// List files
if (!project.project_number)
return error(reply, "Projekt nemá číslo projektu");

View File

@@ -19,6 +19,7 @@ import {
invalidateOffer,
getNextOfferNumber,
} from "../../services/offers.service";
import { nasOffersManager } from "../../services/nas-offers-manager";
const LOCK_TIMEOUT_MS = 10 * 1000; // 10 seconds — lock expires if no heartbeat
@@ -122,7 +123,6 @@ export default async function quotationsRoutes(
const data = await getOffer(id);
if (!data) return error(reply, "Nabídka nenalezena", 404);
// Include lock info
const quotation = await prisma.quotations.findUnique({
where: { id },
select: { locked_by: true, locked_at: true },
@@ -305,6 +305,14 @@ export default async function quotationsRoutes(
const existing = await deleteOffer(id);
if (!existing) return error(reply, "Nabídka nenalezena", 404);
// Delete PDF from NAS
if (existing.quotation_number && existing.created_at) {
const yr = new Date(existing.created_at).getFullYear();
nasOffersManager.deleteOfferPdf(
nasOffersManager.buildRelativePath(existing.quotation_number, yr),
);
}
await logAudit({
request,
authData: request.authData,
@@ -316,4 +324,38 @@ export default async function quotationsRoutes(
return success(reply, null, 200, "Nabídka smazána");
},
);
// GET /api/admin/offers/:id/file — serve PDF from NAS
fastify.get<{ Params: { id: string } }>(
"/:id/file",
{ preHandler: requirePermission("offers.view") },
async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const offer = await prisma.quotations.findUnique({
where: { id },
select: { quotation_number: true, created_at: true },
});
if (!offer?.quotation_number)
return error(reply, "Nabídka nenalezena", 404);
const year = offer.created_at
? new Date(offer.created_at).getFullYear()
: new Date().getFullYear();
const relPath = nasOffersManager.buildRelativePath(
offer.quotation_number,
year,
);
const file = nasOffersManager.readOfferPdf(relPath);
if (!file) return error(reply, "PDF soubor nenalezen", 404);
return reply
.type("application/pdf")
.header(
"Content-Disposition",
`inline; filename="${offer.quotation_number}.pdf"`,
)
.send(file.data);
},
);
}

View File

@@ -11,6 +11,7 @@ import {
CreateReceivedInvoiceSchema,
UpdateReceivedInvoiceSchema,
} from "../../schemas/received-invoices.schema";
import { nasFinancialsManager } from "../../services/nas-financials-manager";
const VALID_STATUSES = ["unpaid", "paid"] as const;
const ALLOWED_SORT_FIELDS = [
@@ -160,16 +161,31 @@ export default async function receivedInvoicesRoutes(
if (id === null) return;
const invoice = await prisma.received_invoices.findUnique({
where: { id },
select: { file_data: true, file_name: true, file_mime: true },
select: {
file_name: true,
file_mime: true,
year: true,
month: true,
},
});
if (!invoice?.file_data) return error(reply, "Soubor nenalezen", 404);
if (!invoice?.file_name) return error(reply, "Soubor nenalezen", 404);
const relPath = nasFinancialsManager.buildReceivedPath(
invoice.file_name,
invoice.year,
invoice.month,
);
const nasFile = nasFinancialsManager.readReceivedInvoice(relPath);
if (!nasFile) return error(reply, "Soubor na NAS nenalezen", 404);
const mime = invoice.file_mime || "application/pdf";
const filename = invoice.file_name || `received-invoice-${id}.pdf`;
return reply
.type(mime)
.header("Content-Disposition", `inline; filename="${filename}"`)
.send(Buffer.from(invoice.file_data));
.header(
"Content-Disposition",
`inline; filename="${invoice.file_name}"`,
)
.send(nasFile.data);
},
);
@@ -183,9 +199,10 @@ export default async function receivedInvoicesRoutes(
where: { id },
});
if (!invoice) return error(reply, "Přijatá faktura nenalezena", 404);
// Don't send file_data in detail response (can be large)
const { file_data: _fileData, ...rest } = invoice;
return success(reply, rest);
return success(reply, {
...invoice,
has_file: !!invoice.file_name,
});
},
);
@@ -230,6 +247,8 @@ export default async function receivedInvoicesRoutes(
const now = new Date();
const createdIds: number[] = [];
const useNas = nasFinancialsManager.isConfigured();
for (let i = 0; i < files.length; i++) {
const file = files[i];
const meta = invoicesMeta[i] || {};
@@ -241,10 +260,34 @@ export default async function receivedInvoicesRoutes(
? Math.round((amount - amount / (1 + vatRate / 100)) * 100) / 100
: 0;
const issueDate = meta.issue_date
? new Date(String(meta.issue_date))
: null;
const invoiceMonth = issueDate
? issueDate.getMonth() + 1
: Number(meta.month) || now.getMonth() + 1;
const invoiceYear = issueDate
? issueDate.getFullYear()
: Number(meta.year) || now.getFullYear();
if (!useNas) {
return error(reply, "NAS úložiště není nakonfigurováno", 503);
}
const nasResult = nasFinancialsManager.saveReceivedInvoice(
file.name,
invoiceYear,
invoiceMonth,
file.data,
);
if ("error" in nasResult) {
return error(reply, nasResult.error, 503);
}
const invoice = await prisma.received_invoices.create({
data: {
month: Number(meta.month) || now.getMonth() + 1,
year: Number(meta.year) || now.getFullYear(),
month: invoiceMonth,
year: invoiceYear,
supplier_name: meta.supplier_name
? String(meta.supplier_name)
: file.name,
@@ -263,7 +306,6 @@ export default async function receivedInvoicesRoutes(
status: "unpaid",
notes: meta.notes ? String(meta.notes) : null,
uploaded_by: request.authData?.userId,
file_data: Uint8Array.from(file.data),
file_name: file.name,
file_mime: file.mime,
file_size: file.size,
@@ -488,6 +530,15 @@ export default async function receivedInvoicesRoutes(
});
if (!existing) return error(reply, "Přijatá faktura nenalezena", 404);
if (existing.file_name) {
const relPath = nasFinancialsManager.buildReceivedPath(
existing.file_name,
existing.year,
existing.month,
);
nasFinancialsManager.deleteReceivedInvoice(relPath);
}
await prisma.received_invoices.delete({ where: { id } });
await logAudit({
request,

View File

@@ -27,7 +27,6 @@ export default async function scopeTemplatesRoutes(
const query = request.query as Record<string, unknown>;
const action = query.action ? String(query.action) : null;
// Item templates
if (action === "items") {
const items = await prisma.item_templates.findMany({
where: { is_deleted: false },
@@ -70,7 +69,6 @@ export default async function scopeTemplatesRoutes(
category: body.category ? String(body.category) : null,
};
// Update existing item if id is provided
if (body.id) {
const existingItem = await prisma.item_templates.findUnique({
where: { id: Number(body.id) },
@@ -92,7 +90,6 @@ export default async function scopeTemplatesRoutes(
return success(reply, { id: item.id }, 201, "Položka byla vytvořena");
}
// Scope template create (original logic below)
const scopeParsed = parseBody(CreateScopeTemplateSchema, request.body);
if ("error" in scopeParsed) return error(reply, scopeParsed.error, 400);
const body = scopeParsed.data;

View File

@@ -21,7 +21,6 @@ function parseUserAgent(ua: string | null): {
icon: "monitor",
};
// Browser detection
let browser = "Neznámý prohlížeč";
if (ua.includes("Edg/")) browser = "Edge";
else if (ua.includes("OPR/") || ua.includes("Opera")) browser = "Opera";
@@ -29,7 +28,6 @@ function parseUserAgent(ua: string | null): {
else if (ua.includes("Safari/") && !ua.includes("Chrome")) browser = "Safari";
else if (ua.includes("Firefox/")) browser = "Firefox";
// OS detection
let os = "Neznámý systém";
if (ua.includes("Windows")) os = "Windows";
else if (ua.includes("Mac OS X") || ua.includes("Macintosh")) os = "macOS";
@@ -37,7 +35,6 @@ function parseUserAgent(ua: string | null): {
else if (ua.includes("Android")) os = "Android";
else if (ua.includes("iPhone") || ua.includes("iPad")) os = "iOS";
// Device icon
let icon = "monitor";
if (
ua.includes("Mobile") ||
@@ -76,7 +73,7 @@ export default async function sessionsRoutes(
is_current: currentHash ? s.token_hash === currentHash : false,
device_info,
ip_address: s.ip_address || "",
created_at: s.created_at ? s.created_at.toISOString() : "",
created_at: s.created_at ?? "",
};
});

View File

@@ -44,7 +44,6 @@ export default async function totpRoutes(
return error(reply, "Secret a kód jsou povinné", 400);
}
// Verify the code first
const totp = new OTPAuthLib.TOTP({
secret: OTPAuthLib.Secret.fromBase32(String(secret)),
algorithm: "SHA1",
@@ -66,7 +65,6 @@ export default async function totpRoutes(
backupCodesHashed.push(bcrypt.hashSync(code, 10));
}
// Encrypt and store
const encryptedSecret = encrypt(String(secret));
await prisma.users.update({
where: { id: request.authData!.userId },
@@ -237,7 +235,6 @@ export default async function totpRoutes(
return error(reply, "Neplatný záložní kód", 401);
}
// Remove used backup code
backupCodes.splice(matchIndex, 1);
await prisma.users.update({
where: { id: user.id },
@@ -249,7 +246,6 @@ export default async function totpRoutes(
},
});
// Delete used login token
await prisma.totp_login_tokens.delete({ where: { id: storedToken.id } });
// Create tokens (same as /login/totp flow)

View File

@@ -176,13 +176,11 @@ export default async function tripsRoutes(
end_km: Number(body.end_km),
route_from: String(body.route_from),
route_to: String(body.route_to),
is_business:
!!body.is_business,
is_business: !!body.is_business,
notes: body.notes ? String(body.notes) : null,
},
});
// Update vehicle actual_km
await prisma.vehicles.update({
where: { id: Number(body.vehicle_id) },
data: { actual_km: Number(body.end_km) },
@@ -227,9 +225,7 @@ export default async function tripsRoutes(
if (body.route_from !== undefined)
data.route_from = String(body.route_from);
if (body.route_to !== undefined) data.route_to = String(body.route_to);
if (body.is_business !== undefined)
data.is_business =
!!body.is_business;
if (body.is_business !== undefined) data.is_business = !!body.is_business;
if (body.notes !== undefined)
data.notes = body.notes ? String(body.notes) : null;

View File

@@ -107,9 +107,7 @@ export default async function vehiclesRoutes(
actual_km:
body.actual_km !== undefined ? Number(body.actual_km) : undefined,
is_active:
body.is_active !== undefined
? !!body.is_active
: undefined,
body.is_active !== undefined ? !!body.is_active : undefined,
},
});
await logAudit({