"use server"; import { cookies } from "next/headers"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { z } from "zod"; import { getDb, projects, eq, and } from "@fetcha/db"; import { newId } from "@fetcha/core"; import { getWorkspace, PROJECT_COOKIE, requestMeta } from "@/lib/session"; import { writeAudit } from "@/lib/audit"; import { slugify } from "@/lib/utils"; export type ActionResult = { ok: true; data?: T } | { ok: false; error: string; field?: string }; const projectSchema = z.object({ name: z.string().trim().min(2, "Name must be at least 2 characters").max(64), description: z.string().trim().max(280).optional().or(z.literal("")), environment: z.enum(["production", "development"]).default("production"), defaultCountry: z.string().trim().length(2).toUpperCase().optional().or(z.literal("")), defaultNetwork: z.enum(["auto", "datacenter", "residential", "isp", "mobile"]).default("auto"), logLevel: z.enum(["none", "metadata", "headers", "full"]).default("metadata"), softLimitUsd: z.coerce.number().min(0).optional().or(z.literal("")), hardLimitUsd: z.coerce.number().min(0).optional().or(z.literal("")), monthlyRequestLimit: z.coerce.number().int().min(0).optional().or(z.literal("")), }); function num(v: number | "" | undefined): number | null { return v === "" || v === undefined || Number.isNaN(v) ? null : v; } export async function createProject(formData: FormData): Promise> { const ws = await getWorkspace(); const parsed = projectSchema.safeParse(Object.fromEntries(formData)); if (!parsed.success) return { ok: false, error: parsed.error.issues[0]?.message ?? "Invalid input", field: String(parsed.error.issues[0]?.path[0] ?? "") }; const d = parsed.data; const db = getDb(); const id = newId("proj"); let slug = slugify(d.name); const existing = await db.select({ id: projects.id }).from(projects).where(and(eq(projects.organizationId, ws.organization.id), eq(projects.slug, slug))); if (existing.length) slug = `${slug}-${id.slice(-4)}`; await db.insert(projects).values({ id, organizationId: ws.organization.id, name: d.name, slug, description: d.description || null, environment: d.environment, defaultCountry: d.defaultCountry || null, defaultNetwork: d.defaultNetwork, logLevel: d.logLevel, softLimitUsd: num(d.softLimitUsd), hardLimitUsd: num(d.hardLimitUsd), monthlyRequestLimit: num(d.monthlyRequestLimit), }); const meta = await requestMeta(); await writeAudit({ userId: ws.user.id, organizationId: ws.organization.id, action: "project.created", target: id, metadata: { name: d.name }, ipAddress: meta.ip, userAgent: meta.userAgent }); (await cookies()).set(PROJECT_COOKIE, id, { path: "/", httpOnly: true, sameSite: "lax", maxAge: 60 * 60 * 24 * 365 }); revalidatePath("/dashboard", "layout"); return { ok: true, data: { id } }; } export async function updateProject(projectId: string, formData: FormData): Promise { const ws = await getWorkspace(); const parsed = projectSchema.safeParse(Object.fromEntries(formData)); if (!parsed.success) return { ok: false, error: parsed.error.issues[0]?.message ?? "Invalid input", field: String(parsed.error.issues[0]?.path[0] ?? "") }; const d = parsed.data; const db = getDb(); const [p] = await db.select().from(projects).where(and(eq(projects.id, projectId), eq(projects.organizationId, ws.organization.id))).limit(1); if (!p) return { ok: false, error: "Project not found" }; await db .update(projects) .set({ name: d.name, description: d.description || null, environment: d.environment, defaultCountry: d.defaultCountry || null, defaultNetwork: d.defaultNetwork, logLevel: d.logLevel, softLimitUsd: num(d.softLimitUsd), hardLimitUsd: num(d.hardLimitUsd), monthlyRequestLimit: num(d.monthlyRequestLimit), updatedAt: new Date(), }) .where(eq(projects.id, projectId)); const meta = await requestMeta(); await writeAudit({ userId: ws.user.id, organizationId: ws.organization.id, action: "project.updated", target: projectId, ipAddress: meta.ip, userAgent: meta.userAgent }); revalidatePath("/dashboard", "layout"); return { ok: true }; } export async function archiveProject(projectId: string): Promise { const ws = await getWorkspace(); if (ws.projects.length <= 1) return { ok: false, error: "You need at least one active project." }; const db = getDb(); const [p] = await db.select().from(projects).where(and(eq(projects.id, projectId), eq(projects.organizationId, ws.organization.id))).limit(1); if (!p) return { ok: false, error: "Project not found" }; await db.update(projects).set({ archivedAt: new Date(), updatedAt: new Date() }).where(eq(projects.id, projectId)); const meta = await requestMeta(); await writeAudit({ userId: ws.user.id, organizationId: ws.organization.id, action: "project.archived", target: projectId, ipAddress: meta.ip, userAgent: meta.userAgent }); const store = await cookies(); if (store.get(PROJECT_COOKIE)?.value === projectId) store.delete(PROJECT_COOKIE); revalidatePath("/dashboard", "layout"); return { ok: true }; } export async function selectProject(projectId: string, redirectTo?: string): Promise { const ws = await getWorkspace(); if (!ws.projects.some((p) => p.id === projectId)) return; (await cookies()).set(PROJECT_COOKIE, projectId, { path: "/", httpOnly: true, sameSite: "lax", maxAge: 60 * 60 * 24 * 365 }); revalidatePath("/dashboard", "layout"); if (redirectTo) redirect(redirectTo); }