brackt/app/routes/api.webhooks.cloudinary.ts

110 lines
3.8 KiB
TypeScript
Raw Permalink Normal View History

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, unknown> | 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<string, unknown> };
}
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<string, unknown>)[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 });
}