104 lines
5.5 KiB
Markdown
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.
|