diff --git a/src/actions/admin-media.ts b/src/actions/admin-media.ts new file mode 100644 index 00000000..c88936f3 --- /dev/null +++ b/src/actions/admin-media.ts @@ -0,0 +1,66 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; +import { writeFile, mkdir } from "fs/promises"; +import path from "path"; +import { requireStaff } from "@/lib/admin/guard"; + +const MEDIA_DIR = "public/assets/images/media"; +const MAX_SIZE = 5 * 1024 * 1024; // 5MB +const ALLOWED = ["image/png", "image/jpeg", "image/gif", "image/webp"]; + +export async function uploadMedia(formData: FormData): Promise { + await requireStaff(); + const file = formData.get("file") as File | null; + if (!file || file.size === 0) return; + if (file.size > MAX_SIZE) throw new Error("File too large (max 5MB)"); + if (!ALLOWED.includes(file.type)) throw new Error("Invalid file type"); + + const dir = path.join(process.cwd(), MEDIA_DIR); + await mkdir(dir, { recursive: true }); + + const ext = file.name.split(".").pop() ?? "png"; + const name = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`; + const bytes = await file.arrayBuffer(); + await writeFile(path.join(dir, name), Buffer.from(bytes)); + + revalidatePath("/api/media"); + revalidatePath("/admin/media"); +} + +export async function deleteMedia(name: string): Promise { + await requireStaff(); + const { unlink } = await import("fs/promises"); + const dir = path.join(process.cwd(), MEDIA_DIR); + const filePath = path.join(dir, name); + // Prevent path traversal + if (name.includes("..") || name.includes("/")) return; + try { + await unlink(filePath); + } catch { + // File may not exist + } + revalidatePath("/api/media"); + revalidatePath("/admin/media"); +} + +export async function uploadMediaAndReturn(formData: FormData): Promise { + await requireStaff(); + const file = formData.get("file") as File | null; + if (!file || file.size === 0) return ""; + if (file.size > MAX_SIZE) return ""; + if (!ALLOWED.includes(file.type)) return ""; + + const dir = path.join(process.cwd(), MEDIA_DIR); + await mkdir(dir, { recursive: true }); + + const ext = file.name.split(".").pop() ?? "png"; + const name = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`; + const bytes = await file.arrayBuffer(); + await writeFile(path.join(dir, name), Buffer.from(bytes)); + + revalidatePath("/api/media"); + revalidatePath("/admin/media"); + return `/assets/images/media/${name}`; +} diff --git a/src/app/admin/articles/[id]/page.tsx b/src/app/admin/articles/[id]/page.tsx index e7411be2..6882045e 100644 --- a/src/app/admin/articles/[id]/page.tsx +++ b/src/app/admin/articles/[id]/page.tsx @@ -1,7 +1,7 @@ import Link from "next/link"; import { notFound } from "next/navigation"; import { deleteArticle, updateArticle } from "@/actions/admin-articles"; -import { RichText } from "@/components/admin/rich-text"; +import { ArticleForm } from "@/components/admin/article-form"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -14,7 +14,7 @@ export default async function EditArticle({ const { id } = await params; const article = await prisma.websiteArticles.findUnique({ where: { id: BigInt(id) }, - select: { id: true, slug: true, title: true, shortStory: true, fullStory: true, image: true, createdAt: true, updatedAt: true }, + select: { id: true, slug: true, title: true, shortStory: true, fullStory: true, image: true }, }).catch(() => null); if (!article) notFound(); @@ -24,14 +24,19 @@ export default async function EditArticle({ ← Articles

Edit article

-
- - - -