Compare commits

..

2 Commits

Author SHA1 Message Date
BOHA
907ee574e6 chore(lint): remove all 34 dead-code warnings (verified site by site)
unused-vars (25): leftover imports/types across pages, routes and services;
write-only sickHours accumulator; unused holidayCount pair; test helpers
authPatch/authDelete; TOut dropped from ApiMutationOptions (the hook keeps
its own generic — no call-site changes); requireAuth no longer importable
in customers/warehouse routes (matches the no-bare-auth-reads convention).

useless-assignment (5): initializers proven overwritten on every path
(systemQty x2, hours, writtenSize -> let x: number) and the seed's dead
adminUser reassignment (create kept, assignment dropped).

useless-escape (4): [eE+\-] -> [eE+-] and [\/...] -> [/...] — identical
semantics.

Behavior-neutral; lint 102 -> 68 warnings (0 errors), suite 641 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 00:13:52 +02:00
BOHA
b51ea2505e feat(db): stock CHECK constraints + audit_logs DATETIME (2038 fix)
Migration 20260612234500: CHECK (quantity >= 0) on sklad_batches and
sklad_item_locations — the DB itself now rejects negative stock, so a
future C2-class bug (cumulative decrements driving a batch negative and
silently vanishing from totals) becomes a loud error instead of hidden
corruption. All three databases verified clean of negative rows before
the migration.

audit_logs.created_at TIMESTAMP(0) -> DATETIME(0): TIMESTAMP dies in 2038
and converts through the session timezone; existing wall times preserved
by the MODIFY.

