Files
app/CLAUDE.md
BOHA 82919d39f6 fix: remove manual project creation, smart sequence release, received-invoices schema fix
- Remove ProjectCreate page, POST /projects endpoint, and next-number endpoint
- Projects can only be created through orders (shared numbering sequence)
- Remove dead CreateProjectSchema and createProject service function
- Delete 'order' row from number_sequences (unused; code uses 'shared')
- Smart sequence release: decrement last_number only when deleting the highest number
- Fix received-invoices stats referencing non-existent is_deleted and amount_czk columns
- Update deploy instructions in CLAUDE.md (npm install, prisma migrate deploy)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 11:36:08 +02:00

12 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 (57 .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. Commit and tag (git tag -a vX.Y.Z)
  4. Push to Gitea (git push origin master && git push origin vX.Y.Z)
  5. Create tarball: tar -czf app-ts-X.Y.Z.tar.gz dist dist-client prisma package.json package-lock.json scripts
  6. Deploy via SSH to production server (boha_admin@192.168.50.100):
    • Path: /var/www/app-ts
    • Remove old files: rm -rf dist dist-client prisma scripts package.json package-lock.json
    • Copy tarball to server: scp app-ts-X.Y.Z.tar.gz boha_admin@192.168.50.100:/tmp/
    • Extract tarball: tar -xzf /tmp/app-ts-X.Y.Z.tar.gz
    • Install dependencies: npm install --omit=dev
    • Apply Prisma migrations: npx prisma migrate deploy
    • Restart: pm2 restart app-ts --update-env

Do not push directly to production or restart services without confirmation.