diff --git a/.gitignore b/.gitignore index 87e5610..996fc0e 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ yarn-debug.log* yarn-error.log* # local env files +.env .env.local .env.development.local .env.test.local diff --git a/bun.lockb b/bun.lockb new file mode 100644 index 0000000..a4ac935 Binary files /dev/null and b/bun.lockb differ diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..5f6e03a --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'drizzle-kit'; +import { ENV } from './src/config/env'; + +export default defineConfig({ + out: './drizzle', + schema: './src/db/schema.ts', + dialect: 'postgresql', + dbCredentials: { + url: ENV.DATABASE_URL!, + }, +}); diff --git a/drizzle/0000_eager_marvel_zombies.sql b/drizzle/0000_eager_marvel_zombies.sql new file mode 100644 index 0000000..6cb2781 --- /dev/null +++ b/drizzle/0000_eager_marvel_zombies.sql @@ -0,0 +1,27 @@ +CREATE TABLE "projects" ( + "project_id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" text, + "object" json, + "name" text, + "description" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "uploads" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "filename" text NOT NULL, + "url" text NOT NULL, + "projectId" uuid, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "users" ( + "user_id" text PRIMARY KEY NOT NULL, + "paid_status" text NOT NULL, + "expires_in" timestamp NOT NULL +); +--> statement-breakpoint +ALTER TABLE "projects" ADD CONSTRAINT "projects_user_id_users_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("user_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "uploads" ADD CONSTRAINT "uploads_projectId_projects_project_id_fk" FOREIGN KEY ("projectId") REFERENCES "public"."projects"("project_id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/0001_useful_nighthawk.sql b/drizzle/0001_useful_nighthawk.sql new file mode 100644 index 0000000..400f70a --- /dev/null +++ b/drizzle/0001_useful_nighthawk.sql @@ -0,0 +1,2 @@ +ALTER TABLE "users" ALTER COLUMN "paid_status" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "users" ALTER COLUMN "expires_in" DROP NOT NULL; \ No newline at end of file diff --git a/drizzle/0002_broad_eternity.sql b/drizzle/0002_broad_eternity.sql new file mode 100644 index 0000000..46be296 --- /dev/null +++ b/drizzle/0002_broad_eternity.sql @@ -0,0 +1 @@ +ALTER TABLE "users" ALTER COLUMN "expires_in" SET DATA TYPE text; \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..405a5c4 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,188 @@ +{ + "id": "7a9f9e79-63fc-4d2b-8b25-616f96161308", + "prevId": "00000000-0000-0000-0000-000000000000", + "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 + }, + "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 + }, + "paid_status": { + "name": "paid_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_in": { + "name": "expires_in", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "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/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..4d52a9a --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,188 @@ +{ + "id": "5443181c-129a-488a-9001-d65b03b0119f", + "prevId": "7a9f9e79-63fc-4d2b-8b25-616f96161308", + "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 + }, + "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 + }, + "paid_status": { + "name": "paid_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_in": { + "name": "expires_in", + "type": "timestamp", + "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/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..5ae009a --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,188 @@ +{ + "id": "88a4eebc-30cb-457f-9fc1-c6bbff735742", + "prevId": "5443181c-129a-488a-9001-d65b03b0119f", + "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 + }, + "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 + }, + "paid_status": { + "name": "paid_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_in": { + "name": "expires_in", + "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 new file mode 100644 index 0000000..35f8261 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,27 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1737800628545, + "tag": "0000_eager_marvel_zombies", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1737804031716, + "tag": "0001_useful_nighthawk", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1737806743289, + "tag": "0002_broad_eternity", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 118fb4e..7529ed0 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,29 @@ "version": "1.0.50", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "bun run --watch src/index.ts" + "db:studio": "drizzle-kit studio", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:push": "drizzle-kit push:pg", + "dev": "bun run --watch src/app.ts" }, "dependencies": { - "elysia": "latest" + "@clerk/backend": "^1.23.7", + "@elysiajs/cors": "^1.2.0", + "@elysiajs/swagger": "^1.2.0", + "dotenv": "^16.4.7", + "drizzle-orm": "^0.38.4", + "elysia": "latest", + "jose": "^5.9.6", + "minio": "^8.0.3", + "pg": "^8.13.1", + "postgres": "^3.4.5" }, "devDependencies": { - "bun-types": "latest" + "@types/pg": "^8.11.10", + "bun-types": "latest", + "drizzle-kit": "^0.30.2", + "tsx": "^4.19.2" }, - "module": "src/index.js" + "module": "src/app.js" } \ No newline at end of file diff --git a/src/api/auth/auth.controller.ts b/src/api/auth/auth.controller.ts new file mode 100644 index 0000000..5f5476c --- /dev/null +++ b/src/api/auth/auth.controller.ts @@ -0,0 +1,68 @@ +import { createClerkClient } from "@clerk/backend"; +import { ENV } from "../../config/env" +import { users } from "../../db/schema"; +import { db } from "../../db"; +import { eq } from "drizzle-orm"; + +// Initialize Clerk with your API key +const clerk = createClerkClient({ secretKey: ENV.CLERK_SECRET_KEY }); + +export const getUserData = async (userId: string) => { + try { + const [user, checkInDB] = await Promise.all([ + clerk.users.getUser(userId), + checkUserInDB(userId) + ]); + + if (user && !checkInDB.found) { + const userData = await createUser(user.id); + return { status: 200, message: "User retrieved successfully", data: userData }; + } + if (user && checkInDB.found) { + return { status: 200, message: "User retrieved successfully", data: checkInDB }; + } + if (!user) { + return { status: 404, message: "User not found" }; + } + } catch (error: any) { + console.error("Error in getUserData:", error.message || error.toString()); + return { status: 500, message: `An error occurred while getting the 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 }; + } 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` }; + } +}; + +export const createUser = async (id: string) => { + try { + const [saveUser] = await db.insert(users).values({ id }).returning({ insertedId: users.id }); + + if (!saveUser || !saveUser.insertedId) { + throw new Error("Failed to create user or missing insertedId"); + } + + return { status: 200, message: "User created successfully", data: saveUser.insertedId }; + } catch (error: any) { + console.error("Error in createUser:", error.message || error.toString()); + return { status: 500, message: `An error occurred while creating the user` }; + } +}; + +export const updateUser = async (id: string, body) => { + try { + const updateUserData = await db.update(users).set({ paid_status: body?.paid_status, expires_in: body?.package_expire_date }).where(eq(users.id, id)).returning({ updatedId: users.id }); + + return { status: 200, message: "User updated successfully", updateUserData }; + + } catch (error: any) { + console.error("Error in updateUser:", error.message || error.toString()); + return { status: 500, message: `An error occurred while updating the user` }; + } +} \ No newline at end of file diff --git a/src/api/auth/auth.route.ts b/src/api/auth/auth.route.ts new file mode 100644 index 0000000..41db313 --- /dev/null +++ b/src/api/auth/auth.route.ts @@ -0,0 +1,14 @@ +import Elysia from "elysia"; +import { getUserData, updateUser } from "./auth.controller"; + +export const authRoute = new Elysia({ + prefix: "/auth", + tags: ["Auth"], + detail: { + description: "Routes for managing users", + } +}) + +authRoute.get("/user/:userId", ({ params: { userId } }) => getUserData(userId)); + +authRoute.post("/user/update/:userId", ({ params: { userId }, body }) => updateUser(userId, body)); \ No newline at end of file diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..a8e4876 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,15 @@ +import Elysia from "elysia"; +import { projectRoutes } from "./project/project.route"; +import { uploadRoutes } from "./upload/upload.route"; +import { verifyAuth } from "../middlewares/auth.middlewares"; +import { authRoute } from "./auth/auth.route"; + +export const api = new Elysia({ + prefix: "/api", +}); + +// api.derive(verifyAuth); + +api.use(authRoute); +api.use(projectRoutes); +api.use(uploadRoutes); \ No newline at end of file diff --git a/src/api/project/project.controller.ts b/src/api/project/project.controller.ts new file mode 100644 index 0000000..34cdc8d --- /dev/null +++ b/src/api/project/project.controller.ts @@ -0,0 +1,37 @@ +export const getEachProjects = async (id: string) => { + try { + console.log(id); + return { id: id } + } catch (error) { + console.log(error.msg) + return { status: 500, message: "An error occurred while fetching projects" } + } +} + +export const getAllProjects = async () => { + try { + // this will return all the project associated with the user + } catch (error) { + console.log(error.msg); + return { status: 500, message: "An error occurred while fetching projects" } + } +} + +export const updateProject = async (id: string, data: any) => { + try { + + } catch (error) { + console.log(error.msg); + return { status: 500, message: "An error occurred while updating projects" } + } +} + +export const deleteProject = async (id: string) => { + try { + + } catch (error) { + console.log(error.msg); + return { status: 500, message: "An error occurred while deleting projects" } + } +} + diff --git a/src/api/project/project.route.ts b/src/api/project/project.route.ts new file mode 100644 index 0000000..cab000d --- /dev/null +++ b/src/api/project/project.route.ts @@ -0,0 +1,19 @@ +import { Elysia } from "elysia"; +import { deleteProject, getAllProjects, getEachProjects, updateProject } from "./project.controller"; + +export const projectRoutes = new Elysia({ + prefix: "/projects", + tags: ["Projects"], + detail: { + description: "Routes for managing projects", + } +}) + +projectRoutes.get("/:id", ({ params }) => getEachProjects(params.id)); + +projectRoutes.get("/", () => getAllProjects()); + +projectRoutes.put("/update/:id", ({ request, params }) => updateProject(params.id, request.body)); + +projectRoutes.delete("/delete/:id", ({ params }) => deleteProject(params.id)); + diff --git a/src/api/upload/upload.controller.ts b/src/api/upload/upload.controller.ts new file mode 100644 index 0000000..d78d6c4 --- /dev/null +++ b/src/api/upload/upload.controller.ts @@ -0,0 +1,104 @@ +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) => { + try { + // Use the formData API to extract the data from the request + const formData = await req.formData(); + + 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"); + } + + 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" }; + } + } catch (error) { + console.error("Error processing file:", error); + return { status: 500, message: "An error occurred while uploading the photo" }; + } +}; + +export const deletePhoto = async (id: string) => { + try { + if (!id) { + throw new Error("Invalid or missing file ID"); + } + + const deleteFile = await db + .delete(uploads) + .where(eq(uploads.id, id)) + .returning(); + + // Ensure there's a file to delete + if (!deleteFile || deleteFile.length === 0) { + throw new Error("File not found or already deleted"); + } + + const { projectId, filename } = deleteFile[0]; + + // Ensure projectId and filename are valid + if (!projectId || !filename) { + throw new Error("Project ID or filename is missing"); + } + + const minioRemove = await removeFromMinio(projectId, filename); + + return { status: 200, message: minioRemove.msg }; + + } catch (error) { + console.error("Error processing file:", error); + return { status: 500, message: `An error occurred while deleting the photo: ${error.message}` }; + } +}; + +export const getAllPhoto = async (id: string) => { + try { + // project id + if (!id) { + throw new Error("Invalid or missing project ID"); + } + 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, data: { msg: "Photos retrieved successfully", data: getAllPhoto } }; + } catch (error) { + 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 new file mode 100644 index 0000000..474801b --- /dev/null +++ b/src/api/upload/upload.route.ts @@ -0,0 +1,16 @@ +import { Elysia } from "elysia"; +import { deletePhoto, getAllPhoto, uploadPhoto } from "./upload.controller"; + +export const uploadRoutes = new Elysia({ + prefix: "/uploads", + tags: ["Uploads"], + detail: { + description: "Routes for uploading and managing photos", + } +}); + +uploadRoutes.post("/add", async ({ request }) => uploadPhoto(request)); + +uploadRoutes.delete("/delete/:id", async ({ params }) => deletePhoto(params.id)); + +uploadRoutes.get("/get/:id", async ({ params }) => getAllPhoto(params.id)); \ No newline at end of file diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..36d9477 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,44 @@ +import { Elysia } from "elysia"; +import swagger from '@elysiajs/swagger'; + +import { ENV } from "./config/env"; +import cors from "@elysiajs/cors"; +import { api } from "./api"; + +const app = new Elysia() + .use(cors()) + .use(swagger({ + path: "/docs", + documentation: { + info: { + title: "Canvas API", + version: "1.0.0", + description: "Canvas API Documentation", + }, + tags: [ + { + name: "Projects", + description: "All APIs related to Projects", + }, + { + name: "Uploads", + description: "All APIs related to Uploads" + } + ], + } + })) + .onError(({ code, error }) => { + if (code === 'NOT_FOUND') + return 'Not Found :('; + console.error(error) + }); + + +// all routes here +app.use(api); + +app.listen(ENV.SERVER_PORT, () => { + console.log(`🦊 Elysia is running at ${ENV.SERVER_URL}:${ENV.SERVER_PORT}`) +}) + + diff --git a/src/config/env.ts b/src/config/env.ts new file mode 100644 index 0000000..0f231a5 --- /dev/null +++ b/src/config/env.ts @@ -0,0 +1,12 @@ +import 'dotenv/config' + +export const ENV = { + SERVER_URL: process.env.SERVER_URL, + SERVER_PORT: process.env.SERVER_PORT || 5000, + DATABASE_URL: process.env.DATABASE_URL, + MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, + MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, + MINIO_ENDPOINT: process.env.MINIO_ENDPOINT, + MINIO_PORT: process.env.MINIO_PORT, + CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY, +} \ No newline at end of file diff --git a/src/config/minioClient.ts b/src/config/minioClient.ts new file mode 100644 index 0000000..798b1a1 --- /dev/null +++ b/src/config/minioClient.ts @@ -0,0 +1,10 @@ +import { Client } from "minio"; +import { ENV } from "../config/env"; + +export const minioClient = new Client({ + endPoint: ENV.MINIO_ENDPOINT!, + port: ENV.MINIO_PORT, + useSSL: false, + accessKey: ENV.MINIO_ACCESS_KEY, + secretKey: ENV.MINIO_SECRET_KEY, +}) \ No newline at end of file diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 0000000..eb8b284 --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1,5 @@ +import { drizzle } from "drizzle-orm/node-postgres"; + +import { ENV } from "../config/env"; + +export const db = drizzle(ENV.DATABASE_URL!); \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 0000000..bad7cf9 --- /dev/null +++ b/src/db/schema.ts @@ -0,0 +1,26 @@ +import { json, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; + +export const users = pgTable("users", { + id: text("user_id").primaryKey().notNull(), + paid_status: text("paid_status"), + expires_in: text("expires_in"), +}); + +export const projects = pgTable("projects", { + id: uuid("project_id").defaultRandom().primaryKey(), + userId: text("user_id").references(() => users.id), + object: json(), + name: text("name"), + description: text("description"), + created_at: timestamp("created_at").defaultNow(), + updated_at: timestamp("updated_at").defaultNow(), +}); + +export const uploads = pgTable("uploads", { + id: uuid().defaultRandom().primaryKey(), + filename: text("filename").notNull(), + url: text("url").notNull(), + projectId: uuid().references(() => projects.id), + created_at: timestamp("created_at").defaultNow(), + updated_at: timestamp("updated_at").defaultNow(), +}); diff --git a/src/helper/projects/createProject.ts b/src/helper/projects/createProject.ts new file mode 100644 index 0000000..a41b989 --- /dev/null +++ b/src/helper/projects/createProject.ts @@ -0,0 +1,22 @@ +import { db } from "../../db"; +import { projects } from "../../db/schema"; + +export const createEmptyProject = async (userId: string): Promise<{ id: string }> => { + try { + // Insert a new row with default values + const [newProject] = await db + .insert(projects) + .values({ + userId: userId, + object: {}, // Empty object as default + name: "", // Empty name + description: "", // Empty description + }) + .returning({ id: projects.id }); // Returning the ID of the created project + // Return the newly created project's ID + return { id: newProject.id }; + } catch (error) { + console.error("Error creating an empty project:", error); + throw new Error("Failed to create an empty project"); + } +}; \ No newline at end of file diff --git a/src/helper/upload/createBucket.ts b/src/helper/upload/createBucket.ts new file mode 100644 index 0000000..0e97947 --- /dev/null +++ b/src/helper/upload/createBucket.ts @@ -0,0 +1,35 @@ +import { minioClient } from "../../config/minioClient"; + +export const createBucket = async (bucketName: string) => { + try { + const bucketExists = await minioClient.bucketExists(bucketName); + if (!bucketExists) { + // Create the bucket + await minioClient.makeBucket(bucketName, "us-east-1"); + + // Set the bucket policy to make it public + const bucketPolicy = JSON.stringify({ + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Principal: "*", + Action: ["s3:GetObject"], + Resource: [`arn:aws:s3:::${bucketName}/*`], + }, + ], + }); + + await minioClient.setBucketPolicy(bucketName, bucketPolicy); + + return bucketName; // Return the bucket name if created successfully + } else { + return bucketName; // Return the bucket name if it already exists + } + } catch (error) { + console.error("Error creating or configuring bucket:", error); + + // Optionally rethrow the error with additional context + throw new Error(`Error creating bucket "${bucketName}": ${error.message}`); + } +}; diff --git a/src/helper/upload/removeFromMinio.ts b/src/helper/upload/removeFromMinio.ts new file mode 100644 index 0000000..069e712 --- /dev/null +++ b/src/helper/upload/removeFromMinio.ts @@ -0,0 +1,16 @@ +import { minioClient } from "../../config/minioClient"; + +interface RemoveFromMinioResponse { + msg: string; +} + +export const removeFromMinio = async (bucketName: string, objectName: string): Promise => { + try { + // Remove the object from MinIO + await minioClient.removeObject(bucketName, objectName); + return { msg: `Successfully removed ${objectName}` }; + } catch (error) { + console.error("Error removing object from MinIO:", error); + throw new Error(`Failed to remove ${objectName} from bucket ${bucketName}: ${error.message}`); + } +}; \ No newline at end of file diff --git a/src/helper/upload/uploadToMinio.ts b/src/helper/upload/uploadToMinio.ts new file mode 100644 index 0000000..5f8de86 --- /dev/null +++ b/src/helper/upload/uploadToMinio.ts @@ -0,0 +1,19 @@ +import { minioClient } from "../../config/minioClient"; + +export const uploadToMinio = async (file: File, bucketName: string, objectName: string) => { + const buffer = Buffer.from(await file.arrayBuffer()); // Convert file to buffer + try { + // Ensure the file is uploaded to MinIO + await minioClient.putObject(bucketName, objectName, buffer); + + // Construct the public URL to access the uploaded file + const publicUrl = `${minioClient.protocol}//${minioClient.host}:${minioClient.port}/${bucketName}/${objectName}`; + + return { url: publicUrl }; + + } catch (error) { + console.error("Error uploading file to MinIO:", error); + + throw new Error(`Error uploading file: ${error.message}`); + } +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 9c1f7a1..0000000 --- a/src/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Elysia } from "elysia"; - -const app = new Elysia().get("/", () => "Hello Elysia").listen(3000); - -console.log( - `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` -); diff --git a/src/middlewares/auth.middlewares.ts b/src/middlewares/auth.middlewares.ts new file mode 100644 index 0000000..5bcb526 --- /dev/null +++ b/src/middlewares/auth.middlewares.ts @@ -0,0 +1,9 @@ + +export const verifyAuth = (request: Request) => { + const authHeader = request.headers.get('Authorization'); + if (!authHeader) { + return new Response('Unauthorized', { status: 401 }); + } + const token = authHeader.split(' ')[1]; + // Verify the token here (e.g., using a library like `jsonwebtoken` or `jose`) +} \ No newline at end of file