Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,23 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work
go.work.sum
181 changes: 19 additions & 162 deletions app/api/save/route.ts
Original file line number Diff line number Diff line change
@@ -1,157 +1,7 @@
import { FILE_TYPES } from "@/components/studio/export/types";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { useLastSavedTime } from "@/store/use-last-save";
import { diff, Jimp } from 'jimp';
import { revalidatePath } from "next/cache";
import sharp from "sharp";

export const runtime = "nodejs";

const SIMILARITY_THRESHOLD = 70;

async function compareImages(img1Buffer: Buffer, img2Buffer: Buffer): Promise<number> {
try {
const jimage1 = await Jimp.read(img1Buffer);
const jimage2 = await Jimp.read(img2Buffer);

const { percent } = diff(jimage1, jimage2, 0.1);
const howSimilarImagesAre = 100 - (percent * 100);

return howSimilarImagesAre;
} catch (error) {
console.error('compareImages error:', error);
throw error;
}
}

async function checkImageSimilarity(newImageBuffer: Buffer, viz: "PUBLIC" | "PRIVATE"): Promise<{ isSimilar: boolean; whichImage?: string }> {
const existingImages = await prisma.userImage.findMany({
where: {
visibility: viz,
},
});

for (const image of existingImages) {
try {
const buffer = await fetch(image.cloudflareUrl).then((res) => res.arrayBuffer());
const existingImageBuffer = Buffer.from(buffer);

const similarityPercentage = await compareImages(newImageBuffer, existingImageBuffer);

if (similarityPercentage > SIMILARITY_THRESHOLD) return { isSimilar: true, whichImage: image.id };
else continue;
} catch (error) {
console.error(error);
throw new Error("Failed to compare images");
}
}

return { isSimilar: false };
}

export type UploadImageNonExisting = {
imageUrl: string;
identifier: string;
}

export type UploadImageExisting = {
id: string;
cloudflareUrl: string;
identifier: string;
isOwner: boolean;
}

async function uploadImageToCloudflare(file: FormData, userId: string, viz: "PUBLIC" | "PRIVATE"): Promise<UploadImageNonExisting | UploadImageExisting> {
const identifier = file.get("identifier") as string;
const imageFile = file.get("file") as File;
file.delete("identifier");

const arrayBuffer = await imageFile.arrayBuffer();
let fileBuffer = Buffer.from(arrayBuffer);

const fileType = imageFile.type.split('/')[1].toUpperCase();

if (!FILE_TYPES.includes(fileType as any)) {
fileBuffer = await sharp(fileBuffer)
.png()
.toBuffer();
}

try {
const { isSimilar, whichImage } = await checkImageSimilarity(fileBuffer, viz);

if (isSimilar) {
const image = await prisma.userImage.findUnique({
where: { id: whichImage },
});

if (!image) {
throw new Error("Image not found");
}

return {
...image,
identifier,
isOwner: image.userId === userId,
}
}

const processedFormData = new FormData();
processedFormData.append("file", new Blob([fileBuffer]), `image.${fileType.toLowerCase()}`);
processedFormData.append("requireSignedURLs", "false");

const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDFLARE_ACCOUNT_ID}/images/v1`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.CLOUDFLARE_BEARER_TOKEN}`,
},
body: processedFormData,
}
);

if (!response.ok) {
throw new Error(response.statusText);
}

const result = await response.json();
return {
imageUrl: result.result.variants[0],
identifier,
};
} catch (error) {
console.error(error);
throw new Error(error instanceof Error ? error.message : "Failed to upload image to Cloudflare");
}
}

async function saveOrUpdateUserImage(userId: string, imageUrl: string, identifier: string, visibility: "PUBLIC" | "PRIVATE"): Promise<string> {
const existingImage = await prisma.userImage.findFirst({
where: { userId, identifier },
});

if (existingImage) {
await prisma.userImage.update({
where: { id: existingImage.id },
data: { cloudflareUrl: imageUrl, updatedAt: new Date(), visibility },
});
return "Image updated successfully";
} else {
await prisma.userImage.create({
data: {
userId,
cloudflareUrl: imageUrl,
visibility,
identifier,
createdAt: new Date(),
updatedAt: new Date(),
},
});
return "Image saved successfully";
}
}

