Files
app/CLAUDE.md
BOHA 07cb428287 1.5.2
- feat: order confirmation PDF generation with VAT support
- feat: order confirmation modal with custom item editing
- fix: attendance negative duration clamping and switchProject timing
- fix: Quill editor locked to Tahoma 14px, PDF heading sizes
- fix: invoice/offer PDF font consistency (Tahoma enforcement)
- fix: invoice alert cron improvements
- fix: NAS financials manager edge cases
- refactor: numbering service with unique sequence constraints

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 17:23:10 +02:00

11 KiB

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

# 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:

// 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:

// 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

// 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:

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 <feature>.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.