# CLAUDE.md — boha-app-ts Business management system for a Czech company, rewritten from PHP to TypeScript/Node.js. Handles attendance, invoicing, leave/trips, projects, vehicles, and HR operations. --- ## Tech Stack | Layer | Technology | | -------------- | ------------------------------------------------------------- | | Runtime | Node.js, TypeScript 5.9.3 (strict) | | HTTP Framework | Fastify 5.8.2 | | ORM | Prisma 6.19.2 → MySQL | | Auth | JWT (HS256, 15 min) + TOTP 2FA (RFC 6238, otpauth) + bcryptjs | | Validation | Zod 4.3.6 | | Frontend | React 18.3.1 + Vite 8.0.0 | | Testing | Vitest 4.1.0 + Supertest | | PDF | Puppeteer 24.x | | Email | nodemailer 8.x | | Cron | node-cron 4.x | --- ## Project Structure ``` src/ ├── server.ts # Fastify server entry point — plugins, routes, error handler ├── routes/admin/ # HTTP route handlers (one file per entity) ├── services/ # Business logic (no classes, exported functions, uses Prisma directly) ├── schemas/ # Zod validation schemas (one file per entity) ├── middleware/ # auth.ts (requireAuth, requirePermission, optionalAuth) │ # security.ts (CSP, HSTS, security headers) ├── utils/ # totp.ts, pdf.ts, email.ts, audit.ts, formatters, etc. ├── config/ # env.ts (config singleton, Date.toJSON override) ├── types/ # index.ts (AuthData, JwtPayload, ApiResponse, re-exports from Prisma) ├── admin/ # React 18 frontend (56 .tsx files) │ ├── AdminApp.tsx # Router + lazy-loaded pages │ ├── contexts/ # AuthContext, AlertContext │ ├── components/ # Layout, modals, tables, editors │ ├── pages/ # One file per page/feature │ ├── hooks/ # useApiCall, useListData, useTableSort, etc. │ └── utils/ # api.ts (fetch wrapper with token refresh), formatters, helpers └── __tests__/ # Vitest tests (auth, numbering) prisma/ ├── schema.prisma # 32 models, MySQL, snake_case columns └── migrations/ # Applied migrations dist/ # Compiled server (CommonJS, ES2022) dist-client/ # Built frontend (Vite, ES2020) ``` --- ## Commands ```bash # Development npm run dev # Starts server in watch mode (manage frontend separately) npm run dev:server # tsx watch src/server.ts npm run dev:client # Vite dev server # Build npm run build # Build server + client npm run build:server # tsc -p tsconfig.server.json → dist/ npm run build:client # vite build → dist-client/ # Run (production) npm start # node dist/server.js # Tests npm test # vitest run (single pass) npm run test:watch # vitest watch # Database npx prisma migrate dev # Apply migrations (dev) npx prisma migrate deploy # Apply migrations (prod) npx prisma generate # Regenerate Prisma client after schema changes npx prisma studio # DB browser GUI ``` **Do not start the dev server.** The user manages it separately. --- ## Environment Variables Required: ``` DATABASE_URL=mysql://user:pass@host:3306/dbname JWT_SECRET=<64-char hex string> TOTP_ENCRYPTION_KEY=<64-char hex string> ``` Optional (with defaults): ``` PORT=3001 # Production port (dev default: 3000) HOST=127.0.0.1 APP_ENV=local|production # Default: local. Controls CSP, CORS, HSTS ACCESS_TOKEN_EXPIRY=900 # 15 minutes REFRESH_TOKEN_SESSION_EXPIRY=3600 # 1 hour REFRESH_TOKEN_REMEMBER_EXPIRY=2592000 # 30 days NAS_PATH=Z:/02_PROJEKTY # Network share for project files MAX_UPLOAD_SIZE=52428800 # 50MB CONTACT_EMAIL_TO= CONTACT_EMAIL_FROM= SMTP_FROM= LEAVE_NOTIFY_EMAIL= APP_URL= # Used in email links CORS_ORIGINS= # Comma-separated, production only ``` Use `.env` for dev, `.env.test` for tests. --- ## Architecture & Key Patterns ### Request Flow ``` Request → CORS → Cookie → Rate-limit → Security headers → requirePermission() or requireAuth() → Zod schema validation (parseBody helper) → Route handler → Service function → Prisma → success(reply, data) or error(reply, message, status) ``` ### Response Format All responses use this shape: ```typescript // Success { success: true, data: T, message?: string, pagination?: {...} } // Error { success: false, error: string } ``` Use the `success()` and `error()` helpers in routes — never write raw `reply.send()`. ### Service Pattern Services are plain exported async functions, no classes: ```typescript // src/services/foo.service.ts export async function getFoo(id: number) { const result = await prisma.foo.findUnique({ where: { id } }); if (!result) return { error: "Not found", status: 404 }; return { data: result }; } // src/routes/admin/foo.ts const result = await getFoo(id); if ("error" in result) return error(reply, result.error, result.status ?? 400); return success(reply, result.data); ``` ### Error Handling - Routes map service errors to HTTP responses using the pattern above. - Global error handler in `server.ts` catches all unhandled exceptions; returns 500 with Czech message. - **Never silently swallow errors.** Even if a failure is non-fatal, log it: `app.log.error(e, 'context')`. - Error messages are in Czech (this is intentional — user-facing messages, Czech company). ### Permissions ```typescript // Route-level guard fastify.addHook("preHandler", requirePermission("invoices.view")); // or multiple fastify.addHook( "preHandler", requirePermission("invoices.view", "invoices.edit"), ); // Admin role bypasses all permission checks // Permissions follow the pattern: "entity.action" (e.g., "users.create", "invoices.delete") ``` ### Audit Logging Call `logAudit()` from `src/utils/audit.ts` whenever data is created/updated/deleted. Pass `oldData` and `newData` so the diff is stored. Audit failures are non-fatal. ### Validation Use Zod schemas from `src/schemas/`. All route bodies must be validated: ```typescript const body = parseBody(FooSchema, request.body); if ("error" in body) return error(reply, body.error, 400); ``` --- ## Date & Timezone Handling (Critical Gotcha) `src/config/env.ts` sets `process.env.TZ = 'Europe/Prague'` and overrides `Date.prototype.toJSON()` to return local time (not UTC). This means: - `JSON.stringify(new Date())` returns local Czech time, not UTC. - All API responses with Date fields will contain local time strings. - Prisma stores dates as UTC internally, but they read back as local due to the TZ setting. - **Never assume UTC** when working with Date objects in this codebase. - When writing new date comparisons or DB queries, use `new Date()` (already local) — do not manually offset. - The override exists for PHP migration compatibility and Czech date display. --- ## TOTP / 2FA - Secret stored AES-256-GCM encrypted in `users.totp_secret`. - Supports two encoding formats: PHP legacy (base64 iv+cipher+tag) and TS (hex). - Backup codes stored as encrypted JSON array in `users.totp_backup_codes`. - When `company_settings.require_2fa = true`, all users must enroll before accessing the app. - Login flow: password → if 2FA enabled → issue `loginToken` (5 min, single-use) → TOTP verify → issue access + refresh tokens. --- ## Testing Tests live in `src/__tests__/`. They use Vitest + Supertest against a real test database (`.env.test`). - Test coverage is minimal: only `auth` and `numbering` are tested. - Use `buildApp()` helper to spin up the Fastify instance for tests. - Tests use `vitest.config.ts` with `environment: 'node'` and 15s timeout. - **Do not mock Prisma** — tests hit a real database to catch schema/query bugs. When adding new features, add tests in `src/__tests__/`. Name test files `.test.ts`. --- ## Frontend Conventions - Pages are lazy-loaded via `React.lazy()` in `AdminApp.tsx`. - Auth state lives in `AuthContext`; use `useAuth()` hook to access it. - Alerts/toasts use `AlertContext`; use `useAlert()` to show them. - API calls go through `src/admin/utils/api.ts` which handles token refresh automatically (deduplicates concurrent refresh calls). - Custom hooks: `useApiCall`, `useListData`, `useTableSort`, `useDebounce`, `useModalLock`. - Styling: CSS files in `src/admin/` — no CSS-in-JS, no Tailwind. Use CSS variables. --- ## Database Conventions - All models use `snake_case` column names; Prisma maps to camelCase in TypeScript. - Soft-delete via `is_deleted` boolean (not all tables, check schema). - Timestamps: `created_at`, `updated_at` (auto-managed by Prisma). - Number sequences (`number_sequences` table) manage invoice/quotation numbering — never hardcode numbering logic. - All significant tables have audit log entries. Check `audit_logs` model for the schema. --- ## Known Issues & Gotchas 1. **Date.prototype.toJSON override** — global monkey-patch in `src/config/env.ts`. Side-effects on third-party libraries that serialize dates. Do not remove without migrating all date serialization. 2. **CJS/ESM mismatch in tests** — Server compiles to CommonJS (`tsconfig.server.json`), but Vitest runs in ESM by default. The `vitest.config.ts` resolves this, but be careful when adding dependencies that only support ESM. 3. **Mixed error patterns** — Some services return `{ error, status }`, others return discriminated unions `{ type: 'success' | 'error' }`. Prefer `{ error, status }` for consistency with existing routes. 4. **Silent error catches** — A few service functions swallow errors in catch blocks. Always log at minimum; never use empty catch blocks. 5. **HTML sanitization gap** — Rich text fields in invoices use DOMPurify, but quotation scope and order scope fields may not. If modifying those, add sanitization. 6. **Puppeteer PDF generation** — Runs a headless browser. Input to the HTML template must be sanitized. Do not pass unsanitized user data into PDF templates. 7. **NAS_PATH file access** — Project file uploads write to a network share path. In dev, this path may not be mounted. Features using `NAS_PATH` will fail gracefully (or not) if the path is unavailable. 8. **Prisma client regeneration** — After any schema change, run `npx prisma generate`. The generated client is not committed to git. 9. **No CSRF tokens** — CSRF protection relies on `SameSite=Strict` cookies + CORS. Do not weaken CORS configuration. 10. **Czech locale hardcoded** — Error messages, month names, and some business logic strings are Czech. This is intentional. --- ## Release Process 1. Bump version in `package.json` 2. `npm run build` 3. Create a tarball 4. Tag the release in Gitea 5. Deploy via SSH to production server (`boha_admin@192.168.50.100`) Do not push directly to production or restart services without confirmation.