97 lines
2.7 KiB
TypeScript
97 lines
2.7 KiB
TypeScript
"use server";
|
|
|
|
import { revalidatePath } from "next/cache";
|
|
import type { ContentMutationSnapshot } from "@/features/housekeeping/domains/content/services/mutations";
|
|
import { createCorrelationId } from "@/features/housekeeping/foundation/contracts";
|
|
import { requirePermission, type StaffUser } from "@/lib/admin/guard";
|
|
import { PERMS } from "@/lib/permissions";
|
|
|
|
const MAX_SIZE = 2 * 1024 * 1024;
|
|
const ALLOWED = [
|
|
"image/png",
|
|
"image/jpeg",
|
|
"image/gif",
|
|
"image/webp",
|
|
"image/x-icon",
|
|
"image/svg+xml",
|
|
];
|
|
|
|
async function executeAuditedFaviconMutation(
|
|
staff: StaffUser,
|
|
operation: "favicon.save" | "favicon.delete",
|
|
input: unknown,
|
|
): Promise<ContentMutationSnapshot> {
|
|
const [
|
|
{ contentProductionMutationAdapter },
|
|
{ getHousekeepingCapabilityContext },
|
|
] = await Promise.all([
|
|
import(
|
|
"@/features/housekeeping/domains/content/services/mutations-production"
|
|
),
|
|
import("@/features/housekeeping/foundation/server-capability-context"),
|
|
]);
|
|
const capability = await getHousekeepingCapabilityContext();
|
|
if (capability.actor.id !== staff.id)
|
|
throw new Error("Authenticated staff changed during favicon mutation");
|
|
return contentProductionMutationAdapter.execute(operation, input, {
|
|
capability,
|
|
correlationId: createCorrelationId(),
|
|
legacy: true,
|
|
});
|
|
}
|
|
|
|
export async function saveFavicon(
|
|
formData: FormData,
|
|
): Promise<{ success: boolean; url?: string; error?: string }> {
|
|
const staff = await requirePermission(PERMS.SETTINGS_VIEW);
|
|
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 result = await executeAuditedFaviconMutation(staff, "favicon.save", {
|
|
file,
|
|
});
|
|
siteRevalidate();
|
|
return {
|
|
success: true,
|
|
...(typeof result.output?.url === "string"
|
|
? { url: result.output.url }
|
|
: {}),
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function deleteFavicon(): Promise<{
|
|
success: boolean;
|
|
error?: string;
|
|
}> {
|
|
const staff = await requirePermission(PERMS.SETTINGS_VIEW);
|
|
try {
|
|
await executeAuditedFaviconMutation(staff, "favicon.delete", {});
|
|
siteRevalidate();
|
|
return { success: true };
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
};
|
|
}
|
|
}
|
|
|
|
function siteRevalidate(): void {
|
|
revalidatePath("/", "layout");
|
|
revalidatePath("/admin/favicon");
|
|
}
|