brackt/app/lib/cloudinary.server.ts

94 lines
2.6 KiB
TypeScript
Raw Normal View History

import { v2 as cloudinary } from "cloudinary";
type AvatarTarget = "user" | "team";
interface UploadAvatarImageArgs {
buffer: Buffer;
contentType: string;
target: AvatarTarget;
entityId: string;
notificationUrl: string;
}
function requireCloudinaryConfig() {
const cloudName = process.env.CLOUDINARY_CLOUD_NAME;
const apiKey = process.env.CLOUDINARY_API_KEY;
const apiSecret = process.env.CLOUDINARY_API_SECRET;
if (!cloudName || !apiKey || !apiSecret) {
throw new Error("Cloudinary environment variables are not configured");
}
cloudinary.config({
cloud_name: cloudName,
api_key: apiKey,
api_secret: apiSecret,
secure: true,
});
}
function toDataUri(buffer: Buffer, contentType: string): string {
return `data:${contentType};base64,${buffer.toString("base64")}`;
}
export async function uploadAvatarImage({
buffer,
contentType,
target,
entityId,
notificationUrl,
}: UploadAvatarImageArgs) {
requireCloudinaryConfig();
return await cloudinary.uploader.upload(toDataUri(buffer, contentType), {
resource_type: "image",
folder: "avatars",
public_id: `${target}-${entityId}-${Date.now()}`,
overwrite: true,
moderation: "aws_rek",
notification_url: notificationUrl,
context: {
avatar_target: target,
entity_id: entityId,
},
transformation: [{ width: 256, height: 256, crop: "fill", gravity: "face" }],
});
}
export function cloudinaryPublicIdFromUrl(url: string | null | undefined): string | null {
if (!url) return null;
const match = url.match(/\/avatars\/([^/.]+)/);
return match ? `avatars/${match[1]}` : null;
}
export function buildWebhookUrl(request: Request): string {
const baseUrl = process.env.APP_URL ?? process.env.BETTER_AUTH_URL ?? new URL(request.url).origin;
return new URL("/api/webhooks/cloudinary", baseUrl).toString();
}
export async function deleteCloudinaryImageByUrl(url: string | null | undefined): Promise<void> {
const publicId = cloudinaryPublicIdFromUrl(url);
if (!publicId) return;
requireCloudinaryConfig();
await cloudinary.uploader.destroy(publicId, { resource_type: "image", invalidate: true });
}
export function verifyCloudinaryWebhookSignature({
body,
signature,
timestamp,
}: {
body: string;
signature: string | null;
timestamp: string | null;
}): boolean {
requireCloudinaryConfig();
if (!signature || !timestamp) return false;
const parsedTimestamp = Number(timestamp);
if (!Number.isFinite(parsedTimestamp)) return false;
return cloudinary.utils.verifyNotificationSignature(body, parsedTimestamp, signature);
}