diff --git a/drizzle/0002_serious_green_goblin.sql b/drizzle/0002_serious_green_goblin.sql new file mode 100644 index 0000000..f7d6087 --- /dev/null +++ b/drizzle/0002_serious_green_goblin.sql @@ -0,0 +1,2 @@ +ALTER TABLE "projects" ADD COLUMN "is_active" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "projects" ADD COLUMN "preview_url" text; \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..d6aea8d --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,231 @@ +{ + "id": "97be3edd-38c0-499d-accc-13797e7318aa", + "prevId": "b6897b47-e0f0-48c5-8917-696944c8524b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "object": { + "name": "object", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "preview_url": { + "name": "preview_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_user_id_users_user_id_fk": { + "name": "projects_user_id_users_user_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.uploads": { + "name": "uploads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "uploads_projectId_projects_project_id_fk": { + "name": "uploads_projectId_projects_project_id_fk", + "tableFrom": "uploads", + "tableTo": "projects", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "project_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paid_status": { + "name": "paid_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_in": { + "name": "expires_in", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index fb24b31..9a3ba45 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1737876981144, "tag": "0001_shallow_umar", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1738213216210, + "tag": "0002_serious_green_goblin", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/api/auth/auth.controller.ts b/src/api/auth/auth.controller.ts index f6bffcf..ea1e26d 100644 --- a/src/api/auth/auth.controller.ts +++ b/src/api/auth/auth.controller.ts @@ -97,9 +97,8 @@ export const generateToken = async (context: any) => { maxAge: 7 * 24 * 60 * 60, // 7 days in seconds }); - return { status: 200, message: "Token generated successfully", token: accessToken }; + return { status: 200, message: "Token generated successfully", token: accessToken, userId: user?.id }; } - return { status: 500, message: "An error occurred while storing the refresh token" }; } else { @@ -115,7 +114,7 @@ export const verifyToken = async (context: any) => { try { // if token is in cookie, verify it // const token_cookie = context.cookie.access_token.value; - const verify = await verifyAuth(context.cookie); + const verify = await verifyAuth(context.cookie, context.request); return verify; diff --git a/src/api/project/project.controller.ts b/src/api/project/project.controller.ts index 9cae8b8..c5ca65c 100644 --- a/src/api/project/project.controller.ts +++ b/src/api/project/project.controller.ts @@ -1,37 +1,116 @@ +import { eq } from "drizzle-orm"; +import { db } from "../../db"; +import { projects } from "../../db/schema"; +import { createEmptyProject } from "../../helper/projects/createProject"; +import { createBucket } from "../../helper/upload/createBucket"; +import { removeBucket } from "../../helper/upload/removeBucket"; + export const getEachProjects = async (id: string) => { try { - console.log(id); - return { id: id } + const project = await db.select().from(projects).where(eq(projects.id, id)).limit(1); + if (project.length === 0) { + return { status: 404, message: "Project not found" }; + } + return { status: 200, message: "Project fetched successfully", data: project[0] }; } catch (error: any) { - console.log(error.msg) - return { status: 500, message: "An error occurred while fetching projects" } + console.log(error.message); + return { status: 500, message: "An error occurred while fetching projects" }; } -} +}; export const getAllProjects = async (userId: string) => { try { - // this will return all the project associated with the user - } catch (error: any) { - console.log(error.msg); - return { status: 500, message: "An error occurred while fetching projects" } - } -} + // Fetch all projects for the given user + const allProjects = await db.select().from(projects).where(eq(projects.userId, userId)); -export const updateProject = async (id: string, data: any) => { + if (allProjects.length === 0) { + return { status: 404, message: "No projects found" }; + } + + // Filter out projects where `object` is empty (null or an empty object) + const validProjects = []; + for (const project of allProjects) { + if (!project.object || Object.keys(project.object).length === 0) { + // Remove the project from the database + await db.delete(projects).where(eq(projects.id, project.id)); + + // Remove the associated MinIO bucket + await removeBucket(project.id); + } else { + validProjects.push(project); + } + } + + if (validProjects.length === 0) { + return { status: 404, message: "No projects found" }; + } + return { status: 200, message: "Projects fetched successfully", data: validProjects }; + } catch (error: any) { + console.log(error.message); + return { status: 500, message: "An error occurred while fetching projects" }; + } +}; + +export const createProject = async (userId: string) => { try { + const { id } = await createEmptyProject(userId); + const bucket = await createBucket(id); + return { status: 200, message: "New project created and file uploaded successfully", data: { id, bucketName: bucket } }; } catch (error: any) { - console.log(error.msg); - return { status: 500, message: "An error occurred while updating projects" } + console.log(error.message); + return { status: 500, message: "An error occurred while creating projects" } } } +export const updateProject = async (id: string, body: any) => { + try { + // 1. Validate if project exists + const existingProject = await db.select().from(projects).where(eq(projects.id, id)).limit(1); + if (existingProject.length === 0) { + return { status: 404, message: "Project not found" }; + } + + const { object, name, description, preview_url } = body; + // The preview_url will come from client-side as well, where before updating the project a project capture will be taken and uploaded to the bucket. than the url will be sent to the server.And rest of them are normal process + + const updatedProject = await db.update(projects).set({ + object, + name, + description, + preview_url + }).where(eq(projects.id, id)).returning(); + + if (updatedProject.length === 0) { + return { status: 500, message: "Failed to update the project" }; + } + return { status: 200, message: "Project updated successfully", data: updatedProject[0] }; + } catch (error: any) { + console.log("Error updating project:", error.message || error.toString()); + return { status: 500, message: "An error occurred while updating the project" }; + } +}; + + export const deleteProject = async (id: string) => { try { + const deleteProject = await db.delete(projects).where(eq(projects.id, id)).returning({ id: projects.id }); + if (deleteProject.length === 0) { + return { status: 404, message: "Project not found" }; + } + const projectId = deleteProject[0].id; + + const bucketDeletionResult = await removeBucket(projectId); + + if (bucketDeletionResult.status !== 200) { + return { status: bucketDeletionResult.status, message: `Error deleting bucket: ${bucketDeletionResult.message}` }; + } + return { status: 200, message: "Project and associated bucket deleted successfully" }; } catch (error: any) { - console.log(error.msg); - return { status: 500, message: "An error occurred while deleting projects" } + console.log("Error in deleteProject:", error.message || error.toString()); + return { status: 500, message: "An error occurred while deleting the project" }; } -} +}; + diff --git a/src/api/project/project.route.ts b/src/api/project/project.route.ts index 1ebc54c..2f51696 100644 --- a/src/api/project/project.route.ts +++ b/src/api/project/project.route.ts @@ -1,5 +1,5 @@ import { Elysia, t } from "elysia"; -import { deleteProject, getAllProjects, getEachProjects, updateProject } from "./project.controller"; +import { createProject, deleteProject, getAllProjects, getEachProjects, updateProject } from "./project.controller"; import { verifyAuth } from "../../middlewares/auth.middlewares"; export const projectRoutes = new Elysia({ @@ -8,29 +8,43 @@ export const projectRoutes = new Elysia({ detail: { description: "Routes for managing projects", } -}).derive(({ cookie }) => { verifyAuth(cookie) }); +}).derive(async ({ cookie, request }) => { + const authData = await verifyAuth(cookie, request); + if (authData.status !== 200) { + return { authData }; + } + return { userId: authData.userId }; // Inject into context +}); -projectRoutes.get("/each/:id", ({ params }) => getEachProjects(params.id), { +projectRoutes.get("/each/:project_id", ({ params: { project_id } }) => getEachProjects(project_id), { params: t.Object({ - id: t.String() + project_id: t.String() }) }); -projectRoutes.get("/:userId", ({ params }) => getAllProjects(params.userId), { - params: t.Object({ +projectRoutes.get("/", ({ userId }: any) => getAllProjects(userId), { + body: t.Object({ userId: t.String() }) }); -projectRoutes.put("/update/:id", ({ request, params }) => updateProject(params.id, request.body), { +projectRoutes.post("/create", ({ userId }: any) => createProject(userId)); + +projectRoutes.put("/update/:project_id", ({ request, params: { project_id } }) => updateProject(project_id, request.body), { params: t.Object({ - id: t.String() + project_id: t.String() + }), + body: t.Object({ + object: t.Record(t.String(), t.Any()), // Allows any JSON object + name: t.String({ minLength: 1 }), + description: t.String({ minLength: 1 }), + preview_url: t.String(), }) }); -projectRoutes.delete("/delete/:id", ({ params }) => deleteProject(params.id), { +projectRoutes.delete("/delete/:project_id", ({ params: { project_id } }) => deleteProject(project_id), { params: t.Object({ - id: t.String() + project_id: t.String() }) }); diff --git a/src/api/upload/upload.controller.ts b/src/api/upload/upload.controller.ts index d78d6c4..ce7728c 100644 --- a/src/api/upload/upload.controller.ts +++ b/src/api/upload/upload.controller.ts @@ -1,86 +1,79 @@ import { eq } from "drizzle-orm"; import { db } from "../../db"; import { uploads } from "../../db/schema"; -import { createEmptyProject } from "../../helper/projects/createProject"; -import { createBucket } from "../../helper/upload/createBucket"; import { uploadToMinio } from "../../helper/upload/uploadToMinio"; import { removeFromMinio } from "../../helper/upload/removeFromMinio"; -export const uploadPhoto = async (req: Request) => { +export const uploadPhoto = async (formData: FormData, userId: string) => { try { - // Use the formData API to extract the data from the request - const formData = await req.formData(); + // Validate userId + if (!userId || typeof userId !== "string") { + return { status: 400, message: "Invalid user ID" }; + } + // Extract form data const projectId = formData.get("id"); - const userId = formData.get("userId"); const file = formData.get("file"); - // Validate the file input - if (!file || !(file instanceof File)) { - throw new Error("Invalid or missing file in form data"); + // Validate projectId + if (!projectId || typeof projectId !== "string") { + return { status: 400, message: "Invalid project ID" }; } - if (userId) { - if (projectId) { - const urlLink = await uploadToMinio(file, projectId, file.name); - - const saveFile = await db - .insert(uploads) - .values({ filename: file.name, url: urlLink?.url, projectId }) - .returning(); - - return { status: 200, data: { msg: "File uploaded successfully", data: saveFile } }; - } else { - - const newProjectId = await createEmptyProject(userId.toString()); - const bucket = await createBucket(newProjectId?.id); - const urlLink = await uploadToMinio(file, bucket, file.name); - - const saveFile = await db - .insert(uploads) - .values({ filename: file.name, url: urlLink?.url, projectId: newProjectId?.id }) - .returning(); - - return { status: 200, data: { msg: "New project created and file uploaded successfully", data: saveFile } }; - } - - } else { - return { status: 404, message: "User not found" }; + // Validate file input + if (!file || !(file instanceof File) || !file.name) { + return { status: 400, message: "Invalid or missing file" }; } - } catch (error) { + + // Upload file + const urlLink = await uploadToMinio(file, projectId, file.name); + if (!urlLink || !urlLink.url) { + return { status: 500, message: "File upload failed" }; + } + + // Save file info in DB + const saveFile = await db.insert(uploads).values({ + filename: file.name, + url: urlLink.url, + projectId + }).returning(); + + return { status: 200, message: "File uploaded successfully", data: saveFile }; + + } catch (error: any) { console.error("Error processing file:", error); return { status: 500, message: "An error occurred while uploading the photo" }; } }; -export const deletePhoto = async (id: string) => { +export const deletePhoto = async (url: string) => { try { - if (!id) { - throw new Error("Invalid or missing file ID"); + if (!url) { + return { status: 404, message: "File ID is missing" } } const deleteFile = await db .delete(uploads) - .where(eq(uploads.id, id)) + .where(eq(uploads.url, url)) .returning(); // Ensure there's a file to delete if (!deleteFile || deleteFile.length === 0) { - throw new Error("File not found or already deleted"); + return { status: 404, message: "File not found" }; } const { projectId, filename } = deleteFile[0]; // Ensure projectId and filename are valid if (!projectId || !filename) { - throw new Error("Project ID or filename is missing"); + return { status: 400, message: "Invalid project ID or filename" }; } const minioRemove = await removeFromMinio(projectId, filename); return { status: 200, message: minioRemove.msg }; - } catch (error) { + } catch (error: any) { console.error("Error processing file:", error); return { status: 500, message: `An error occurred while deleting the photo: ${error.message}` }; } @@ -90,14 +83,15 @@ export const getAllPhoto = async (id: string) => { try { // project id if (!id) { - throw new Error("Invalid or missing project ID"); + return { status: 404, message: "Project ID is missing" } } const getAllPhoto = await db.select().from(uploads).where(eq(uploads.projectId, id)); if (getAllPhoto.length === 0) { - return { status: 200, data: { msg: "No photos found for the given project ID", data: [] } } + return { status: 200, message: "No photos found for the given project ID", data: [] } } - return { status: 200, data: { msg: "Photos retrieved successfully", data: getAllPhoto } }; - } catch (error) { + return { status: 200, message: "All photos retrieved successfully", data: getAllPhoto }; + + } catch (error: any) { console.log(`Error getting photos: ${error.message}`); return { status: 500, message: "An error occurred while getting the photos" } } diff --git a/src/api/upload/upload.route.ts b/src/api/upload/upload.route.ts index 474801b..24ec60c 100644 --- a/src/api/upload/upload.route.ts +++ b/src/api/upload/upload.route.ts @@ -1,5 +1,6 @@ -import { Elysia } from "elysia"; +import { Elysia, t } from "elysia"; import { deletePhoto, getAllPhoto, uploadPhoto } from "./upload.controller"; +import { verifyAuth } from "../../middlewares/auth.middlewares"; export const uploadRoutes = new Elysia({ prefix: "/uploads", @@ -7,10 +8,33 @@ export const uploadRoutes = new Elysia({ detail: { description: "Routes for uploading and managing photos", } +}).derive(async ({ cookie, request }) => { + const authData = await verifyAuth(cookie, request); + if (authData.status !== 200) { + return { authData }; + } + return { userId: authData.userId }; // Inject into context }); -uploadRoutes.post("/add", async ({ request }) => uploadPhoto(request)); +uploadRoutes.post("/add", async ({ request, userId }) => { + const user_id: String | any = userId; + const formData = await request.formData(); + return uploadPhoto(formData, user_id); +}, { + body: t.Object({ + file: t.File(), + id: t.String(), + }) +}); -uploadRoutes.delete("/delete/:id", async ({ params }) => deletePhoto(params.id)); +uploadRoutes.delete("/delete/:uploads_url", async ({ params: { uploads_url } }) => deletePhoto(uploads_url), { + params: t.Object({ + uploads_url: t.String(), + }) +}); -uploadRoutes.get("/get/:id", async ({ params }) => getAllPhoto(params.id)); \ No newline at end of file +uploadRoutes.get("/getAll/:id", async ({ params: { id } }) => getAllPhoto(id), { + params: t.Object({ + id: t.String() + }) +}); \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 06247e6..f539a4a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,9 +5,15 @@ import { ENV } from "./config/env"; import cors from "@elysiajs/cors"; import { api } from "./api"; +const allowedOrigins = [ + "http://localhost:5175", + "http://localhost:5173", + "https://your-production-site.com", +]; + const app = new Elysia() .use(cors({ - origin: "http://localhost:5175", + origin: allowedOrigins, credentials: true, })) .use(swagger({ diff --git a/src/db/schema.ts b/src/db/schema.ts index baee876..2bffc72 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,4 +1,4 @@ -import { json, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { boolean, json, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; export const users = pgTable("users", { id: text("user_id").primaryKey().notNull(), @@ -17,6 +17,8 @@ export const projects = pgTable("projects", { object: json(), name: text("name"), description: text("description"), + is_public: boolean("is_active").notNull().default(false), + preview_url: text("preview_url"), created_at: timestamp("created_at").defaultNow(), updated_at: timestamp("updated_at").defaultNow(), }); diff --git a/src/helper/auth/auth.helper.ts b/src/helper/auth/auth.helper.ts index 6560f23..5a08fa5 100644 --- a/src/helper/auth/auth.helper.ts +++ b/src/helper/auth/auth.helper.ts @@ -14,7 +14,12 @@ type User = { export const checkUserInDB = async (id: string) => { try { const user = await db.select().from(users).where(eq(users.id, id)); - return { status: 200, found: user?.length > 0 }; + if (user.length > 0) { + return { found: true, id: user[0]?.id }; + } + else { + return { found: false }; + } } catch (error: any) { console.error("Error in checkUserInDB:", error.message || error.toString()); return { status: 500, message: `An error occurred while checking the user in DB` }; diff --git a/src/helper/upload/removeBucket.ts b/src/helper/upload/removeBucket.ts new file mode 100644 index 0000000..4b875b4 --- /dev/null +++ b/src/helper/upload/removeBucket.ts @@ -0,0 +1,30 @@ +import { minioClient } from "../../config/minioClient"; + +export const removeBucket = async (bucketName: string) => { + try { + // Check if the bucket exists before proceeding + const bucketExists = await minioClient.bucketExists(bucketName); + if (!bucketExists) { + return { status: 404, message: `Bucket ${bucketName} does not exist` }; + } + + // List objects in the bucket, which returns a stream + const objects = minioClient.listObjects(bucketName); + + // Iterate over the stream of objects using 'for await...of' + for await (const obj of objects) { + await minioClient.removeObject(bucketName, obj.name); + console.log(`Removed object: ${obj.name}`); + } + + // Now remove the bucket after clearing all objects + await minioClient.removeBucket(bucketName); + + return { status: 200, message: `Bucket ${bucketName} and its data removed successfully` }; + } catch (error: any) { + console.log(`Error removing bucket ${bucketName}: ${error.message}`); + return { status: 500, message: `Error removing bucket ${bucketName}: ${error.message}` }; + } +}; + + diff --git a/src/helper/upload/removeFromMinio.ts b/src/helper/upload/removeFromMinio.ts index 069e712..615a6f9 100644 --- a/src/helper/upload/removeFromMinio.ts +++ b/src/helper/upload/removeFromMinio.ts @@ -9,7 +9,7 @@ export const removeFromMinio = async (bucketName: string, objectName: string): P // Remove the object from MinIO await minioClient.removeObject(bucketName, objectName); return { msg: `Successfully removed ${objectName}` }; - } catch (error) { + } catch (error: any) { console.error("Error removing object from MinIO:", error); throw new Error(`Failed to remove ${objectName} from bucket ${bucketName}: ${error.message}`); } diff --git a/src/middlewares/auth.middlewares.ts b/src/middlewares/auth.middlewares.ts index 4e38ef0..d7c7664 100644 --- a/src/middlewares/auth.middlewares.ts +++ b/src/middlewares/auth.middlewares.ts @@ -5,7 +5,7 @@ import { users } from "../db/schema"; import { db } from "../db"; import { eq } from "drizzle-orm"; -export const verifyAuth = async (cookie: any) => { +export const verifyAuth = async (cookie: any, request: Request) => { try { const access_cookie = cookie?.access_token?.value; const refresh_cookie = cookie?.refresh_token?.value; @@ -16,7 +16,7 @@ export const verifyAuth = async (cookie: any) => { // Query the user from the database const findUser = await db.select().from(users).where(eq(users.id, verify_cookie.userId)); if (findUser.length > 0) { - return { status: 200, message: "Token verified successfully" }; + return { status: 200, message: "Token verified successfully", userId: findUser[0].id }; } else { throw { status: 401, message: "Unauthorized" }; @@ -45,7 +45,7 @@ export const verifyAuth = async (cookie: any) => { maxAge: 3 * 60 * 60, // 3 hours in seconds }); - return { status: 200, message: "Token verified successfully", token: accessToken }; + return { status: 200, message: "Token verified successfully", token: accessToken, userId: findUser[0].id }; } }