"use server"; import { revalidatePath } from "next/cache"; import { mkdir, writeFile, unlink } from "fs/promises"; import path from "path"; import { prisma } from "@/lib/prisma"; import { siteSettings } from "@/lib/services/site-settings"; const FAVICON_DIR = "public/assets/images/media/favicon"; const MAX_SIZE = 2 * 1024 * 1024; // 2MB const ALLOWED = ["image/png", "image/jpeg", "image/gif", "image/webp", "image/x-icon", "image/svg+xml"]; export async function saveFavicon( formData: FormData, ): Promise<{ success: boolean; url?: string; error?: string }> { try { const file = formData.get("file") as File | null; if (!file || file.size === 0) return { success: false, error: "No file provided" }; if (file.size > MAX_SIZE) return { success: false, error: "File too large (max 2MB)" }; if (!ALLOWED.includes(file.type)) return { success: false, error: "Invalid file type. Allowed: PNG, JPEG, GIF, WebP, ICO, SVG" }; const mimeExt: Record = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp", "image/x-icon": "ico", "image/svg+xml": "svg", }; const ext = mimeExt[file.type] ?? "png"; const filename = `favicon-${Date.now()}.${ext}`; const baseDir = path.resolve(process.cwd(), FAVICON_DIR); const filePath = path.resolve(baseDir, filename); if (!filePath.startsWith(baseDir + path.sep)) { return { success: false, error: "Invalid path" }; } const buffer = Buffer.from(await file.arrayBuffer()); // eslint-disable-next-line security/detect-non-literal-fs-filename await mkdir(baseDir, { recursive: true }); // eslint-disable-next-line security/detect-non-literal-fs-filename await writeFile(filePath, buffer); const url = `/api/media/favicon/${filename}`; // Remove old favicon file if it exists const oldUrl = await siteSettings.get("cms_favicon"); if (oldUrl && oldUrl.startsWith("/api/media/favicon/")) { const oldName = oldUrl.replace("/api/media/favicon/", ""); if (!oldName.includes("..") && !oldName.includes("/")) { const oldPath = path.resolve(baseDir, oldName); if (oldPath.startsWith(baseDir + path.sep)) { try { // eslint-disable-next-line security/detect-non-literal-fs-filename await unlink(oldPath); } catch { /* ignore if file doesn't exist */ } } } } await prisma.websiteSetting.upsert({ where: { key: "cms_favicon" }, update: { value: url }, create: { key: "cms_favicon", value: url, comment: "Favicon URL" }, }); siteSettings.reload(); revalidatePath("/", "layout"); revalidatePath("/admin/favicon"); return { success: true, url }; } catch (e) { return { success: false, error: e instanceof Error ? e.message : "Unknown error" }; } } export async function deleteFavicon(): Promise<{ success: boolean; error?: string }> { try { const oldUrl = await siteSettings.get("cms_favicon"); if (oldUrl && oldUrl.startsWith("/api/media/favicon/")) { const baseDir = path.resolve(process.cwd(), FAVICON_DIR); const oldName = oldUrl.replace("/api/media/favicon/", ""); if (!oldName.includes("..") && !oldName.includes("/")) { const oldPath = path.resolve(baseDir, oldName); if (oldPath.startsWith(baseDir + path.sep)) { try { // eslint-disable-next-line security/detect-non-literal-fs-filename await unlink(oldPath); } catch { /* ignore */ } } } } await prisma.websiteSetting.delete({ where: { key: "cms_favicon" } }).catch(() => {}); siteSettings.reload(); revalidatePath("/", "layout"); revalidatePath("/admin/favicon"); return { success: true }; } catch (e) { return { success: false, error: e instanceof Error ? e.message : "Unknown error" }; } }