diff --git a/bun.lockb b/bun.lockb index a4ac935..1b1c7bd 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/drizzle/0000_eager_marvel_zombies.sql b/drizzle/0000_tidy_echo.sql similarity index 94% rename from drizzle/0000_eager_marvel_zombies.sql rename to drizzle/0000_tidy_echo.sql index 6cb2781..b9801ad 100644 --- a/drizzle/0000_eager_marvel_zombies.sql +++ b/drizzle/0000_tidy_echo.sql @@ -19,8 +19,9 @@ CREATE TABLE "uploads" ( --> statement-breakpoint CREATE TABLE "users" ( "user_id" text PRIMARY KEY NOT NULL, - "paid_status" text NOT NULL, - "expires_in" timestamp NOT NULL + "paid_status" text, + "expires_in" text, + "refresh_token" text ); --> 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 diff --git a/drizzle/0001_shallow_umar.sql b/drizzle/0001_shallow_umar.sql new file mode 100644 index 0000000..4d97447 --- /dev/null +++ b/drizzle/0001_shallow_umar.sql @@ -0,0 +1,4 @@ +ALTER TABLE "users" ADD COLUMN "email" text NOT NULL;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "last_name" text;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "first_name" text;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "image" text; \ No newline at end of file diff --git a/drizzle/0001_useful_nighthawk.sql b/drizzle/0001_useful_nighthawk.sql deleted file mode 100644 index 400f70a..0000000 --- a/drizzle/0001_useful_nighthawk.sql +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 46be296..0000000 --- a/drizzle/0002_broad_eternity.sql +++ /dev/null @@ -1 +0,0 @@ -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 index 405a5c4..8849ab9 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "7a9f9e79-63fc-4d2b-8b25-616f96161308", + "id": "a0fe5e52-63bf-4a92-adb0-ae296fb9f33e", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -156,13 +156,19 @@ "name": "paid_status", "type": "text", "primaryKey": false, - "notNull": true + "notNull": false }, "expires_in": { "name": "expires_in", - "type": "timestamp", + "type": "text", "primaryKey": false, - "notNull": true + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false } }, "indexes": {}, diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json index 4d52a9a..ff6944c 100644 --- a/drizzle/meta/0001_snapshot.json +++ b/drizzle/meta/0001_snapshot.json @@ -1,6 +1,6 @@ { - "id": "5443181c-129a-488a-9001-d65b03b0119f", - "prevId": "7a9f9e79-63fc-4d2b-8b25-616f96161308", + "id": "b6897b47-e0f0-48c5-8917-696944c8524b", + "prevId": "a0fe5e52-63bf-4a92-adb0-ae296fb9f33e", "version": "7", "dialect": "postgresql", "tables": { @@ -152,6 +152,30 @@ "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", @@ -160,7 +184,13 @@ }, "expires_in": { "name": "expires_in", - "type": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", "primaryKey": false, "notNull": false } diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json deleted file mode 100644 index 5ae009a..0000000 --- a/drizzle/meta/0002_snapshot.json +++ /dev/null @@ -1,188 +0,0 @@ -{ - "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 index 35f8261..fb24b31 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -5,22 +5,15 @@ { "idx": 0, "version": "7", - "when": 1737800628545, - "tag": "0000_eager_marvel_zombies", + "when": 1737876637906, + "tag": "0000_tidy_echo", "breakpoints": true }, { "idx": 1, "version": "7", - "when": 1737804031716, - "tag": "0001_useful_nighthawk", - "breakpoints": true - }, - { - "idx": 2, - "version": "7", - "when": 1737806743289, - "tag": "0002_broad_eternity", + "when": 1737876981144, + "tag": "0001_shallow_umar", "breakpoints": true } ] diff --git a/package.json b/package.json index 7529ed0..abb47f0 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,14 @@ }, "dependencies": { "@clerk/backend": "^1.23.7", + "@elysiajs/cookie": "^0.8.0", "@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", + "jsonwebtoken": "^9.0.2", "minio": "^8.0.3", "pg": "^8.13.1", "postgres": "^3.4.5" diff --git a/src/api/auth/auth.controller.ts b/src/api/auth/auth.controller.ts index 5f5476c..eb91598 100644 --- a/src/api/auth/auth.controller.ts +++ b/src/api/auth/auth.controller.ts @@ -4,6 +4,11 @@ import { users } from "../../db/schema"; import { db } from "../../db"; import { eq } from "drizzle-orm"; +// @ts-ignore +import jwt from "jsonwebtoken"; + +import { checkUserInDB, createUser, storeRefreshToken } from "../../helper/auth/auth.helper"; + // Initialize Clerk with your API key const clerk = createClerkClient({ secretKey: ENV.CLERK_SECRET_KEY }); @@ -15,7 +20,18 @@ export const getUserData = async (userId: string) => { ]); if (user && !checkInDB.found) { - const userData = await createUser(user.id); + + // Validate and transform user data + const userDBData = { + id: user.id, + email: user.emailAddresses[0].emailAddress, // Assuming the first email address + firstName: user.firstName || "N/A", // Provide a default value if needed + lastName: user.lastName || "N/A", + image: user.imageUrl, + }; + + const userData = await createUser(userDBData); + return { status: 200, message: "User retrieved successfully", data: userData }; } if (user && checkInDB.found) { @@ -30,31 +46,6 @@ export const getUserData = async (userId: string) => { } }; -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 }); @@ -65,4 +56,88 @@ export const updateUser = async (id: string, body) => { 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 +} + +export const generateToken = async (context: any) => { + try { + const userId = context?.params?.userId; + // generating accessToken and refreshToken + const user = await checkUserInDB(userId); + if (user?.found === true) { + const accessSecret = ENV.JWT_ACCESS_TOKEN_SECRET; + const refreshSecret = ENV.JWT_REFRESH_TOKEN_SECRET; + + // generate access token + const accessToken = jwt.sign({ userId }, accessSecret, { expiresIn: '3h' }); + + // generate refresh token + const refreshToken = jwt.sign({ userId }, refreshSecret, { expiresIn: '7d' }); + + // store refresh token in db + const storeRToken = await storeRefreshToken(userId, refreshToken); + + if (storeRToken.status === 200) { + context.cookie.access_token = { + value: accessToken, + httpOnly: true, + secure: true, + sameSite: 'none', + path: "/", + maxAge: 3 * 60 * 60 * 1000, // 3 hours + } + + return { status: 200, message: "Token generated successfully", token: accessToken }; + } + + return { status: 500, message: "An error occurred while storing the refresh token" }; + } + else { + return { status: 404, message: "Unauthorized!!!" }; + } + } catch (error: any) { + console.error("Error in generateToken:", error.message || error.toString()); + return { status: 500, message: `An error occurred while generating the token` }; + } +} + +export const verifyToken = async (context: any) => { + try { + // if token is in cookie, verify it + const token_cookie = context.cookie.access_token.value; + if (token_cookie) { + const verify_cookie = jwt.verify(token_cookie, ENV.JWT_REFRESH_TOKEN_SECRET); + if (verify_cookie) { + return { status: 200, message: "Token verified successfully" }; + } + else { + return { status: 401, message: "Unauthorized!!!" }; + } + } + // if token is not in cookie, then check in header and verify it + else { + const token_header = context.headers.authorization.split("Bearer ")[1]; + + if (token_header) { + const verify_header = jwt.decode(token_header); + + if (verify_header?.userId) { + context.params.userId = verify_header.userId; + await generateToken(context); + } + else { + return { status: 401, message: "Unauthorized!!!" }; + } + } + else { + return { status: 401, message: "Unauthorized!!!" }; + } + } + + } catch (error: any) { + console.log("Error in verifyToken:", error.message || error.toString()); + return { status: 500, message: `An error occurred while verifying the token` }; + } +} + + + diff --git a/src/api/auth/auth.route.ts b/src/api/auth/auth.route.ts index 41db313..b782c4e 100644 --- a/src/api/auth/auth.route.ts +++ b/src/api/auth/auth.route.ts @@ -1,5 +1,5 @@ import Elysia from "elysia"; -import { getUserData, updateUser } from "./auth.controller"; +import { generateToken, getUserData, updateUser, verifyToken } from "./auth.controller"; export const authRoute = new Elysia({ prefix: "/auth", @@ -9,6 +9,11 @@ export const authRoute = new Elysia({ } }) -authRoute.get("/user/:userId", ({ params: { userId } }) => getUserData(userId)); +authRoute.get("/user/:userId", async ({ params: { userId } }) => await getUserData(userId)); + +authRoute.post("/user/update/:userId", async ({ params: { userId }, body }) => await updateUser(userId, body)); + +authRoute.get("/generate-token/:userId", async (context) => await generateToken(context)); + +authRoute.get("/verify-token", async (context) => await verifyToken(context)); -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 index a8e4876..4e41b50 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -3,13 +3,14 @@ import { projectRoutes } from "./project/project.route"; import { uploadRoutes } from "./upload/upload.route"; import { verifyAuth } from "../middlewares/auth.middlewares"; import { authRoute } from "./auth/auth.route"; +import cookie from "@elysiajs/cookie"; export const api = new Elysia({ prefix: "/api", }); // api.derive(verifyAuth); - +api.use(cookie()); api.use(authRoute); api.use(projectRoutes); api.use(uploadRoutes); \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 36d9477..06247e6 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,7 +6,10 @@ import cors from "@elysiajs/cors"; import { api } from "./api"; const app = new Elysia() - .use(cors()) + .use(cors({ + origin: "http://localhost:5175", + credentials: true, + })) .use(swagger({ path: "/docs", documentation: { diff --git a/src/config/env.ts b/src/config/env.ts index 0f231a5..8bd4ea4 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -9,4 +9,6 @@ export const ENV = { MINIO_ENDPOINT: process.env.MINIO_ENDPOINT, MINIO_PORT: process.env.MINIO_PORT, CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY, + JWT_ACCESS_TOKEN_SECRET: process.env.JWT_ACCESS_TOKEN_SECRET, + JWT_REFRESH_TOKEN_SECRET: process.env.JWT_REFRESH_TOKEN_SECRET, } \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts index bad7cf9..baee876 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -2,8 +2,13 @@ import { json, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; export const users = pgTable("users", { id: text("user_id").primaryKey().notNull(), + email: text("email").notNull(), + lastName: text("last_name"), + firstName: text("first_name"), + image: text("image"), paid_status: text("paid_status"), expires_in: text("expires_in"), + refresh_token: text("refresh_token"), }); export const projects = pgTable("projects", { diff --git a/src/helper/auth/auth.helper.ts b/src/helper/auth/auth.helper.ts new file mode 100644 index 0000000..6560f23 --- /dev/null +++ b/src/helper/auth/auth.helper.ts @@ -0,0 +1,51 @@ +import { eq } from "drizzle-orm"; +import { db } from "../../db"; +import { users } from "../../db/schema"; + +type User = { + id: string; + email: string; + lastName: string; + firstName: string; + image: string; +} + +// this will check the user into our local canvas database +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 (body: User) => { + try { + const { id, email, lastName, firstName, image } = body; + + const [saveUser] = await db.insert(users).values({ id, email, lastName, firstName, image }).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` }; + } +}; + +// this will store the refresh token in the database +export const storeRefreshToken = async (userId: string, refreshToken: string) => { + try { + // store refresh token in db + const storeRToken = await db.update(users).set({ refresh_token: refreshToken }).where(eq(users.id, userId)).returning({ updatedId: users.id }); + return { status: 200, message: "Refresh token stored successfully" }; + } catch (error: any) { + console.error("Error in storeRToken:", error.message || error.toString()); + return { status: 500, message: `An error occurred while storing the refresh token` } + } +}