"use server"; import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import { revalidatePath } from "next/cache"; import { checkPassword } from "@/lib/auth"; import { ADMIN_COOKIE, createSessionToken, SESSION_MAX_AGE, } from "@/lib/session"; import { createPost, updatePost, deletePost, replaceAllPosts, type PostInput, } from "@/lib/posts"; import { saveSettings, normalizeSettings, type Settings, } from "@/lib/settings"; import { THEMES, isThemeId, type ThemeId } from "@/themes/registry"; function s(formData: FormData, key: string): string { const v = formData.get(key); return typeof v === "string" ? v : ""; } // Refresh the whole site after a content/settings change. The layout-level // revalidation covers branding + theme that live in the shared shell. function revalidateSite() { revalidatePath("/", "layout"); } /* ------------------------------- auth -------------------------------- */ export async function loginAction(formData: FormData) { const password = s(formData, "password"); const next = s(formData, "next"); if (!checkPassword(password)) { redirect( `/admin/login?error=1${next ? `&next=${encodeURIComponent(next)}` : ""}` ); } const token = await createSessionToken(); (await cookies()).set(ADMIN_COOKIE, token, { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/", maxAge: SESSION_MAX_AGE, }); redirect(next && next.startsWith("/admin") ? next : "/admin"); } export async function logoutAction() { (await cookies()).delete(ADMIN_COOKIE); redirect("/admin/login"); } /* ------------------------------- posts ------------------------------- */ function postInputFromForm(formData: FormData): PostInput { return { slug: s(formData, "slug"), title: s(formData, "title"), excerpt: s(formData, "excerpt"), body: s(formData, "body"), author: s(formData, "author"), tags: s(formData, "tags"), createdAt: s(formData, "createdAt"), }; } export async function savePostAction(formData: FormData) { const input = postInputFromForm(formData); const idRaw = s(formData, "id"); if (!input.title.trim()) { redirect(idRaw ? `/admin/posts/${idRaw}/edit?error=title` : "/admin/posts/new?error=title"); } if (idRaw) { updatePost(Number(idRaw), input); } else { createPost(input); } revalidateSite(); redirect("/admin/posts"); } export async function deletePostAction(formData: FormData) { const id = Number(s(formData, "id")); if (Number.isFinite(id)) deletePost(id); revalidateSite(); redirect("/admin/posts"); } /* ------------------------------ settings ----------------------------- */ export async function saveSettingsAction(formData: FormData) { const allowed = formData .getAll("allowedThemes") .map(String) .filter(isThemeId) as ThemeId[]; const defaultTheme = s(formData, "defaultTheme"); const next: Settings = normalizeSettings({ title: s(formData, "title"), subtitle: s(formData, "subtitle"), footer: s(formData, "footer"), version: s(formData, "version"), defaultTheme: isThemeId(defaultTheme) ? defaultTheme : undefined, publicThemeToggle: formData.get("publicThemeToggle") === "on", allowedThemes: allowed.length ? allowed : THEMES.map((t) => t.id), }); saveSettings(next); revalidateSite(); redirect("/admin/settings?saved=1"); } /* ------------------------------- import ------------------------------ */ export async function importAction(formData: FormData) { const file = formData.get("file"); const pasted = s(formData, "pasted"); const wants = formData.getAll("what").map(String); const wantSettings = wants.includes("settings"); const wantPosts = wants.includes("posts"); const postMode = s(formData, "postMode") || "replace"; let raw = pasted; if (file && file instanceof File && file.size > 0) { raw = await file.text(); } if (!raw.trim()) redirect("/admin/settings?import=empty"); let parsed: unknown; try { parsed = JSON.parse(raw); } catch { redirect("/admin/settings?import=invalid"); } const obj = parsed as Record; if (wantSettings && obj.settings) { saveSettings(normalizeSettings(obj.settings)); } if (wantPosts && Array.isArray(obj.posts)) { const posts = (obj.posts as Record[]).map( (p): PostInput => ({ slug: typeof p.slug === "string" ? p.slug : undefined, title: typeof p.title === "string" ? p.title : "Untitled", excerpt: typeof p.excerpt === "string" ? p.excerpt : "", body: typeof p.body === "string" ? p.body : "", author: typeof p.author === "string" ? p.author : "webmaster", tags: Array.isArray(p.tags) ? (p.tags as string[]).join(",") : typeof p.tags === "string" ? p.tags : "", createdAt: typeof p.createdAt === "string" ? p.createdAt : typeof p.created_at === "string" ? (p.created_at as string) : undefined, }) ); if (postMode === "append") { for (const p of posts) createPost(p); } else { replaceAllPosts(posts); } } revalidateSite(); redirect("/admin/settings?import=ok"); }