- Remove .prettierrc (dead config, Biome replaces Prettier) - Rename lighthouserc.json to lighthouserc.cjs with module.exports for ESM compat - Add logger.warn to empty catch blocks in auth, register, site-settings, prisma-cache, redis, security, rate-limit - Unify ActionResult type: action-helper.ts uses 'ok' consistent with safe-action-shared.ts - Add noUnusedLocals + noUnusedParameters to tsconfig + fix 25 pre-existing unused vars - Replace barrel export src/types/index.ts with direct @/types/common imports - Make trustHost conditional (development only) in auth.ts - Add pre-flight URL validation to update-Nitrov3.sh to catch image.library.url misconfigurations - Improve NITRO_IMAGE_LIBRARY_URL content validation in pre-flight & post-compute checks
135 lines
3.7 KiB
TypeScript
135 lines
3.7 KiB
TypeScript
"use server";
|
|
|
|
import { revalidatePath } from "next/cache";
|
|
import { redirect } from "next/navigation";
|
|
import { z } from "zod";
|
|
import { requirePermission } from "@/lib/admin/guard";
|
|
import { formPositiveBigInt } from "@/lib/form-data";
|
|
import { logger } from "@/lib/logger";
|
|
import { PERMS } from "@/lib/permissions";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { adminAction } from "@/lib/safe-action";
|
|
import { ActionError, actionOk } from "@/lib/safe-action-shared";
|
|
import { logStaffActivity } from "@/lib/services/staff-activity";
|
|
|
|
// CRUD for website advertisements (website_ads). Emulator does not own this
|
|
// table; it only stores an image URL rendered in the site layout/widgets.
|
|
|
|
export async function createAd(formData: FormData): Promise<void> {
|
|
const staff = await requirePermission(PERMS.PAGES_EDIT);
|
|
const image = String(formData.get("image") ?? "")
|
|
.normalize("NFC")
|
|
.trim()
|
|
.slice(0, 255);
|
|
if (!image) return;
|
|
|
|
const now = new Date();
|
|
try {
|
|
const ad = await prisma.websiteAds.create({
|
|
data: { image, createdAt: now, updatedAt: now },
|
|
});
|
|
await logStaffActivity({
|
|
staffId: staff.id,
|
|
action: "ad_create",
|
|
description: `Created advertisement #${ad.id} (${image})`,
|
|
targetType: "website_ad",
|
|
targetId: Number(ad.id),
|
|
});
|
|
} catch (err) {
|
|
logger.error("Action failed: createAd", {
|
|
action: "createAd",
|
|
error: err instanceof Error ? err.message : "DB error",
|
|
});
|
|
revalidatePath("/admin/ads");
|
|
return;
|
|
}
|
|
redirect("/admin/ads");
|
|
}
|
|
|
|
export async function updateAd(formData: FormData): Promise<void> {
|
|
const staff = await requirePermission(PERMS.PAGES_EDIT);
|
|
const raw = String(formData.get("id") ?? "").normalize("NFC");
|
|
if (!/^\d+$/.test(raw)) return;
|
|
const id = BigInt(raw);
|
|
const image = String(formData.get("image") ?? "")
|
|
.normalize("NFC")
|
|
.trim()
|
|
.slice(0, 255);
|
|
if (!image) return;
|
|
|
|
try {
|
|
await prisma.websiteAds.update({
|
|
where: { id },
|
|
data: { image, updatedAt: new Date() },
|
|
});
|
|
await logStaffActivity({
|
|
staffId: staff.id,
|
|
action: "ad_update",
|
|
description: `Updated advertisement #${id} (${image})`,
|
|
targetType: "website_ad",
|
|
targetId: Number(id),
|
|
});
|
|
} catch (err) {
|
|
logger.error("Action failed: updateAd", {
|
|
action: "updateAd",
|
|
id: Number(id),
|
|
error: err instanceof Error ? err.message : "DB error",
|
|
});
|
|
revalidatePath(`/admin/ads/${id}`);
|
|
return;
|
|
}
|
|
redirect("/admin/ads");
|
|
}
|
|
|
|
const deleteAdInput = z.object({
|
|
id: z
|
|
.union([z.string(), z.number(), z.bigint()])
|
|
.transform((v) => BigInt(String(v))),
|
|
});
|
|
|
|
export const deleteAd = adminAction(
|
|
{ permission: PERMS.PAGES_EDIT, schema: deleteAdInput },
|
|
async (ctx) => {
|
|
const id = ctx.data.id;
|
|
try {
|
|
await prisma.websiteAds.delete({ where: { id } });
|
|
} catch {
|
|
throw new ActionError("Advertisement not found");
|
|
}
|
|
await logStaffActivity({
|
|
staffId: Number(ctx.session.user.id),
|
|
action: "ad_delete",
|
|
description: `Deleted advertisement #${id}`,
|
|
targetType: "website_ad",
|
|
targetId: Number(id),
|
|
});
|
|
revalidatePath("/admin/ads");
|
|
return actionOk();
|
|
},
|
|
);
|
|
|
|
/** Legacy form POST delete — kept for compatibility; prefer client deleteAd action. */
|
|
export async function deleteAdForm(formData: FormData): Promise<void> {
|
|
const staff = await requirePermission(PERMS.PAGES_EDIT);
|
|
const id = formPositiveBigInt(formData, "id");
|
|
if (!id) return;
|
|
|
|
try {
|
|
await prisma.websiteAds.delete({ where: { id } });
|
|
await logStaffActivity({
|
|
staffId: staff.id,
|
|
action: "ad_delete",
|
|
description: `Deleted advertisement #${id}`,
|
|
targetType: "website_ad",
|
|
targetId: Number(id),
|
|
});
|
|
} catch (err) {
|
|
logger.error("Action failed: deleteAdForm", {
|
|
action: "deleteAdForm",
|
|
id: Number(id),
|
|
error: err instanceof Error ? err.message : "DB error",
|
|
});
|
|
}
|
|
redirect("/admin/ads");
|
|
}
|