From a16c6cb55e3bc51ebbbab46771f2d273d37d2378 Mon Sep 17 00:00:00 2001 From: ShiljaBabu Date: Mon, 9 Feb 2026 22:32:16 +0530 Subject: [PATCH 01/61] feat(auth): implemented signup with admin approval and login flow --- .../src/controllers/admin.controllers.ts | 35 +++++++++++ .../src/controllers/auth.controller.ts | 33 +++++++++- apps/core-api/src/db/connect.ts | 6 +- .../src/models/pendingSignup.models.ts | 25 ++++++++ apps/core-api/src/queue/rabbit.ts | 3 + .../src/repositories/Signup.repository.ts | 36 +++++++++++ .../src/repositories/user.repository.ts | 21 ++++++- apps/core-api/src/routes/admin.route.ts | 11 ++++ apps/core-api/src/routes/auth.routes.ts | 6 +- apps/core-api/src/routes/index.ts | 2 + apps/core-api/src/server.ts | 1 + apps/core-api/src/services/auth.service.ts | 49 ++++++++++++++- .../src/services/verification.service.ts | 62 +++++++++++++++++++ 13 files changed, 280 insertions(+), 10 deletions(-) create mode 100644 apps/core-api/src/controllers/admin.controllers.ts create mode 100644 apps/core-api/src/models/pendingSignup.models.ts create mode 100644 apps/core-api/src/repositories/Signup.repository.ts create mode 100644 apps/core-api/src/routes/admin.route.ts create mode 100644 apps/core-api/src/services/verification.service.ts diff --git a/apps/core-api/src/controllers/admin.controllers.ts b/apps/core-api/src/controllers/admin.controllers.ts new file mode 100644 index 0000000..6d4af38 --- /dev/null +++ b/apps/core-api/src/controllers/admin.controllers.ts @@ -0,0 +1,35 @@ +import type { Request, Response, NextFunction } from "express"; + +import { approveSignupService, rejectSignupService } from "../services/verification.service.js"; + +export const approveSignupController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { email } = req.body; + + const result = await approveSignupService(email); + + res.status(200).json({ success: true, data: result }); + } catch (error) { + next(error); + } +}; + +export const rejectSignupController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { email, reason } = req.body; + + const result = await rejectSignupService(email, reason); + + res.status(200).json({ success: true, data: result }); + } catch (error) { + next(error); + } +}; diff --git a/apps/core-api/src/controllers/auth.controller.ts b/apps/core-api/src/controllers/auth.controller.ts index 65ba41f..523008c 100644 --- a/apps/core-api/src/controllers/auth.controller.ts +++ b/apps/core-api/src/controllers/auth.controller.ts @@ -1,8 +1,37 @@ -import type { NextFunction, Request, Response } from "express"; +import type { NextFunction, Request, Response } from "express"; -import { loginService } from "../services/auth.service.js"; +import { loginService, signupService } from "../services/auth.service.js"; import type { HttpError } from "../modules/auth/http-error.js"; +export const signupController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { email, password, role, documents } = req.body; + + if (!email || !password || !role) { + const err: HttpError = new Error("Missing required fields"); + err.statusCode = 400; + throw err; + } + + const result = await signupService({ + email, + password, + role, + documents: documents ?? [], + }); + + res.status(201).json({ + success: true, + data: result, + }); + } catch (error) { + next(error); + } +}; export const loginController = async ( req: Request, diff --git a/apps/core-api/src/db/connect.ts b/apps/core-api/src/db/connect.ts index b0323c4..d779be6 100644 --- a/apps/core-api/src/db/connect.ts +++ b/apps/core-api/src/db/connect.ts @@ -1,4 +1,4 @@ -import mongoose from "mongoose"; +import mongoose from "mongoose"; import { logger } from "../utils/logger.js"; @@ -10,8 +10,10 @@ if (!mongoUri) { export const connectDB = async () => { try { - await mongoose.connect(mongoUri); + await mongoose.connect(mongoUri, { dbName: "noillin" }); logger.info("MongoDB connected successfully"); + logger.info(`CONNECTED DB: ${mongoose.connection.name}`); + } catch (err: unknown) { logger.error(`MongoDB connection failed: ${String(err)}`); process.exit(1); diff --git a/apps/core-api/src/models/pendingSignup.models.ts b/apps/core-api/src/models/pendingSignup.models.ts new file mode 100644 index 0000000..55282d9 --- /dev/null +++ b/apps/core-api/src/models/pendingSignup.models.ts @@ -0,0 +1,25 @@ +import { Schema, model } from "mongoose"; + +const PendingSignupSchema = new Schema( + { + email: { type: String, required: true, unique: true }, + passwordHash: { type: String, required: true }, + role: { + type: String, + enum: ["INFLUENCER", "BRAND"], + required: true, + }, + documents: [{ type: String }], + status: { + type: String, + enum: ["PENDING", "APPROVED", "REJECTED"], + default: "PENDING", + }, + }, + { timestamps: true } +); + +export const PendingSignup = model( + "PendingSignup", + PendingSignupSchema +); diff --git a/apps/core-api/src/queue/rabbit.ts b/apps/core-api/src/queue/rabbit.ts index 2c0c074..69808a9 100644 --- a/apps/core-api/src/queue/rabbit.ts +++ b/apps/core-api/src/queue/rabbit.ts @@ -14,6 +14,9 @@ export const connectRabbit = async() => { const connection = await amqp.connect(rabbitUrl) logger.info("RabbitMQ is connected") + connection.on("error", (err) => { + logger.error("RabbitMQ connection error", err); + }); connection.on("close", () => { logger.warn("RabbitMQ connection closed"); }); diff --git a/apps/core-api/src/repositories/Signup.repository.ts b/apps/core-api/src/repositories/Signup.repository.ts new file mode 100644 index 0000000..8a36c9b --- /dev/null +++ b/apps/core-api/src/repositories/Signup.repository.ts @@ -0,0 +1,36 @@ +import { PendingSignup } from "../models/pendingSignup.models.js"; + +interface CreatePendingSignupInput { + email: string; + role: string; + status: "PENDING" | "APPROVED" | "REJECTED"; + passwordHash: string; + documents: string[]; + + +} + +class PendingSignupRepository { + create(data: CreatePendingSignupInput) { + return PendingSignup.create(data); + } + + findByEmail(email: string) { + return PendingSignup.findOne({ email }); + } + + updateStatus(email: string, status: "APPROVED" | "REJECTED") { + return PendingSignup.findOneAndUpdate( + { email }, + { status }, + { new: true } + ); + } + + deleteByEmail(email: string) { + return PendingSignup.findOneAndDelete({ email }); + } +} + +export const pendingSignupRepository = + new PendingSignupRepository(); diff --git a/apps/core-api/src/repositories/user.repository.ts b/apps/core-api/src/repositories/user.repository.ts index 9e0ef02..07c8de6 100644 --- a/apps/core-api/src/repositories/user.repository.ts +++ b/apps/core-api/src/repositories/user.repository.ts @@ -2,13 +2,30 @@ import { User } from "../modules/users/user.model.js"; class UserRepository{ async findEmailWithPassword(email: string) { - return User.findOne({ email }).select("+password"); -} + const normalizedEmail = email.trim().toLowerCase(); + return User.findOne({ email: normalizedEmail }).select("+password"); +} + + + async findByEmail(email: string) { + return User.findOne({ email }); + } async saveRefreshToken(userId:string, refreshToken:string){ return User.findByIdAndUpdate(userId,{refreshToken}) } + + async create(data: { + email: string; + password: string; + role: string; + isEmailVerified: boolean; + status: string; + }) { + return User.create(data); + } } + export const userRepository = new UserRepository() \ No newline at end of file diff --git a/apps/core-api/src/routes/admin.route.ts b/apps/core-api/src/routes/admin.route.ts new file mode 100644 index 0000000..5d771a3 --- /dev/null +++ b/apps/core-api/src/routes/admin.route.ts @@ -0,0 +1,11 @@ +import { Router } from "express"; + +import { authenticate } from "../middlewares/auth.middleware.js"; +import { approveSignupController, rejectSignupController } from "../controllers/admin.controllers.js"; + +const router: Router = Router(); + +router.post("/signup/approve", authenticate, approveSignupController); +router.post("/signup/reject", authenticate, rejectSignupController); + +export default router; diff --git a/apps/core-api/src/routes/auth.routes.ts b/apps/core-api/src/routes/auth.routes.ts index 109b7d8..98a17c2 100644 --- a/apps/core-api/src/routes/auth.routes.ts +++ b/apps/core-api/src/routes/auth.routes.ts @@ -1,7 +1,9 @@ -import { Router } from "express"; +import { Router } from "express"; -import { loginController } from "../controllers/auth.controller.js"; +import { loginController, signupController } from "../controllers/auth.controller.js"; const router: Router = Router() router.post("/login", loginController) +router.post("/signup", signupController); + export default router \ No newline at end of file diff --git a/apps/core-api/src/routes/index.ts b/apps/core-api/src/routes/index.ts index aa21217..55708af 100644 --- a/apps/core-api/src/routes/index.ts +++ b/apps/core-api/src/routes/index.ts @@ -10,10 +10,12 @@ import orderRoutes from "./orders.routes.js"; import paymentRoutes from "./payments.routes.js"; import searchRoutes from "./search.routes.js"; import healthRoutes from "./health.routes.js"; +import adminRoutes from "./admin.route.js" const router:Router = Router() router.use("/health", healthRoutes) router.use("/auth", authRoutes) +router.use("/admin", adminRoutes) router.use("/users", userRoutes) router.use("/profile", profileRoutes) router.use("/gigs", gigRoutes); diff --git a/apps/core-api/src/server.ts b/apps/core-api/src/server.ts index 75f7cf0..5814bc5 100644 --- a/apps/core-api/src/server.ts +++ b/apps/core-api/src/server.ts @@ -17,6 +17,7 @@ app.use(httpLogger) app.use(express.json()) app.use("/api", router) connectDB() + app.get("/health", (req, res) => { res.status(200).json({ status: "ok", diff --git a/apps/core-api/src/services/auth.service.ts b/apps/core-api/src/services/auth.service.ts index 2b8d7aa..d0b9830 100644 --- a/apps/core-api/src/services/auth.service.ts +++ b/apps/core-api/src/services/auth.service.ts @@ -15,11 +15,52 @@ // } // export const authService = new AuthService(); -import bcrypt from "bcrypt"; +import bcrypt from "bcrypt"; import { signAccessToken, signRefreshToken } from "../modules/auth/auth.utils.js"; import type { HttpError } from "../modules/auth/http-error.js"; import { userRepository } from "../repositories/user.repository.js" +import { pendingSignupRepository } from "../repositories/Signup.repository.js"; +import { logger } from "../utils/logger.js"; + +// import { sendMail } from "../../utils/nodemailer"; + +interface SignupInput { + email: string; + password: string; + role: "INFLUENCER" | "BRAND"; + documents: string[]; +} + +export const signupService = async (data: SignupInput) => { + const existingUser = await userRepository.findEmailWithPassword(data.email); + if (existingUser) { + const err: HttpError = new Error("User already exists"); + err.statusCode = 409; + throw err; + } + + const existingRequest = + await pendingSignupRepository.findByEmail(data.email); + + if (existingRequest) { + const err: HttpError = new Error("Signup request already submitted"); + err.statusCode = 409; + throw err; + } + + const passwordHash = await bcrypt.hash(data.password, 10); + + await pendingSignupRepository.create({ + email: data.email.toLowerCase(), + passwordHash, + role: data.role, + documents: data.documents, + status: "PENDING", + }); + + return { message: "Signup request submitted for review" }; +}; interface LoginResult { @@ -36,9 +77,12 @@ export const loginService = async( email : string, password : string ): Promise => { + const user = await userRepository.findEmailWithPassword(email) + logger.info(`EMAIL: ${email}`); + if (!user) { - const err: HttpError = new Error("Invalid credentials") + const err: HttpError = new Error("Invalid credentialss") err.statusCode = 401 throw err } @@ -55,6 +99,7 @@ export const loginService = async( throw err } + const isMatch = await bcrypt.compare(password, user.password) console.log("PASSWORD FROM DB:", user.password); diff --git a/apps/core-api/src/services/verification.service.ts b/apps/core-api/src/services/verification.service.ts new file mode 100644 index 0000000..5da8839 --- /dev/null +++ b/apps/core-api/src/services/verification.service.ts @@ -0,0 +1,62 @@ +import type { HttpError } from "../modules/auth/http-error.js"; +import { pendingSignupRepository } from "../repositories/Signup.repository.js"; +import { userRepository } from "../repositories/user.repository.js"; +// import { sendMail } from "../../utils/nodemailer"; + +export const approveSignupService = async (email: string) => { + const pending = await pendingSignupRepository.findByEmail(email); + + if (!pending) { + const err: HttpError = new Error("Pending signup not found"); + err.statusCode = 404; + throw err; + } + + if (pending.status !== "PENDING") { + const err: HttpError = new Error("Signup already processed"); + err.statusCode = 400; + throw err; + } + + + await userRepository.create({ + email: pending.email, + password: pending.passwordHash, + role: pending.role, + isEmailVerified: true, + status: "ACTIVE", + }); + + await pendingSignupRepository.deleteByEmail(email); + + // await sendMail( + // email, + // "Signup Approved", + // "Your account has been approved. You can now log in." + // ); + + return { message: "Signup approved successfully" }; +}; + +export const rejectSignupService = async ( + email: string, + reason?: string +) => { + const pending = await pendingSignupRepository.findByEmail(email); + + if (!pending) { + const err: HttpError = new Error("Pending signup not found"); + err.statusCode = 404; + throw err; + } + + await pendingSignupRepository.deleteByEmail(email); + + // await sendMail( + // email, + // "Signup Rejected", + // reason ?? "Your signup request was rejected by the admin." + // ); + + return { message: "Signup rejected successfully" }; +}; From 7ebbbba9d0fb154fd5ad3bb74493d3fef440efcc Mon Sep 17 00:00:00 2001 From: safvan Date: Tue, 10 Feb 2026 09:50:53 +0530 Subject: [PATCH 02/61] feat(login):add login page --- apps/web/app/login/page.tsx | 92 ++++++++++++++++++++++++++++++++++++- apps/web/app/page.tsx | 20 +++++++- 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx index 1f0079e..a1e6674 100644 --- a/apps/web/app/login/page.tsx +++ b/apps/web/app/login/page.tsx @@ -1,3 +1,93 @@ +"use client"; + +import { useState } from "react"; + export default function LoginPage() { - return

Login Page

; + const [role, setRole] = useState<"brand" | "influencer">("brand"); + + return ( +
+
+ +
+

+ Welcome Back +

+

+ Log in to your Noillin account as{" "} + {role} +

+
+ +
+ + + +
+ +
+
+ + +
+ +
+
+ + +
+ + +
+ + +
+ +

+ Secure login · Data protected +

+
+
+ ); } diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index c09c252..ca8d897 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,3 +1,21 @@ +"use client"; +import { useRouter } from "next/navigation"; + export default function HomePage() { - return

Welcome to Noillin Platform

; + const router = useRouter(); + const handleLoginClick = () => { + router.push("/login"); + } + return ( +
+
+

+ Welcome to Noillin Platform +

+ +
+
+ ); } From fdd499d43e2f11ddf88c4f78e04442e57acacca4 Mon Sep 17 00:00:00 2001 From: safvan Date: Tue, 10 Feb 2026 11:16:32 +0530 Subject: [PATCH 03/61] feat(signup):create signup page --- apps/web/app/page.tsx | 2 + apps/web/app/signup/page.tsx | 134 +++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 apps/web/app/signup/page.tsx diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index ca8d897..72fbc26 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -19,3 +19,5 @@ export default function HomePage() { ); } + + diff --git a/apps/web/app/signup/page.tsx b/apps/web/app/signup/page.tsx new file mode 100644 index 0000000..4693485 --- /dev/null +++ b/apps/web/app/signup/page.tsx @@ -0,0 +1,134 @@ +"use client"; + +import { useState } from "react"; + +export default function LoginPage() { + const [role, setRole] = useState<"brand" | "influencer">("brand"); + + return ( +
+
+ +
+

+ Welcome Back +

+

+ Join as a Brand or Influencer + +

+
+ +
+ + + +
+ +
+ +
+ + +
+
+ + +
+ +
+
+ + +
+ + +
+
+ + +
+
+ {role === "brand" ? ( + <> + + + + ) : ( + <> + + + + )} +
+ + + +
+ +

+ Secure login · Data protected +

+
+
+ ); +} \ No newline at end of file From e479776276d43808970608b3f80e49027a6939d2 Mon Sep 17 00:00:00 2001 From: ShiljaBabu Date: Tue, 10 Feb 2026 11:19:14 +0530 Subject: [PATCH 04/61] feat(auth):implemented refresh token endpoint and logout endpoint --- .../src/controllers/auth.controller.ts | 44 ++++++++++- .../src/repositories/user.repository.ts | 8 +- apps/core-api/src/routes/auth.routes.ts | 5 +- apps/core-api/src/services/auth.service.ts | 76 ++++++++++++++++++- apps/core-api/src/types/express.d.ts | 11 +++ 5 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 apps/core-api/src/types/express.d.ts diff --git a/apps/core-api/src/controllers/auth.controller.ts b/apps/core-api/src/controllers/auth.controller.ts index 523008c..4e91248 100644 --- a/apps/core-api/src/controllers/auth.controller.ts +++ b/apps/core-api/src/controllers/auth.controller.ts @@ -1,5 +1,6 @@ -import type { NextFunction, Request, Response } from "express"; +import type { NextFunction, Request, Response } from "express"; +import { logoutService, refreshTokenService } from "../services/auth.service.js"; import { loginService, signupService } from "../services/auth.service.js"; import type { HttpError } from "../modules/auth/http-error.js"; @@ -57,3 +58,44 @@ export const loginController = async ( next(error); } }; + + +export const refreshTokenController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { refreshToken } = req.body; + + const result = await refreshTokenService(refreshToken); + + res.status(200).json({ + success: true, + data: result, + }); + } catch (error) { + next(error); + } +}; + +export const logoutController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const userId = req.user?.userId; + if (!userId) { + return res.status(401).json({ success: false, message: "Unauthorized" }); +} + const result = await logoutService(userId); + + res.status(200).json({ + success: true, + data: result, + }); + } catch (error) { + next(error); + } +}; diff --git a/apps/core-api/src/repositories/user.repository.ts b/apps/core-api/src/repositories/user.repository.ts index 07c8de6..4962cf8 100644 --- a/apps/core-api/src/repositories/user.repository.ts +++ b/apps/core-api/src/repositories/user.repository.ts @@ -12,8 +12,14 @@ class UserRepository{ return User.findOne({ email }); } + async findById(userId: string) { + return User.findById(userId).select("+refreshToken"); +} + + async saveRefreshToken(userId:string, refreshToken:string){ - return User.findByIdAndUpdate(userId,{refreshToken}) + return User.findByIdAndUpdate(userId,{refreshToken}, { new: true } +) } async create(data: { diff --git a/apps/core-api/src/routes/auth.routes.ts b/apps/core-api/src/routes/auth.routes.ts index 98a17c2..1745136 100644 --- a/apps/core-api/src/routes/auth.routes.ts +++ b/apps/core-api/src/routes/auth.routes.ts @@ -1,9 +1,12 @@ import { Router } from "express"; -import { loginController, signupController } from "../controllers/auth.controller.js"; +import { loginController, logoutController, refreshTokenController, signupController } from "../controllers/auth.controller.js"; +import { authenticate } from "../middlewares/auth.middleware.js"; const router: Router = Router() router.post("/login", loginController) router.post("/signup", signupController); +router.post("/refresh", refreshTokenController); +router.post("/logout", authenticate, logoutController); export default router \ No newline at end of file diff --git a/apps/core-api/src/services/auth.service.ts b/apps/core-api/src/services/auth.service.ts index d0b9830..2b386d4 100644 --- a/apps/core-api/src/services/auth.service.ts +++ b/apps/core-api/src/services/auth.service.ts @@ -17,7 +17,7 @@ // export const authService = new AuthService(); import bcrypt from "bcrypt"; -import { signAccessToken, signRefreshToken } from "../modules/auth/auth.utils.js"; +import { signAccessToken, signRefreshToken, verifyRefreshToken } from "../modules/auth/auth.utils.js"; import type { HttpError } from "../modules/auth/http-error.js"; import { userRepository } from "../repositories/user.repository.js" import { pendingSignupRepository } from "../repositories/Signup.repository.js"; @@ -63,6 +63,7 @@ export const signupService = async (data: SignupInput) => { }; + interface LoginResult { accessToken: string, refreshToken: string, @@ -129,4 +130,75 @@ export const loginService = async( adminLevel: user.adminLevel ?? null } } -} \ No newline at end of file +} + + +interface RefreshResult { + accessToken: string; + refreshToken: string; +} +export const refreshTokenService = async ( + refreshToken: string +): Promise => { + if (!refreshToken) { + const err: HttpError = new Error("Refresh token required"); + err.statusCode = 400; + throw err; + } + + let payload; + try { + payload = verifyRefreshToken(refreshToken); + } catch { + const err: HttpError = new Error("Invalid refresh token"); + err.statusCode = 401; + throw err; + } + + const user = await userRepository.findById(payload.userId); + + // ✅ FIRST check user existence + if (!user || !user.refreshToken) { + const err: HttpError = new Error("Refresh token mismatch"); + err.statusCode = 401; + throw err; + } + + + // ✅ Compare after narrowing + if (user.refreshToken.trim() !== refreshToken.trim()) { + const err: HttpError = new Error("Refresh token mismatch"); + err.statusCode = 401; + throw err; + } + + const newPayload = { + userId: user._id.toString(), + role: user.role, + adminLevel: user.adminLevel ?? null, + }; + + const newAccessToken = signAccessToken(newPayload); + const newRefreshToken = signRefreshToken(newPayload); + + await userRepository.saveRefreshToken(user._id.toString(), newRefreshToken); + + return { + accessToken: newAccessToken, + refreshToken: newRefreshToken, + }; +}; + + +export const logoutService = async (userId: string) => { + if (!userId) { + const err: HttpError = new Error("User not authenticated"); + err.statusCode = 401; + throw err; + } + + // Invalidate refresh token + await userRepository.saveRefreshToken(userId, ""); + + return { message: "Logged out successfully" }; +}; diff --git a/apps/core-api/src/types/express.d.ts b/apps/core-api/src/types/express.d.ts new file mode 100644 index 0000000..07a266e --- /dev/null +++ b/apps/core-api/src/types/express.d.ts @@ -0,0 +1,11 @@ +import type { JwtPayload } from "../modules/auth/auth.utils.js"; + +declare global { + namespace Express { + interface Request { + user?: JwtPayload; + } + } +} + +export {}; From 62f8cfdd8321ad6dde1dcc472adc0965df2157f7 Mon Sep 17 00:00:00 2001 From: safvan Date: Tue, 10 Feb 2026 11:27:59 +0530 Subject: [PATCH 05/61] feat(signup):create signup pages --- apps/web/app/page.tsx | 1 - apps/web/app/signup/page.tsx | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 72fbc26..1e75752 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -20,4 +20,3 @@ export default function HomePage() { ); } - diff --git a/apps/web/app/signup/page.tsx b/apps/web/app/signup/page.tsx index 4693485..5b98fa3 100644 --- a/apps/web/app/signup/page.tsx +++ b/apps/web/app/signup/page.tsx @@ -1,3 +1,4 @@ + "use client"; import { useState } from "react"; From c85292b9b0b7cf6eab9a461e727f44bf79c3a4d7 Mon Sep 17 00:00:00 2001 From: safvan Date: Tue, 10 Feb 2026 11:40:26 +0530 Subject: [PATCH 06/61] style(signup): improve responsive UI --- apps/web/app/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 1e75752..72fbc26 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -20,3 +20,4 @@ export default function HomePage() { ); } + From 6945bef502fb881d32ab7e01ea6edf3bdbcd3d0f Mon Sep 17 00:00:00 2001 From: ajay Date: Tue, 10 Feb 2026 17:17:29 +0530 Subject: [PATCH 07/61] feat(axios):created api client --- apps/web/app/register/page.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/web/app/register/page.tsx diff --git a/apps/web/app/register/page.tsx b/apps/web/app/register/page.tsx new file mode 100644 index 0000000..e69de29 From 4a48c19033e62b2bf1ee1237b2ea1ccd69c518ba Mon Sep 17 00:00:00 2001 From: ShiljaBabu Date: Tue, 10 Feb 2026 17:18:58 +0530 Subject: [PATCH 08/61] feat(auth): role based access control --- .../src/middlewares/auth.middleware.ts | 17 ++++++++++++++ apps/core-api/src/rbac/permission.ts | 7 ++++++ apps/core-api/src/rbac/role-permission.ts | 22 +++++++++++++++++++ .../src => apps/core-api/src/rbac}/roles.ts | 0 packages/shared/src/index.ts | 3 +-- packages/shared/src/permissions.ts | 8 ------- 6 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 apps/core-api/src/rbac/permission.ts create mode 100644 apps/core-api/src/rbac/role-permission.ts rename {packages/shared/src => apps/core-api/src/rbac}/roles.ts (100%) delete mode 100644 packages/shared/src/permissions.ts diff --git a/apps/core-api/src/middlewares/auth.middleware.ts b/apps/core-api/src/middlewares/auth.middleware.ts index 12edcb9..4fd0f3c 100644 --- a/apps/core-api/src/middlewares/auth.middleware.ts +++ b/apps/core-api/src/middlewares/auth.middleware.ts @@ -2,6 +2,8 @@ import type { Request, Response, NextFunction } from "express"; import { verifyAccessToken } from "../modules/auth/auth.utils.js"; import type { JwtPayload } from "../modules/auth/auth.utils.js"; +import { RolePermissions } from "../rbac/role-permission.js"; +import type { Permission } from "../rbac/permission.js"; export interface AuthRequest extends Request { user?: JwtPayload; @@ -43,3 +45,18 @@ export const authenticate = ( } }; +export const authorizePermission = (permission: Permission) => { + return (req: AuthRequest, _res: Response, next: NextFunction) => { + if (!req.user) { + return next(Object.assign(new Error("Unauthorized"), { statusCode: 401 })); + } + + const permissions = RolePermissions[req.user.role] ?? []; + + if (!permissions.includes(permission)) { + return next(Object.assign(new Error("Forbidden"), { statusCode: 403 })); + } + + next(); + }; +}; diff --git a/apps/core-api/src/rbac/permission.ts b/apps/core-api/src/rbac/permission.ts new file mode 100644 index 0000000..324efe2 --- /dev/null +++ b/apps/core-api/src/rbac/permission.ts @@ -0,0 +1,7 @@ +export enum Permission { + CREATE_PROFILE = "CREATE_PROFILE", + UPDATE_PROFILE = "UPDATE_PROFILE", + APPROVE_SIGNUP = "APPROVE_SIGNUP", + REJECT_SIGNUP = "REJECT_SIGNUP", + MANAGE_USERS = "MANAGE_USERS", +} diff --git a/apps/core-api/src/rbac/role-permission.ts b/apps/core-api/src/rbac/role-permission.ts new file mode 100644 index 0000000..3158102 --- /dev/null +++ b/apps/core-api/src/rbac/role-permission.ts @@ -0,0 +1,22 @@ + +import { UserRole } from "../modules/users/user.model.js"; + +import { Permission } from "./permission.js"; + +export const RolePermissions: Record = { + [UserRole.ADMIN]: [ + Permission.CREATE_PROFILE, + Permission.UPDATE_PROFILE, + Permission.APPROVE_SIGNUP, + Permission.REJECT_SIGNUP, + Permission.MANAGE_USERS, + ], + [UserRole.BRAND]: [ + Permission.CREATE_PROFILE, + Permission.UPDATE_PROFILE, + ], + [UserRole.INFLUENCER]: [ + Permission.CREATE_PROFILE, + Permission.UPDATE_PROFILE, + ], +}; diff --git a/packages/shared/src/roles.ts b/apps/core-api/src/rbac/roles.ts similarity index 100% rename from packages/shared/src/roles.ts rename to apps/core-api/src/rbac/roles.ts diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 41dd29c..dfd0e33 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,4 +1,3 @@ -export * from "./roles"; -export * from "./permissions"; + export * from "./api-types"; export * from "./validators"; diff --git a/packages/shared/src/permissions.ts b/packages/shared/src/permissions.ts deleted file mode 100644 index 54eb28f..0000000 --- a/packages/shared/src/permissions.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const Permissions = { - CREATE_GIG : "create_gig", - BOOK_GiG : "book_gig", - VERIFY_USER : "verify_user", - MANAGE_USER : "manage_user" -} as const; - -export type Permission = (typeof Permissions)[keyof typeof Permissions] From be091883f1e874d905a3bf00e57b56bc418c6bfc Mon Sep 17 00:00:00 2001 From: ShiljaBabu Date: Tue, 10 Feb 2026 17:40:28 +0530 Subject: [PATCH 09/61] feat(auth): role based access control --- apps/web/app/register/page.tsx | 135 +++++++++++++++++++++++++++++++++ apps/web/app/signup/page.tsx | 135 --------------------------------- 2 files changed, 135 insertions(+), 135 deletions(-) delete mode 100644 apps/web/app/signup/page.tsx diff --git a/apps/web/app/register/page.tsx b/apps/web/app/register/page.tsx index e69de29..5b98fa3 100644 --- a/apps/web/app/register/page.tsx +++ b/apps/web/app/register/page.tsx @@ -0,0 +1,135 @@ + +"use client"; + +import { useState } from "react"; + +export default function LoginPage() { + const [role, setRole] = useState<"brand" | "influencer">("brand"); + + return ( +
+
+ +
+

+ Welcome Back +

+

+ Join as a Brand or Influencer + +

+
+ +
+ + + +
+ +
+ +
+ + +
+
+ + +
+ +
+
+ + +
+ + +
+
+ + +
+
+ {role === "brand" ? ( + <> + + + + ) : ( + <> + + + + )} +
+ + + +
+ +

+ Secure login · Data protected +

+
+
+ ); +} \ No newline at end of file diff --git a/apps/web/app/signup/page.tsx b/apps/web/app/signup/page.tsx deleted file mode 100644 index 5b98fa3..0000000 --- a/apps/web/app/signup/page.tsx +++ /dev/null @@ -1,135 +0,0 @@ - -"use client"; - -import { useState } from "react"; - -export default function LoginPage() { - const [role, setRole] = useState<"brand" | "influencer">("brand"); - - return ( -
-
- -
-

- Welcome Back -

-

- Join as a Brand or Influencer - -

-
- -
- - - -
- -
- -
- - -
-
- - -
- -
-
- - -
- - -
-
- - -
-
- {role === "brand" ? ( - <> - - - - ) : ( - <> - - - - )} -
- - - -
- -

- Secure login · Data protected -

-
-
- ); -} \ No newline at end of file From 893cc80e4a577c90d84430afb7192dcce51aa55f Mon Sep 17 00:00:00 2001 From: safvan Date: Wed, 11 Feb 2026 19:42:25 +0530 Subject: [PATCH 10/61] feat(email-verification):OTP-generation --- apps/core-api/package.json | 3 + apps/core-api/src/cache/redis.ts | 34 ++-- .../src/controllers/auth.controller.ts | 81 +++++++++- .../src/models/pendingSignup.models.ts | 58 ++++++- apps/core-api/src/modules/users/user.model.ts | 6 +- .../src/repositories/Signup.repository.ts | 26 ++- apps/core-api/src/routes/auth.routes.ts | 3 +- apps/core-api/src/server.ts | 71 +++++---- apps/core-api/src/services/auth.service.ts | 19 ++- .../src/services/verification.service.ts | 148 ++++++++++++++++-- apps/core-api/src/utils/generateEmailToken.ts | 9 ++ apps/core-api/src/utils/sendEmail.ts | 31 ++++ pnpm-lock.yaml | 37 +++++ 13 files changed, 448 insertions(+), 78 deletions(-) create mode 100644 apps/core-api/src/utils/generateEmailToken.ts create mode 100644 apps/core-api/src/utils/sendEmail.ts diff --git a/apps/core-api/package.json b/apps/core-api/package.json index 4f9827a..30378d6 100644 --- a/apps/core-api/package.json +++ b/apps/core-api/package.json @@ -25,6 +25,8 @@ "meilisearch": "^0.55.0", "mongoose": "^9.1.5", "morgan": "^1.10.1", + "node-cron": "^4.2.1", + "nodemailer": "^8.0.1", "winston": "^3.19.0" }, "devDependencies": { @@ -34,6 +36,7 @@ "@types/jsonwebtoken": "^9.0.10", "@types/morgan": "^1.9.10", "@types/node": "^25.0.10", + "@types/nodemailer": "^7.0.9", "nodemon": "^3.1.11", "ts-node": "^10.9.2", "tsx": "^4.21.0", diff --git a/apps/core-api/src/cache/redis.ts b/apps/core-api/src/cache/redis.ts index bf9c88b..ac874b3 100644 --- a/apps/core-api/src/cache/redis.ts +++ b/apps/core-api/src/cache/redis.ts @@ -1,20 +1,20 @@ -import IORedis from "ioredis"; -import type {Redis} from "ioredis"; +// import IORedis from "ioredis"; +// import type {Redis} from "ioredis"; -import { logger } from "../utils/logger.js"; -const redis_url = process.env.REDIS_URL -if (!redis_url){ - throw new Error("REDIS_URL is not defined in environment variables") -} -export const redis:Redis = new IORedis.default(redis_url) -redis.on("connect", ()=>{ - logger.info("Redis connected successfully") -}) +// import { logger } from "../utils/logger.js"; +// const redis_url = process.env.REDIS_URL +// if (!redis_url){ +// throw new Error("REDIS_URL is not defined in environment variables") +// } +// export const redis:Redis = new IORedis.default(redis_url) +// redis.on("connect", ()=>{ +// logger.info("Redis connected successfully") +// }) -redis.on("reconnecting", () => { - logger.warn("Redis reconnecting.."); -}); +// redis.on("reconnecting", () => { +// logger.warn("Redis reconnecting.."); +// }); -redis.on("error", (err:unknown)=>{ - logger.error(`Redis error:${String(err)}`) -}) +// redis.on("error", (err:unknown)=>{ +// logger.error(`Redis error:${String(err)}`) +// }) diff --git a/apps/core-api/src/controllers/auth.controller.ts b/apps/core-api/src/controllers/auth.controller.ts index 4e91248..c6b45d6 100644 --- a/apps/core-api/src/controllers/auth.controller.ts +++ b/apps/core-api/src/controllers/auth.controller.ts @@ -1,9 +1,18 @@ -import type { NextFunction, Request, Response } from "express"; -import { logoutService, refreshTokenService } from "../services/auth.service.js"; -import { loginService, signupService } from "../services/auth.service.js"; +import type { NextFunction, Request, Response } from "express"; +import jwt from "jsonwebtoken"; + +import { User } from "../modules/users/user.model.js"; +import { + logoutService, + refreshTokenService, + loginService, + signupService, +} from "../services/auth.service.js"; import type { HttpError } from "../modules/auth/http-error.js"; + +// ================= SIGNUP ================= export const signupController = async ( req: Request, res: Response, @@ -27,6 +36,7 @@ export const signupController = async ( res.status(201).json({ success: true, + message: "Signup successful. Please verify your email.", data: result, }); } catch (error) { @@ -34,6 +44,8 @@ export const signupController = async ( } }; + +// ================= LOGIN ================= export const loginController = async ( req: Request, res: Response, @@ -60,6 +72,55 @@ export const loginController = async ( }; +// ================= EMAIL VERIFICATION ================= +export const verifyEmailController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { token } = req.query; + + if (!token) { + const err: HttpError = new Error("Token missing"); + err.statusCode = 400; + throw err; + } + + const decoded = jwt.verify( + token as string, + process.env.EMAIL_VERIFICATION_SECRET as string + ) as { userId: string }; + + const user = await User.findById(decoded.userId); + + if (!user) { + const err: HttpError = new Error("Invalid token"); + err.statusCode = 400; + throw err; + } + + if (user.isEmailVerified) { + return res.status(400).json({ + success: false, + message: "Email already verified", + }); + } + + user.isEmailVerified = true; + await user.save(); + + res.status(200).json({ + success: true, + message: "Email verified successfully. Await admin approval.", + }); + } catch (error) { + next(error); + } +}; + + +// ================= REFRESH TOKEN ================= export const refreshTokenController = async ( req: Request, res: Response, @@ -79,16 +140,23 @@ export const refreshTokenController = async ( } }; + +// ================= LOGOUT ================= export const logoutController = async ( req: Request, res: Response, next: NextFunction ) => { try { - const userId = req.user?.userId; + const userId = req.user?.userId; + if (!userId) { - return res.status(401).json({ success: false, message: "Unauthorized" }); -} + return res.status(401).json({ + success: false, + message: "Unauthorized", + }); + } + const result = await logoutService(userId); res.status(200).json({ @@ -99,3 +167,4 @@ export const logoutController = async ( next(error); } }; + diff --git a/apps/core-api/src/models/pendingSignup.models.ts b/apps/core-api/src/models/pendingSignup.models.ts index 55282d9..120e268 100644 --- a/apps/core-api/src/models/pendingSignup.models.ts +++ b/apps/core-api/src/models/pendingSignup.models.ts @@ -2,21 +2,73 @@ import { Schema, model } from "mongoose"; const PendingSignupSchema = new Schema( { - email: { type: String, required: true, unique: true }, - passwordHash: { type: String, required: true }, + email: { + type: String, + required: true, + unique: true + }, + + passwordHash: { + type: String, + required: true + }, + role: { type: String, enum: ["INFLUENCER", "BRAND"], required: true, }, + documents: [{ type: String }], + status: { type: String, enum: ["PENDING", "APPROVED", "REJECTED"], default: "PENDING", }, + + // 🔐 OTP FIELDS START HERE + + emailOtpHash: { + type: String, + select: false, + default: null, + +}, + + + emailOtpExpiresAt: { + type: Date, + default: null, + }, + + otpAttempts: { + type: Number, + default: 0, + }, + + otpResendCount: { + type: Number, + default: 0, + }, + + otpLastSentAt: { + type: Date, + default: null, + }, + + otpLockedUntil: { + type: Date, + default: null, + }, + + isEmailVerified: { + type: Boolean, + default: false, + }, + }, - { timestamps: true } + { timestamps: true } // gives createdAt & updatedAt automatically ); export const PendingSignup = model( diff --git a/apps/core-api/src/modules/users/user.model.ts b/apps/core-api/src/modules/users/user.model.ts index cf162b0..cddfd39 100644 --- a/apps/core-api/src/modules/users/user.model.ts +++ b/apps/core-api/src/modules/users/user.model.ts @@ -12,6 +12,7 @@ export enum AdminLevel { } export enum UserStatus { + PENDING = "PENDING", // 🔥 add this ACTIVE = "ACTIVE", SUSPENDED = "SUSPENDED", } @@ -59,7 +60,7 @@ const UserSchema = new Schema( status: { type: String, enum: Object.values(UserStatus), - default: UserStatus.ACTIVE, + default: UserStatus.PENDING, // 🔥 start as pending }, refreshToken: { type: String, @@ -68,6 +69,7 @@ const UserSchema = new Schema( }, { timestamps: true } ); + UserSchema.index({ role: 1, status: 1 }); -export const User = model("User", UserSchema) +export const User = model("User", UserSchema); diff --git a/apps/core-api/src/repositories/Signup.repository.ts b/apps/core-api/src/repositories/Signup.repository.ts index 8a36c9b..4b22a46 100644 --- a/apps/core-api/src/repositories/Signup.repository.ts +++ b/apps/core-api/src/repositories/Signup.repository.ts @@ -1,24 +1,35 @@ import { PendingSignup } from "../models/pendingSignup.models.js"; + interface CreatePendingSignupInput { email: string; - role: string; - status: "PENDING" | "APPROVED" | "REJECTED"; passwordHash: string; + role: "INFLUENCER" | "BRAND"; documents: string[]; + status: "PENDING" | "APPROVED" | "REJECTED"; - + // 🔐 OTP fields (optional) + emailOtpHash?: string | null; + emailOtpExpiresAt?: Date | null; + otpAttempts?: number; + otpResendCount?: number; + otpLastSentAt?: Date | null; + otpLockedUntil?: Date | null; + isEmailVerified?: boolean; } class PendingSignupRepository { + // ================= CREATE ================= create(data: CreatePendingSignupInput) { return PendingSignup.create(data); } + // ================= FIND ================= findByEmail(email: string) { return PendingSignup.findOne({ email }); } - + + // ================= UPDATE STATUS ================= updateStatus(email: string, status: "APPROVED" | "REJECTED") { return PendingSignup.findOneAndUpdate( { email }, @@ -27,9 +38,16 @@ class PendingSignupRepository { ); } + // ================= DELETE ONE ================= deleteByEmail(email: string) { return PendingSignup.findOneAndDelete({ email }); } + + // ================= DELETE MANY (FOR CLEANUP) ================= + deleteMany(filter: Record): Promise { + return PendingSignup.deleteMany(filter); +} + } export const pendingSignupRepository = diff --git a/apps/core-api/src/routes/auth.routes.ts b/apps/core-api/src/routes/auth.routes.ts index 1745136..28764c5 100644 --- a/apps/core-api/src/routes/auth.routes.ts +++ b/apps/core-api/src/routes/auth.routes.ts @@ -1,6 +1,6 @@ import { Router } from "express"; -import { loginController, logoutController, refreshTokenController, signupController } from "../controllers/auth.controller.js"; +import { loginController, logoutController, refreshTokenController, signupController, verifyEmailController } from "../controllers/auth.controller.js"; import { authenticate } from "../middlewares/auth.middleware.js"; const router: Router = Router() @@ -8,5 +8,6 @@ router.post("/login", loginController) router.post("/signup", signupController); router.post("/refresh", refreshTokenController); router.post("/logout", authenticate, logoutController); +router.get("/verify-email", verifyEmailController); export default router \ No newline at end of file diff --git a/apps/core-api/src/server.ts b/apps/core-api/src/server.ts index 5814bc5..a616a0f 100644 --- a/apps/core-api/src/server.ts +++ b/apps/core-api/src/server.ts @@ -1,35 +1,46 @@ -import "dotenv/config" -import express from "express" - -import { httpLogger } from "./middlewares/httpLogger.js" -import { logger } from "./utils/logger.js" -import { errorHandler, notFound } from "./middlewares/errorHandler.js" -import { connectRabbit } from "./queue/rabbit.js" -import "./cache/redis.js"; +import "dotenv/config"; +import express from "express"; +import cron from "node-cron"; + +import { httpLogger } from "./middlewares/httpLogger.js"; +import { logger } from "./utils/logger.js"; +import { errorHandler, notFound } from "./middlewares/errorHandler.js"; +import { connectRabbit } from "./queue/rabbit.js"; +import "./cache/redis.js"; import "./search/meili.js"; -import router from "./routes/index.js" -import { connectDB } from "./db/connect.js" +import router from "./routes/index.js"; +import { connectDB } from "./db/connect.js"; +import { cleanupExpiredSignups } from "./services/verification.service.js"; + +const app = express(); +const PORT = Number(process.env.PORT) || 5000; + +app.use(httpLogger); +app.use(express.json()); +app.use("/api", router); +// 🔥 Connect Database +connectDB(); -const app = express() -const PORT = Number(process.env.PORT) || 5000 -app.use(httpLogger) -app.use(express.json()) -app.use("/api", router) -connectDB() +// 🔥 CRON JOB (Runs every hour) +cron.schedule("0 * * * *", async () => { + logger.info("Running cleanup for expired pending signups..."); + await cleanupExpiredSignups(); +}); app.get("/health", (req, res) => { - res.status(200).json({ - status: "ok", - service: "core-api", - timestamp: new Date().toISOString() - }) -}) - -app.use(notFound) -app.use(errorHandler) -connectRabbit() -app.listen(PORT,"127.0.0.1", () => { - logger.info(`Core API is running at http://localhost:${PORT}`); - -}) \ No newline at end of file + res.status(200).json({ + status: "ok", + service: "core-api", + timestamp: new Date().toISOString(), + }); +}); + +app.use(notFound); +app.use(errorHandler); + +connectRabbit(); + +app.listen(PORT, "127.0.0.1", () => { + logger.info(`Core API is running at http://localhost:${PORT}`); +}); diff --git a/apps/core-api/src/services/auth.service.ts b/apps/core-api/src/services/auth.service.ts index 2b386d4..080c614 100644 --- a/apps/core-api/src/services/auth.service.ts +++ b/apps/core-api/src/services/auth.service.ts @@ -51,19 +51,36 @@ export const signupService = async (data: SignupInput) => { const passwordHash = await bcrypt.hash(data.password, 10); + // 🔥 GENERATE OTP + const otp = Math.floor(100000 + Math.random() * 900000).toString(); + console.log("Generated OTP:", otp); // 👈 ADD THIS LINE + const hashedOtp = await bcrypt.hash(otp, 10); + await pendingSignupRepository.create({ email: data.email.toLowerCase(), passwordHash, role: data.role, documents: data.documents, status: "PENDING", + + // 🔐 OTP fields + emailOtpHash: hashedOtp, + emailOtpExpiresAt: new Date(Date.now() + 5 * 60 * 1000), + otpAttempts: 0, + otpResendCount: 0, + otpLastSentAt: new Date(), + isEmailVerified: false, }); - return { message: "Signup request submitted for review" }; + // 🔥 SEND OTP EMAIL HERE + // await sendOtpEmail(data.email, otp); + + return { message: "OTP sent to your email" }; }; + interface LoginResult { accessToken: string, refreshToken: string, diff --git a/apps/core-api/src/services/verification.service.ts b/apps/core-api/src/services/verification.service.ts index 5da8839..3e94991 100644 --- a/apps/core-api/src/services/verification.service.ts +++ b/apps/core-api/src/services/verification.service.ts @@ -1,8 +1,130 @@ +import bcrypt from "bcrypt"; + import type { HttpError } from "../modules/auth/http-error.js"; import { pendingSignupRepository } from "../repositories/Signup.repository.js"; import { userRepository } from "../repositories/user.repository.js"; -// import { sendMail } from "../../utils/nodemailer"; +// ================= VERIFY OTP ================= +export const verifyOtpService = async ( + email: string, + otp: string +) => { + const pending = await pendingSignupRepository.findByEmail(email); + + if (!pending) { + const err: HttpError = new Error("Signup request not found"); + err.statusCode = 404; + throw err; + } + + if (pending.isEmailVerified) { + const err: HttpError = new Error("Email already verified"); + err.statusCode = 400; + throw err; + } + + const now = new Date(); + + // 🔒 Lock check + if (pending.otpLockedUntil && pending.otpLockedUntil > now) { + const err: HttpError = new Error("Too many attempts. Try again later."); + err.statusCode = 403; + throw err; + } + + // ⏳ Expiry check + if (!pending.emailOtpExpiresAt || pending.emailOtpExpiresAt < now) { + const err: HttpError = new Error("OTP expired"); + err.statusCode = 400; + throw err; + } + + const isMatch = await bcrypt.compare( + otp, + pending.emailOtpHash as string + ); + + if (!isMatch) { + pending.otpAttempts = (pending.otpAttempts || 0) + 1; + + if (pending.otpAttempts >= 5) { + pending.otpLockedUntil = new Date( + now.getTime() + 15 * 60 * 1000 // lock for 15 minutes + ); + } + + await pending.save(); + + const err: HttpError = new Error("Invalid OTP"); + err.statusCode = 401; + throw err; + } + + // ✅ SUCCESS + pending.isEmailVerified = true; + pending.emailOtpHash = null; + pending.emailOtpExpiresAt = null; + pending.otpAttempts = 0; + pending.otpLockedUntil = null; + + await pending.save(); + + return { message: "Email verified successfully" }; +}; + +// ================= RESEND OTP ================= +export const resendOtpService = async (email: string) => { + const pending = await pendingSignupRepository.findByEmail(email); + + if (!pending) { + const err: HttpError = new Error("Signup request not found"); + err.statusCode = 404; + throw err; + } + + if (pending.isEmailVerified) { + const err: HttpError = new Error("Email already verified"); + err.statusCode = 400; + throw err; + } + + const now = new Date(); + + // ⏳ Cooldown check (60 seconds) + if ( + pending.otpLastSentAt && + now.getTime() - pending.otpLastSentAt.getTime() < 60 * 1000 + ) { + const err: HttpError = new Error("Please wait before requesting another OTP"); + err.statusCode = 429; + throw err; + } + + // 🔁 Max resend limit + if ((pending.otpResendCount || 0) >= 5) { + const err: HttpError = new Error("Maximum resend attempts reached"); + err.statusCode = 403; + throw err; + } + + // 🔐 Generate new OTP + const otp = Math.floor(100000 + Math.random() * 900000).toString(); + const hashedOtp = await bcrypt.hash(otp, 10); + + pending.emailOtpHash = hashedOtp; + pending.emailOtpExpiresAt = new Date(now.getTime() + 5 * 60 * 1000); + pending.otpResendCount = (pending.otpResendCount || 0) + 1; + pending.otpLastSentAt = now; + + await pending.save(); + + // TODO: Send OTP email here + // await sendOtpEmail(email, otp); + + return { message: "OTP resent successfully" }; +}; + +// ================= APPROVE SIGNUP ================= export const approveSignupService = async (email: string) => { const pending = await pendingSignupRepository.findByEmail(email); @@ -18,7 +140,6 @@ export const approveSignupService = async (email: string) => { throw err; } - await userRepository.create({ email: pending.email, password: pending.passwordHash, @@ -29,15 +150,10 @@ export const approveSignupService = async (email: string) => { await pendingSignupRepository.deleteByEmail(email); - // await sendMail( - // email, - // "Signup Approved", - // "Your account has been approved. You can now log in." - // ); - return { message: "Signup approved successfully" }; }; +// ================= REJECT SIGNUP ================= export const rejectSignupService = async ( email: string, reason?: string @@ -52,11 +168,15 @@ export const rejectSignupService = async ( await pendingSignupRepository.deleteByEmail(email); - // await sendMail( - // email, - // "Signup Rejected", - // reason ?? "Your signup request was rejected by the admin." - // ); - return { message: "Signup rejected successfully" }; }; + +// ================= CLEANUP EXPIRED SIGNUPS ================= +export const cleanupExpiredSignups = async () => { + const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago + + await pendingSignupRepository.deleteMany({ + isEmailVerified: false, + createdAt: { $lt: cutoff }, + }); +}; diff --git a/apps/core-api/src/utils/generateEmailToken.ts b/apps/core-api/src/utils/generateEmailToken.ts new file mode 100644 index 0000000..7ab7210 --- /dev/null +++ b/apps/core-api/src/utils/generateEmailToken.ts @@ -0,0 +1,9 @@ +import jwt from "jsonwebtoken"; + +export const generateEmailVerificationToken = (userId: string) => { + return jwt.sign( + { userId }, + process.env.EMAIL_VERIFICATION_SECRET as string, + { expiresIn: "20m" } + ); +}; diff --git a/apps/core-api/src/utils/sendEmail.ts b/apps/core-api/src/utils/sendEmail.ts new file mode 100644 index 0000000..e5908bb --- /dev/null +++ b/apps/core-api/src/utils/sendEmail.ts @@ -0,0 +1,31 @@ +import nodemailer from "nodemailer"; + +export const sendEmailVerification = async ( + email: string, + token: string +) => { + const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, + }); + + const verificationLink = `${process.env.CLIENT_URL}/verify-email?token=${token}`; + + await transporter.sendMail({ + from: process.env.EMAIL_USER, + to: email, + subject: "Verify Your Account - Marketplace", + html: ` +

Welcome to Marketplace

+

Click the button below to verify your email:

+ + Verify Email + +

This link expires in 20 minutes.

+ `, + }); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce458cc..d2a3e5d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,6 +70,12 @@ importers: morgan: specifier: ^1.10.1 version: 1.10.1 + node-cron: + specifier: ^4.2.1 + version: 4.2.1 + nodemailer: + specifier: ^8.0.1 + version: 8.0.1 winston: specifier: ^3.19.0 version: 3.19.0 @@ -92,6 +98,9 @@ importers: "@types/node": specifier: ^25.0.10 version: 25.0.10 + "@types/nodemailer": + specifier: ^7.0.9 + version: 7.0.9 nodemon: specifier: ^3.1.11 version: 3.1.11 @@ -1347,6 +1356,12 @@ packages: integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==, } + "@types/nodemailer@7.0.9": + resolution: + { + integrity: sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow==, + } + "@types/qs@6.14.0": resolution: { @@ -3932,6 +3947,13 @@ packages: } engines: { node: ^18 || ^20 || >= 21 } + node-cron@4.2.1: + resolution: + { + integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==, + } + engines: { node: ">=6.0.0" } + node-gyp-build@4.8.4: resolution: { @@ -3945,6 +3967,13 @@ packages: integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==, } + nodemailer@8.0.1: + resolution: + { + integrity: sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==, + } + engines: { node: ">=6.0.0" } + nodemon@3.1.11: resolution: { @@ -5749,6 +5778,10 @@ snapshots: dependencies: undici-types: 7.16.0 + "@types/nodemailer@7.0.9": + dependencies: + "@types/node": 25.0.10 + "@types/qs@6.14.0": {} "@types/range-parser@1.2.7": {} @@ -7481,10 +7514,14 @@ snapshots: node-addon-api@8.5.0: {} + node-cron@4.2.1: {} + node-gyp-build@4.8.4: {} node-releases@2.0.27: {} + nodemailer@8.0.1: {} + nodemon@3.1.11: dependencies: chokidar: 3.6.0 From 305b77c3582cb1703da0c08dc725ca242fe412a4 Mon Sep 17 00:00:00 2001 From: ajay Date: Fri, 13 Feb 2026 10:41:52 +0530 Subject: [PATCH 11/61] feat(web): login and signup frontend integration --- apps/core-api/src/cache/redis.ts | 1 + .../src/controllers/auth.controller.ts | 6 +- .../src/models/pendingSignup.models.ts | 2 +- .../src/repositories/Signup.repository.ts | 2 +- apps/core-api/src/server.ts | 5 +- apps/core-api/src/services/auth.service.ts | 2 +- apps/realtime/src/server.ts | 6 +- apps/web/app/login/page.tsx | 73 +++- apps/web/app/signup/page.tsx | 341 ++++++++++++------ apps/web/lib/axios.client.ts | 38 ++ package.json | 4 + packages/shared/src/roles.ts | 2 +- pnpm-lock.yaml | 7 + 13 files changed, 362 insertions(+), 127 deletions(-) create mode 100644 apps/web/lib/axios.client.ts diff --git a/apps/core-api/src/cache/redis.ts b/apps/core-api/src/cache/redis.ts index bf9c88b..b34c2b3 100644 --- a/apps/core-api/src/cache/redis.ts +++ b/apps/core-api/src/cache/redis.ts @@ -2,6 +2,7 @@ import IORedis from "ioredis"; import type {Redis} from "ioredis"; import { logger } from "../utils/logger.js"; + const redis_url = process.env.REDIS_URL if (!redis_url){ throw new Error("REDIS_URL is not defined in environment variables") diff --git a/apps/core-api/src/controllers/auth.controller.ts b/apps/core-api/src/controllers/auth.controller.ts index 4e91248..29bb366 100644 --- a/apps/core-api/src/controllers/auth.controller.ts +++ b/apps/core-api/src/controllers/auth.controller.ts @@ -1,4 +1,4 @@ -import type { NextFunction, Request, Response } from "express"; +import type { NextFunction, Request, Response } from "express"; import { logoutService, refreshTokenService } from "../services/auth.service.js"; import { loginService, signupService } from "../services/auth.service.js"; @@ -10,7 +10,7 @@ export const signupController = async ( next: NextFunction ) => { try { - const { email, password, role, documents } = req.body; + const { email, password, role, documents: businessInfo } = req.body; if (!email || !password || !role) { const err: HttpError = new Error("Missing required fields"); @@ -22,7 +22,7 @@ export const signupController = async ( email, password, role, - documents: documents ?? [], + documents: businessInfo , }); res.status(201).json({ diff --git a/apps/core-api/src/models/pendingSignup.models.ts b/apps/core-api/src/models/pendingSignup.models.ts index 55282d9..523a83e 100644 --- a/apps/core-api/src/models/pendingSignup.models.ts +++ b/apps/core-api/src/models/pendingSignup.models.ts @@ -9,7 +9,7 @@ const PendingSignupSchema = new Schema( enum: ["INFLUENCER", "BRAND"], required: true, }, - documents: [{ type: String }], + documents: { type: String }, status: { type: String, enum: ["PENDING", "APPROVED", "REJECTED"], diff --git a/apps/core-api/src/repositories/Signup.repository.ts b/apps/core-api/src/repositories/Signup.repository.ts index 8a36c9b..1d98141 100644 --- a/apps/core-api/src/repositories/Signup.repository.ts +++ b/apps/core-api/src/repositories/Signup.repository.ts @@ -5,7 +5,7 @@ interface CreatePendingSignupInput { role: string; status: "PENDING" | "APPROVED" | "REJECTED"; passwordHash: string; - documents: string[]; + documents: string; } diff --git a/apps/core-api/src/server.ts b/apps/core-api/src/server.ts index 5814bc5..889047e 100644 --- a/apps/core-api/src/server.ts +++ b/apps/core-api/src/server.ts @@ -1,5 +1,6 @@ import "dotenv/config" import express from "express" +import cors from "cors" import { httpLogger } from "./middlewares/httpLogger.js" import { logger } from "./utils/logger.js" @@ -15,7 +16,9 @@ const app = express() const PORT = Number(process.env.PORT) || 5000 app.use(httpLogger) app.use(express.json()) -app.use("/api", router) +app.use(cors({origin: "*"})) +app.use("/", router) + connectDB() app.get("/health", (req, res) => { diff --git a/apps/core-api/src/services/auth.service.ts b/apps/core-api/src/services/auth.service.ts index 2b386d4..772b24f 100644 --- a/apps/core-api/src/services/auth.service.ts +++ b/apps/core-api/src/services/auth.service.ts @@ -29,7 +29,7 @@ interface SignupInput { email: string; password: string; role: "INFLUENCER" | "BRAND"; - documents: string[]; + documents: string; } export const signupService = async (data: SignupInput) => { diff --git a/apps/realtime/src/server.ts b/apps/realtime/src/server.ts index 8dd1d2c..83573eb 100644 --- a/apps/realtime/src/server.ts +++ b/apps/realtime/src/server.ts @@ -1,13 +1,15 @@ -import { createServer } from "http"; +import { createServer } from "http"; import express from "express"; -import { Server } from "socket.io"; +import cors from "cors"; +import { Server } from "socket.io"; import { httpLogger } from "./middlewares/httpLogger"; import { logger } from "./utils/logger"; import { errorHandler, notFound } from "./middlewares/errorHandler"; const app = express(); +app.use(cors()) app.use(express.json()) app.use(httpLogger) const httpServer = createServer(app); diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx index a1e6674..e31a477 100644 --- a/apps/web/app/login/page.tsx +++ b/apps/web/app/login/page.tsx @@ -1,9 +1,56 @@ "use client"; -import { useState } from "react"; +import React, { useState } from "react"; + +import api from "@/lib/axios.client"; export default function LoginPage() { - const [role, setRole] = useState<"brand" | "influencer">("brand"); + const [role, setRole] = useState<"BRAND" | "INFLUENCER">("BRAND"); + const[formData,setFormData]=useState({ + email:"", + password:"" + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const handleInputChange=(e: React.ChangeEvent)=>{ + const {name,value}=e.target; + setFormData(prev=>({ + ...prev, + [name]:value + + })); + } + + const handleSubmit=async (e:React.FormEvent)=>{ + e.preventDefault(); + setLoading(true); + setError(""); + console.log("In submit") + try { + const payload={ + email:formData.email, + password:formData.password, + role:role + } + + const response=await api.post("/auth/login",payload); + + if(response.data.accessToken){ + localStorage.setItem("accessToken",response.data.accessToken); + window.location.href="/signup"; + } + + } catch (error) { + const errorMessage=error instanceof Error ? error.message : "Login failed. Please try again."; + setError(errorMessage); + } finally { + setLoading(false); + } + + + } + return (
@@ -22,9 +69,9 @@ export default function LoginPage() {
-
+ + {error &&( +
{error}
+ )}
@@ -72,6 +125,9 @@ export default function LoginPage() {
@@ -80,7 +136,8 @@ export default function LoginPage() { type="submit" className="w-full py-2.5 rounded-lg font-medium text-white transition hover:cursor-pointer bg-[#059669] hover:bg-[#047857]" > - Sign In as {role === "brand" ? "Brand" : "Influencer"} + Sign In as {role === "BRAND" ? "Brand" : "Influencer"} + {loading && ...} diff --git a/apps/web/app/signup/page.tsx b/apps/web/app/signup/page.tsx index 5b98fa3..c6b6a3c 100644 --- a/apps/web/app/signup/page.tsx +++ b/apps/web/app/signup/page.tsx @@ -3,8 +3,85 @@ import { useState } from "react"; +import api from "@/lib/axios.client"; + export default function LoginPage() { - const [role, setRole] = useState<"brand" | "influencer">("brand"); + const [role, setRole] = useState<"BRAND" | "INFLUENCER">("BRAND"); + const [formData, setFormData] = useState({ + fullName: "", + email: "", + password: "", + confirmPassword: "", + businessInfo: "", + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(false); + + const isValidBusinessInfo = (info: string) => { + const gstRegex = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/; + const cinRegex = /^[LUu]{1}[0-9]{5}[A-Z]{2}[0-9]{4}[A-Z]{3}[0-9]{6}$/; + const llpRegex = /^[A-Z]{3}-[0-9]{4}$/; + + return gstRegex.test(info) || cinRegex.test(info) || llpRegex.test(info); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + + if (formData.password !== formData.confirmPassword) { + setError("Passwords do not match"); + return; + } + + if (role === "BRAND" && !isValidBusinessInfo(formData.businessInfo)) { + setError("Please enter a valid GST, CIN, or LLP Number (e.g., AAA-1234)"); + return; + } + + setLoading(true); + try { + const payload = { + fullName: formData.fullName, + email: formData.email, + password: formData.password, + role: role, + ...(role === "BRAND" + ? { gstNumber: formData.businessInfo } + : { socialMediaHandle: formData.businessInfo } + ), + }; + + const response = await api.post("/auth/signup", payload); + + if (response.data.accessToken) { + // localStorage.setItem("accessToken", response.data.accessToken); + // window.location.href = "/dashboard"; + setFormData({ + fullName: "", + email: "", + password: "", + confirmPassword: "", + businessInfo: "", + }); + } + setSuccess(true); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Signup failed. Please try again."; + setError(errorMessage); + } finally { + setLoading(false); + } + }; return (
@@ -12,123 +89,169 @@ export default function LoginPage() {

- Welcome Back + {success ? "Registration Successful" : "Welcome Back"}

- Join as a Brand or Influencer - + {success + ? "Your account has been created successfully." + : "Join as a Brand or Influencer"}

-
- - - -
- -
- -
- - -
-
- - + {success ? ( +
+
+ Application Submitted +
+

+ Please wait for admin approval. We will notify you via email once your account is verified. +

+
+ ) : ( + <> +
+ -
-
- - +
- -
-
- - -
-
- {role === "brand" ? ( - <> - - - - ) : ( - <> - - - - )} -
- - - - - -

- Secure login · Data protected -

+
+ {error && ( +
+ {error} +
+ )} + +
+ + +
+
+ + +
+ +
+
+ + +
+ + +
+
+ + +
+
+ {role === "BRAND" ? ( + <> + + + + ) : ( + <> + + + + )} +
+ + + +
+ +

+ Secure login · Data protected +

+ + )}
); diff --git a/apps/web/lib/axios.client.ts b/apps/web/lib/axios.client.ts new file mode 100644 index 0000000..83c6dcc --- /dev/null +++ b/apps/web/lib/axios.client.ts @@ -0,0 +1,38 @@ +import axios from 'axios'; + +const api=axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, + // withCredentials: true ,// Required to send the Refresh Token Cookie automatically! + + headers:{ + "Content-Type": "application/json" + } +}) + +//Request interceptor +api.interceptors.request.use( + (config) => { + const token =localStorage.getItem("accessToken"); + + if (token){ + config.headers.Authorization=`Bearer ${token}` + } + return config + }, + (error)=>Promise.reject(error) +) + +//Response Interceptor +api.interceptors.response.use( + (response)=>response, + (error)=>{ + if(error.response?.status===401){ + localStorage.removeItem("accessToken"); + // window.location.href="/signup" + } + return Promise.reject(error); + } +) + + +export default api; \ No newline at end of file diff --git a/package.json b/package.json index 419ad72..ca55ec6 100644 --- a/package.json +++ b/package.json @@ -35,5 +35,9 @@ "*.{json,md,yml,yaml}": [ "prettier --write" ] + }, + "dependencies": { + "@types/cors": "^2.8.19", + "cors": "^2.8.6" } } diff --git a/packages/shared/src/roles.ts b/packages/shared/src/roles.ts index 47e132f..6e3f85f 100644 --- a/packages/shared/src/roles.ts +++ b/packages/shared/src/roles.ts @@ -1,7 +1,7 @@ export const Roles = { ADMIN: "admin", INFLUENCER: "influencer", - BRAND: "brand", + BRAND: "BRAND", } as const; export type Role = (typeof Roles)[keyof typeof Roles]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce458cc..20c43c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,13 @@ settings: importers: .: + dependencies: + "@types/cors": + specifier: ^2.8.19 + version: 2.8.19 + cors: + specifier: ^2.8.6 + version: 2.8.6 devDependencies: "@eslint/eslintrc": specifier: ^3.3.3 From f414a91f88f2007634e244c90968a9d592ccf266 Mon Sep 17 00:00:00 2001 From: safvan Date: Fri, 13 Feb 2026 11:12:43 +0530 Subject: [PATCH 12/61] feat(emailverify):email verification --- apps/core-api/src/server.ts | 4 +-- apps/core-api/src/services/auth.service.ts | 6 ++-- apps/core-api/src/utils/generateEmailToken.ts | 9 ------ apps/core-api/src/utils/sendEmail.ts | 31 ------------------- apps/core-api/src/utils/sendotpEmail.ts | 21 +++++++++++++ 5 files changed, 27 insertions(+), 44 deletions(-) delete mode 100644 apps/core-api/src/utils/generateEmailToken.ts delete mode 100644 apps/core-api/src/utils/sendEmail.ts create mode 100644 apps/core-api/src/utils/sendotpEmail.ts diff --git a/apps/core-api/src/server.ts b/apps/core-api/src/server.ts index a616a0f..e966e4f 100644 --- a/apps/core-api/src/server.ts +++ b/apps/core-api/src/server.ts @@ -19,10 +19,10 @@ app.use(httpLogger); app.use(express.json()); app.use("/api", router); -// 🔥 Connect Database +// Connect Database connectDB(); -// 🔥 CRON JOB (Runs every hour) +// CRON JOB (Runs every hour) cron.schedule("0 * * * *", async () => { logger.info("Running cleanup for expired pending signups..."); await cleanupExpiredSignups(); diff --git a/apps/core-api/src/services/auth.service.ts b/apps/core-api/src/services/auth.service.ts index 080c614..5bd5fc0 100644 --- a/apps/core-api/src/services/auth.service.ts +++ b/apps/core-api/src/services/auth.service.ts @@ -22,6 +22,8 @@ import type { HttpError } from "../modules/auth/http-error.js"; import { userRepository } from "../repositories/user.repository.js" import { pendingSignupRepository } from "../repositories/Signup.repository.js"; import { logger } from "../utils/logger.js"; +import { sendOtpEmail } from "../utils/sendotpEmail.js"; + // import { sendMail } from "../../utils/nodemailer"; @@ -51,9 +53,9 @@ export const signupService = async (data: SignupInput) => { const passwordHash = await bcrypt.hash(data.password, 10); - // 🔥 GENERATE OTP + // GENERATE OTP const otp = Math.floor(100000 + Math.random() * 900000).toString(); - console.log("Generated OTP:", otp); // 👈 ADD THIS LINE + await sendOtpEmail(data.email, otp); const hashedOtp = await bcrypt.hash(otp, 10); await pendingSignupRepository.create({ diff --git a/apps/core-api/src/utils/generateEmailToken.ts b/apps/core-api/src/utils/generateEmailToken.ts deleted file mode 100644 index 7ab7210..0000000 --- a/apps/core-api/src/utils/generateEmailToken.ts +++ /dev/null @@ -1,9 +0,0 @@ -import jwt from "jsonwebtoken"; - -export const generateEmailVerificationToken = (userId: string) => { - return jwt.sign( - { userId }, - process.env.EMAIL_VERIFICATION_SECRET as string, - { expiresIn: "20m" } - ); -}; diff --git a/apps/core-api/src/utils/sendEmail.ts b/apps/core-api/src/utils/sendEmail.ts deleted file mode 100644 index e5908bb..0000000 --- a/apps/core-api/src/utils/sendEmail.ts +++ /dev/null @@ -1,31 +0,0 @@ -import nodemailer from "nodemailer"; - -export const sendEmailVerification = async ( - email: string, - token: string -) => { - const transporter = nodemailer.createTransport({ - service: "gmail", - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASS, - }, - }); - - const verificationLink = `${process.env.CLIENT_URL}/verify-email?token=${token}`; - - await transporter.sendMail({ - from: process.env.EMAIL_USER, - to: email, - subject: "Verify Your Account - Marketplace", - html: ` -

Welcome to Marketplace

-

Click the button below to verify your email:

- - Verify Email - -

This link expires in 20 minutes.

- `, - }); -}; diff --git a/apps/core-api/src/utils/sendotpEmail.ts b/apps/core-api/src/utils/sendotpEmail.ts new file mode 100644 index 0000000..f6c402e --- /dev/null +++ b/apps/core-api/src/utils/sendotpEmail.ts @@ -0,0 +1,21 @@ +import nodemailer from "nodemailer"; + +export const sendOtpEmail = async ( + to: string, + otp: string +) => { + const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, + }); + + await transporter.sendMail({ + from: process.env.EMAIL_USER, + to, + subject: "Your OTP Code", + text: `Your verification OTP is: ${otp}`, + }); +}; From e68ad6bc3f736dbec17820a4c88c0b236733caed Mon Sep 17 00:00:00 2001 From: safvan Date: Fri, 13 Feb 2026 11:27:08 +0530 Subject: [PATCH 13/61] chore(emailVerification):completed --- apps/web/package.json | 1 + pnpm-lock.yaml | 92 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 818d6cd..1bb460f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "axios": "^1.13.5", "next": "16.1.4", "react": "19.2.3", "react-dom": "19.2.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 581c786..f177857 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -157,6 +157,9 @@ importers: apps/web: dependencies: + axios: + specifier: ^1.13.5 + version: 1.13.5 next: specifier: 16.1.4 version: 16.1.4(@babel/core@7.28.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -1934,6 +1937,12 @@ packages: integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==, } + asynckit@0.4.0: + resolution: + { + integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==, + } + available-typed-arrays@1.0.7: resolution: { @@ -1948,6 +1957,12 @@ packages: } engines: { node: ">=4" } + axios@1.13.5: + resolution: + { + integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==, + } + axobject-query@4.1.0: resolution: { @@ -2178,6 +2193,13 @@ packages: integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==, } + combined-stream@1.0.8: + resolution: + { + integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==, + } + engines: { node: ">= 0.8" } + commander@14.0.2: resolution: { @@ -2332,6 +2354,13 @@ packages: } engines: { node: ">= 0.4" } + delayed-stream@1.0.0: + resolution: + { + integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==, + } + engines: { node: ">=0.4.0" } + denque@2.1.0: resolution: { @@ -2828,6 +2857,18 @@ packages: integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==, } + follow-redirects@1.15.11: + resolution: + { + integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==, + } + engines: { node: ">=4.0" } + peerDependencies: + debug: "*" + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: { @@ -2835,6 +2876,13 @@ packages: } engines: { node: ">= 0.4" } + form-data@4.0.5: + resolution: + { + integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==, + } + engines: { node: ">= 6" } + forwarded@0.2.0: resolution: { @@ -4237,6 +4285,12 @@ packages: } engines: { node: ">= 0.10" } + proxy-from-env@1.1.0: + resolution: + { + integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==, + } + pstree.remy@1.1.8: resolution: { @@ -6187,12 +6241,22 @@ snapshots: async@3.2.6: {} + asynckit@0.4.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 axe-core@4.11.1: {} + axios@1.13.5: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} balanced-match@1.0.2: {} @@ -6329,6 +6393,10 @@ snapshots: colorette@2.0.20: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@14.0.2: {} concat-map@0.0.1: {} @@ -6406,6 +6474,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delayed-stream@1.0.0: {} + denque@2.1.0: {} depd@2.0.0: {} @@ -6608,7 +6678,7 @@ snapshots: "@next/eslint-plugin-next": 16.1.4 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) @@ -6635,7 +6705,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): dependencies: "@nolyfill/is-core-module": 1.0.39 debug: 4.4.3(supports-color@5.5.0) @@ -6650,14 +6720,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: "@typescript-eslint/parser": 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -6672,7 +6742,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -6904,10 +6974,20 @@ snapshots: fn.name@1.1.0: {} + follow-redirects@1.15.11: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -7682,6 +7762,8 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-from-env@1.1.0: {} + pstree.remy@1.1.8: {} punycode@2.3.1: {} From 51d75745d955e606ae3c565687bba47c3bd94880 Mon Sep 17 00:00:00 2001 From: ajay Date: Fri, 13 Feb 2026 17:35:56 +0530 Subject: [PATCH 14/61] feat(core-api):Influencer and Brand schema created --- apps/core-api/package.json | 2 + .../src/controllers/auth.controller.ts | 46 ++++++++- apps/core-api/src/models/brand.model.ts | 85 ++++++++++++++++ apps/core-api/src/models/influencer.model.ts | 89 +++++++++++++++++ .../{modules/users => models}/user.model.ts | 0 apps/core-api/src/modules/auth/auth.types.ts | 2 +- apps/core-api/src/modules/auth/auth.utils.ts | 2 +- apps/core-api/src/rbac/role-permission.ts | 2 +- .../src/repositories/user.repository.ts | 30 +++--- apps/core-api/src/routes/auth.routes.ts | 2 +- apps/core-api/src/server.ts | 8 +- apps/web/app/login/page.tsx | 85 ++++++++-------- apps/web/lib/axios.client.ts | 97 ++++++++++++++++--- pnpm-lock.yaml | 38 ++++++++ 14 files changed, 402 insertions(+), 86 deletions(-) create mode 100644 apps/core-api/src/models/brand.model.ts create mode 100644 apps/core-api/src/models/influencer.model.ts rename apps/core-api/src/{modules/users => models}/user.model.ts (100%) diff --git a/apps/core-api/package.json b/apps/core-api/package.json index 30378d6..e2348d9 100644 --- a/apps/core-api/package.json +++ b/apps/core-api/package.json @@ -18,6 +18,7 @@ "dependencies": { "amqplib": "^0.10.9", "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", "dotenv": "^17.2.3", "express": "^5.2.1", "ioredis": "^5.9.2", @@ -32,6 +33,7 @@ "devDependencies": { "@types/amqplib": "^0.10.8", "@types/bcrypt": "^6.0.0", + "@types/cookie-parser": "^1.4.10", "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/morgan": "^1.9.10", diff --git a/apps/core-api/src/controllers/auth.controller.ts b/apps/core-api/src/controllers/auth.controller.ts index b8c9eac..a8f8057 100644 --- a/apps/core-api/src/controllers/auth.controller.ts +++ b/apps/core-api/src/controllers/auth.controller.ts @@ -1,7 +1,7 @@ import type { NextFunction, Request, Response } from "express"; import jwt from "jsonwebtoken"; -import { User } from "../modules/users/user.model.js"; +import { User } from "../models/user.model.js"; import { logoutService, refreshTokenService, @@ -30,7 +30,7 @@ export const signupController = async ( email, password, role, - documents: businessInfo , + documents: businessInfo, }); res.status(201).json({ @@ -61,9 +61,21 @@ export const loginController = async ( const data = await loginService(email, password); + // Set refresh token as HttpOnly cookie + res.cookie("refreshToken", data.refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days + }); + + // Send only access token and user info in response res.status(200).json({ success: true, - data, + data: { + accessToken: data.accessToken, + user: data.user, + }, }); } catch (error) { next(error); @@ -126,13 +138,30 @@ export const refreshTokenController = async ( next: NextFunction ) => { try { - const { refreshToken } = req.body; + const refreshToken = req.cookies.refreshToken; + + if (!refreshToken) { + const err: HttpError = new Error("Refresh token missing"); + err.statusCode = 401; + throw err; + } const result = await refreshTokenService(refreshToken); + // Set new refresh token as HttpOnly cookie + res.cookie("refreshToken", result.refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days + }); + + // Send only access token in response res.status(200).json({ success: true, - data: result, + data: { + accessToken: result.accessToken, + }, }); } catch (error) { next(error); @@ -158,6 +187,13 @@ export const logoutController = async ( const result = await logoutService(userId); + // Clear refresh token cookie + res.clearCookie("refreshToken", { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + }); + res.status(200).json({ success: true, data: result, diff --git a/apps/core-api/src/models/brand.model.ts b/apps/core-api/src/models/brand.model.ts new file mode 100644 index 0000000..98caa46 --- /dev/null +++ b/apps/core-api/src/models/brand.model.ts @@ -0,0 +1,85 @@ +import { Schema, model, Types, Document } from "mongoose"; + +export interface IBrandProfile extends Document { + userId: Types.ObjectId; + + companyName: string; + industry: string; + website?: string; + + contactPersonName: string; + contactEmail: string; + contactPhone?: string; + + businessRegistrationNumber?: string; + gstNumber?: string; + companySize?: string; + + documents: string[]; // S3 keys + + isProfileComplete: boolean; + isVerified: boolean; +} + +const BrandProfileSchema = new Schema( + { + userId: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + unique: true, + index: true, + }, + + companyName: { + type: String, + required: true, + trim: true, + }, + + industry: { + type: String, + required: true, + }, + + website: String, + + contactPersonName: { + type: String, + required: true, + }, + + contactEmail: { + type: String, + required: true, + lowercase: true, + }, + + contactPhone: String, + + businessRegistrationNumber: String, + gstNumber: String, + companySize: String, + + documents: { + type: [String], // store S3 keys + default: [], + }, + + isProfileComplete: { + type: Boolean, + default: false, + }, + + isVerified: { + type: Boolean, + default: false, + }, + }, + { timestamps: true } +); + +export const BrandProfile = model( + "BrandProfile", + BrandProfileSchema +); \ No newline at end of file diff --git a/apps/core-api/src/models/influencer.model.ts b/apps/core-api/src/models/influencer.model.ts new file mode 100644 index 0000000..d8a0454 --- /dev/null +++ b/apps/core-api/src/models/influencer.model.ts @@ -0,0 +1,89 @@ +import { Schema, model, Types, Document } from "mongoose"; + +export interface IInfluencerProfile extends Document { + userId: Types.ObjectId; + + fullName: string; + username: string; + bio?: string; + + instagramUrl?: string; + youtubeUrl?: string; + tiktokUrl?: string; + + categories: string[]; + location?: string; + languages: string[]; + + followersCount?: number; + engagementRate?: number; + + isProfileComplete: boolean; + isVerified: boolean; +} + +const InfluencerProfileSchema = new Schema( + { + userId: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + unique: true, + index: true, + }, + + fullName: { + type: String, + required: true, + trim: true, + }, + + username: { + type: String, + required: true, + unique: true, + trim: true, + lowercase: true, + }, + + bio: { + type: String, + maxlength: 500, + }, + + instagramUrl: String, + youtubeUrl: String, + tiktokUrl: String, + + categories: { + type: [String], + default: [], + }, + + location: String, + + languages: { + type: [String], + default: [], + }, + + followersCount: Number, + engagementRate: Number, + + isProfileComplete: { + type: Boolean, + default: false, + }, + + isVerified: { + type: Boolean, + default: false, + }, + }, + { timestamps: true } +); + +export const InfluencerProfile = model( + "InfluencerProfile", + InfluencerProfileSchema +); \ No newline at end of file diff --git a/apps/core-api/src/modules/users/user.model.ts b/apps/core-api/src/models/user.model.ts similarity index 100% rename from apps/core-api/src/modules/users/user.model.ts rename to apps/core-api/src/models/user.model.ts diff --git a/apps/core-api/src/modules/auth/auth.types.ts b/apps/core-api/src/modules/auth/auth.types.ts index fd13016..6dd6f27 100644 --- a/apps/core-api/src/modules/auth/auth.types.ts +++ b/apps/core-api/src/modules/auth/auth.types.ts @@ -1,4 +1,4 @@ -import { UserRole, AdminLevel } from "../users/user.model.js"; +import { UserRole, AdminLevel } from "../../models/user.model.js"; export interface JwtPayload { userId: string; diff --git a/apps/core-api/src/modules/auth/auth.utils.ts b/apps/core-api/src/modules/auth/auth.utils.ts index d39f884..62fca42 100644 --- a/apps/core-api/src/modules/auth/auth.utils.ts +++ b/apps/core-api/src/modules/auth/auth.utils.ts @@ -1,6 +1,6 @@ import jwt from "jsonwebtoken"; -import type { AdminLevel, UserRole } from "../users/user.model.js"; +import type { AdminLevel, UserRole } from "../../models/user.model.js"; const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET!; diff --git a/apps/core-api/src/rbac/role-permission.ts b/apps/core-api/src/rbac/role-permission.ts index 3158102..a46245e 100644 --- a/apps/core-api/src/rbac/role-permission.ts +++ b/apps/core-api/src/rbac/role-permission.ts @@ -1,5 +1,5 @@ -import { UserRole } from "../modules/users/user.model.js"; +import { UserRole } from "../models/user.model.js"; import { Permission } from "./permission.js"; diff --git a/apps/core-api/src/repositories/user.repository.ts b/apps/core-api/src/repositories/user.repository.ts index 4962cf8..0cd5c03 100644 --- a/apps/core-api/src/repositories/user.repository.ts +++ b/apps/core-api/src/repositories/user.repository.ts @@ -1,27 +1,27 @@ -import { User } from "../modules/users/user.model.js"; +import { User } from "../models/user.model.js"; -class UserRepository{ - async findEmailWithPassword(email: string) { - const normalizedEmail = email.trim().toLowerCase(); +class UserRepository { + async findEmailWithPassword(email: string) { + const normalizedEmail = email.trim().toLowerCase(); return User.findOne({ email: normalizedEmail }).select("+password"); -} - - + } + + async findByEmail(email: string) { return User.findOne({ email }); } - async findById(userId: string) { - return User.findById(userId).select("+refreshToken"); -} + async findById(userId: string) { + return User.findById(userId).select("+refreshToken"); + } - async saveRefreshToken(userId:string, refreshToken:string){ - return User.findByIdAndUpdate(userId,{refreshToken}, { new: true } -) - } - + async saveRefreshToken(userId: string, refreshToken: string) { + return User.findByIdAndUpdate(userId, { refreshToken }, { new: true } + ) + } + async create(data: { email: string; password: string; diff --git a/apps/core-api/src/routes/auth.routes.ts b/apps/core-api/src/routes/auth.routes.ts index 28764c5..4bed24c 100644 --- a/apps/core-api/src/routes/auth.routes.ts +++ b/apps/core-api/src/routes/auth.routes.ts @@ -10,4 +10,4 @@ router.post("/refresh", refreshTokenController); router.post("/logout", authenticate, logoutController); router.get("/verify-email", verifyEmailController); -export default router \ No newline at end of file +export default router \ No newline at end of file diff --git a/apps/core-api/src/server.ts b/apps/core-api/src/server.ts index 06c9786..0af61e8 100644 --- a/apps/core-api/src/server.ts +++ b/apps/core-api/src/server.ts @@ -2,6 +2,7 @@ import cors from "cors" import "dotenv/config"; import express from "express"; +import cookieParser from "cookie-parser"; import cron from "node-cron"; import { httpLogger } from "./middlewares/httpLogger.js"; @@ -20,8 +21,11 @@ const app = express() const PORT = Number(process.env.PORT) || 5000 app.use(httpLogger) app.use(express.json()) -app.use(cors({origin: "*"})) -app.use("/", router) +app.use(cookieParser()); +app.use(cors({ + origin: process.env.FRONTEND_URL || "http://localhost:3000", + credentials: true, +})) connectDB() app.use(httpLogger); diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx index e31a477..0e9ef3b 100644 --- a/apps/web/app/login/page.tsx +++ b/apps/web/app/login/page.tsx @@ -1,56 +1,55 @@ "use client"; -import React, { useState } from "react"; +import React, { useState } from "react"; -import api from "@/lib/axios.client"; +import api, { setAccessToken } from "@/lib/axios.client"; export default function LoginPage() { const [role, setRole] = useState<"BRAND" | "INFLUENCER">("BRAND"); - const[formData,setFormData]=useState({ - email:"", - password:"" + const [formData, setFormData] = useState({ + email: "", + password: "" }); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); - const handleInputChange=(e: React.ChangeEvent)=>{ - const {name,value}=e.target; - setFormData(prev=>({ + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, - [name]:value - + [name]: value + })); } - const handleSubmit=async (e:React.FormEvent)=>{ - e.preventDefault(); - setLoading(true); - setError(""); - console.log("In submit") - try { - const payload={ - email:formData.email, - password:formData.password, - role:role + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(""); + try { + const payload = { + email: formData.email, + password: formData.password, + role: role } - const response=await api.post("/auth/login",payload); - - if(response.data.accessToken){ - localStorage.setItem("accessToken",response.data.accessToken); - window.location.href="/signup"; - } + const response = await api.post("/auth/login", payload); - } catch (error) { - const errorMessage=error instanceof Error ? error.message : "Login failed. Please try again."; - setError(errorMessage); - } finally { - setLoading(false); + // Access token is now in response.data.data.accessToken + if (response.data.data?.accessToken) { + setAccessToken(response.data.data.accessToken); + window.location.href = "/dashboard"; } - + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Login failed. Please try again."; + setError(errorMessage); + } finally { + setLoading(false); } - + } + return (
@@ -70,11 +69,10 @@ export default function LoginPage() { @@ -82,18 +80,17 @@ export default function LoginPage() {
- {error &&( + {error && (
{error}
)}
diff --git a/apps/web/lib/axios.client.ts b/apps/web/lib/axios.client.ts index 83c6dcc..2db0017 100644 --- a/apps/web/lib/axios.client.ts +++ b/apps/web/lib/axios.client.ts @@ -1,35 +1,100 @@ import axios from 'axios'; -const api=axios.create({ - baseURL: process.env.NEXT_PUBLIC_API_URL, - // withCredentials: true ,// Required to send the Refresh Token Cookie automatically! +// Store access token in memory (more secure than localStorage) +let accessToken: string | null = null; + +export const setAccessToken = (token: string | null) => { + accessToken = token; +}; - headers:{ +export const getAccessToken = () => accessToken; + +const api = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, + withCredentials: true, // Required to send cookies automatically! + headers: { "Content-Type": "application/json" } }) -//Request interceptor +// Request interceptor api.interceptors.request.use( (config) => { - const token =localStorage.getItem("accessToken"); + const token = getAccessToken(); - if (token){ - config.headers.Authorization=`Bearer ${token}` + if (token) { + config.headers.Authorization = `Bearer ${token}` } - return config + return config }, - (error)=>Promise.reject(error) + (error) => Promise.reject(error) ) -//Response Interceptor +// Response Interceptor with token refresh +let isRefreshing = false; +let failedQueue: Array<{ resolve: (value?: unknown) => void; reject: (reason?: unknown) => void }> = []; + +const processQueue = (error: Error | null, token: string | null = null) => { + failedQueue.forEach(prom => { + if (error) { + prom.reject(error); + } else { + prom.resolve(token); + } + }); + failedQueue = []; +}; + api.interceptors.response.use( - (response)=>response, - (error)=>{ - if(error.response?.status===401){ - localStorage.removeItem("accessToken"); - // window.location.href="/signup" + (response) => response, + async (error) => { + const originalRequest = error.config; + + // If error is 401 and we haven't tried to refresh yet + if (error.response?.status === 401 && !originalRequest._retry) { + if (isRefreshing) { + // If already refreshing, queue this request + return new Promise((resolve, reject) => { + failedQueue.push({ resolve, reject }); + }).then(token => { + originalRequest.headers.Authorization = `Bearer ${token}`; + return api(originalRequest); + }).catch(err => { + return Promise.reject(err); + }); + } + + originalRequest._retry = true; + isRefreshing = true; + + try { + // Try to refresh the token + const response = await api.post("/auth/refresh"); + const newAccessToken = response.data.data.accessToken; + + // Save new access token in memory + setAccessToken(newAccessToken); + + // Update authorization header + api.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`; + originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; + + // Process queued requests + processQueue(null, newAccessToken); + + // Retry original request + return api(originalRequest); + } catch (refreshError) { + // Refresh failed, clear token and redirect to login + processQueue(refreshError as Error, null); + setAccessToken(null); + window.location.href = "/signup"; + return Promise.reject(refreshError); + } finally { + isRefreshing = false; + } } + return Promise.reject(error); } ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 581c786..63d432f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: bcrypt: specifier: ^6.0.0 version: 6.0.0 + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 dotenv: specifier: ^17.2.3 version: 17.2.3 @@ -93,6 +96,9 @@ importers: "@types/bcrypt": specifier: ^6.0.0 version: 6.0.0 + "@types/cookie-parser": + specifier: ^1.4.10 + version: 1.4.10(@types/express@5.0.6) "@types/express": specifier: ^5.0.6 version: 5.0.6 @@ -1291,6 +1297,14 @@ packages: integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==, } + "@types/cookie-parser@1.4.10": + resolution: + { + integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==, + } + peerDependencies: + "@types/express": "*" + "@types/cors@2.8.19": resolution: { @@ -2211,6 +2225,19 @@ packages: integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, } + cookie-parser@1.4.7: + resolution: + { + integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==, + } + engines: { node: ">= 0.8.0" } + + cookie-signature@1.0.6: + resolution: + { + integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==, + } + cookie-signature@1.2.2: resolution: { @@ -5741,6 +5768,10 @@ snapshots: dependencies: "@types/node": 25.0.10 + "@types/cookie-parser@1.4.10(@types/express@5.0.6)": + dependencies: + "@types/express": 5.0.6 + "@types/cors@2.8.19": dependencies: "@types/node": 25.0.10 @@ -6339,6 +6370,13 @@ snapshots: convert-source-map@2.0.0: {} + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + + cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} From 6061028ab8066ca31de9a3aca4db0fdb6244a24c Mon Sep 17 00:00:00 2001 From: safvan Date: Sat, 14 Feb 2026 10:38:33 +0530 Subject: [PATCH 15/61] feat(Reset-Pass):reset-password --- .../src/controllers/auth.controller.ts | 87 ++++++++++++ apps/core-api/src/modules/users/user.model.ts | 41 +++++- .../src/repositories/user.repository.ts | 129 +++++++++++++++--- apps/core-api/src/routes/auth.routes.ts | 6 +- apps/core-api/src/services/auth.service.ts | 119 +++++++++++++++- 5 files changed, 360 insertions(+), 22 deletions(-) diff --git a/apps/core-api/src/controllers/auth.controller.ts b/apps/core-api/src/controllers/auth.controller.ts index b8c9eac..b999398 100644 --- a/apps/core-api/src/controllers/auth.controller.ts +++ b/apps/core-api/src/controllers/auth.controller.ts @@ -7,8 +7,12 @@ import { refreshTokenService, loginService, signupService, + forgotPasswordService, + verifyOtpService, + resetPasswordService, } from "../services/auth.service.js"; import type { HttpError } from "../modules/auth/http-error.js"; +// import { log } from "winston"; // ================= SIGNUP ================= @@ -167,3 +171,86 @@ export const logoutController = async ( } }; +// ================= FORGOT PASSWORD ================= +export const forgotPasswordController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { email } = req.body; + + if (!email) { + const err: HttpError = new Error("Email is required"); + err.statusCode = 400; + throw err; + } + + await forgotPasswordService(email); + + res.status(200).json({ + success: true, + message: "If account exists, OTP sent", + + }); + + } catch (error) { + next(error); + } +}; + +// ================= VERIFY OTP ================= +export const verifyOtpController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { email, otp } = req.body; + + if (!email || !otp) { + const err: HttpError = new Error("Email and OTP required"); + err.statusCode = 400; + throw err; + } + + const resetSessionToken = await verifyOtpService(email, otp); + + res.status(200).json({ + success: true, + message: "OTP verified", + data: { resetSessionToken }, + }); + } catch (error) { + next(error); + } +}; + +// ================= RESET PASSWORD ================= +export const resetPasswordController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { email, newPassword, resetSessionToken } = req.body; + + if (!email || !newPassword || !resetSessionToken) { + const err: HttpError = new Error("Missing required fields"); + err.statusCode = 400; + throw err; + } + + await resetPasswordService(email, newPassword, resetSessionToken); + + res.status(200).json({ + success: true, + message: "Password reset successful", + }); + } catch (error) { + next(error); + } +}; + + + diff --git a/apps/core-api/src/modules/users/user.model.ts b/apps/core-api/src/modules/users/user.model.ts index cddfd39..f0e225f 100644 --- a/apps/core-api/src/modules/users/user.model.ts +++ b/apps/core-api/src/modules/users/user.model.ts @@ -12,7 +12,7 @@ export enum AdminLevel { } export enum UserStatus { - PENDING = "PENDING", // 🔥 add this + PENDING = "PENDING", ACTIVE = "ACTIVE", SUSPENDED = "SUSPENDED", } @@ -25,6 +25,12 @@ export interface IUser extends Document { isEmailVerified: boolean; status: UserStatus; refreshToken?: string; + + // 🔐 Forgot Password Fields + resetOtp?: string; + resetOtpExpiry?: Date; + resetSessionToken?: string; + resetSessionExpiry?: Date; } const UserSchema = new Schema( @@ -38,38 +44,67 @@ const UserSchema = new Schema( index: true, immutable: true, }, + password: { type: String, required: true, - select: false, + select: false, // 🔐 never return password }, + role: { type: String, enum: Object.values(UserRole), required: true, }, + adminLevel: { type: String, enum: Object.values(AdminLevel), default: null, }, + isEmailVerified: { type: Boolean, default: false, }, + status: { type: String, enum: Object.values(UserStatus), - default: UserStatus.PENDING, // 🔥 start as pending + default: UserStatus.PENDING, }, + refreshToken: { type: String, select: false, }, + + // ================================ + // 🔐 FORGOT PASSWORD FIELDS + // ================================ + + resetOtp: { + type: String, + select: false, // never return OTP + }, + + resetOtpExpiry: { + type: Date, + }, + + resetSessionToken: { + type: String, + select: false, // never expose session token + }, + + resetSessionExpiry: { + type: Date, + }, }, { timestamps: true } ); +// compound index (already yours) UserSchema.index({ role: 1, status: 1 }); export const User = model("User", UserSchema); diff --git a/apps/core-api/src/repositories/user.repository.ts b/apps/core-api/src/repositories/user.repository.ts index 4962cf8..eb14d3b 100644 --- a/apps/core-api/src/repositories/user.repository.ts +++ b/apps/core-api/src/repositories/user.repository.ts @@ -1,27 +1,123 @@ import { User } from "../modules/users/user.model.js"; -class UserRepository{ - async findEmailWithPassword(email: string) { - const normalizedEmail = email.trim().toLowerCase(); +class UserRepository { + // =============================== + // FIND USER WITH PASSWORD + // =============================== + async findEmailWithPassword(email: string) { + const normalizedEmail = email.trim().toLowerCase(); return User.findOne({ email: normalizedEmail }).select("+password"); -} - - + } + + // =============================== + // FIND USER WITH RESET FIELDS + // =============================== + async findByEmailWithResetFields(email: string) { + const normalizedEmail = email.trim().toLowerCase(); + + return User.findOne({ email: normalizedEmail }) + .select("+password +resetOtp +resetSessionToken"); + } + + // =============================== + // NORMAL FIND BY EMAIL + // =============================== async findByEmail(email: string) { - return User.findOne({ email }); + const normalizedEmail = email.trim().toLowerCase(); + return User.findOne({ email: normalizedEmail }); } - async findById(userId: string) { - return User.findById(userId).select("+refreshToken"); -} + // =============================== + // FIND BY ID + // =============================== + async findById(userId: string) { + return User.findById(userId).select("+refreshToken"); + } + + // =============================== + // SAVE REFRESH TOKEN + // =============================== + async saveRefreshToken(userId: string, refreshToken: string) { + return User.findByIdAndUpdate( + userId, + { refreshToken }, + { new: true } + ); + } + // =============================== + // SAVE RESET OTP + // =============================== + async saveResetOtp( + userId: string, + hashedOtp: string, + expiry: Date + ) { + return User.findByIdAndUpdate( + userId, + { + resetOtp: hashedOtp, + resetOtpExpiry: expiry, + }, + { new: true } + ); + } - async saveRefreshToken(userId:string, refreshToken:string){ - return User.findByIdAndUpdate(userId,{refreshToken}, { new: true } -) - } - + // =============================== + // SAVE RESET SESSION TOKEN + // =============================== + async saveResetSession( + userId: string, + token: string, + expiry: Date + ) { + return User.findByIdAndUpdate( + userId, + { + resetOtp: undefined, + resetOtpExpiry: undefined, + resetSessionToken: token, + resetSessionExpiry: expiry, + }, + { new: true } + ); + } + + // =============================== + // UPDATE PASSWORD (Production-Safe) + // =============================== + async updatePassword(userId: string, hashedPassword: string) { + return User.findByIdAndUpdate( + userId, + { + password: hashedPassword, + refreshToken: "", // 🔐 invalidate all sessions + }, + { + new: true, + runValidators: true, + } + ); + } + + // =============================== + // CLEAR RESET SESSION + // =============================== + async clearResetSession(userId: string) { + return User.findByIdAndUpdate( + userId, + { + resetSessionToken: undefined, + resetSessionExpiry: undefined, + }, + { new: true } + ); + } + + // =============================== + // CREATE USER + // =============================== async create(data: { email: string; password: string; @@ -33,5 +129,4 @@ class UserRepository{ } } - -export const userRepository = new UserRepository() \ No newline at end of file +export const userRepository = new UserRepository(); diff --git a/apps/core-api/src/routes/auth.routes.ts b/apps/core-api/src/routes/auth.routes.ts index 28764c5..f7898ce 100644 --- a/apps/core-api/src/routes/auth.routes.ts +++ b/apps/core-api/src/routes/auth.routes.ts @@ -1,6 +1,6 @@ import { Router } from "express"; -import { loginController, logoutController, refreshTokenController, signupController, verifyEmailController } from "../controllers/auth.controller.js"; +import { forgotPasswordController, loginController, logoutController, refreshTokenController, resetPasswordController, signupController, verifyEmailController, verifyOtpController } from "../controllers/auth.controller.js"; import { authenticate } from "../middlewares/auth.middleware.js"; const router: Router = Router() @@ -9,5 +9,9 @@ router.post("/signup", signupController); router.post("/refresh", refreshTokenController); router.post("/logout", authenticate, logoutController); router.get("/verify-email", verifyEmailController); +router.post("/forgot-password", forgotPasswordController); +router.post("/verify-otp", verifyOtpController); +router.post("/reset-password", resetPasswordController); + export default router \ No newline at end of file diff --git a/apps/core-api/src/services/auth.service.ts b/apps/core-api/src/services/auth.service.ts index d38c769..d1cfa75 100644 --- a/apps/core-api/src/services/auth.service.ts +++ b/apps/core-api/src/services/auth.service.ts @@ -15,7 +15,9 @@ // } // export const authService = new AuthService(); -import bcrypt from "bcrypt"; +import crypto from "crypto"; + +import bcrypt from "bcrypt"; import { signAccessToken, signRefreshToken, verifyRefreshToken } from "../modules/auth/auth.utils.js"; import type { HttpError } from "../modules/auth/http-error.js"; @@ -209,3 +211,118 @@ export const logoutService = async (userId: string) => { return { message: "Logged out successfully" }; }; + + +// forgotPasswordService + +export const forgotPasswordService = async (email: string) => { + const user = await userRepository.findEmailWithPassword(email); + + if (!user) return; // security: don't reveal existence + + const otp = Math.floor(100000 + Math.random() * 900000).toString(); + console.log("RESET OTP:", otp); // 👈 ADD THIS LINE + console.log("Forgot password controller triggered"); + + const hashedOtp = await bcrypt.hash(otp, 10); + + const expiry = new Date(Date.now() + 10 * 60 * 1000); + + await userRepository.saveResetOtp( + user._id.toString(), + hashedOtp, + expiry + ); + + await sendOtpEmail(email, otp); + + logger.info(`Reset OTP sent to ${email}`); +}; + +// verifyOtpService + +export const verifyOtpService = async ( + email: string, + otp: string +): Promise => { + + const user = await userRepository.findByEmailWithResetFields(email); + + if (!user || !user.resetOtp || !user.resetOtpExpiry) { + const err: HttpError = new Error("Invalid request"); + err.statusCode = 400; + throw err; + } + + if (user.resetOtpExpiry < new Date()) { + const err: HttpError = new Error("OTP expired"); + err.statusCode = 400; + throw err; + } + + const isMatch = await bcrypt.compare(otp, user.resetOtp); + + if (!isMatch) { + const err: HttpError = new Error("Invalid OTP"); + err.statusCode = 400; + throw err; + } + + const resetSessionToken = crypto.randomBytes(32).toString("hex"); + + const sessionExpiry = new Date(Date.now() + 10 * 60 * 1000); + + await userRepository.saveResetSession( + user._id.toString(), + resetSessionToken, + sessionExpiry + ); + + return resetSessionToken; +}; + +// resetPasswordService + +export const resetPasswordService = async ( + email: string, + newPassword: string, + resetSessionToken: string +) => { + + const user = await userRepository.findByEmailWithResetFields(email); + + if ( + !user || + !user.resetSessionToken || + !user.resetSessionExpiry + ) { + const err: HttpError = new Error("Invalid request"); + err.statusCode = 400; + throw err; + } + + if (user.resetSessionToken !== resetSessionToken) { + const err: HttpError = new Error("Invalid session"); + err.statusCode = 400; + throw err; + } + + if (user.resetSessionExpiry < new Date()) { + const err: HttpError = new Error("Session expired"); + err.statusCode = 400; + throw err; + } + + const hashedPassword = await bcrypt.hash(newPassword, 10); + + await userRepository.updatePassword( + user._id.toString(), + hashedPassword + ); + + await userRepository.clearResetSession(user._id.toString()); + + logger.info(`Password reset successful for ${email}`); +}; + + From d79e82e4ede32235ee77d8811ff540869ab66cc6 Mon Sep 17 00:00:00 2001 From: safvan Date: Sat, 14 Feb 2026 10:55:23 +0530 Subject: [PATCH 16/61] feat(Reset-Pass):Reset-Passwords --- .../src/repositories/user.repository.ts | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/apps/core-api/src/repositories/user.repository.ts b/apps/core-api/src/repositories/user.repository.ts index eb14d3b..36fd97a 100644 --- a/apps/core-api/src/repositories/user.repository.ts +++ b/apps/core-api/src/repositories/user.repository.ts @@ -2,17 +2,17 @@ import { User } from "../modules/users/user.model.js"; class UserRepository { - // =============================== + // FIND USER WITH PASSWORD - // =============================== + async findEmailWithPassword(email: string) { const normalizedEmail = email.trim().toLowerCase(); return User.findOne({ email: normalizedEmail }).select("+password"); } - // =============================== + // FIND USER WITH RESET FIELDS - // =============================== + async findByEmailWithResetFields(email: string) { const normalizedEmail = email.trim().toLowerCase(); @@ -20,24 +20,23 @@ class UserRepository { .select("+password +resetOtp +resetSessionToken"); } - // =============================== + // NORMAL FIND BY EMAIL - // =============================== + async findByEmail(email: string) { const normalizedEmail = email.trim().toLowerCase(); return User.findOne({ email: normalizedEmail }); } - // =============================== // FIND BY ID - // =============================== + async findById(userId: string) { return User.findById(userId).select("+refreshToken"); } - // =============================== + // SAVE REFRESH TOKEN - // =============================== + async saveRefreshToken(userId: string, refreshToken: string) { return User.findByIdAndUpdate( userId, @@ -46,9 +45,9 @@ class UserRepository { ); } - // =============================== + // SAVE RESET OTP - // =============================== + async saveResetOtp( userId: string, hashedOtp: string, @@ -64,9 +63,9 @@ class UserRepository { ); } - // =============================== + // SAVE RESET SESSION TOKEN - // =============================== + async saveResetSession( userId: string, token: string, @@ -84,9 +83,9 @@ class UserRepository { ); } - // =============================== + // UPDATE PASSWORD (Production-Safe) - // =============================== + async updatePassword(userId: string, hashedPassword: string) { return User.findByIdAndUpdate( userId, @@ -101,9 +100,9 @@ class UserRepository { ); } - // =============================== + // CLEAR RESET SESSION - // =============================== + async clearResetSession(userId: string) { return User.findByIdAndUpdate( userId, @@ -115,9 +114,9 @@ class UserRepository { ); } - // =============================== + // CREATE USER - // =============================== + async create(data: { email: string; password: string; From bd4e2e38f574399c37cada4fe1c84793e0703be2 Mon Sep 17 00:00:00 2001 From: ajay Date: Sat, 14 Feb 2026 11:28:33 +0530 Subject: [PATCH 17/61] chore:user model --- apps/core-api/src/models/user.model.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/core-api/src/models/user.model.ts b/apps/core-api/src/models/user.model.ts index cddfd39..25a58c8 100644 --- a/apps/core-api/src/models/user.model.ts +++ b/apps/core-api/src/models/user.model.ts @@ -12,7 +12,7 @@ export enum AdminLevel { } export enum UserStatus { - PENDING = "PENDING", // 🔥 add this + PENDING = "PENDING", // add this ACTIVE = "ACTIVE", SUSPENDED = "SUSPENDED", } @@ -60,7 +60,7 @@ const UserSchema = new Schema( status: { type: String, enum: Object.values(UserStatus), - default: UserStatus.PENDING, // 🔥 start as pending + default: UserStatus.PENDING, // start as pending }, refreshToken: { type: String, From 4fca1106881c93c2b9e5bf639f6668bc59507355 Mon Sep 17 00:00:00 2001 From: ajay Date: Sat, 14 Feb 2026 11:35:08 +0530 Subject: [PATCH 18/61] Revert "chore:user model" This reverts commit bd4e2e38f574399c37cada4fe1c84793e0703be2. --- apps/core-api/src/models/user.model.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/core-api/src/models/user.model.ts b/apps/core-api/src/models/user.model.ts index 25a58c8..cddfd39 100644 --- a/apps/core-api/src/models/user.model.ts +++ b/apps/core-api/src/models/user.model.ts @@ -12,7 +12,7 @@ export enum AdminLevel { } export enum UserStatus { - PENDING = "PENDING", // add this + PENDING = "PENDING", // 🔥 add this ACTIVE = "ACTIVE", SUSPENDED = "SUSPENDED", } @@ -60,7 +60,7 @@ const UserSchema = new Schema( status: { type: String, enum: Object.values(UserStatus), - default: UserStatus.PENDING, // start as pending + default: UserStatus.PENDING, // 🔥 start as pending }, refreshToken: { type: String, From 4d87fa246f979d38cdf46cb97266bfb0dbe73a80 Mon Sep 17 00:00:00 2001 From: ShiljaBabu Date: Sun, 15 Feb 2026 12:45:36 +0530 Subject: [PATCH 19/61] feat(auth):implemented zustand for state management --- apps/core-api/src/server.ts | 1 + apps/web/app/login/page.tsx | 10 +++++++--- apps/web/app/signup/page.tsx | 2 +- apps/web/lib/axios.client.ts | 17 ++++++----------- apps/web/package.json | 3 ++- apps/web/store/auth.store.ts | 25 +++++++++++++++++++++++++ pnpm-lock.yaml | 29 +++++++++++++++++++++++++++++ 7 files changed, 71 insertions(+), 16 deletions(-) create mode 100644 apps/web/store/auth.store.ts diff --git a/apps/core-api/src/server.ts b/apps/core-api/src/server.ts index fde2763..8ead6a0 100644 --- a/apps/core-api/src/server.ts +++ b/apps/core-api/src/server.ts @@ -30,6 +30,7 @@ app.use(cors({ connectDB() app.use(httpLogger); app.use(express.json()); + app.use("/api", router); // Connect Database diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx index 0e9ef3b..e377843 100644 --- a/apps/web/app/login/page.tsx +++ b/apps/web/app/login/page.tsx @@ -2,7 +2,10 @@ import React, { useState } from "react"; -import api, { setAccessToken } from "@/lib/axios.client"; +import api from "@/lib/axios.client"; +import { useAuthStore } from "@/store/auth.store"; + + export default function LoginPage() { const [role, setRole] = useState<"BRAND" | "INFLUENCER">("BRAND"); @@ -12,7 +15,7 @@ export default function LoginPage() { }); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); - + const setAuth = useAuthStore((state)=>state.setAuth) const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; @@ -38,7 +41,8 @@ export default function LoginPage() { // Access token is now in response.data.data.accessToken if (response.data.data?.accessToken) { - setAccessToken(response.data.data.accessToken); + const {accessToken, user} = response.data.data + setAuth(accessToken, user) window.location.href = "/dashboard"; } diff --git a/apps/web/app/signup/page.tsx b/apps/web/app/signup/page.tsx index c6b6a3c..f281847 100644 --- a/apps/web/app/signup/page.tsx +++ b/apps/web/app/signup/page.tsx @@ -61,7 +61,7 @@ export default function LoginPage() { ), }; - const response = await api.post("/auth/signup", payload); + const response = await api.post("api/auth/signup", payload); if (response.data.accessToken) { // localStorage.setItem("accessToken", response.data.accessToken); diff --git a/apps/web/lib/axios.client.ts b/apps/web/lib/axios.client.ts index 2db0017..ae7c2d1 100644 --- a/apps/web/lib/axios.client.ts +++ b/apps/web/lib/axios.client.ts @@ -1,13 +1,7 @@ import axios from 'axios'; -// Store access token in memory (more secure than localStorage) -let accessToken: string | null = null; +import { useAuthStore } from '@/store/auth.store'; -export const setAccessToken = (token: string | null) => { - accessToken = token; -}; - -export const getAccessToken = () => accessToken; const api = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL, @@ -17,10 +11,11 @@ const api = axios.create({ } }) + // Request interceptor api.interceptors.request.use( (config) => { - const token = getAccessToken(); + const token = useAuthStore.getState().accessToken; if (token) { config.headers.Authorization = `Bearer ${token}` @@ -71,9 +66,9 @@ api.interceptors.response.use( // Try to refresh the token const response = await api.post("/auth/refresh"); const newAccessToken = response.data.data.accessToken; - + const currentUser = useAuthStore.getState().user // Save new access token in memory - setAccessToken(newAccessToken); + useAuthStore.getState().setAuth(newAccessToken, currentUser); // Update authorization header api.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`; @@ -87,7 +82,7 @@ api.interceptors.response.use( } catch (refreshError) { // Refresh failed, clear token and redirect to login processQueue(refreshError as Error, null); - setAccessToken(null); + useAuthStore.getState().clearAuth(); window.location.href = "/signup"; return Promise.reject(refreshError); } finally { diff --git a/apps/web/package.json b/apps/web/package.json index 1bb460f..3b4e959 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,7 +12,8 @@ "axios": "^1.13.5", "next": "16.1.4", "react": "19.2.3", - "react-dom": "19.2.3" + "react-dom": "19.2.3", + "zustand": "^5.0.11" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/apps/web/store/auth.store.ts b/apps/web/store/auth.store.ts new file mode 100644 index 0000000..beabaa5 --- /dev/null +++ b/apps/web/store/auth.store.ts @@ -0,0 +1,25 @@ +import { create } from "zustand"; + +interface User { + id: string; + role: "ADMIN" | "BRAND" | "INFLUENCER"; + adminLevel?: "SUPER" | "NORMAL" | null; +} + +interface AuthState { + accessToken: string | null; + user: User | null; + setAuth: (token: string, user: User) => void; + clearAuth: () => void; +} + +export const useAuthStore = create((set) => ({ + accessToken: null, + user: null, + + setAuth: (token, user) => + set({ accessToken: token, user }), + + clearAuth: () => + set({ accessToken: null, user: null }) +})); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2de6f90..a6a6ca3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -175,6 +175,9 @@ importers: react-dom: specifier: 19.2.3 version: 19.2.3(react@19.2.3) + zustand: + specifier: ^5.0.11 + version: 5.0.11(@types/react@19.2.9)(react@19.2.3) devDependencies: "@tailwindcss/postcss": specifier: ^4 @@ -5266,6 +5269,27 @@ packages: integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==, } + zustand@5.0.11: + resolution: + { + integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==, + } + engines: { node: ">=12.20.0" } + peerDependencies: + "@types/react": ">=18.0.0" + immer: ">=9.0.6" + react: ">=18.0.0" + use-sync-external-store: ">=1.2.0" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: "@alloc/quick-lru@5.2.0": {} @@ -8498,3 +8522,8 @@ snapshots: zod: 4.3.5 zod@4.3.5: {} + + zustand@5.0.11(@types/react@19.2.9)(react@19.2.3): + optionalDependencies: + "@types/react": 19.2.9 + react: 19.2.3 From 5748d5ba33bfe95d5dfaf6604aaace93a9182877 Mon Sep 17 00:00:00 2001 From: ShiljaBabu Date: Sun, 15 Feb 2026 12:55:51 +0530 Subject: [PATCH 20/61] feat(auth):implemented zustand for state management --- apps/web/lib/axios.client.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/web/lib/axios.client.ts b/apps/web/lib/axios.client.ts index ae7c2d1..1a69129 100644 --- a/apps/web/lib/axios.client.ts +++ b/apps/web/lib/axios.client.ts @@ -67,6 +67,11 @@ api.interceptors.response.use( const response = await api.post("/auth/refresh"); const newAccessToken = response.data.data.accessToken; const currentUser = useAuthStore.getState().user + if (!currentUser) { + useAuthStore.getState().clearAuth(); + window.location.href = "/login"; + return Promise.reject(new Error("User not found")); + } // Save new access token in memory useAuthStore.getState().setAuth(newAccessToken, currentUser); From e0389ff195285811423ac3c077313aa457f71775 Mon Sep 17 00:00:00 2001 From: safvan Date: Mon, 16 Feb 2026 16:43:34 +0530 Subject: [PATCH 21/61] feat(otp-verification):signup --- .../src/controllers/auth.controller.ts | 75 ++++----- apps/core-api/src/modules/users/user.model.ts | 110 +++++++++++++ .../src/repositories/Signup.repository.ts | 9 +- .../src/repositories/user.repository.ts | 2 +- apps/core-api/src/routes/auth.routes.ts | 6 +- apps/core-api/src/services/auth.service.ts | 87 +++++++++-- apps/core-api/src/utils/sendotpEmail.ts | 5 +- apps/web/(auth)/forget-Password/page.tsx | 39 +++++ apps/web/(auth)/login/page.tsx | 147 ++++++++++++++++++ apps/web/{app => (auth)}/register/page.tsx | 0 apps/web/(auth)/reset-password/page.tsx | 38 +++++ apps/web/(auth)/verify-otp/page.tsx | 40 +++++ 12 files changed, 490 insertions(+), 68 deletions(-) create mode 100644 apps/core-api/src/modules/users/user.model.ts create mode 100644 apps/web/(auth)/forget-Password/page.tsx create mode 100644 apps/web/(auth)/login/page.tsx rename apps/web/{app => (auth)}/register/page.tsx (100%) create mode 100644 apps/web/(auth)/reset-password/page.tsx create mode 100644 apps/web/(auth)/verify-otp/page.tsx diff --git a/apps/core-api/src/controllers/auth.controller.ts b/apps/core-api/src/controllers/auth.controller.ts index b796cae..ee89d48 100644 --- a/apps/core-api/src/controllers/auth.controller.ts +++ b/apps/core-api/src/controllers/auth.controller.ts @@ -10,6 +10,7 @@ import { forgotPasswordService, verifyOtpService, resetPasswordService, + verifySignupOtpService, } from "../services/auth.service.js"; import type { HttpError } from "../modules/auth/http-error.js"; // import { log } from "winston"; @@ -87,52 +88,7 @@ export const loginController = async ( }; -// ================= EMAIL VERIFICATION ================= -export const verifyEmailController = async ( - req: Request, - res: Response, - next: NextFunction -) => { - try { - const { token } = req.query; - if (!token) { - const err: HttpError = new Error("Token missing"); - err.statusCode = 400; - throw err; - } - - const decoded = jwt.verify( - token as string, - process.env.EMAIL_VERIFICATION_SECRET as string - ) as { userId: string }; - - const user = await User.findById(decoded.userId); - - if (!user) { - const err: HttpError = new Error("Invalid token"); - err.statusCode = 400; - throw err; - } - - if (user.isEmailVerified) { - return res.status(400).json({ - success: false, - message: "Email already verified", - }); - } - - user.isEmailVerified = true; - await user.save(); - - res.status(200).json({ - success: true, - message: "Email verified successfully. Await admin approval.", - }); - } catch (error) { - next(error); - } -}; // ================= REFRESH TOKEN ================= @@ -207,6 +163,33 @@ export const logoutController = async ( } }; +// ================= VERIFY SIGNUP OTP ================= +export const verifySignupOtpController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { email, otp } = req.body; + + if (!email || !otp) { + const err: HttpError = new Error("Email and OTP required"); + err.statusCode = 400; + throw err; + } + + await verifySignupOtpService(email, otp); + + res.status(200).json({ + success: true, + message: "Email verified successfully", + }); + } catch (error) { + next(error); + } +}; + + // ================= FORGOT PASSWORD ================= export const forgotPasswordController = async ( req: Request, @@ -236,7 +219,7 @@ export const forgotPasswordController = async ( }; // ================= VERIFY OTP ================= -export const verifyOtpController = async ( +export const verifyResetOtpController = async ( req: Request, res: Response, next: NextFunction diff --git a/apps/core-api/src/modules/users/user.model.ts b/apps/core-api/src/modules/users/user.model.ts new file mode 100644 index 0000000..92db70d --- /dev/null +++ b/apps/core-api/src/modules/users/user.model.ts @@ -0,0 +1,110 @@ +import { Schema, model, Document } from "mongoose"; + +export enum UserRole { + INFLUENCER = "INFLUENCER", + BRAND = "BRAND", + ADMIN = "ADMIN", +} + +export enum AdminLevel { + SUPER = "SUPER", + NORMAL = "NORMAL", +} + +export enum UserStatus { + PENDING = "PENDING", + ACTIVE = "ACTIVE", + SUSPENDED = "SUSPENDED", +} + +export interface IUser extends Document { + email: string; + password: string; + role: UserRole; + adminLevel?: AdminLevel; + isEmailVerified: boolean; + status: UserStatus; + refreshToken?: string; + + // 🔐 Forgot Password Fields + resetOtp?: string; + resetOtpExpiry?: Date; + resetSessionToken?: string; + resetSessionExpiry?: Date; +} + +const UserSchema = new Schema( + { + email: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true, + index: true, + immutable: true, + }, + + password: { + type: String, + required: true, + select: false, // 🔐 never return password + }, + + role: { + type: String, + enum: Object.values(UserRole), + required: true, + }, + + adminLevel: { + type: String, + enum: Object.values(AdminLevel), + default: null, + }, + + isEmailVerified: { + type: Boolean, + default: false, + }, + + status: { + type: String, + enum: Object.values(UserStatus), + default: UserStatus.PENDING, + }, + + refreshToken: { + type: String, + select: false, + }, + + // ================================ + // FORGOT PASSWORD FIELDS + // ================================ + + resetOtp: { + type: String, + select: false, // never return OTP + }, + + resetOtpExpiry: { + type: Date, + }, + + resetSessionToken: { + type: String, + select: false, // never expose session token + }, + + resetSessionExpiry: { + type: Date, + }, + }, + { timestamps: true } +); + +// compound index (already yours) +UserSchema.index({ role: 1, status: 1 }); + +export const User = model("User", UserSchema); diff --git a/apps/core-api/src/repositories/Signup.repository.ts b/apps/core-api/src/repositories/Signup.repository.ts index 12e0f81..7cdb90a 100644 --- a/apps/core-api/src/repositories/Signup.repository.ts +++ b/apps/core-api/src/repositories/Signup.repository.ts @@ -26,9 +26,12 @@ class PendingSignupRepository { } // ================= FIND ================= - findByEmail(email: string) { - return PendingSignup.findOne({ email }); - } + findByEmail(email: string) { + return PendingSignup + .findOne({ email }) + .select("+emailOtpHash"); +} + // ================= UPDATE STATUS ================= updateStatus(email: string, status: "APPROVED" | "REJECTED") { diff --git a/apps/core-api/src/repositories/user.repository.ts b/apps/core-api/src/repositories/user.repository.ts index b802771..5c86d14 100644 --- a/apps/core-api/src/repositories/user.repository.ts +++ b/apps/core-api/src/repositories/user.repository.ts @@ -91,7 +91,7 @@ class UserRepository { userId, { password: hashedPassword, - refreshToken: "", // 🔐 invalidate all sessions + refreshToken: "", // invalidate all sessions }, { new: true, diff --git a/apps/core-api/src/routes/auth.routes.ts b/apps/core-api/src/routes/auth.routes.ts index bfd394a..c793e39 100644 --- a/apps/core-api/src/routes/auth.routes.ts +++ b/apps/core-api/src/routes/auth.routes.ts @@ -1,6 +1,6 @@ import { Router } from "express"; -import { forgotPasswordController, loginController, logoutController, refreshTokenController, resetPasswordController, signupController, verifyEmailController, verifyOtpController } from "../controllers/auth.controller.js"; +import { forgotPasswordController, loginController, logoutController, refreshTokenController, resetPasswordController, signupController, verifyResetOtpController, verifySignupOtpController } from "../controllers/auth.controller.js"; import { authenticate } from "../middlewares/auth.middleware.js"; const router: Router = Router() @@ -8,9 +8,9 @@ router.post("/login", loginController) router.post("/signup", signupController); router.post("/refresh", refreshTokenController); router.post("/logout", authenticate, logoutController); -router.get("/verify-email", verifyEmailController); +router.post("/verify-signup-otp", verifySignupOtpController); router.post("/forgot-password", forgotPasswordController); -router.post("/verify-otp", verifyOtpController); +router.post("/verify-reset-otp", verifyResetOtpController); router.post("/reset-password", resetPasswordController); diff --git a/apps/core-api/src/services/auth.service.ts b/apps/core-api/src/services/auth.service.ts index d1cfa75..36e9c82 100644 --- a/apps/core-api/src/services/auth.service.ts +++ b/apps/core-api/src/services/auth.service.ts @@ -15,9 +15,9 @@ // } // export const authService = new AuthService(); -import crypto from "crypto"; +import crypto from "crypto"; -import bcrypt from "bcrypt"; +import bcrypt from "bcrypt"; import { signAccessToken, signRefreshToken, verifyRefreshToken } from "../modules/auth/auth.utils.js"; import type { HttpError } from "../modules/auth/http-error.js"; @@ -27,7 +27,7 @@ import { logger } from "../utils/logger.js"; import { sendOtpEmail } from "../utils/sendotpEmail.js"; -// import { sendMail } from "../../utils/nodemailer"; + interface SignupInput { email: string; @@ -55,24 +55,37 @@ export const signupService = async (data: SignupInput) => { const passwordHash = await bcrypt.hash(data.password, 10); - // GENERATE OTP + // 🔥 1️⃣ Generate OTP const otp = Math.floor(100000 + Math.random() * 900000).toString(); - await sendOtpEmail(data.email, otp); + + // 🔥 2️⃣ Hash OTP before saving const hashedOtp = await bcrypt.hash(otp, 10); + // 🔥 3️⃣ Save pending signup with OTP await pendingSignupRepository.create({ email: data.email.toLowerCase(), passwordHash, role: data.role, documents: data.documents, status: "PENDING", + + emailOtpHash: hashedOtp, + emailOtpExpiresAt: new Date(Date.now() + 5 * 60 * 1000), + otpAttempts: 0, + otpResendCount: 0, + otpLastSentAt: new Date(), + isEmailVerified: false, }); - return { message: "Signup request submitted for review" }; + // 🔥 4️⃣ Send REAL Gmail OTP + await sendOtpEmail(data.email, otp); + + return { message: "OTP sent to your email" }; }; + interface LoginResult { accessToken: string, refreshToken: string, @@ -166,7 +179,7 @@ export const refreshTokenService = async ( const user = await userRepository.findById(payload.userId); - // ✅ FIRST check user existence + // FIRST check user existence if (!user || !user.refreshToken) { const err: HttpError = new Error("Refresh token mismatch"); err.statusCode = 401; @@ -174,7 +187,7 @@ export const refreshTokenService = async ( } - // ✅ Compare after narrowing + // Compare after narrowing if (user.refreshToken.trim() !== refreshToken.trim()) { const err: HttpError = new Error("Refresh token mismatch"); err.statusCode = 401; @@ -213,16 +226,68 @@ export const logoutService = async (userId: string) => { }; +// ================= VERIFY SIGNUP OTP ================= +export const verifySignupOtpService = async ( + email: string, + otp: string +): Promise => { + + const pending = await pendingSignupRepository.findByEmail(email); + + if (!pending) { + const err: HttpError = new Error("Signup request not found"); + err.statusCode = 404; + throw err; + } + + if (pending.isEmailVerified) { + const err: HttpError = new Error("Email already verified"); + err.statusCode = 400; + throw err; + } + + if (!pending.emailOtpHash || !pending.emailOtpExpiresAt) { + const err: HttpError = new Error("OTP not found"); + err.statusCode = 400; + throw err; + } + + if (pending.emailOtpExpiresAt < new Date()) { + const err: HttpError = new Error("OTP expired"); + err.statusCode = 400; + throw err; + } + + const isMatch = await bcrypt.compare( + otp, + pending.emailOtpHash + ); + + if (!isMatch) { + const err: HttpError = new Error("Invalid OTP"); + err.statusCode = 400; + throw err; + } + + // SUCCESS + pending.isEmailVerified = true; + pending.emailOtpHash = null; + pending.emailOtpExpiresAt = null; + + await pending.save(); +}; + + + // forgotPasswordService export const forgotPasswordService = async (email: string) => { const user = await userRepository.findEmailWithPassword(email); - if (!user) return; // security: don't reveal existence + if (!user) return; const otp = Math.floor(100000 + Math.random() * 900000).toString(); - console.log("RESET OTP:", otp); // 👈 ADD THIS LINE - console.log("Forgot password controller triggered"); + console.log("RESET OTP:", otp); const hashedOtp = await bcrypt.hash(otp, 10); diff --git a/apps/core-api/src/utils/sendotpEmail.ts b/apps/core-api/src/utils/sendotpEmail.ts index f6c402e..e27e681 100644 --- a/apps/core-api/src/utils/sendotpEmail.ts +++ b/apps/core-api/src/utils/sendotpEmail.ts @@ -1,9 +1,6 @@ import nodemailer from "nodemailer"; -export const sendOtpEmail = async ( - to: string, - otp: string -) => { +export const sendOtpEmail = async (to: string,otp: string) => { const transporter = nodemailer.createTransport({ service: "gmail", auth: { diff --git a/apps/web/(auth)/forget-Password/page.tsx b/apps/web/(auth)/forget-Password/page.tsx new file mode 100644 index 0000000..889ff55 --- /dev/null +++ b/apps/web/(auth)/forget-Password/page.tsx @@ -0,0 +1,39 @@ +export default function ForgotPassword() { + return ( +
+
+ +

+ Forgot Password +

+ +

+ Enter your email to receive OTP +

+ + + + + + + +

+ Remembered password?{" "} + + Back to Login + +

+ +
+
+ ); +} diff --git a/apps/web/(auth)/login/page.tsx b/apps/web/(auth)/login/page.tsx new file mode 100644 index 0000000..bb18948 --- /dev/null +++ b/apps/web/(auth)/login/page.tsx @@ -0,0 +1,147 @@ +"use client"; + +import React, { useState } from "react"; + +import api, { setAccessToken } from "@/lib/axios.client"; + +export default function LoginPage() { + const [role, setRole] = useState<"BRAND" | "INFLUENCER">("BRAND"); + const [formData, setFormData] = useState({ + email: "", + password: "" + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + + })); + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(""); + try { + const payload = { + email: formData.email, + password: formData.password, + role: role + } + + const response = await api.post("/auth/login", payload); + + // Access token is now in response.data.data.accessToken + if (response.data.data?.accessToken) { + setAccessToken(response.data.data.accessToken); + window.location.href = "/dashboard"; + } + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Login failed. Please try again."; + setError(errorMessage); + } finally { + setLoading(false); + } + } + + + return ( +
+
+ +
+

+ Welcome Back +

+

+ Log in to your Noillin account as{" "} + {role} +

+
+ +
+ + + +
+ +
+ {error && ( +
{error}
+ )} +
+ + +
+ +
+
+ + +
+ + +
+ + +
+ +

+ Secure login · Data protected +

+
+
+ ); +} diff --git a/apps/web/app/register/page.tsx b/apps/web/(auth)/register/page.tsx similarity index 100% rename from apps/web/app/register/page.tsx rename to apps/web/(auth)/register/page.tsx diff --git a/apps/web/(auth)/reset-password/page.tsx b/apps/web/(auth)/reset-password/page.tsx new file mode 100644 index 0000000..aaff75e --- /dev/null +++ b/apps/web/(auth)/reset-password/page.tsx @@ -0,0 +1,38 @@ +export default function ResetPassword() { + return ( +
+
+ +

+ Reset Password +

+ +

+ Enter your new password +

+ +
+ + + + + +
+ +
+
+ ); +} diff --git a/apps/web/(auth)/verify-otp/page.tsx b/apps/web/(auth)/verify-otp/page.tsx new file mode 100644 index 0000000..d572133 --- /dev/null +++ b/apps/web/(auth)/verify-otp/page.tsx @@ -0,0 +1,40 @@ +export default function VerifyOtp() { + return ( +
+
+ +

+ Verify OTP +

+ +

+ Enter the 6-digit OTP sent to your email +

+ +
+ + + +
+ +

+ Didn’t receive OTP?{" "} + + Resend + +

+ +
+
+ ); +} From a5bf0157cf5e3780ae55490ffe7eeedc67ddbc3d Mon Sep 17 00:00:00 2001 From: safvan Date: Mon, 16 Feb 2026 16:56:13 +0530 Subject: [PATCH 22/61] apps/chore(signup):OTP-verification --- apps/web/(auth)/login/page.tsx | 296 +++++++++++++++++---------------- 1 file changed, 149 insertions(+), 147 deletions(-) diff --git a/apps/web/(auth)/login/page.tsx b/apps/web/(auth)/login/page.tsx index bb18948..ba6d4cb 100644 --- a/apps/web/(auth)/login/page.tsx +++ b/apps/web/(auth)/login/page.tsx @@ -1,147 +1,149 @@ -"use client"; - -import React, { useState } from "react"; - -import api, { setAccessToken } from "@/lib/axios.client"; - -export default function LoginPage() { - const [role, setRole] = useState<"BRAND" | "INFLUENCER">("BRAND"); - const [formData, setFormData] = useState({ - email: "", - password: "" - }); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(""); - - - const handleInputChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setFormData(prev => ({ - ...prev, - [name]: value - - })); - } - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setLoading(true); - setError(""); - try { - const payload = { - email: formData.email, - password: formData.password, - role: role - } - - const response = await api.post("/auth/login", payload); - - // Access token is now in response.data.data.accessToken - if (response.data.data?.accessToken) { - setAccessToken(response.data.data.accessToken); - window.location.href = "/dashboard"; - } - - } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Login failed. Please try again."; - setError(errorMessage); - } finally { - setLoading(false); - } - } - - - return ( -
-
- -
-

- Welcome Back -

-

- Log in to your Noillin account as{" "} - {role} -

-
- -
- - - -
- -
- {error && ( -
{error}
- )} -
- - -
- -
-
- - -
- - -
- - -
- -

- Secure login · Data protected -

-
-
- ); -} +// "use client"; + + + +// import React, { useState } from "react"; + +// import api, { setAccessToken } from "@/lib/axios.client"; + +// export default function LoginPage() { +// const [role, setRole] = useState<"BRAND" | "INFLUENCER">("BRAND"); +// const [formData, setFormData] = useState({ +// email: "", +// password: "" +// }); +// const [loading, setLoading] = useState(false); +// const [error, setError] = useState(""); + + +// const handleInputChange = (e: React.ChangeEvent) => { +// const { name, value } = e.target; +// setFormData(prev => ({ +// ...prev, +// [name]: value + +// })); +// } + +// const handleSubmit = async (e: React.FormEvent) => { +// e.preventDefault(); +// setLoading(true); +// setError(""); +// try { +// const payload = { +// email: formData.email, +// password: formData.password, +// role: role +// } + +// const response = await api.post("/auth/login", payload); + +// // Access token is now in response.data.data.accessToken +// if (response.data.data?.accessToken) { +// setAccessToken(response.data.data.accessToken); +// window.location.href = "/dashboard"; +// } + +// } catch (error) { +// const errorMessage = error instanceof Error ? error.message : "Login failed. Please try again."; +// setError(errorMessage); +// } finally { +// setLoading(false); +// } +// } + + +// return ( +//
+//
+ +//
+//

+// Welcome Back +//

+//

+// Log in to your Noillin account as{" "} +// {role} +//

+//
+ +//
+// + +// +//
+ +//
+// {error && ( +//
{error}
+// )} +//
+// +// +//
+ +//
+//
+// +// +//
+ +// +//
+ +// +//
+ +//

+// Secure login · Data protected +//

+//
+//
+// ); +// } From ddcb0c4b040ab419896868fda06e8f0a561a3a9d Mon Sep 17 00:00:00 2001 From: safvan Date: Mon, 16 Feb 2026 20:43:03 +0530 Subject: [PATCH 23/61] feat(email-verification):OTP --- apps/web/(auth)/forget-Password/page.tsx | 13 +++++++------ apps/web/(auth)/reset-password/page.tsx | 17 +++++++++-------- apps/web/(auth)/verify-otp/page.tsx | 14 +++++++------- apps/web/app/page.tsx | 2 ++ 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/apps/web/(auth)/forget-Password/page.tsx b/apps/web/(auth)/forget-Password/page.tsx index 889ff55..4d10296 100644 --- a/apps/web/(auth)/forget-Password/page.tsx +++ b/apps/web/(auth)/forget-Password/page.tsx @@ -1,13 +1,14 @@ + export default function ForgotPassword() { return (
-

+

Forgot Password

-

+

Enter your email to receive OTP

@@ -15,18 +16,18 @@ export default function ForgotPassword() { -

+

Remembered password?{" "} Back to Login @@ -36,4 +37,4 @@ export default function ForgotPassword() {

); -} +} \ No newline at end of file diff --git a/apps/web/(auth)/reset-password/page.tsx b/apps/web/(auth)/reset-password/page.tsx index aaff75e..e0fbc3b 100644 --- a/apps/web/(auth)/reset-password/page.tsx +++ b/apps/web/(auth)/reset-password/page.tsx @@ -1,13 +1,14 @@ + export default function ResetPassword() { - return ( + return (<>
-

+

Reset Password

-

+

Enter your new password

@@ -15,18 +16,18 @@ export default function ResetPassword() { @@ -34,5 +35,5 @@ export default function ResetPassword() {
- ); -} + ); +} \ No newline at end of file diff --git a/apps/web/(auth)/verify-otp/page.tsx b/apps/web/(auth)/verify-otp/page.tsx index d572133..c62c6f2 100644 --- a/apps/web/(auth)/verify-otp/page.tsx +++ b/apps/web/(auth)/verify-otp/page.tsx @@ -1,9 +1,9 @@ export default function VerifyOtp() { - return ( + return (<>
-

+

Verify OTP

@@ -16,18 +16,18 @@ export default function VerifyOtp() { type="text" maxLength={6} placeholder="Enter OTP" - className="w-full px-4 py-2 text-center tracking-widest border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + className="w-full px-4 text-gray-400 py-2 text-center tracking-widest border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500" /> -

+

Didn’t receive OTP?{" "} Resend @@ -36,5 +36,5 @@ export default function VerifyOtp() {

- ); -} + ); +} \ No newline at end of file diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 72fbc26..d0417a4 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -21,3 +21,5 @@ export default function HomePage() { } + + From 77d73fdc052ad8932c692aa184b813b918731155 Mon Sep 17 00:00:00 2001 From: ShiljaBabu Date: Tue, 17 Feb 2026 15:31:51 +0530 Subject: [PATCH 24/61] feat: implemented the profile-setup for both brand and the influencer --- .../src/controllers/auth.controller.ts | 5 +- .../src/controllers/profile.controller.ts | 65 +++ apps/core-api/src/models/influencer.model.ts | 2 +- .../src/repositories/profile.repository.ts | 51 ++ apps/core-api/src/routes/profile.routes.ts | 15 +- apps/core-api/src/services/auth.service.ts | 103 +++- apps/core-api/src/services/profile.service.ts | 74 +++ .../src/services/verification.service.ts | 32 +- apps/web/app/home/page.tsx | 36 ++ apps/web/app/layout.tsx | 3 + apps/web/app/login/page.tsx | 8 +- apps/web/app/profile-setup/page.tsx | 505 ++++++++++++++++++ apps/web/app/profile/page.tsx | 52 ++ apps/web/app/signup/page.tsx | 2 +- apps/web/components/AuthInitializer.tsx | 29 + apps/web/components/Navbar.tsx | 210 ++++++++ apps/web/lib/axios.client.ts | 21 +- apps/web/next.config.ts | 2 +- apps/web/store/auth.store.ts | 1 + 19 files changed, 1169 insertions(+), 47 deletions(-) create mode 100644 apps/core-api/src/controllers/profile.controller.ts create mode 100644 apps/core-api/src/repositories/profile.repository.ts create mode 100644 apps/core-api/src/services/profile.service.ts create mode 100644 apps/web/app/home/page.tsx create mode 100644 apps/web/app/profile-setup/page.tsx create mode 100644 apps/web/app/profile/page.tsx create mode 100644 apps/web/components/AuthInitializer.tsx create mode 100644 apps/web/components/Navbar.tsx diff --git a/apps/core-api/src/controllers/auth.controller.ts b/apps/core-api/src/controllers/auth.controller.ts index b796cae..ee34be1 100644 --- a/apps/core-api/src/controllers/auth.controller.ts +++ b/apps/core-api/src/controllers/auth.controller.ts @@ -135,7 +135,6 @@ export const verifyEmailController = async ( }; -// ================= REFRESH TOKEN ================= export const refreshTokenController = async ( req: Request, res: Response, @@ -165,6 +164,8 @@ export const refreshTokenController = async ( success: true, data: { accessToken: result.accessToken, + user: result.user, + }, }); } catch (error) { @@ -173,7 +174,6 @@ export const refreshTokenController = async ( }; -// ================= LOGOUT ================= export const logoutController = async ( req: Request, res: Response, @@ -207,7 +207,6 @@ export const logoutController = async ( } }; -// ================= FORGOT PASSWORD ================= export const forgotPasswordController = async ( req: Request, res: Response, diff --git a/apps/core-api/src/controllers/profile.controller.ts b/apps/core-api/src/controllers/profile.controller.ts new file mode 100644 index 0000000..8c44ed5 --- /dev/null +++ b/apps/core-api/src/controllers/profile.controller.ts @@ -0,0 +1,65 @@ +import type { Response, NextFunction } from "express"; + +import { + getMyProfileService, + updateProfileService +} from "../services/profile.service.js"; +import type { AuthRequest } from "../middlewares/auth.middleware.js"; + + +export const getMyProfileController = async ( + req: AuthRequest, + res: Response, + next: NextFunction +) => { + try { + + if (!req.user) { + return res.status(401).json({ + success: false, + message: "Unauthorized", + }); + } + + const userId = req.user.userId; + + const profile = await getMyProfileService(userId); + + res.status(200).json({ + success: true, + data: profile + }); + + } catch (error) { + next(error); + } +}; + + +export const updateProfileController = async ( + req: AuthRequest, + res: Response, + next: NextFunction +) => { + try { + + if (!req.user) { + return res.status(401).json({ + success: false, + message: "Unauthorized", + }); + } + + const userId = req.user.userId; + + const profile = await updateProfileService(userId, req.body); + + res.status(200).json({ + success: true, + data: profile + }); + + } catch (error) { + next(error); + } +}; diff --git a/apps/core-api/src/models/influencer.model.ts b/apps/core-api/src/models/influencer.model.ts index d8a0454..02b17fe 100644 --- a/apps/core-api/src/models/influencer.model.ts +++ b/apps/core-api/src/models/influencer.model.ts @@ -34,7 +34,7 @@ const InfluencerProfileSchema = new Schema( fullName: { type: String, - required: true, + default: "", trim: true, }, diff --git a/apps/core-api/src/repositories/profile.repository.ts b/apps/core-api/src/repositories/profile.repository.ts new file mode 100644 index 0000000..844a08c --- /dev/null +++ b/apps/core-api/src/repositories/profile.repository.ts @@ -0,0 +1,51 @@ +import { BrandProfile } from "../models/brand.model.js"; +import type { IBrandProfile } from "../models/brand.model.js" +import { InfluencerProfile} from "../models/influencer.model.js"; +import type { IInfluencerProfile } from "../models/influencer.model.js" + +export class ProfileRepository { + + async findInfluencerByUserId(userId: string): Promise { + return InfluencerProfile.findOne({ userId }); + } + + async findBrandByUserId(userId: string): Promise { + return BrandProfile.findOne({ userId }); + } + + async createInfluencer( + data: Partial + ): Promise { + return InfluencerProfile.create(data); + } + + async createBrand( + data: Partial + ): Promise { + return BrandProfile.create(data); + } + + async updateInfluencer( + userId: string, + data: Partial + ): Promise { + return InfluencerProfile.findOneAndUpdate( + { userId }, + data, + { new: true } + ); + } + + async updateBrand( + userId: string, + data: Partial + ): Promise { + return BrandProfile.findOneAndUpdate( + { userId }, + data, + { new: true } + ); + } +} + +export const profileRepository = new ProfileRepository(); diff --git a/apps/core-api/src/routes/profile.routes.ts b/apps/core-api/src/routes/profile.routes.ts index ee617b7..a4d82d0 100644 --- a/apps/core-api/src/routes/profile.routes.ts +++ b/apps/core-api/src/routes/profile.routes.ts @@ -1,5 +1,14 @@ -import { Router } from "express"; +import { Router } from "express"; -const router : Router = Router() +import { authenticate } from "../middlewares/auth.middleware.js"; +import { + getMyProfileController, + updateProfileController +} from "../controllers/profile.controller.js"; -export default router \ No newline at end of file +const router: Router = Router(); + +router.get("/get_profile", authenticate, getMyProfileController); +router.patch("/update_profile", authenticate, updateProfileController); + +export default router; diff --git a/apps/core-api/src/services/auth.service.ts b/apps/core-api/src/services/auth.service.ts index d1cfa75..f84e9f4 100644 --- a/apps/core-api/src/services/auth.service.ts +++ b/apps/core-api/src/services/auth.service.ts @@ -1,23 +1,7 @@ -// class AuthService { -// async loginUser(_email: string, _password: string) { -// // find user -// // compare password -// // create tokens -// // save refresh token -// // return tokens -// } - -// async refreshSession(_token: string) { -// // verify refresh token -// // issue new access token -// } -// } +import crypto from "crypto"; -// export const authService = new AuthService(); -import crypto from "crypto"; - -import bcrypt from "bcrypt"; +import bcrypt from "bcrypt"; import { signAccessToken, signRefreshToken, verifyRefreshToken } from "../modules/auth/auth.utils.js"; import type { HttpError } from "../modules/auth/http-error.js"; @@ -58,7 +42,7 @@ export const signupService = async (data: SignupInput) => { // GENERATE OTP const otp = Math.floor(100000 + Math.random() * 900000).toString(); await sendOtpEmail(data.email, otp); - const hashedOtp = await bcrypt.hash(otp, 10); + // const hashedOtp = await bcrypt.hash(otp, 10); await pendingSignupRepository.create({ email: data.email.toLowerCase(), @@ -142,10 +126,77 @@ export const loginService = async( } +// interface RefreshResult { +// accessToken: string; +// refreshToken: string; +// } +// export const refreshTokenService = async ( +// refreshToken: string +// ): Promise => { +// if (!refreshToken) { +// const err: HttpError = new Error("Refresh token required"); +// err.statusCode = 400; +// throw err; +// } + +// let payload; +// try { +// payload = verifyRefreshToken(refreshToken); +// } catch { +// const err: HttpError = new Error("Invalid refresh token"); +// err.statusCode = 401; +// throw err; +// } + +// const user = await userRepository.findById(payload.userId); + +// if (!user || !user.refreshToken) { +// const err: HttpError = new Error("Refresh token mismatch"); +// err.statusCode = 401; +// throw err; +// } + + +// if (user.refreshToken.trim() !== refreshToken.trim()) { +// const err: HttpError = new Error("Refresh token mismatch"); +// err.statusCode = 401; +// throw err; +// } + +// const newPayload = { +// userId: user._id.toString(), +// role: user.role, +// adminLevel: user.adminLevel ?? null, +// }; + +// const newAccessToken = signAccessToken(newPayload); +// const newRefreshToken = signRefreshToken(newPayload); + +// await userRepository.saveRefreshToken(user._id.toString(), newRefreshToken); + +// return { +// accessToken: newAccessToken, +// refreshToken: newRefreshToken, +// user: { +// id: user._id.toString(), +// email: user.email, +// role: user.role, +// adminLevel: user.adminLevel ?? null, +// }, +// }; +// }; + interface RefreshResult { accessToken: string; refreshToken: string; + user: { + id: string; + email: string; + role: string; + adminLevel: string | null; + }; } + export const refreshTokenService = async ( refreshToken: string ): Promise => { @@ -166,15 +217,12 @@ export const refreshTokenService = async ( const user = await userRepository.findById(payload.userId); - // ✅ FIRST check user existence if (!user || !user.refreshToken) { const err: HttpError = new Error("Refresh token mismatch"); err.statusCode = 401; throw err; } - - // ✅ Compare after narrowing if (user.refreshToken.trim() !== refreshToken.trim()) { const err: HttpError = new Error("Refresh token mismatch"); err.statusCode = 401; @@ -190,11 +238,20 @@ export const refreshTokenService = async ( const newAccessToken = signAccessToken(newPayload); const newRefreshToken = signRefreshToken(newPayload); - await userRepository.saveRefreshToken(user._id.toString(), newRefreshToken); + await userRepository.saveRefreshToken( + user._id.toString(), + newRefreshToken + ); return { accessToken: newAccessToken, refreshToken: newRefreshToken, + user: { + id: user._id.toString(), + email: user.email, + role: user.role, + adminLevel: user.adminLevel ?? null, + }, }; }; diff --git a/apps/core-api/src/services/profile.service.ts b/apps/core-api/src/services/profile.service.ts new file mode 100644 index 0000000..d22025f --- /dev/null +++ b/apps/core-api/src/services/profile.service.ts @@ -0,0 +1,74 @@ +import { profileRepository } from "../repositories/profile.repository.js"; +import { userRepository } from "../repositories/user.repository.js"; +import type { HttpError } from "../modules/auth/http-error.js"; +import type { IInfluencerProfile } from "../models/influencer.model.js"; +import type { IBrandProfile } from "../models/brand.model.js"; + + +export const getMyProfileService = async (userId: string) => { + + const user = await userRepository.findById(userId); + + if (!user) { + const err: HttpError = new Error("User not found"); + err.statusCode = 404; + throw err; + } + + if (user.role === "INFLUENCER") { + + const profile = await profileRepository.findInfluencerByUserId(userId); + + if (!profile) { + const err: HttpError = new Error("Profile not found"); + err.statusCode = 404; + throw err; + } + + + return profile; + } + + if (user.role === "BRAND") { + + const profile = await profileRepository.findBrandByUserId(userId); + + if (!profile) { + const err: HttpError = new Error("Profile not found"); + err.statusCode = 404; + throw err; + } + + + return profile; + } + + const err: HttpError = new Error("Invalid role"); + err.statusCode = 400; + throw err; +}; + + +export const updateProfileService = async ( + userId: string, + data: unknown + +) => { + + const user = await userRepository.findById(userId); + if (!user) { + const err: HttpError = new Error("User not found"); + err.statusCode = 404; + throw err; + } + + if (user.role === "INFLUENCER") { + return profileRepository.updateInfluencer(userId, data as Partial); + } + + if (user.role === "BRAND") { + return profileRepository.updateBrand(userId, data as Partial); + } + + throw new Error("Invalid role"); +}; diff --git a/apps/core-api/src/services/verification.service.ts b/apps/core-api/src/services/verification.service.ts index 0152024..b4cb575 100644 --- a/apps/core-api/src/services/verification.service.ts +++ b/apps/core-api/src/services/verification.service.ts @@ -3,6 +3,7 @@ import bcrypt from "bcrypt"; import type { HttpError } from "../modules/auth/http-error.js"; import { pendingSignupRepository } from "../repositories/Signup.repository.js"; import { userRepository } from "../repositories/user.repository.js"; +import { profileRepository } from "../repositories/profile.repository.js"; // ================= VERIFY OTP ================= export const verifyOtpService = async ( @@ -140,7 +141,7 @@ export const approveSignupService = async (email: string) => { throw err; } - await userRepository.create({ + const user = await userRepository.create({ email: pending.email, password: pending.passwordHash, role: pending.role, @@ -148,6 +149,32 @@ export const approveSignupService = async (email: string) => { status: "ACTIVE", }); + if (user.role === "INFLUENCER") { + await profileRepository.createInfluencer({ + userId: user._id, + fullName: "", + username: user.email.split("@")[0], + categories: [], + languages: [], + isProfileComplete: false, + isVerified: false, + }); + } + + if (user.role === "BRAND") { + await profileRepository.createBrand({ + userId: user._id, + companyName: "", + industry: "", + contactPersonName: "", + contactEmail: user.email, + documents: [], + isProfileComplete: false, + isVerified: false, + }); + } + + await pendingSignupRepository.deleteByEmail(email); return { message: "Signup approved successfully" }; @@ -171,9 +198,8 @@ export const rejectSignupService = async ( return { message: "Signup rejected successfully" }; }; -// ================= CLEANUP EXPIRED SIGNUPS ================= export const cleanupExpiredSignups = async () => { - const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago + const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000); await pendingSignupRepository.deleteMany({ isEmailVerified: false, diff --git a/apps/web/app/home/page.tsx b/apps/web/app/home/page.tsx new file mode 100644 index 0000000..cf77d15 --- /dev/null +++ b/apps/web/app/home/page.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; + +import { useAuthStore } from "@/store/auth.store"; + +export default function HomePage() { + const router = useRouter(); + const user = useAuthStore((state) => state.user); + + useEffect(() => { + if (!user) { + router.push("/login"); + } + }, [user, router]); + + if (!user) return null; // Prevent render before redirect + + return ( +
+

Dashboard

+

Welcome {user.email}

+ +
+ +
+
+ ); + +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index f7fa87e..fb785c8 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; + import "./globals.css"; +import AuthInitializer from "@/components/AuthInitializer"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -27,6 +29,7 @@ export default function RootLayout({ + {children} diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx index e377843..369eadb 100644 --- a/apps/web/app/login/page.tsx +++ b/apps/web/app/login/page.tsx @@ -1,13 +1,17 @@ "use client"; import React, { useState } from "react"; +import { useRouter } from "next/navigation"; import api from "@/lib/axios.client"; import { useAuthStore } from "@/store/auth.store"; + export default function LoginPage() { + const router = useRouter(); + const [role, setRole] = useState<"BRAND" | "INFLUENCER">("BRAND"); const [formData, setFormData] = useState({ email: "", @@ -43,7 +47,9 @@ export default function LoginPage() { if (response.data.data?.accessToken) { const {accessToken, user} = response.data.data setAuth(accessToken, user) - window.location.href = "/dashboard"; + router.push("/home"); + + // window.location.href = "/home"; } } catch (error) { diff --git a/apps/web/app/profile-setup/page.tsx b/apps/web/app/profile-setup/page.tsx new file mode 100644 index 0000000..3e5c03e --- /dev/null +++ b/apps/web/app/profile-setup/page.tsx @@ -0,0 +1,505 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; + +import Navbar from "@/components/Navbar"; +import { useAuthStore } from "@/store/auth.store"; +import api from "@/lib/axios.client"; + +export default function ProfileSetupPage() { + const router = useRouter(); + const [loading, setLoading] = useState(false); + + const user = useAuthStore((state) => state.user); + + const userType = user?.role as "INFLUENCER" | "BRAND" | undefined; + + const [commonData, setCommonData] = useState({ + profilePicture: null as File | null, + bio: "", + location: "", + phoneNumber: "", + }); + + const [influencerData, setInfluencerData] = useState({ + fullName: "", + username: "", + niche: "", + gender: "", + dob: "", + instagram: "", + youtube: "", + tiktok: "", + }); + + const [brandData, setBrandData] = useState({ + companyName: "", + industry: "", + website: "", + companySize: "", + }); + + useEffect(() => { + if (!userType) return; + + const fetchProfile = async () => { + try { + const res = await api.get("/profile/get_profile"); + const data = res.data.data; + + if (userType === "INFLUENCER") { + setInfluencerData({ + fullName: data.fullName || "", + username: data.username || "", + niche: data.categories?.[0] || "", + gender: "", + dob: "", + instagram: data.instagramUrl || "", + youtube: data.youtubeUrl || "", + tiktok: data.tiktokUrl || "", + }); + + setCommonData((prev) => ({ + ...prev, + bio: data.bio || "", + location: data.location || "", + })); + } + + if (userType === "BRAND") { + setBrandData({ + companyName: data.companyName || "", + industry: data.industry || "", + website: data.website || "", + companySize: data.companySize || "", + }); + } + } catch (err) { + console.error(err); + } + }; + + fetchProfile(); + }, [userType]); + + const handleCommonChange = ( + e: React.ChangeEvent + ) => { + const { name, value } = e.target; + setCommonData((prev) => ({ ...prev, [name]: value })); + }; + + const handleInfluencerChange = ( + e: React.ChangeEvent + ) => { + const { name, value } = e.target; + setInfluencerData((prev) => ({ ...prev, [name]: value })); + }; + + const handleBrandChange = ( + e: React.ChangeEvent + ) => { + const { name, value } = e.target; + setBrandData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + + try { + if (!userType) return; + type InfluencerPayload = { + bio?: string; + location?: string; + fullName?: string; + username?: string; + categories?: string[]; + instagramUrl?: string; + youtubeUrl?: string; + tiktokUrl?: string; + languages?: string[]; +}; + +type BrandPayload = { + bio?: string; + location?: string; + companyName?: string; + industry?: string; + website?: string; + companySize?: string; +}; + +let payload: InfluencerPayload | BrandPayload = { + + + bio: commonData.bio, + location: commonData.location, + }; + + if (userType === "INFLUENCER") { + payload = { + ...payload, + fullName: influencerData.fullName, + username: influencerData.username, + categories: influencerData.niche + ? [influencerData.niche] + : [], + instagramUrl: influencerData.instagram, + youtubeUrl: influencerData.youtube, + tiktokUrl: influencerData.tiktok, + languages: [], + }; + } + + if (userType === "BRAND") { + payload = { + ...payload, + companyName: brandData.companyName, + industry: brandData.industry, + website: brandData.website, + companySize: brandData.companySize, + }; + } + + await api.patch("/profile/update_profile", payload); + + alert("Profile Updated Successfully"); + router.push("/home"); + } catch (err) { + console.error(err); + alert("Failed to update profile"); + } finally { + setLoading(false); + } + }; + + if (!user) { + return
Loading profile...
; + } + + + return ( +
+ + +
+
+ {/* Header */} +
+ {/*

+ Setup your profile +

+

+ Join thousands of creators and brands building the future of influencer marketing. +

*/} +
+ + {/* Type Switcher */} + {/*
+
+ + +
+
*/} + + {/* Main Card */} +
+
+
+ + {/* Section: Basic Info */} +
+
+

Basic Information

+

This will be displayed on your public profile.

+
+ +
+ {/* Avatar Upload */} +
+
+ {commonData.profilePicture ? ( + Preview + ) : ( +
+ + + + Upload Photo +
+ )} + {/* */} +
+
+ + {/* Basic Inputs */} +
+
+ +