Commit Graph

6 Commits

Author SHA1 Message Date
d5e7a9a0b5 feat(schema): Phase 6 — VarChar bounds, updatedAt, key index, prisma seed
Schema hardening pass. After reading the onboarding/edit forms I
reconsidered the original 'migrate String? -> DateTime?' suggestion:
the bornDate/passedDate placeholders are 'нпр. 1960' and the column
deliberately accepts imprecise values like '1960', 'early 1990s',
'12 May 1960'. Forcing DateTime? would silently break that feature
and require users to enter exact dates they often don't know. The
responsible fix is to keep the free-text semantics but bound the
column length and document the intent in a schema comment. Future
structured-date queries should add a parallel DateTime? column.

Schema changes (prisma/schema.prisma):
- @db.VarChar bounds added to every string column, sized to match
  the existing client-side maxLength / app-constant caps:
    User.subdomain -> 32, User.title -> 100, User.description -> 2000,
    User.bornDate/passedDate -> 50, User.email -> 255, etc.
    Image.url/key -> 255, Image.id/userId -> 30.
    AdminUser.username -> 50, passwordHash -> 100.
    Code.code -> 12, usedByUserId -> 100 (matches Clerk userId scale).
- Image.key: added @@index('Image_key_idx') to support the
  /api/image lookup we tightened in Phase 1 (currently a scan).
- updatedAt added to Image, AdminUser, Code (previously only User).
- Inline schema comment on Code.createdById documenting the existing
  onDelete: Restrict behaviour (Prisma default) — the migration does
  not change it, just makes the audit-trail intent explicit.

Migration:
- 20260802000000_phase6_schema_hardening/migration.sql: explicit
  ALTER TABLE ... SET DATA TYPE VARCHAR(N) statements for every
  bounded column; ADD COLUMN updatedAt with DEFAULT CURRENT_TIMESTAMP
  (so existing rows back-fill immediately); CREATE INDEX for the
  Image.key column; a defensive LEFT() truncation UPDATE for any rows
  whose bornDate/passedDate exceed 50 chars (the inputs always capped
  at 50 on the client side, so this is belt-and-braces). Will be
  applied on next deploy via 'prisma migrate deploy'.

Seed:
- prisma/seed.ts: upserts the SUPER_ADMIN row from env
  SUPER_ADMIN_USERNAME + SUPER_ADMIN_PASSWORD_HASH. This resolves
  the Phase 1 problem where an env-only super-admin had no row in
  AdminUser, which meant the Code.createdById FK prevented them from
  ever creating codes. The seed is idempotent and safe to run on
  every boot (uses upsert).
- package.json: 'prisma.seed' wired to 'tsx prisma/seed.ts'.
- 'db:seed' npm script also added for manual provisioning.
- tsx added as devDependency (executes TypeScript straight from disk
  without pre-build).
2026-08-02 14:47:41 +02:00
1b917540f4 test: Phase 4 — vitest setup + unit tests for admin-session and rate-limit
Establishes the test suite. The repo previously had zero tests and no
test framework installed.

Tooling:
- Vitest 2 added as devDependency. Chosen for ESM-native + TypeScript
  out-of-the-box, no Babel/ts-node, and fast cold starts.
- vitest.config.ts sets environment=node and wires the '@/'
  path alias so tests can import app modules by the same path the
  app uses.
- package.json scripts: 'test' (vitest run, CI-friendly) and
  'test:watch'; also adds the long-missing 'typecheck' wrapper for
  'tsc --noEmit'.
- tsconfig.json now excludes *.test.ts from the app's build graph so
  the production bundle doesn't pull in test files (the editor still
  type-checks them via vitest).

Tests:
- admin-session.test.ts (8 cases): sign/verify round-trip for ADMIN
  and SUPER_ADMIN, tampered payload rejection (privilege-escalation
  attempt — should be rejected because the HMAC no longer matches),
  tampered signature rejection, missing-separator token, invalid
  base64/JSON payload, and cookie option flags (httpOnly, sameSite,
  path, secure under NODE_ENV=test vs production).
- rate-limit.test.ts (4 cases): basic token bucket within window,
  independent key tracking, refill after window elapses (fake
  timers), and remaining-counter accounting.

12 tests, all green.
2026-08-02 13:05:06 +02:00
3ff24cda0b feat(security): Phase 1 — harden auth, rate limiting, CSRF, upload validation
Security hardening covering credentials, brute-force protection, CSRF,
TOCTOU races, upload validation, and migration failure handling.

Secret rotation is deferred; the existing secrets in .env will be
rotated in a later phase. This phase reduces the attack surface and
removes the most exploitable issues.

Removed:
- Hardcoded 'super'/'admin' super-admin credentials in
  src/app/api/admin/login/route.ts. Username/hash are now loaded from
  env (SUPER_ADMIN_USERNAME / SUPER_ADMIN_PASSWORD_HASH) and verified
  via bcrypt like regular admins.

Added:
- src/lib/config.ts: single source of truth for APP_DOMAIN,
  APP_URL, subdomain regex/lengths, validation bounds, admin password
  policy, image-key allow-list regex, and super-admin env credentials.
- src/lib/rate-limit.ts: in-memory LRU (via lru-cache) rate limiters —
  admin login (5/min), validate-code (20/min), check-subdomain
  (60/min), upload (10/min) — keyed by client IP, returning 429 with
  X-RateLimit-* headers.
- requireAdmin / requireAdminPost / requireSuperAdminPost guards in
  src/lib/admin-session.ts. All admin mutating routes now enforce
  same-origin (Origin/Referer/Host check against NEXT_PUBLIC_APP_URL)
  before running — CSRF protection for the custom admin auth layer.
- Logout now imports COOKIE_NAME_ADMIN instead of hardcoding the string.
- Server-side magic-byte detection for image uploads (no new dep) —
  rejects spoofed Content-Type. GIF removed from allowed types.
- /api/publish is now transactional (prisma.) with
  explicit P2002 → 409 handling for subdomain collisions.
- /api/image validates the key against an allow-list regex and returns
  Macedonian error messages (was the only English-localized file).
- /api/validate-code enforces a 12-hex-char pattern and uses
  updateMany with usedByUserId=null guard to make the claim atomic.
- /api/check-subdomain validates the slug against the shared regex
  before hitting the DB and returns a short private Cache-Control.
- Admin password minimum length bumped from 6 to 12 with letter+digit
  complexity requirement across change-password, users POST and
  users/[id] PUT.

Changed:
- scripts/start.sh: prisma migrate deploy failures now exit non-zero
  instead of silently continuing (prevents schema drift in prod).
- .env.example: documents SUPER_ADMIN_USERNAME /
  SUPER_ADMIN_PASSWORD_HASH with example bcrypt-hash generation.
- package.json: lru-cache added as direct dependency (already present
  transitively, promoted to explicit).
2026-08-02 10:24:28 +02:00
8569b506d2 feat(db): add AdminUser and Code models with migration
- Add AdminUser model (username, passwordHash, role enum)
- Add Code model (code, createdBy, usedByUserId, timestamps)
- Install bcryptjs for password hashing
- Run migration add_admin_and_code
2026-07-29 18:54:40 +02:00
47670f8313 deploy fix 2026-06-22 03:53:47 +02:00
4fdb51f583 init 2026-06-20 18:17:30 +02:00