Performance and transport-layer hardening.
ImageUploader.tsx:
- Replaced sequential for-loop uploads with Promise.allSettled, so
multiple files upload concurrently. Partial failures no longer abort
the whole batch — successful uploads are kept, failed ones surface
a concatenated error (and a subsequent retry is still possible).
- Order indices are pre-computed from the existing images.length so
the parallel results stay correctly ordered.
next.config.ts:
- PoweredByHeader: false (no longer advertises Next.js).
- compress: true explicitly (default, but documented).
- Added Strict-Transport-Security, X-Frame-Options, X-Content-Type-
Options, Referrer-Policy, Permissions-Policy and a defensive CSP
(script-src allows 'unsafe-eval' for Next.js dev/HMR invariants,
connect-src whitelists Clerk endpoints).
- remotePatterns is now only populated when S3_ENDPOINT is set, and
parsed with URL() so a trailing path no longer produces a phantom
hostname. Still effectively unused because the app uses raw <img>;
the migration to next/image is deferred.
Code quality pass driven by the plan. Eliminates the duplicated
constants, breaks the 216-line templates file into per-template files,
and removes the third-party QR dependency that was leaking memorial
URLs to api.qrserver.com.
Templates (src/lib/templates.tsx → src/lib/templates/):
- Split into Elegance.tsx, Cinematic.tsx, Serene.tsx and a shared.tsx
holding formatDates() and MemorialFooter.
- New index.tsx re-exports everything plus renderTemplate(), so the
existing '@@/lib/templates' import paths are unchanged.
- Adds eslint-disable-next-line @next/next/no-img-element markers on
the raw <img> tags so the linter (once it works again) won't flag
them; full migration to next/image is deferred to a later phase
pending next.config remotePatterns verification.
QR consolidation:
- dashboard/page.tsx now uses lib/qrcode.ts::generateMonumentQR
(server-rendered async) instead of api.qrserver.com. The external
service was logging every memorial URL to a third party.
- lib/qrcode.ts reads APP_DOMAIN from the shared config (was
process.env.NEXT_PUBLIC_APP_DOMAIN inline). Added
generateMonumentQRPng() helper for the download path.
- Dashboard 'Превземи QR код' link now points at the data: URL in-app
and downloads as '{subdomain}-qr.png'.
Config consolidation:
- SubdomainPicker.tsx now imports APP_DOMAIN, SUBDOMAIN_MIN_LENGTH
and SUBDOMAIN_MAX_LENGTH from lib/config; previously the slug was
normalized with a regex that didn't match the server's stricter
/^[a-z0-9][a-z0-9-]*[a-z0-9]$/ rule and the success message
hardcoded '.testbed.mk'.
- ImageUploader.tsx re-imports MAX_FILE_SIZE / ALLOWED_TYPES /
MAX_FILES from lib/upload (they were literally redefined inline,
causing drift risk). The remaining client-side filter uses a
string-cast to satisfy the readonly-tuple type.
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).