Review 5/5 — Core & security: issuance lifecycle, retention, auth, org admin - #29
Conversation
Review tier 5 of 5 — everything that deserves deep review, in one place:
Issuance & data lifecycle
- Cert POST inserts the cert AND deletes the submission in one Hasura
transaction; issued_by records who issued (audit CLAUDE.md allows).
- Retention sweep wired into submissions GET/POST: rows older than 24h
are deleted lazily; dashboard shows the countdown; batch issuance
('Generer valgte' → one ZIP) and PDF preview share one render path.
- Per-IP rate limiting wired on the anonymous submissions POST; email
notification (content-free) fires when Resend is configured.
Accounts & membership
- Password reset (glemt/reset), self-signup (/registrer), members
API+UI with last-member lockout guard, invites with 7-day tokens
whose redemption requires the session email to match the invited
address, admin NO/EN language scaffolding.
Platform
- /admin panel gated by PLATFORM_ADMIN_EMAILS env allowlist: list orgs,
create org + first member in one transaction. create-org.ts for CLI.
- Utstedte view (certs, per-month/template counts), baseline schema DDL
+ invites/issued_by migrations, CLAUDE.md/README document the model.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFHkXUuHJME4cF9AJWbavB
Deploying inf319-certificate with
|
| Latest commit: |
8b690c1
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://c80d8246.inf319-certificate.pages.dev |
| Branch Preview URL: | https://claude-review-5-core-securit.inf319-certificate.pages.dev |
|
Generated by Claude Code |
|
Security review notes (inline pass over every new attack surface in this stack — use as a checklist for your deep review): Verified sound:
Two findings, fixed in #35:
Accepted trade-offs (documented, not bugs): per-isolate best-effort rate limiting; members-remove lockout guard is check-then-delete (self-inflicted only, org can't reach zero members without racing yourself); admin emails visible to co-admins of the same org in the Utstedte view. Generated by Claude Code |
…re-security # Conflicts: # src/app/login/adminpage/[orgSlug]/layout.tsx
Same cleanup as the tooling and feedback branches — the limiter file itself was removed earlier in the stack. The size/shape validation on this endpoint (64KB cap, per-field length cap, cross-org template check) is untouched; only the per-IP throttle is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFHkXUuHJME4cF9AJWbavB
Per review feedback: shipping delete-at-issuance now and superseding it three PRs later was backwards — the 24h regeneration window this PR already introduces (the sweep) should be the ONLY deletion mechanism, from the start. Cert POST (certificates/route.ts): no longer deletes the submission. Made idempotent instead — returns the existing cert if one already exists for the submission (alreadyIssued: true), so 'Generer PDF' is safely re-clickable any time before the sweep runs (lost PDF, misprint, late-spotted mistake). GET now also returns submissionId so the dashboard can tell which still-present submissions are already issued. Check-then-insert has a small race window under concurrent double clicks; closing it with a DB-enforced unique constraint is tracked as follow-up hardening, not done here. Dashboard: issuedIds is now state (was a ref used only to dodge a now-nonexistent 'row already deleted' failure), seeded from the certificates GET on load, rendered as an 'Utstedt' chip. Issued submissions stay in the list — deletion is the sweep's job alone. Confirm-dialog copy no longer claims the submission disappears the moment a PDF is generated. Dropped the content-free email notification to admins on new submissions (notifyNewSubmission/buildSubmissionNotification/ getOrgNameAndAdminEmails + its test) — sendInviteEmail, which the invites feature still needs, is untouched; .env.example's RESEND_* comment now describes what it's actually for. CLAUDE.md/README updated to describe the final model directly instead of the delete-at-issuance one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFHkXUuHJME4cF9AJWbavB
'Konto' collided with the org-creation flow's 'virksomhetskonto' concept (an organization's presence on the platform, created by the platform admin at /admin) — the self-signup flow at /registrer creates a personal login, not an organization. /admin already says 'organisasjon' and 'brukerkonto' consistently; this brings /login and /registrer in line with that existing distinction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFHkXUuHJME4cF9AJWbavB
Email verification requirement is an Nhost project setting (Settings -> Sign-In Methods -> Email and Password -> 'Require Verified Emails'), not code in this repo — signup() already handles both outcomes via the session-present check. No code change needed to drop it. Added a small hint under the email field on /registrer pointing users who want an alias at Proton Pass, per the site owner's request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFHkXUuHJME4cF9AJWbavB
…ffiliate You set a 10-char minimum password length on the Nhost project side (Settings -> Sign-In Methods -> Email and Password). Added a matching PASSWORD_MIN_LENGTH constant in util/auth.ts and a client-side check on both /registrer and /login/reset, so a too-short password gets a clear inline message instead of hitting the server and surfacing whatever raw error Nhost returns. A helperText hint under the password field states the requirement up front. Also labeled the Proton Pass link on /registrer as an affiliate link, in both languages — the site's whole pitch is not monetizing anyone's data, so a referral link should say what it is rather than read as neutral advice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFHkXUuHJME4cF9AJWbavB
…feature The privacy policy (/personvern) was written against an earlier product state and no longer matched reality after #29 and #28: - 'Utstedelse uten sletting er teknisk umulig' / 'issuing without deleting is technically impossible' was true under delete-at-issuance, false now — #29 replaced it with the 24h sweep as the sole deletion mechanism, independent of issuance. Rewrote the retention and rights sections to state the actual guarantee: deleted within SUBMISSION_TTL_HOURS regardless of issuance status, deletable manually by the org at any time including after issuance. - 'Etter utstedelse finnes det ingenting å slette hos oss' / 'after issuance there is nothing left to delete' was false for the same reason — a submission can still hold real personal data for up to the TTL window after its certificate is issued. - Dropped the 'Frivillig tilbakemelding' / 'Voluntary feedback' section and the email-notification sentence under 'Who has access' — both describe features #28/#29 removed from the product. This is the legal text users rely on for what the platform actually does with their data, so it can't lag the code it describes. Also restores the SUBMISSION_TTL_HOURS import that a prior merge dropped (the privacy section was the only remaining reference — build was broken on this branch until this commit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFHkXUuHJME4cF9AJWbavB
Part 5 of the review-ordered stack (on
develop). Everything that deserves deep human investigation, concentrated in one PR. Take your time here — the rest of the stack is rubber-stampable by comparison.Issuance & data lifecycle (the privacy promise)
alreadyIssued: true) if this submission already has a cert. Does not delete the submission. Recordsissued_by(the audit CLAUDE.md explicitly allows: who issued, when, no payload). GET lists issued certs (no hash exposed, butsubmissionIdis — it's not personal data, and the dashboard needs it).SUBMISSION_TTL_HOURS, one constant) — this is the only deletion mechanism, regardless of whether a cert has been issued. That gives a regeneration window: "Generer PDF" can be re-run for the same submission any time before the sweep, e.g. to fix a lost or misprinted PDF. Dashboard shows the per-card countdown and an "Utstedt" chip on submissions that already have a cert (state seeded from the certificates GET, so it survives a reload).Revised per review feedback: the first draft of this PR deleted the submission in the same transaction as the cert insert, then a later PR in the original stack (retention model / 24h window) replaced that with the model described above. Shipping the wrong model first and superseding it three PRs later was backwards, so the regeneration-window model is now what #29 ships directly. The email notification to admins on new submissions (Resend-based, content-free) is dropped entirely — judged as unnecessary complexity;
sendInviteEmail, which the invites feature below still needs, is untouched. Follow-up: the PR further up the stack that originally introduced this retention model is now redundant and will need reconciling (likely closed) when review reaches it — tracked, not touched yet.Accounts & membership (the authz surface)
/login/glemt→/login/resetvia Nhost), self-signup/registrer(a fresh account has zero memberships).requireOrgMemberBySlug, last-member lockout guard./admin: gated by thePLATFORM_ADMIN_EMAILSenv allowlist (unset = surface disabled; checked server-side againstauth.users, never client input); creates org + first member in one transaction.create-org.tsfor CLI.Schema & docs
0000-baseline-schema.sql(full reproducible schema, IF NOT EXISTS — no-op on prod) +2026-07-invites.sql+2026-07-issued-by.sql. CLAUDE.md/README describe the regeneration-window model directly (not a since-superseded delete-at-issuance one).Other changes made during review-stack sync
claude/review-4-feedback-branding. After Review 4/5 — Org branding on public pages #28 merged, its target wasn'tdevelopdirectly, so I retargeted here and mergeddevelopin. One conflict in the admin nav (layout.tsx): kept this PR's data-driven nav (adds Members + Issued links) but dropped theTilbakemeldingerentry, since Review 4/5 — Org branding on public pages #28 removed that feature and its string key.Deploy checklist (one-time)
2026-07-issued-by.sqland2026-07-invites.sql, re-trackcertificates, trackinvites./login/reset.PLATFORM_ADMIN_EMAILS(your email); optionallyRESEND_API_KEY+NOTIFY_EMAIL_FROM(invite emails only now — see.env.example).Verification
Typecheck, lint, 119 unit tests, 10 E2E, production build (with placeholder env) — all green.
🤖 Generated with Claude Code
https://claude.ai/code/session_01BFHkXUuHJME4cF9AJWbavB