From 278aa030cb0575f2f1d2d8770471e410d4785d3a Mon Sep 17 00:00:00 2001 From: Saimon8420 Date: Sat, 25 Jan 2025 18:23:36 +0600 Subject: [PATCH] init commit --- .gitignore | 1 + bun.lockb | Bin 0 -> 80307 bytes drizzle.config.ts | 11 ++ drizzle/0000_eager_marvel_zombies.sql | 27 ++++ drizzle/0001_useful_nighthawk.sql | 2 + drizzle/0002_broad_eternity.sql | 1 + drizzle/meta/0000_snapshot.json | 188 ++++++++++++++++++++++++++ drizzle/meta/0001_snapshot.json | 188 ++++++++++++++++++++++++++ drizzle/meta/0002_snapshot.json | 188 ++++++++++++++++++++++++++ drizzle/meta/_journal.json | 27 ++++ package.json | 24 +++- src/api/auth/auth.controller.ts | 68 ++++++++++ src/api/auth/auth.route.ts | 14 ++ src/api/index.ts | 15 ++ src/api/project/project.controller.ts | 37 +++++ src/api/project/project.route.ts | 19 +++ src/api/upload/upload.controller.ts | 104 ++++++++++++++ src/api/upload/upload.route.ts | 16 +++ src/app.ts | 44 ++++++ src/config/env.ts | 12 ++ src/config/minioClient.ts | 10 ++ src/db/index.ts | 5 + src/db/schema.ts | 26 ++++ src/helper/projects/createProject.ts | 22 +++ src/helper/upload/createBucket.ts | 35 +++++ src/helper/upload/removeFromMinio.ts | 16 +++ src/helper/upload/uploadToMinio.ts | 19 +++ src/index.ts | 7 - src/middlewares/auth.middlewares.ts | 9 ++ 29 files changed, 1124 insertions(+), 11 deletions(-) create mode 100644 bun.lockb create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_eager_marvel_zombies.sql create mode 100644 drizzle/0001_useful_nighthawk.sql create mode 100644 drizzle/0002_broad_eternity.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/0001_snapshot.json create mode 100644 drizzle/meta/0002_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 src/api/auth/auth.controller.ts create mode 100644 src/api/auth/auth.route.ts create mode 100644 src/api/index.ts create mode 100644 src/api/project/project.controller.ts create mode 100644 src/api/project/project.route.ts create mode 100644 src/api/upload/upload.controller.ts create mode 100644 src/api/upload/upload.route.ts create mode 100644 src/app.ts create mode 100644 src/config/env.ts create mode 100644 src/config/minioClient.ts create mode 100644 src/db/index.ts create mode 100644 src/db/schema.ts create mode 100644 src/helper/projects/createProject.ts create mode 100644 src/helper/upload/createBucket.ts create mode 100644 src/helper/upload/removeFromMinio.ts create mode 100644 src/helper/upload/uploadToMinio.ts delete mode 100644 src/index.ts create mode 100644 src/middlewares/auth.middlewares.ts 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 0000000000000000000000000000000000000000..a4ac9357708e8f47c7776cd12c67f1dc8c8ac7e4 GIT binary patch literal 80307 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 literal 0 HcmV?d00001 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