export async function POST(request: Request) {
try {
Expand All @@ -162,7 +12,7 @@ export async function POST(request: Request) {
return new Response("Unauthorized: User is not logged in", { status: 401 });
}

const session = await prisma.session.findFirstOrThrow({
const session = await prisma.session.findFirst({
where: { userId },
});

Expand All @@ -172,23 +22,30 @@ export async function POST(request: Request) {

const formData = await request.formData();
const visibility = formData.get("visibility") as "PUBLIC" | "PRIVATE";
if (!visibility || !["PUBLIC", "PRIVATE"].includes(visibility)) throw new Error("Visibility must be provided");
if (!visibility || !["PUBLIC", "PRIVATE"].includes(visibility)) {
return new Response("Invalid visibility. Must be PUBLIC or PRIVATE", { status: 400 });
}
formData.append("userId", userId);

const maybeExists = await uploadImageToCloudflare(formData, userId, visibility);
const goResponse = await fetch(`${process.env.BACKEND_API}/api/v1/save`, {
method: "POST",
body: formData,
});

if (!goResponse.ok) {
throw new Error(await goResponse.text());
}

if ('id' in maybeExists) {
const { id, cloudflareUrl, identifier, isOwner } = maybeExists;
// 204 status means duplicate image
return Response.json({ id, cloudflareUrl, identifier, isOwner, status: 204, type: "DUPLICATE", visibility, message: "Design already exists" });
} else {
const { imageUrl, identifier } = maybeExists;
const message = await saveOrUpdateUserImage(userId, imageUrl, identifier, visibility);
const result = await goResponse.json();

if (result.type === "NEW_SAVE") {
useLastSavedTime.getState().setLastSavedTime(new Date());
revalidatePath("/community");
revalidatePath(`/${userData.user.name}/profile`);
revalidatePath(`/${encodeURIComponent(userData?.user?.name??"")}/profile`);
return Response.json({ message, status: 200, type: "NEW_SAVE", visibility });
revalidatePath(`/${encodeURIComponent(userData?.user?.name ?? "")}/profile`);
}

return Response.json(result);
} catch (error: any) {
console.error(error);
return new Response(error.message, { status: 500 });
Expand Down
41 changes: 9 additions & 32 deletions app/api/users/route.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
import { auth, signOut } from "@/lib/auth";
import { prisma } from "@/lib/prisma";

async function deleteImageFromCloudflare(imageId: string): Promise<void> {
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDFLARE_ACCOUNT_ID}/images/v1/${imageId}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.CLOUDFLARE_BEARER_TOKEN}`,
},
}
);

if (!response.ok) {
throw new Error(`Failed to delete image from Cloudflare: ${response.statusText}`);
}
}

export async function DELETE(request: Request) {
try {
const userData = await auth();
Expand All @@ -29,29 +13,22 @@ export async function DELETE(request: Request) {

if (authUser?.id !== userId) return Response.json({ error: "Unauthorized: Invalid userId" }, { status: 403 });

const user = await prisma.user.findUnique({
where: { id: userId },
include: {
UserImage: true,
const response = await fetch(`${process.env.BACKEND_API}/api/v1/users/${userId}`, {
method: "DELETE",
headers: {
"X-Authenticated-User-ID": userId,
},
});

if (!user) return Response.json({ error: "User not found" }, { status: 404 });

const deleteImagePromises = user.UserImage.map(async (image) => {
const cloudflareId = image.cloudflareUrl.split("/")[4];
if (cloudflareId) await deleteImageFromCloudflare(cloudflareId);
});

await Promise.all(deleteImagePromises);
if (!response.ok) {
throw new Error(`Failed to delete user`);
}

await signOut({ redirect: false });

await prisma.user.delete({ where: { id: userId } });

return Response.json({ message: "Account successfully deleted" }, { status: 200 });
return new Response(JSON.stringify({ message: "Account successfully deleted" }), { status: 200 });
} catch (error: any) {
console.error("Error deleting account:", error.message);
return Response.json({ error: error.message }, { status: 500 });
return new Response(JSON.stringify({ error: error.message }), { status: 500 });
}
}
Loading