"use server" import { headers } from "next/headers" import { redirect } from "next/navigation" import { revalidatePath } from "next/cache" import { eq } from "drizzle-orm" import { z } from "zod" import { getAdminSession } from "@/lib/session" import { logAdminAction } from "@/lib/admin/audit" import { setMaintenanceMode } from "@/lib/settings" import { setAiProvider, type AiProvider } from "@/lib/ai/provider" import { auth } from "@/lib/auth" import { db } from "@/lib/db" import { profiles, user as userTable } from "@/lib/db/schema" import { executeAccountDeletion } from "@/lib/gdpr/delete" // ── gate ──────────────────────────────────────────────────────────────────── // Every server action re-verifies the caller is an admin. NEVER skip — these // mutate any user's data and bypass user_id scoping. async function guard() { const a = await getAdminSession() if (!a) throw new Error("Forbidden") return a } const planSchema = z.enum(["starter", "pro", "landlord", "lifetime"]) // ── change plan ───────────────────────────────────────────────────────────── export async function changeUserPlan(userId: string, plan: string) { const a = await guard() const nextPlan = planSchema.parse(plan) const existing = await db.query.profiles.findFirst({ where: eq(profiles.id, userId) }) const oldPlan = existing?.plan ?? null await db.update(profiles).set({ plan: nextPlan }).where(eq(profiles.id, userId)) await logAdminAction({ adminId: a.user.id, action: "plan_change", targetUserId: userId, metadata: { from: oldPlan, to: nextPlan }, }) revalidatePath(`/admin/users/${userId}`) return { ok: true } } // ── ban ───────────────────────────────────────────────────────────────────── export async function banUser(userId: string, reason?: string) { const a = await guard() if (userId === a.user.id) throw new Error("You cannot ban yourself") await auth.api.banUser({ body: { userId, banReason: reason || "Banned by admin" }, headers: await headers(), }) await logAdminAction({ adminId: a.user.id, action: "ban", targetUserId: userId, metadata: { reason: reason || "Banned by admin" }, }) revalidatePath(`/admin/users/${userId}`) return { ok: true } } // ── unban ─────────────────────────────────────────────────────────────────── export async function unbanUser(userId: string) { const a = await guard() await auth.api.unbanUser({ body: { userId }, headers: await headers(), }) await logAdminAction({ adminId: a.user.id, action: "unban", targetUserId: userId, }) revalidatePath(`/admin/users/${userId}`) return { ok: true } } // ── impersonate ───────────────────────────────────────────────────────────── export async function impersonateUser(userId: string) { const a = await guard() if (userId === a.user.id) throw new Error("You cannot impersonate yourself") await auth.api.impersonateUser({ body: { userId }, headers: await headers(), }) await logAdminAction({ adminId: a.user.id, action: "impersonate", targetUserId: userId, }) redirect("/dashboard") } // ── delete ────────────────────────────────────────────────────────────────── export async function deleteUser(userId: string) { const a = await guard() if (userId === a.user.id) throw new Error("You cannot delete yourself") // Full GDPR-grade purge: cancels Stripe billing, deletes stored files, and // removes the user row (FK cascade erases the whole portfolio + sessions). const outcome = await executeAccountDeletion(userId) await logAdminAction({ adminId: a.user.id, action: "delete_user", targetUserId: userId, metadata: { ...outcome }, }) redirect("/admin/users") } // ── mark email verified ───────────────────────────────────────────────────── export async function markEmailVerified(userId: string) { const a = await guard() await db.update(userTable).set({ emailVerified: true }).where(eq(userTable.id, userId)) await logAdminAction({ adminId: a.user.id, action: "resend_verification", targetUserId: userId, metadata: { markedVerified: true }, }) revalidatePath(`/admin/users/${userId}`) return { ok: true } } // ── site maintenance mode ───────────────────────────────────────────────────── // Toggles the site-wide maintenance flag (persisted in app_settings). When on, // the marketing site and dashboard show a maintenance page to everyone except // admins. Revalidates the whole app so the change takes effect immediately. export async function setSiteMaintenance(enabled: boolean, message?: string) { const a = await guard() const trimmed = message?.trim() || null await setMaintenanceMode({ enabled: Boolean(enabled), message: trimmed }) await logAdminAction({ adminId: a.user.id, action: "maintenance_mode", metadata: { enabled: Boolean(enabled), message: trimmed }, }) revalidatePath("/", "layout") return { ok: true } } // ── AI provider ─────────────────────────────────────────────────────────────── // Chooses which LLM provider powers all AI features (OpenAI or Anthropic/Claude), // persisted in app_settings. Applies immediately to every AI route. export async function setAiProviderAction(provider: string) { const a = await guard() if (provider !== "openai" && provider !== "anthropic") throw new Error("Invalid AI provider") await setAiProvider(provider as AiProvider) await logAdminAction({ adminId: a.user.id, action: "ai_provider", metadata: { provider }, }) revalidatePath("/admin/system") return { ok: true } }