Pinned by db-constraints.test.ts (4 tests, each watched fail pre-migration:
negative insert/update/location all succeeded before, 2098 audit timestamp
was rejected before).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 23:56:56 +02:00
29 changed files with 179 additions and 71 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "app-ts", "name": "app-ts",
"version": "2.4.32", "version": "2.4.34",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "app-ts", "name": "app-ts",
"version": "2.4.32", "version": "2.4.34",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.102.0", "@anthropic-ai/sdk": "^0.102.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "app-ts", "name": "app-ts",
"version": "2.4.32", "version": "2.4.34",
"description": "", "description": "",
"main": "dist/server.js", "main": "dist/server.js",
"scripts": { "scripts": {

View File

@@ -0,0 +1,14 @@
-- AlterTable
ALTER TABLE `audit_logs` MODIFY `created_at` DATETIME(0) NULL DEFAULT CURRENT_TIMESTAMP(0);
-- Stock invariants enforced by the database itself: a future cumulative-
-- decrement bug (the C2 class — duplicate issue lines driving a batch
-- negative and silently vanishing from stock totals) becomes a loud error
-- instead of hidden corruption. All databases verified clean of negative
-- rows before this migration (2026-06-12).
ALTER TABLE `sklad_batches`
ADD CONSTRAINT `chk_sklad_batches_quantity_nonneg` CHECK (`quantity` >= 0);
ALTER TABLE `sklad_item_locations`
ADD CONSTRAINT `chk_sklad_item_locations_quantity_nonneg` CHECK (`quantity` >= 0);

View File

@@ -65,7 +65,9 @@ model audit_logs {
new_values String? @db.LongText new_values String? @db.LongText
user_agent String? @db.Text user_agent String? @db.Text
session_id String? @db.VarChar(128) session_id String? @db.VarChar(128)
created_at DateTime? @default(now()) @db.Timestamp(0) // DATETIME, not TIMESTAMP — TIMESTAMP dies in 2038 and converts through
// the session timezone (2026-06-12 hardening migration).
created_at DateTime? @default(now()) @db.DateTime(0)
@@index([action], map: "idx_audit_logs_action") @@index([action], map: "idx_audit_logs_action")
@@index([created_at], map: "idx_audit_logs_created") @@index([created_at], map: "idx_audit_logs_created")

View File

@@ -391,12 +391,12 @@ async function main() {
console.log(` Admin role now has all ${allPerms.length} permissions`); console.log(` Admin role now has all ${allPerms.length} permissions`);
// 4. Ensure admin user exists // 4. Ensure admin user exists
let adminUser = await prisma.users.findFirst({ const adminUser = await prisma.users.findFirst({
where: { username: "admin" }, where: { username: "admin" },
}); });
if (!adminUser) { if (!adminUser) {
const hash = bcrypt.hashSync("admin", 10); const hash = bcrypt.hashSync("admin", 10);
adminUser = await prisma.users.create({ await prisma.users.create({
data: { data: {
username: "admin", username: "admin",
email: "admin@boha.local", email: "admin@boha.local",

View File

@@ -59,7 +59,7 @@ async function main() {
// fails we KEEP file_data and report the row as failed. // fails we KEEP file_data and report the row as failed.
const expectedSize = rec.file_data.length; const expectedSize = rec.file_data.length;
const readBack = nasFinancialsManager.readReceivedInvoice(result.filePath); const readBack = nasFinancialsManager.readReceivedInvoice(result.filePath);
let writtenSize = -1; let writtenSize: number;
if (readBack) { if (readBack) {
writtenSize = readBack.data.length; writtenSize = readBack.data.length;
} else { } else {

View File

@@ -0,0 +1,136 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import prisma from "../config/database";
/**
* Pins the 2026-06-12 DB hardening migration:
* 1. CHECK (quantity >= 0) on sklad_batches + sklad_item_locations — the DB
* itself rejects negative stock, so a future C2-class bug (cumulative
* decrements driving a batch negative) becomes a loud error instead of
* silently hidden corruption.
* 2. audit_logs.created_at is DATETIME, not TIMESTAMP — no 2038 cap.
*/
const N = "db_constraints_";
let itemId: number;
let locationId: number;
let receiptId: number;
let lineAId: number;
let lineBId: number;
async function cleanup() {
await prisma.sklad_batches.deleteMany({
where: { item: { name: { contains: N } } },
});
await prisma.sklad_item_locations.deleteMany({
where: { item: { name: { contains: N } } },
});
await prisma.sklad_receipt_lines.deleteMany({
where: { item: { name: { contains: N } } },
});
await prisma.sklad_receipts.deleteMany({ where: { notes: { contains: N } } });
await prisma.sklad_items.deleteMany({ where: { name: { contains: N } } });
await prisma.sklad_locations.deleteMany({
where: { name: { contains: N } },
});
await prisma.audit_logs.deleteMany({
where: { description: { contains: N } },
});
}
beforeAll(async () => {
await cleanup();
const item = await prisma.sklad_items.create({
data: { name: `${N}item`, unit: "ks" },
});
itemId = item.id;
const location = await prisma.sklad_locations.create({
data: { code: `DBC${Date.now() % 100000}`, name: `${N}loc` },
});
locationId = location.id;
const receipt = await prisma.sklad_receipts.create({
data: { notes: `${N}receipt`, status: "CONFIRMED" },
});
receiptId = receipt.id;
const lineA = await prisma.sklad_receipt_lines.create({
data: {
receipt_id: receiptId,
item_id: itemId,
quantity: 5,
unit_price: 1,
},
});
lineAId = lineA.id;
const lineB = await prisma.sklad_receipt_lines.create({
data: {
receipt_id: receiptId,
item_id: itemId,
quantity: 5,
unit_price: 1,
},
});
lineBId = lineB.id;
});
afterAll(async () => {
await cleanup();
await prisma.$disconnect();
});
describe("stock CHECK constraints", () => {
it("rejects inserting a negative batch quantity", async () => {
await expect(
prisma.sklad_batches.create({
data: {
item_id: itemId,
receipt_line_id: lineAId,
quantity: -1,
original_qty: 5,
unit_price: 1,
is_consumed: false,
},
}),
).rejects.toThrow();
});
it("rejects updating a batch quantity below zero", async () => {
const batch = await prisma.sklad_batches.create({
data: {
item_id: itemId,
receipt_line_id: lineBId,
quantity: 5,
original_qty: 5,
unit_price: 1,
is_consumed: false,
},
});
await expect(
prisma.$executeRaw`UPDATE sklad_batches SET quantity = -2 WHERE id = ${batch.id}`,
).rejects.toThrow();
const fresh = await prisma.sklad_batches.findUnique({
where: { id: batch.id },
});
expect(Number(fresh?.quantity)).toBe(5);
});
it("rejects a negative item_location quantity", async () => {
await expect(
prisma.sklad_item_locations.create({
data: { item_id: itemId, location_id: locationId, quantity: -1 },
}),
).rejects.toThrow();
});
});
describe("audit_logs.created_at has no 2038 cap", () => {
it("accepts a far-future timestamp (DATETIME, not TIMESTAMP)", async () => {
const row = await prisma.audit_logs.create({
data: {
action: "test",
description: `${N}future`,
created_at: new Date(Date.UTC(2098, 0, 15, 12, 0, 0)),
},
});
expect(row.created_at?.getUTCFullYear()).toBe(2098);
});
});

View File

@@ -1090,26 +1090,6 @@ async function authPost(path: string, token: string, body: unknown) {
}); });
} }
async function authPatch(path: string, token: string, body: unknown) {
return app.inject({
method: "PATCH",
url: path,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
payload: body as object,
});
}
async function authDelete(path: string, token: string) {
return app.inject({
method: "DELETE",
url: path,
headers: { Authorization: `Bearer ${token}` },
});
}
beforeAll(async () => { beforeAll(async () => {
app = await buildApp(); app = await buildApp();

View File

@@ -148,7 +148,7 @@ const TrashIcon = (
); );
export default function PlanCellModal(props: Props) { export default function PlanCellModal(props: Props) {
const { open, mode, onClose } = props; const { open, mode } = props;
if (!open || mode.kind === "closed") return null; if (!open || mode.kind === "closed") return null;
if (mode.kind === "view") return <ViewModal {...props} />; if (mode.kind === "view") return <ViewModal {...props} />;
if (mode.kind === "day-in-range") if (mode.kind === "day-in-range")

View File

@@ -234,7 +234,6 @@ function computeUserTotals(
} }
const holidayDates = holidaysInMonth(yr, mo + 1); const holidayDates = holidaysInMonth(yr, mo + 1);
const holidayCount = holidayDates.length;
for (const uid of Object.keys(totals)) { for (const uid of Object.keys(totals)) {
const t = totals[uid]; const t = totals[uid];
@@ -476,14 +475,12 @@ function buildUserSectionHtml(
// Sum of vacation/sick/unpaid hours (in hours, not minutes). The // Sum of vacation/sick/unpaid hours (in hours, not minutes). The
// "holiday" leave_type is auto-computed from Czech holidays further down // "holiday" leave_type is auto-computed from Czech holidays further down
// and no longer selectable in the UI, so it's deliberately omitted here. // and no longer selectable in the UI, so it's deliberately omitted here.
let vacationHours = 0, let vacationHours = 0;
sickHours = 0;
for (const r of userRecords) { for (const r of userRecords) {
const lt = r.leave_type || "work"; const lt = r.leave_type || "work";
if (lt === "work") continue; if (lt === "work") continue;
const h = Number(r.leave_hours) || 8; const h = Number(r.leave_hours) || 8;
if (lt === "vacation") vacationHours += h; if (lt === "vacation") vacationHours += h;
else if (lt === "sick") sickHours += h;
} }
// Odpracováno: floor(worked / 8) × 8. // Odpracováno: floor(worked / 8) × 8.
@@ -492,7 +489,6 @@ function buildUserSectionHtml(
// Free Svátek: 8h per holiday date the user did not work. // Free Svátek: 8h per holiday date the user did not work.
const holidayDates = holidaysInMonth(yr, mo); const holidayDates = holidaysInMonth(yr, mo);
const holidayCount = holidayDates.length;
const workedDates = new Set( const workedDates = new Set(
userRecords userRecords
.filter((r) => (r.leave_type || "work") === "work") .filter((r) => (r.leave_type || "work") === "work")

View File

@@ -92,7 +92,7 @@ async function performMutation<TIn, TOut>(opts: {
return result.data as TOut; return result.data as TOut;
} }
export interface ApiMutationOptions<TIn, TOut> { export interface ApiMutationOptions<TIn> {
url: string | ((input: TIn) => string); url: string | ((input: TIn) => string);
method: HttpMethod | ((input: TIn) => HttpMethod); method: HttpMethod | ((input: TIn) => HttpMethod);
/** Query-key prefixes to invalidate on success. Broad per CLAUDE.md. */ /** Query-key prefixes to invalidate on success. Broad per CLAUDE.md. */
@@ -122,7 +122,7 @@ export interface ApiMutationOptions<TIn, TOut> {
* as `ApiError` (with `.status` attached) so callers can branch on HTTP code. * as `ApiError` (with `.status` attached) so callers can branch on HTTP code.
*/ */
export function useApiMutation<TIn, TOut>( export function useApiMutation<TIn, TOut>(
opts: ApiMutationOptions<TIn, TOut> & opts: ApiMutationOptions<TIn> &
Omit<UseMutationOptions<TOut, ApiError, TIn>, "mutationFn">, Omit<UseMutationOptions<TOut, ApiError, TIn>, "mutationFn">,
) { ) {
const { url, method, invalidate, envelope, onSuccess, ...rest } = opts; const { url, method, invalidate, envelope, onSuccess, ...rest } = opts;

View File

@@ -1,5 +1,4 @@
import { queryOptions } from "@tanstack/react-query"; import { queryOptions } from "@tanstack/react-query";
import apiFetch from "../../utils/api";
import { jsonQuery } from "../apiAdapter"; import { jsonQuery } from "../apiAdapter";
export interface PlanUser { export interface PlanUser {

View File

@@ -1,5 +1,5 @@
import { useState, useMemo, useRef } from "react"; import { useState, useMemo, useRef } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { useAuth } from "../context/AuthContext"; import { useAuth } from "../context/AuthContext";
@@ -9,7 +9,6 @@ import { iconBadgeSx } from "../theme";
import { import {
attendanceHistoryOptions, attendanceHistoryOptions,
type AttendanceRecord, type AttendanceRecord,
type ProjectLogEntry,
} from "../lib/queries/attendance"; } from "../lib/queries/attendance";
import { import {
formatDate, formatDate,
@@ -201,7 +200,6 @@ const renderProjectCell = (record: AttendanceRecord) => {
export default function AttendanceHistory() { export default function AttendanceHistory() {
const { user, hasPermission } = useAuth(); const { user, hasPermission } = useAuth();
const queryClient = useQueryClient();
const { data: companySettings } = useQuery(companySettingsOptions()); const { data: companySettings } = useQuery(companySettingsOptions());
const companyName = companySettings?.company_name || ""; const companyName = companySettings?.company_name || "";
const printRef = useRef<HTMLDivElement>(null); const printRef = useRef<HTMLDivElement>(null);

View File

@@ -26,10 +26,7 @@ const LocationMap = styled("div")(({ theme }) => ({
})); }));
import { formatDate, formatTime } from "../utils/attendanceHelpers"; import { formatDate, formatTime } from "../utils/attendanceHelpers";
import { import { attendanceLocationOptions } from "../lib/queries/attendance";
attendanceLocationOptions,
type LocationRecord,
} from "../lib/queries/attendance";
import { Button, Card, LoadingState, PageEnter, PageHeader } from "../ui"; import { Button, Card, LoadingState, PageEnter, PageHeader } from "../ui";
const BackIcon = ( const BackIcon = (

View File

@@ -3,10 +3,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useAlert } from "../context/AlertContext"; import { useAlert } from "../context/AlertContext";
import { useAuth } from "../context/AuthContext"; import { useAuth } from "../context/AuthContext";
import Forbidden from "../components/Forbidden"; import Forbidden from "../components/Forbidden";
import { import { companySettingsOptions } from "../lib/queries/settings";
companySettingsOptions,
type CompanySettingsData,
} from "../lib/queries/settings";
import { bankAccountsOptions, type BankAccount } from "../lib/queries/common"; import { bankAccountsOptions, type BankAccount } from "../lib/queries/common";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
@@ -23,7 +20,6 @@ import {
TextField, TextField,
Select, Select,
CheckboxField, CheckboxField,
StatusChip,
FileUpload, FileUpload,
LoadingState, LoadingState,
type DataColumn, type DataColumn,

View File

@@ -1,14 +1,7 @@
import { useState, useEffect, useMemo, useRef } from "react"; import { useState, useEffect, useMemo, useRef } from "react";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { import { orderDetailOptions, type OrderItem } from "../lib/queries/orders";
orderDetailOptions,
type OrderData,
type OrderItem,
type OrderSection,
type OrderInvoice,
type OrderProject,
} from "../lib/queries/orders";
import { useApiMutation } from "../lib/queries/mutations"; import { useApiMutation } from "../lib/queries/mutations";
import { useAlert } from "../context/AlertContext"; import { useAlert } from "../context/AlertContext";
import { useAuth } from "../context/AuthContext"; import { useAuth } from "../context/AuthContext";

View File

@@ -11,7 +11,6 @@ import MenuItem from "@mui/material/MenuItem";
import { import {
projectDetailOptions, projectDetailOptions,
type ProjectData,
type ProjectNote, type ProjectNote,
} from "../lib/queries/projects"; } from "../lib/queries/projects";
import { userListOptions, type User as ApiUser } from "../lib/queries/users"; import { userListOptions, type User as ApiUser } from "../lib/queries/users";

View File

@@ -199,7 +199,7 @@ export default function Trips() {
TripForm, TripForm,
{ message?: string; error?: string } { message?: string; error?: string }
>({ >({
url: (formData) => url: () =>
editingTrip ? `${API_BASE}/trips/${editingTrip.id}` : `${API_BASE}/trips`, editingTrip ? `${API_BASE}/trips/${editingTrip.id}` : `${API_BASE}/trips`,
method: () => (editingTrip ? "PUT" : "POST"), method: () => (editingTrip ? "PUT" : "POST"),
invalidate: ["trips", "vehicles"], invalidate: ["trips", "vehicles"],

View File

@@ -23,7 +23,7 @@ interface InventoryItem {
} }
function parseDecimal(raw: string): number { function parseDecimal(raw: string): number {
const cleaned = raw.replace(/[eE+\-]/g, ""); const cleaned = raw.replace(/[eE+-]/g, "");
const n = Number(cleaned); const n = Number(cleaned);
return Number.isFinite(n) ? n : 0; return Number.isFinite(n) ? n : 0;
} }

View File

@@ -65,7 +65,7 @@ interface BatchesResponse {
} }
function parseDecimal(raw: string): number { function parseDecimal(raw: string): number {
const cleaned = raw.replace(/[eE+\-]/g, ""); const cleaned = raw.replace(/[eE+-]/g, "");
const n = Number(cleaned); const n = Number(cleaned);
return Number.isFinite(n) ? n : 0; return Number.isFinite(n) ? n : 0;
} }

View File

@@ -50,7 +50,7 @@ interface MovementItem {
} }
function parseDecimal(raw: string): number { function parseDecimal(raw: string): number {
const cleaned = raw.replace(/[eE+\-]/g, ""); const cleaned = raw.replace(/[eE+-]/g, "");
const n = Number(cleaned); const n = Number(cleaned);
return Number.isFinite(n) ? n : 0; return Number.isFinite(n) ? n : 0;
} }

View File

@@ -1,7 +1,6 @@
import { FastifyRequest, FastifyReply } from "fastify"; import { FastifyRequest, FastifyReply } from "fastify";
import { verifyAccessToken } from "../services/auth"; import { verifyAccessToken } from "../services/auth";
import { error } from "../utils/response"; import { error } from "../utils/response";
import { AuthData } from "../types";
export async function requireAuth( export async function requireAuth(
request: FastifyRequest, request: FastifyRequest,

View File

@@ -1,6 +1,6 @@
import { FastifyInstance } from "fastify"; import { FastifyInstance } from "fastify";
import prisma from "../../config/database"; import prisma from "../../config/database";
import { requireAuth, requirePermission } from "../../middleware/auth"; import { requirePermission } from "../../middleware/auth";
import { logAudit } from "../../services/audit"; import { logAudit } from "../../services/audit";
import { success, error, parseId, paginated } from "../../utils/response"; import { success, error, parseId, paginated } from "../../utils/response";
import { parsePagination, buildPaginationMeta } from "../../utils/pagination"; import { parsePagination, buildPaginationMeta } from "../../utils/pagination";

View File

@@ -13,7 +13,6 @@ import {
markOverdueInvoices, markOverdueInvoices,
listInvoices, listInvoices,
getInvoiceListTotals, getInvoiceListTotals,
getNextInvoiceNumberFormatted,
getNextInvoiceNumberPreview, getNextInvoiceNumberPreview,
getInvoiceStats, getInvoiceStats,
getOrderDataForInvoice, getOrderDataForInvoice,

View File

@@ -199,7 +199,7 @@ export default async function projectFilesRoutes(
const subPath = parsedQuery.data.path || ""; const subPath = parsedQuery.data.path || "";
const rawFileName = file.filename; const rawFileName = file.filename;
const fileName = rawFileName.replace(/[\/\\:*?"<>|]/g, "_"); const fileName = rawFileName.replace(/[/\\:*?"<>|]/g, "_");
const buffer = await file.toBuffer(); const buffer = await file.toBuffer();
const err = await fm.uploadFile( const err = await fm.uploadFile(

View File

@@ -2,7 +2,7 @@ import { FastifyInstance } from "fastify";
import multipart from "@fastify/multipart"; import multipart from "@fastify/multipart";
import prisma from "../../config/database"; import prisma from "../../config/database";
import { config } from "../../config/env"; import { config } from "../../config/env";
import { requireAuth, requirePermission } from "../../middleware/auth"; import { requirePermission } from "../../middleware/auth";
import { logAudit } from "../../services/audit"; import { logAudit } from "../../services/audit";
import { success, error, parseId, paginated } from "../../utils/response"; import { success, error, parseId, paginated } from "../../utils/response";
import { contentDisposition } from "../../utils/content-disposition"; import { contentDisposition } from "../../utils/content-disposition";
@@ -2209,7 +2209,7 @@ export default async function warehouseRoutes(
// Build items with auto-filled system_qty and calculated difference // Build items with auto-filled system_qty and calculated difference
const itemsData = await Promise.all( const itemsData = await Promise.all(
body.items.map(async (item) => { body.items.map(async (item) => {
let systemQty = 0; let systemQty: number;
if (item.location_id != null) { if (item.location_id != null) {
// Get stock at specific location // Get stock at specific location
@@ -2318,7 +2318,7 @@ export default async function warehouseRoutes(
if (body.items) { if (body.items) {
itemsData = await Promise.all( itemsData = await Promise.all(
body.items.map(async (item) => { body.items.map(async (item) => {
let systemQty = 0; let systemQty: number;
if (item.location_id != null) { if (item.location_id != null) {
const loc = await prisma.sklad_item_locations.findUnique({ const loc = await prisma.sklad_item_locations.findUnique({

View File

@@ -791,7 +791,7 @@ export async function getProjectReport(year: number) {
// Use detailed project logs (started_at/ended_at or hours/minutes) // Use detailed project logs (started_at/ended_at or hours/minutes)
for (const log of rec.attendance_project_logs) { for (const log of rec.attendance_project_logs) {
let hours = 0; let hours: number;
if (log.hours != null || log.minutes != null) { if (log.hours != null || log.minutes != null) {
hours = (Number(log.hours) || 0) + (Number(log.minutes) || 0) / 60; hours = (Number(log.hours) || 0) + (Number(log.minutes) || 0) / 60;
} else if (log.started_at && log.ended_at) { } else if (log.started_at && log.ended_at) {

View File

@@ -1,7 +1,7 @@
import crypto from "crypto"; import crypto from "crypto";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import { FastifyRequest, FastifyReply } from "fastify"; import { FastifyRequest } from "fastify";
import prisma from "../config/database"; import prisma from "../config/database";
import { config } from "../config/env"; import { config } from "../config/env";
import { AuthData, JwtPayload } from "../types"; import { AuthData, JwtPayload } from "../types";

View File

@@ -1,4 +1,4 @@
import { PaginationQuery, PaginationMeta } from "../types"; import { PaginationMeta } from "../types";
/** /**
* Parse common pagination/sort/search query params. * Parse common pagination/sort/search query params.