Purpose: turn
implementation.mdinto an execute‑ready plan with security fixes (CSRF, rate‑limits), single alert worker, safe log retrieval, and webhook hardening. Keep prompts minimal; each step is atomic and verifiable.
- Stacks: Node/Express, Prisma(Postgres), React/Vite, PM2, Nginx/CF.
- Features: Container actions, Log Bookmarks + Deep‑links, Log Pattern Alerts.
- Non‑negotiables: CSRF for all mutations; input validation (zod); audit log; rate limits; safe log I/O.
- Trust proxy (CF/Nginx):
app.set('trust proxy', true). - CSRF (double‑submit cookie):
- On
/api/auth/me: issuecsrftoken (httpOnly false, SameSite=Lax) + return{csrf}in body. - Middleware: require
x-csrf-tokenonPOST|PATCH|DELETEand match cookie value. - Client: attach
credentials: "include"andheaders: {'x-csrf-token': token}.
- On
- Rate limits (per IP + user):
/api/actions/*and/api/alerts/*(e.g., 30/min IP, 10/min user). - Validation: zod DTOs → request bodies; refuse on parse errors.
- Audit: capture
user_id, action, target, status, ip, ua, ts. - CORS: origin allowlist (container.piapps.dev only).
model LogBookmark {
id String @id @default(cuid())
userId String @index
containerId String @index
label String
ts DateTime @index(map: "bm_container_ts")
createdAt DateTime @default(now())
@@index([containerId, ts], name: "container_ts")
}
model Alert {
id String @id @default(cuid())
userId String @index
containerId String @index
pattern String // regex string (server‑validated, anchored if needed)
windowSec Int // evaluate logs from now - windowSec
threshold Int // min matches to trigger
cooldownSec Int @default(300)
enabled Boolean @default(true) @index
webhookUrl String?
secret String? // HMAC signer
lastFiredAt DateTime?
createdAt DateTime @default(now())
}Add indices: Alert(userId, enabled), LogBookmark(containerId, ts).
POST /api/actions/:id/(start|stop|restart|pause|unpause|remove)
Body:{ force?: boolean }→200 {ok:true}; RBAC: ADMIN only; audit.GET /api/containers/:id/bookmarks→200 LogBookmark[]POST /api/containers/:id/bookmarksBody:{label:string, ts:number}GET /api/bookmarks/:id/deeplink→302 /logs?container=:id#t=:ts(or signed URL, 24h)GET /api/containers/:id/logs/stream(SSE) Query:since, follow=true, tail<=5000POST /api/alertsBody:{containerId, pattern, windowSec, threshold, cooldownSec?, webhookUrl?}PATCH /api/alerts/:id{enabled?:boolean, ...}GET /api/alerts→ user‑scoped listPOST /api/webhooks/test{alertId}→ sends signed test event
All mutation routes: CSRF + rate‑limit + zod.
- Implement
getContainerLogs(opts)inserver/src/docker.ts:- Source: prefer Docker Engine API via
dockerode.modem.dialordocker logs --sinceviachild_process.spawn. - Bounds:
--since <iso>,--tail <N>; cap bytes (e.g., 5 MB) and truncate with notice. - Pattern: compile safe
RegExp(timeout via line‑by‑line test; no user flags likem/sunless whitelisted). - SSE: stream lines; on client, use virtualized list + backpressure.
- Source: prefer Docker Engine API via
- Deny patterns that look catastrophic (e.g., catastrophic backtracking) via a basic heuristic or RE2 wrapper if available.
- New entry:
server/src/worker/alerts.ts - Locking strategy (pick one):
- PM2 single: run as dedicated process
pm2 start npm --name cy-alerts -- run alerts(instances: 1) - Redis + BullMQ: repeatable job
"alerts:eval"every 30s with queue‑level concurrency 1.
- PM2 single: run as dedicated process
- Eval loop (pseudo):
- Load
enabledalerts. - For each: read logs
[now-windowSec, now]with cap; count matches. - If
count >= thresholdandnow - lastFiredAt >= cooldownSec: fire. - Update
lastFiredAtand write audit.
- Load
- Webhook send:
- Headers:
X-CY-Alert-Id,X-CY-Signature: sha256=<hex>where HMAC over body usingalert.secretor per‑user secret. - Retries: 3 with 2^n backoff; DLQ table on permanent failure.
- SSRF defense: block RFC1918, loopback, link‑local,
.local, IPv6 ULA. Resolve DNS first, then check IPs.
- Headers:
- Global: fetch
csrffrom/api/auth/me; store inAuthStore; attach header. - LogsViewer: SSE endpoint, tail control (max 5000), jump to bookmark by timestamp anchor.
- Bookmarks UI: sidebar + create at current cursor time; optimistic update with rollback.
- Alerts UI: form with live regex preview (client‑side only for UX), cooldown selector, toggle enable, webhooks test button.
- Confirmations: destructive ops require modal;
removeblocked if container is running unlessforce=truecheckbox. - Toasts: success/failure; include audit id on success.
- Tail max = 5000 lines; bytes cap per request = 5 MB.
- Max alerts per user = 100 (enforce at API + UI).
- Rate limit actions: 10/min per user; alerts CRUD: 30/min per IP.
- Regex length ≤ 512 chars; windowSec ≤ 86400; threshold ≤ 10_000.
- Unit: mock Dockerode; validate zod DTOs; CSRF middleware happy/sad.
- E2E (Playwright):
- Login →
/api/auth/mereturnscsrf. - Start/stop/restart a test container → audit row exists.
- Create bookmark → open deep‑link → log scroller focuses timestamp.
- Create alert with known pattern → emit webhook to test server → signature verifies.
- Rate‑limit and CSRF negative tests.
- Login →
- Load smoke: run multiple SSE clients; ensure server stays ≤ expected CPU/RAM.
- PM2:
cy-api(instances: max 2),cy-web(vite preview/build),cy-alerts(instances: 1).
- Nginx/CF: ensure sticky cookies, gzip for SSE disabled, proper
X-Forwarded-*. - Feature flag:
ALERTS_ENABLEDto admins first.
- Add CSRF + zod middleware; update all mutations; add trust proxy.
- Prisma migrate with models above; add composite indices.
- Implement docker log util + SSE with caps; wire GET
/logs/stream. - Container actions routes + audit + rate limit.
- Bookmarks routes + UI + deep‑link redirect.
- Alert worker + webhook signer + SSRF guard; UI to manage alerts.
- Tests (unit + e2e) and basic load smoke.
- PM2/Nginx changes; enable feature flag; rollout to admins, then all.
- All mutations require valid CSRF and pass zod validation.
- Single alert evaluator running; no duplicate firings in logs.
- Log viewer never exceeds caps; UI remains responsive with 5k‑line tails.
- Webhook recipients can verify HMAC; SSRF blocked (tests cover private IPs).
- e2e suite green; basic load smoke within CPU/RAM budget.