import { deleteCloudinaryImageByUrl, verifyCloudinaryWebhookSignature, } from "~/lib/cloudinary.server"; import { logger } from "~/lib/logger"; import { findTeamById, updateTeam } from "~/models/team"; import { findUserById, updateUser } from "~/models/user"; import type { Route } from "./+types/api.webhooks.cloudinary"; type CloudinaryContext = Record | string | undefined; interface CloudinaryWebhookPayload { secure_url?: string; moderation?: Array<{ status?: string }>; moderation_status?: string; info?: { moderation?: { status?: string } | Array<{ status?: string }>; }; context?: CloudinaryContext | { custom?: Record }; } function getContextValue(context: CloudinaryWebhookPayload["context"], key: string): string | null { if (!context) return null; if (typeof context === "string") { const pairs = context.split("|").map((part) => part.split("=")); const found = pairs.find(([pairKey]) => pairKey === key); return found?.[1] ?? null; } const custom = "custom" in context && context.custom && typeof context.custom === "object" ? context.custom : context; const value = (custom as Record)[key]; return typeof value === "string" ? value : null; } function moderationStatus(payload: CloudinaryWebhookPayload): string | null { const direct = payload.moderation?.[0]?.status ?? payload.moderation_status; if (direct) return direct; const infoModeration = payload.info?.moderation; if (Array.isArray(infoModeration)) return infoModeration[0]?.status ?? null; return infoModeration?.status ?? null; } export async function action({ request }: Route.ActionArgs) { const body = await request.text(); const signature = request.headers.get("X-Cld-Signature"); const timestamp = request.headers.get("X-Cld-Timestamp"); if (!verifyCloudinaryWebhookSignature({ body, signature, timestamp })) { return Response.json({ error: "Invalid signature" }, { status: 401 }); } let payload: CloudinaryWebhookPayload; try { payload = JSON.parse(body) as CloudinaryWebhookPayload; } catch { return Response.json({ error: "Invalid JSON" }, { status: 400 }); } const status = moderationStatus(payload); if (status !== "approved") { logger.info("Cloudinary avatar not approved:", { status }); return Response.json({ status: "ignored" }); } if (!payload.secure_url) { return Response.json({ error: "Missing secure_url" }, { status: 400 }); } const target = getContextValue(payload.context, "avatar_target"); const entityId = getContextValue(payload.context, "entity_id"); if (!target || !entityId) { return Response.json({ error: "Missing avatar context" }, { status: 400 }); } if (target === "user") { const existingUser = await findUserById(entityId); await updateUser(entityId, { customAvatarUrl: payload.secure_url, avatarType: "uploaded", }); if (existingUser?.customAvatarUrl && existingUser.customAvatarUrl !== payload.secure_url) { deleteCloudinaryImageByUrl(existingUser.customAvatarUrl).catch((error) => { logger.error("Failed to delete replaced user avatar from Cloudinary:", error); }); } return Response.json({ status: "updated" }); } if (target === "team") { const existingTeam = await findTeamById(entityId); await updateTeam(entityId, { logoUrl: payload.secure_url, avatarType: "uploaded", }); if (existingTeam?.logoUrl && existingTeam.logoUrl !== payload.secure_url) { deleteCloudinaryImageByUrl(existingTeam.logoUrl).catch((error) => { logger.error("Failed to delete replaced team avatar from Cloudinary:", error); }); } return Response.json({ status: "updated" }); } return Response.json({ error: "Unknown avatar target" }, { status: 400 }); }