security: full hardening pass — auth guards, rate limiting, headers, internal secret

- Add requireAdmin() to all unprotected admin server actions
  (clients/new, clients/[id], timer-actions — 17 functions total)
- Protect /api/internal/* endpoints with X-Internal-Secret header
  (proxy.ts sends it; routes reject requests without it)
- Randomize auto-generated client slugs with 4-char suffix
  to prevent enumeration via predictable name-based slugs
- Add in-memory rate limiting to /api/client/approve (20/min)
  and /api/client/comment (10/min) per IP
- Add security headers: X-Frame-Options, X-Content-Type-Options,
  Referrer-Policy, Permissions-Policy, X-DNS-Prefetch-Control
- Reduce JWT session from 30 days to 7 days with daily rotation
- Remove hardcoded NEXTAUTH_URL from Dockerfile (pass via Coolify env)
- Genericize client API error messages to not leak data structure
- Update .env.example with all required variables including INTERNAL_SECRET

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 14:36:16 +02:00
parent eab88c9f63
commit a478462aa4
13 changed files with 128 additions and 15 deletions
+16 -2
View File
@@ -4,17 +4,30 @@ import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { db } from "@/db";
import { clients, projects, payments } from "@/db/schema";
async function requireAdmin() {
const session = await getServerSession(authOptions);
if (!session) throw new Error("Non autorizzato");
}
function randomAlpha(len: number): string {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
return Array.from({ length: len }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
}
function toSlug(name: string): string {
return name
const base = name
.toLowerCase()
.normalize("NFD")
.replace(/[̀-ͯ]/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 50);
.slice(0, 44);
return `${base}-${randomAlpha(4)}`;
}
async function uniqueSlug(base: string): Promise<string | null> {
@@ -34,6 +47,7 @@ const createClientSchema = z.object({
});
export async function createClient(formData: FormData) {
await requireAdmin();
const raw = {
name: formData.get("name") as string,
brand_name: formData.get("brand_name") as string,