Files
clienthub/src/app/admin/timer-actions.ts
T
simone a478462aa4 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>
2026-05-22 14:36:16 +02:00

80 lines
2.5 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { db } from "@/db";
async function requireAdmin() {
const session = await getServerSession(authOptions);
if (!session) throw new Error("Non autorizzato");
}
import { time_entries, projects } from "@/db/schema";
import { eq, isNull, asc } from "drizzle-orm";
import { nanoid } from "nanoid";
export async function startTimer(projectId: string): Promise<{ entryId: string }> {
await requireAdmin();
// Stop any currently running session before starting a new one
const running = await db
.select({ id: time_entries.id })
.from(time_entries)
.where(isNull(time_entries.ended_at));
for (const r of running) {
const now = new Date();
const entry = await db
.select({ started_at: time_entries.started_at })
.from(time_entries)
.where(eq(time_entries.id, r.id))
.limit(1);
if (entry[0]) {
const secs = Math.round((now.getTime() - new Date(entry[0].started_at).getTime()) / 1000);
await db
.update(time_entries)
.set({ ended_at: now, duration_seconds: secs })
.where(eq(time_entries.id, r.id));
}
}
const id = nanoid();
await db.insert(time_entries).values({ id, project_id: projectId });
revalidatePath("/admin");
revalidatePath("/admin/projects");
revalidatePath(`/admin/projects/${projectId}`);
return { entryId: id };
}
export async function startTimerForClient(clientId: string): Promise<{ entryId: string }> {
await requireAdmin();
const projectRows = await db
.select({ id: projects.id })
.from(projects)
.where(eq(projects.client_id, clientId))
.orderBy(asc(projects.created_at))
.limit(1);
const projectId = projectRows[0]?.id;
if (!projectId) throw new Error("Nessun progetto trovato per questo cliente");
return startTimer(projectId);
}
export async function stopTimer(entryId: string): Promise<void> {
await requireAdmin();
const rows = await db
.select({ started_at: time_entries.started_at })
.from(time_entries)
.where(eq(time_entries.id, entryId))
.limit(1);
if (!rows[0]) return;
const now = new Date();
const secs = Math.round((now.getTime() - new Date(rows[0].started_at).getTime()) / 1000);
await db
.update(time_entries)
.set({ ended_at: now, duration_seconds: secs })
.where(eq(time_entries.id, entryId));
revalidatePath("/admin");
revalidatePath("/admin/projects");
}