Phases 4-8: admin backend, storage driver, local LLM, Docker, polish
Closes audit findings C1, H4 (sandboxing follow-up parked), H6, M2.
Phase 4 - admin backend.
- routes/admin.ts mounted at /admin/*, gated by requireAuth +
requireAdmin.
- GET / PATCH / DELETE /admin/users (last-admin protection;
self-mutation refused).
- GET / PATCH /admin/org-settings with typed validation per
field; audit_events records every change.
- GET /admin/audit paginated; optional action and user_id
filters.
- Frontend admin pages land later; the API is complete.
Phase 5 - storage driver + at-rest encryption + download tokens.
- lib/storageLocal.ts: local driver. Each file on disk is
[12-byte IV][16-byte GCM tag][ciphertext]. Master key is
SHA-256(STORAGE_ENCRYPTION_KEY). Path traversal: resolved
absolute path must stay under STORAGE_LOCAL_PATH.
- lib/storage.ts becomes a facade keyed on STORAGE_DRIVER:
local (default for Docker) or s3 (existing R2 / MinIO).
Signed URLs return null in local mode; callers fall back to
/download/:token.
- End-to-end verified: 25-byte plaintext -> 53-byte on-disk
file; plaintext does not appear; round-trip preserved.
- lib/downloadTokens.ts payload adds u (user_id) + exp;
/download/:token refuses tokens issued to a different user
or past expiry. TTL defaults to 24h, env-tunable. Closes C1.
Phase 6 - local LLM + EXTERNAL_AI_DISABLED.
- lib/llm/local.ts: OpenAI-compatible adapter for Ollama /
vLLM / LM Studio. Streaming + completion. Models prefixed
"local/" route here; suffix sent verbatim as the upstream
model id. Tools not yet wired (follow-up).
- lib/llm/index.ts adds assertProviderAllowed: when
EXTERNAL_AI_DISABLED=true, refuses Claude/Gemini/OpenAI
dispatch. Local always passes.
- Provider enum extended to include "local".
- tabular.ts missingModelApiKey skips the check for local
models (gated by LOCAL_LLM_BASE_URL on the backend).
Phase 7 - Docker / Caddy / deployment.
- backend/Dockerfile multi-stage (deps -> tsc build -> slim
runtime). LibreOffice baked in. Non-root user. Entrypoint
runs `npm run migrate` (toggle via MIGRATE_ON_BOOT) then
starts the server.
- frontend/Dockerfile multi-stage. next.config.ts:
output: "standalone".
- docker-compose.yml at repo root. Services:
postgres, backend, frontend, ollama, caddy, backup.
Every persistent volume mounts under ${DATA_ROOT}. Backend
healthcheck uses /ready. Backup sidecar runs nightly
pg_dump with BACKUP_KEEP_DAYS retention.
- caddy/Caddyfile terminates TLS via Let's Encrypt for
${PIP_DOMAIN}; forwards backend paths to backend:3001 and
everything else to frontend:3000.
- .env.compose.example documents every required and optional
var.
Phase 8 - rate limits + structure_tree sanitisation.
- rate-limit keyGenerator keys on res.locals.userId for chat,
chat-create, and upload paths; pre-auth requests still fall
back to IP. Closes H6.
- sanitiseStructureTitle strips control chars, escapes angle
brackets, caps length. Applied to PDF outline titles and
DOCX mammoth-extracted lines before storage. Closes M2.
Type-check clean across the backend. All 16 migrations apply
cleanly. Storage driver smoke-tested.
Remaining outstanding (post-MVP polish):
- Frontend workspace switcher + workspace settings page.
- Frontend admin pages (Users / AI Policy / Audit).
- Frontend Account page polish for the new per-user fields.
- Tool support on the local LLM adapter so edit / generate /
read flows work with Ollama-class models.
| Repository | cpatpa/PIP |
|---|---|
| Author | Claude <noreply@anthropic.com> |
| Authored | |
| Parents | 510d7e86 |
| Stats | 24 files changed , +1349 , -108 |
| Part of | Phases 4-8 - admin backend, local storage driver, local LLM, Docker, admin/account frontend |
Capture this commit into my fork
Download a Markdown prompt that tells Claude how to port this
exact commit into your working tree. Run it via
claude -p < capture-commit-9166a01d.md
from inside the repo you want the change in.