diff --git a/apps/web/app/api/avatar/[uuid]/route.ts b/apps/web/app/api/avatar/[uuid]/route.ts index ccd746d2d6..348bb567ca 100644 --- a/apps/web/app/api/avatar/[uuid]/route.ts +++ b/apps/web/app/api/avatar/[uuid]/route.ts @@ -5,6 +5,7 @@ import { NextResponse } from "next/server"; import { z } from "zod"; import { AVATAR_FALLBACK, WEBAPP_URL } from "@calcom/lib/constants"; +import { convertSvgToPng } from "@calcom/lib/server/imageUtils"; import prisma from "@calcom/prisma"; const querySchema = z.object({ @@ -44,7 +45,19 @@ async function handler(req: NextRequest, { params }: { params: Promise } data: true, }, }); - img = data; + + // Convert SVG to PNG if needed and update the database + if (data.startsWith("data:image/svg+xml;base64,")) { + const pngData = await convertSvgToPng(data); + + await prisma.avatar.update({ + where: { objectKey }, + data: { data: pngData }, + }); + img = pngData; + } else { + img = data; + } } catch (e) { // If anything goes wrong or avatar is not found, use default avatar const url = new URL(AVATAR_FALLBACK, WEBAPP_URL).toString(); diff --git a/apps/web/package.json b/apps/web/package.json index 4036a850fb..1aaf5b9faf 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -139,6 +139,7 @@ "rrule": "^2.7.1", "sanitize-html": "^2.10.0", "schema-dts": "^1.1.0", + "sharp": "0.33.5", "short-uuid": "^4.2.0", "sonner": "^1.7.4", "stripe": "^9.16.0", diff --git a/packages/lib/package.json b/packages/lib/package.json index 4df48f8898..00231f4478 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -27,6 +27,7 @@ "next-collect": "^0.2.1", "next-i18next": "^15.4.2", "rrule": "^2.7.1", + "sharp": "0.33.5", "sonner": "^1.7.4", "tsdav": "2.0.3", "tslog": "^4.9.2", diff --git a/packages/lib/server/avatar.ts b/packages/lib/server/avatar.ts index 48b2cdd463..6f852320e9 100644 --- a/packages/lib/server/avatar.ts +++ b/packages/lib/server/avatar.ts @@ -2,8 +2,11 @@ import { v4 as uuidv4 } from "uuid"; import { prisma } from "@calcom/prisma"; +import { convertSvgToPng } from "./imageUtils"; + export const uploadAvatar = async ({ userId, avatar: data }: { userId: number; avatar: string }) => { const objectKey = uuidv4(); + const processedData = await convertSvgToPng(data); await prisma.avatar.upsert({ where: { @@ -15,12 +18,12 @@ export const uploadAvatar = async ({ userId, avatar: data }: { userId: number; a }, create: { userId: userId, - data, + data: processedData, objectKey, isBanner: false, }, update: { - data, + data: processedData, objectKey, }, }); @@ -38,6 +41,7 @@ export const uploadLogo = async ({ isBanner?: boolean; }): Promise => { const objectKey = uuidv4(); + const processedData = await convertSvgToPng(data); await prisma.avatar.upsert({ where: { @@ -49,12 +53,12 @@ export const uploadLogo = async ({ }, create: { teamId, - data, + data: processedData, objectKey, isBanner, }, update: { - data, + data: processedData, objectKey, }, }); diff --git a/packages/lib/server/imageUtils.ts b/packages/lib/server/imageUtils.ts new file mode 100644 index 0000000000..5254f9fa09 --- /dev/null +++ b/packages/lib/server/imageUtils.ts @@ -0,0 +1,31 @@ +import sharp from "sharp"; + +// Maximum allowed size for SVG data (5MB) +const MAX_SVG_SIZE = 5 * 1024 * 1024; + +/** + * Converts an SVG image to PNG format + * @param data Base64 encoded image data + * @returns Base64 encoded PNG data or a placeholder image if conversion fails + */ +export const convertSvgToPng = async (data: string) => { + if (data.startsWith("data:image/svg+xml;base64,")) { + try { + const base64Data = data.replace(/^data:image\/svg\+xml;base64,/, ""); + const buffer = Buffer.from(base64Data, "base64"); + + // Check if the SVG data exceeds the size limit + if (buffer.length > MAX_SVG_SIZE) { + throw new Error("SVG data exceeds maximum allowed size"); + } + + const pngBuffer = await sharp(buffer).png().toBuffer(); + return `data:image/png;base64,${pngBuffer.toString("base64")}`; + } catch (error) { + console.error("Error converting SVG to PNG", error); + // Return a 1x1 transparent PNG as placeholder + return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; + } + } + return data; +}; diff --git a/yarn.lock b/yarn.lock index 3a299a5591..76651d6976 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3165,6 +3165,7 @@ __metadata: next-collect: ^0.2.1 next-i18next: ^15.4.2 rrule: ^2.7.1 + sharp: 0.33.5 sonner: ^1.7.4 tsdav: 2.0.3 tslog: ^4.9.2 @@ -3951,6 +3952,7 @@ __metadata: rrule: ^2.7.1 sanitize-html: ^2.10.0 schema-dts: ^1.1.0 + sharp: 0.33.5 short-uuid: ^4.2.0 sonner: ^1.7.4 stripe: ^9.16.0 @@ -4480,11 +4482,11 @@ __metadata: linkType: hard "@emnapi/runtime@npm:^1.2.0": - version: 1.3.1 - resolution: "@emnapi/runtime@npm:1.3.1" + version: 1.4.0 + resolution: "@emnapi/runtime@npm:1.4.0" dependencies: tslib: ^2.4.0 - checksum: 9a16ae7905a9c0e8956cf1854ef74e5087fbf36739abdba7aa6b308485aafdc993da07c19d7af104cd5f8e425121120852851bb3a0f78e2160e420a36d47f42f + checksum: 90cca909ceb842e95cb5e16ab64e4c4d8925c6f4ed5527848e53faad8c4e23885e4a001a6099e26a76d099a67758032a9f208b13252176e2b68c860d19f5c1f9 languageName: node linkType: hard @@ -41502,7 +41504,7 @@ __metadata: languageName: node linkType: hard -"sharp@npm:^0.33.5": +"sharp@npm:0.33.5, sharp@npm:^0.33.5": version: 0.33.5 resolution: "sharp@npm:0.33.5" dependencies: