Files
2026-04-20 15:49:01 -05:00

104 lines
5.5 KiB
Markdown

# Architecture
## Runtime
- **Single container.** Node 20 (alpine) running Next.js in production mode. One process serves both the UI and the REST API.
- **Single volume (`/data`).** SQLite database (`app.db`), user uploads (`uploads/`), and backups (`backups/`) live side-by-side so the whole application state is one directory to back up.
- **Stateless app, stateful volume.** Nothing in the image needs to survive a rebuild except the schema, which is applied to `/data/app.db` at container start via `prisma migrate deploy`.
- **No background workers yet.** All work happens inline on a request. PDF generation and STEP thumbnailing will stay request-scoped until throughput forces otherwise.
## Request flow
```
Browser ──TLS── Reverse proxy ── HTTP ── Next.js (app+api) ── Prisma ── SQLite
└─► /data/uploads
```
- Reverse proxy terminates TLS (handled by Unraid SWAG / NPM / Caddy / Traefik etc.).
- The container listens on `:3000` with `HOSTNAME=0.0.0.0`.
- Health probe: `GET /api/health` runs `SELECT 1` against SQLite.
## Authentication
Two roles, no external identity providers.
### Admin
- Email + password. Password hashed with bcrypt (cost 12).
- Longer session default (`ADMIN_SESSION_HOURS`, default 8h).
### Operator
- Name + 4-digit PIN. PIN hashed with bcrypt (cost 12).
- Device session default 12h (`OPERATOR_SESSION_HOURS`).
- 5 failed attempts lock the account for 15 minutes (configurable).
- The operator login UI is a tile grid of active operator names, followed by a numeric keypad; no email/username typing on a phone.
### Sessions
- Server-side, backed by the `Session` table.
- Cookie `mrp_session` holds a random 32-byte token (base64url). Only the SHA-256 of the token is stored server-side, so a DB leak cannot hand out live sessions.
- `httpOnly`, `sameSite=lax`, `secure` in production, path `/`, `expires` matched to DB TTL.
- `lastSeenAt` is touched at most once per minute to avoid write amplification.
- `purgeExpiredSessions()` is exposed for a future cron.
## Data model
See [`prisma/schema.prisma`](../prisma/schema.prisma) for the canonical definition. Highlights:
- **Project → Assembly → Part → Operation.** `@@unique([assemblyId, code])` and `@@unique([partId, sequence])` keep the hierarchy tidy.
- **Operation.claimedByUserId** enforces single-claim at the app layer; the status/claim fields are indexed for dashboard queries.
- **TimeLog** is separate from Operation so a step can have multiple start/stop sessions over time.
- **QCRecord** supports both `inline` (checkbox on the step card) and `dedicated` (a QC operation type) via its `kind` field.
- **FileAsset** is a single table for all uploads; parts and purchase orders reference by nullable foreign key.
- **AuditLog** captures mutations with before/after snapshots as JSON strings (SQLite does not have a native Json type).
Enums are stored as strings rather than Prisma enums because SQLite has no enum support; validation is done at the Zod layer.
## API
- Planned surface: `/api/v1/*` (REST + JSON). The auth, operator list, and health endpoints in the current commit are the internal surface and will move under `/api/v1` when step 10 formalizes the public API.
- Every mutation writes an `AuditLog` row with actor, IP, and before/after payloads.
- Zod schemas validate request bodies; they will also drive OpenAPI generation in step 10.
## Front-end structure
- `app/login/*` — public login pages (chooser, admin form, operator tile grid + keypad).
- `app/admin/*` — admin shell (requires admin session, top-nav layout).
- `app/op/*` — operator shell (requires operator session, mobile-friendly layout).
- `app/api/*` — route handlers.
- `components/` — shared client components (initially just `LogoutButton`).
Every protected layout calls `requireAdmin()` or `requireOperator()` in its server component, which redirects unauthenticated requests to `/login`. There is intentionally no edge middleware because Prisma does not run on the Edge runtime.
## Files on disk
```
/data
├── app.db SQLite database
├── app.db-wal, app.db-shm SQLite WAL sidecars (when enabled)
├── backups/ point-in-time SQLite snapshots (future)
└── uploads/ user uploads, organized by content hash
├── step/
├── pdf/
├── dxf/
├── svg/
└── image/
```
Upload handling (step 2) will write content-addressed files: hash the incoming bytes, store at `uploads/<kind>/<sha256>.<ext>`, and reference via `FileAsset.path`. Duplicate uploads collapse to one file on disk.
## Build-time vs. run-time
- `npm run build` runs `prisma generate` and then `next build`. No database required.
- `prisma migrate deploy` runs **at container start**, not at build time, so the image is portable across environments.
- The bootstrap admin seed runs at start too, idempotently (it does nothing if an admin already exists).
## Security posture
- Passwords and PINs: bcrypt with cost 12.
- Session tokens: cryptographically random, never stored plaintext.
- Cookies: `httpOnly`, `sameSite=lax`, `secure` in prod.
- Input validation: Zod on every route.
- Audit log for every mutation and every login attempt (success or failure).
- PIN lockout: 5 attempts / 15 min (configurable). Addresses the small 4-digit search space.
- `APP_SECRET` is not yet used to sign QR tokens — that arrives in step 3. It is validated at boot so deploy-time misconfiguration fails loudly.