complete authentication for platform level and website level both

This commit is contained in:
S M Fahim Hossen 2025-09-15 13:40:23 +06:00
parent 4417d1eba3
commit 41221b786a
15 changed files with 1541 additions and 143 deletions

View file

@ -16,7 +16,7 @@ app.use("/api/v1/local", express.static(path.join(__dirname, "app/local")));
app.get("/", (req, res) => {
res.json({
success: true,
message: "Welcome to the Learnup Bangladesh API v1.0",
message: "Welcome to the Website Builder API v1.0",
});
});

View file

@ -1,5 +1,5 @@
const admin = require("firebase-admin");
const serviceAccount = require("../../../path/to/firebaseServiceAccount.json");
const serviceAccount = require("./website-builder.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),

View file

@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "website-builder-dev-bd652",
"private_key_id": "51696b3e6b19bcc14f625b39a3e8ae43e1ccdffd",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1m0XQWkn3Cef0\nZ2OEmzMmlTALUeM4taDsX/CJStoI7A9f1P5okekWcWMbTWXTLP1vTxcMIn8/I6ke\nVWIsQnpCC5JthyySS8e7z6kiIzRdb8Vbvto0pNirCF/T4+lYD096OPs+WERXzlu+\nby1turUEfvbWUbzH8jtpRngnw9EFUmDszanmWFZdfA/kNBoEb5hgjtQBOc7RHnxW\nroTYZSarkCQ3344VUnJmR7EPT2tDMwJo6/H1ljTcCoxPE6FAgjQdhuhvWFxvvFzS\nI5dt83pnGJDqIh40KEBLXROtUJsRqTKD39gU64BwF3/FGDW53VRgNwV3yYd9yw7l\nNkUdumftAgMBAAECggEAJYV9wKUO+CQLUSNVW+4G/PdA9G3IUQyxwXS3CV62Orav\nHIEwySEwc+sca2Ur3xxCBo/kNqUf/kcbZ4GDpZpckS275LZTfqx6iXLHwp5ccNup\nfqGpisvuWjQCPJCvwvpy1NFWE5UAFdY0fV2SGMRFJzcmP7SmmPcS0Fb3d1vNYVhA\ntp17dpG7q7CuKvrfcXURRVEBevq3jl0JDfIyO4au45ZXV0Q7G+k/P1nf8TX0KsSL\nzUBFuUKT2WDr9I6RyigutH0OIqI6tAJyk3V/K6BR9BwOpYyFTkmigd4756jyx5oZ\nhGo5lqs1yn8xDwclTAAz+qtFeU6ahxrPFHa6v1F5EQKBgQDxM5+uK0Lz23TO+kML\n91LDJ1Qyh9dNsG8NY7Q1jA0yCZntL4BuAgj2A0MK415zA/q9Jg/JWkykz0VVzJJW\neMYYegVxrQvVK2mB4M1Ip9U3rM7UHsrEluxM7nMyNQ4Cd4TXHoB/ZwQQ747Y74th\n6htjVr2gAavzbAo5yJqauR+JSQKBgQDAv6HlYfSukbbA4x2eTLQdybSPm6h3XttT\nZzCROsMqQU8HRgOX0yZ8yC7jupAAfGMN12LUCdEn/aF7t2xT9uDHe9hOsW18LIIl\nyOx1anrVZOqsCpbBQJ0V2Q6QKlwXodk9gUOXi2nVKp/ITbRy4GBM9iqLjlU90xdy\nq9X/L8RthQKBgQCvNWbK59YMud+R8iz95jySUmFRC2gUoRMqUMC5HPEA5gSQTK8o\nOgY0Xo42vI8BUWS0PY++HgAKwB2Hg8DRW43afdiyiJdN0+kiVSAJpRC3Dqp3X56/\nSzp/b77yO6Pfmt4+PYPuB/DmNH45i1heWeZnNL0uG8jCXDrZWqUju1HzMQKBgBKX\n6dQo2OOmJs0Am7DTkWR2OrbHuz6YpeurvqGj24PN/QOkm2Af/Ex5Oxy4uH9zgFKr\nflsZ/1UuQE+g6BBJdnGH3tvofblGyd8/PKAu/15qd8DU3KoTw5OB9setbmjRMhWe\nzJhn2HO5wuQdqtSSFHgYHw6LUmm+XTqwnNobD4XJAoGBANmoazI2EibjUB2cH1IA\ngZ0+3adadd40akbqm7nPwsJqYgFZRB+4btsDD6OcrhuOHVeLsLXEBrZZJum1h+9S\nx9xzBmIcFSHQiEhpA+lqHE0dEC3u3oQ22GC1YtXQlvVl7ZbUO7UwutE3GLclH++H\nN/UeXVZNm+/Ot3bn2/7d3Ep3\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-fbsvc@website-builder-dev-bd652.iam.gserviceaccount.com",
"client_id": "110829170051006661307",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40website-builder-dev-bd652.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View file

@ -0,0 +1,99 @@
// middleware/projectAuth.js
const httpStatus = require("http-status");
const supabase = require("../config/supabaseClient");
const AppError = require("../errors/AppError");
const catchAsync = require("../utils/catchAsync");
const { verifyToken } = require("../utils/jwt");
const projectCustomerAuth = () => {
return catchAsync(async (req, res, next) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
throw new AppError(httpStatus.UNAUTHORIZED, "Access token required");
}
try {
const decoded = verifyToken(token);
if (decoded.type !== "project_member") {
throw new AppError(httpStatus.UNAUTHORIZED, "Invalid token type");
}
const { userId, projectId } = decoded;
const { data: user, error: userError } = await supabase
.from("users")
.select("*")
.eq("id", userId)
.single();
if (userError || !user) {
throw new AppError(httpStatus.UNAUTHORIZED, "User not found");
}
const { data: project, error: projectError } = await supabase
.from("projects")
.select("*")
.eq("id", projectId)
.eq("is_published", true)
.single();
if (projectError || !project) {
throw new AppError(
httpStatus.NOT_FOUND,
"Project not found or not available"
);
}
const { data: membership, error: memberError } = await supabase
.from("project_members")
.select("*")
.eq("project_id", projectId)
.eq("user_id", userId)
.single();
if (memberError || !membership) {
throw new AppError(
httpStatus.FORBIDDEN,
"Not a member of this project"
);
}
if (!user.is_verified) {
throw new AppError(httpStatus.FORBIDDEN, "Email not verified");
}
if (user.status !== "active") {
throw new AppError(httpStatus.FORBIDDEN, "Account not active");
}
req.user = {
userId: user.id,
email: user.email,
name: user.name,
role: user.role,
projectId: projectId,
projectRole: membership.role,
};
req.project = {
id: project.id,
name: project.name,
domain: project.domain,
subdomain: project.subdomain,
};
next();
} catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError(httpStatus.UNAUTHORIZED, "Invalid token");
}
});
};
module.exports = {
projectCustomerAuth,
};

View file

@ -0,0 +1,78 @@
const httpStatus = require("http-status");
const supabase = require("../config/supabaseClient");
const AppError = require("../errors/AppError");
const catchAsync = require("../utils/catchAsync");
const detectProjectFromSubdomain = () => {
return catchAsync(async (req, res, next) => {
const host = req.get("host") || req.get("x-forwarded-host") || "";
const origin = req.get("origin") || req.get("referer") || "";
const protocol = req.get("x-forwarded-proto") || req.protocol || "https";
const frontendUrl = origin || `${protocol}://${host}`;
console.log("Frontend URL:", frontendUrl);
console.log("Host:", host);
console.log("Origin:", origin);
console.log("Protocol:", protocol);
let subdomain = null;
let isCustomDomain = false;
const hostParts = host.split(".");
if (hostParts.length >= 3) {
subdomain = hostParts[0];
} else {
isCustomDomain = true;
}
let project = null;
if (isCustomDomain) {
const { data: customDomainProject, error: customError } = await supabase
.from("projects")
.select("*")
.eq("domain", host)
.eq("is_published", true)
.eq("status", "active")
.single();
if (!customError && customDomainProject) {
project = customDomainProject;
}
}
if (!project && subdomain) {
const { data: subdomainProject, error: subdomainError } = await supabase
.from("projects")
.select("*")
.eq("sub_domain", subdomain)
.eq("is_published", true)
.eq("status", "active")
.single();
if (!subdomainError && subdomainProject) {
project = subdomainProject;
}
}
if (!project) {
throw new AppError(
httpStatus.NOT_FOUND,
`No active project found for domain: ${host}. Please check the URL.`
);
}
req.project = project;
req.projectId = project.id;
req.frontendUrl = frontendUrl;
req.subdomain = subdomain;
req.host = host;
next();
});
};
module.exports = {
detectProjectFromSubdomain,
};

View file

@ -1,90 +1,111 @@
//auth.controller.js
const transporter = require("../../utils/transporterMail");
const catchAsync = require("../../utils/catchAsync");
const { createToken, verifyToken } = require("../../utils/jwt");
const sendConfirmationEmail = require("../../utils/sendConfirmationEmail");
const sendResponse = require("../../utils/sendResponse");
const sendConfirmationEmail = require("../../utils/sendConfirmationEmail");
const sendResetPassEmail = require("../../utils/sendResetPassEmail");
const {
registerUser,
loginUser,
firebaseLogin: firebaseAuth,
} = require("./auth.service");
const httpStatus = require("http-status").default;
const httpStatus = require("http-status");
const AppError = require("../../errors/AppError");
const hashPassword = require("../../utils/hashedPassword");
const supabase = require("../../config/supabaseClient");
const bcrypt = require("bcrypt");
const { client_url } = require("../../config");
const sendResetPassEmail = require("../../utils/sendResetPassEmail");
const login = catchAsync(async (req, res) => {
const loginData = req.body;
const result = await loginUser(loginData);
sendResponse(res, {
success: true,
message: "Login successful",
data: result,
statusCode: 200,
});
});
const firebaseLogin = catchAsync(async (req, res) => {
const firebasePayload = req.body;
const result = await firebaseAuth(firebasePayload);
sendResponse(res, {
success: true,
message: "Login successful",
data: result,
statusCode: 200,
});
});
const register = catchAsync(async (req, res) => {
const registerData = req.body;
const user = await registerUser(registerData);
const verificationToken = createToken({ email: user.email, role: user.role });
// sendConfirmationEmail(user.email, verificationToken);
const user = await registerUser(req.body);
const verificationToken = createToken(
{ email: user.email, userId: user.id },
"24h"
);
await sendConfirmationEmail(user.email, verificationToken);
sendResponse(res, {
success: true,
message: "Registration successful.",
data: user,
message:
"Registration successful. Please check your email for verification.",
data: { id: user.id, name: user.name, email: user.email },
statusCode: httpStatus.CREATED,
});
});
const verfiyEmail = catchAsync(async (req, res) => {
const token = req.query.token;
const decoded = verifyToken(token);
const { email } = decoded;
const user = await UserModel.findOne({ email });
if (!user) {
throw new AppError(httpStatus.NOT_FOUND, "User not found");
}
if (user.isVerified) {
throw new AppError(httpStatus.BAD_REQUEST, "User already verified");
}
user.isVerified = true;
await user.save();
const login = catchAsync(async (req, res) => {
const result = await loginUser(req.body);
sendResponse(res, {
success: true,
message: "Email verified successfully and account activated",
message: "Login successful",
data: result,
statusCode: httpStatus.OK,
});
});
const firebaseLogin = catchAsync(async (req, res) => {
const result = await firebaseAuth(req.body);
sendResponse(res, {
success: true,
message: "Login successful",
data: result,
statusCode: httpStatus.OK,
});
});
const verifyEmail = catchAsync(async (req, res) => {
const { token } = req.query;
if (!token) {
throw new AppError(httpStatus.BAD_REQUEST, "Verification token required");
}
const decoded = verifyToken(token);
const { error } = await supabase
.from("users")
.update({ is_verified: true })
.eq("email", decoded.email)
.eq("id", decoded.userId);
if (error) {
throw new AppError(httpStatus.BAD_REQUEST, "Verification failed");
}
sendResponse(res, {
success: true,
message: "Email verified successfully",
data: null,
statusCode: httpStatus.OK,
});
});
const resendVerification = catchAsync(async (req, res) => {
const user = await UserModel.findOne({ email: req.body.email });
if (!user) {
const { email } = req.body;
const { data: user, error } = await supabase
.from("users")
.select("*")
.eq("email", email)
.single();
if (error || !user) {
throw new AppError(httpStatus.NOT_FOUND, "User not found");
}
if (user.isVerified) {
if (user.is_verified) {
throw new AppError(httpStatus.BAD_REQUEST, "User already verified");
}
const verificationToken = createToken({ email: user.email, role: user.role });
sendConfirmationEmail(user.email, verificationToken);
const verificationToken = createToken(
{ email: user.email, userId: user.id },
"24h"
);
await sendConfirmationEmail(user.email, verificationToken);
sendResponse(res, {
success: true,
message: "Verification email sent",
@ -95,11 +116,27 @@ const resendVerification = catchAsync(async (req, res) => {
const forgotPassword = catchAsync(async (req, res) => {
const { email } = req.body;
const user = await UserModel.findOne({ email });
if (!user) return res.status(404).json({ message: "User not found" });
const token = createToken({ email: user.email }, "15m");
const resetLink = `${client_url}/reset-password?token=${token}`;
await sendResetPassEmail(user.email, resetLink);
const { data: user } = await supabase
.from("users")
.select("*")
.eq("email", email)
.single();
if (!user) {
sendResponse(res, {
success: true,
message: "If email exists, reset link has been sent",
data: null,
statusCode: httpStatus.OK,
});
return;
}
const resetToken = createToken({ email, userId: user.id }, "15m");
const resetLink = `${client_url}/reset-password?token=${resetToken}`;
await sendResetPassEmail(email, resetLink);
sendResponse(res, {
success: true,
@ -111,14 +148,20 @@ const forgotPassword = catchAsync(async (req, res) => {
const resetPassword = catchAsync(async (req, res) => {
const { newPassword, token } = req.body;
const decoded = verifyToken(token);
const user = await UserModel.findOne({ email: decoded.email });
if (!user) return res.status(404).json({ message: "User not found" });
await UserModel.updateOne(
{ email: user.email },
{ $set: { password: await hashPassword(newPassword) } }
);
const decoded = verifyToken(token);
const hashedPassword = await bcrypt.hash(newPassword, 10);
const { error } = await supabase
.from("users")
.update({ password_hash: hashedPassword })
.eq("email", decoded.email)
.eq("id", decoded.userId);
if (error) {
throw new AppError(httpStatus.BAD_REQUEST, "Password reset failed");
}
sendResponse(res, {
success: true,
message: "Password reset successful",
@ -127,13 +170,36 @@ const resetPassword = catchAsync(async (req, res) => {
});
});
const getProfile = catchAsync(async (req, res) => {
const { data: user, error } = await supabase
.from("users")
.select(
"id, name, email, phone, role, is_verified, profile_picture, created_at"
)
.eq("id", req.user.userId)
.single();
if (error) {
throw new AppError(httpStatus.NOT_FOUND, "User not found");
}
sendResponse(res, {
success: true,
message: "Profile retrieved successfully",
data: user,
statusCode: httpStatus.OK,
});
});
const AuthController = {
login,
register,
verfiyEmail,
login,
firebaseLogin,
verfiyEmail: verifyEmail,
resendVerification,
forgotPassword,
resetPassword,
firebaseLogin,
getProfile,
};
module.exports = AuthController;

View file

@ -33,9 +33,6 @@ router.post(
AuthController.resetPassword
);
const authRoutes = router;
module.exports = authRoutes;
/**
* @swagger
* /auth/login:
@ -166,6 +163,8 @@ module.exports = authRoutes;
* type: string
* name:
* type: string
* phone:
* type: string
* responses:
* 200:
* description: Successful login
@ -174,3 +173,6 @@ module.exports = authRoutes;
* 401:
* description: Invalid Firebase token
*/
const authRoutes = router;
module.exports = authRoutes;

View file

@ -4,9 +4,10 @@ const bcrypt = require("bcrypt");
const compareValidPass = require("../../utils/validPass");
const { createToken } = require("../../utils/jwt");
const supabase = require("../../config/supabaseClient");
const UserService = require("../User/user.service");
const admin = require("../../config/firebase");
const registerUser = async (payload) => {
// Check if user exists
const { data: existingUser } = await supabase
.from("users")
.select("*")
@ -17,30 +18,67 @@ const registerUser = async (payload) => {
throw new AppError(httpStatus.BAD_REQUEST, "User already exists");
}
// Hash password
const hashedPassword = await bcrypt.hash(payload.password, 10);
const { data: createdUser, error } = await supabase
// Create user
const { data: user, error: userError } = await supabase
.from("users")
.insert({
name: payload.name,
email: payload.email,
phone: payload.phone,
password: hashedPassword,
password_hash: hashedPassword,
role: "admin",
is_verified: false,
})
.select()
.single();
if (error) {
if (userError) {
throw new AppError(
httpStatus.INTERNAL_SERVER_ERROR,
"User creation failed"
);
}
return createdUser;
// Create default project
const projectName = `${payload.name}'s Store`;
const subdomain =
payload.email.split("@")[0].toLowerCase() + Date.now().toString().slice(-4);
const { data: project, error: projectError } = await supabase
.from("projects")
.insert({
owner_id: user.id,
name: projectName,
sub_domain: subdomain,
status: "draft",
})
.select()
.single();
if (projectError) {
// Clean up user if project fails
await supabase.from("users").delete().eq("id", user.id);
throw new AppError(
httpStatus.INTERNAL_SERVER_ERROR,
"Project creation failed"
);
}
// Add user as project owner
await supabase.from("project_members").insert({
project_id: project.id,
user_id: user.id,
role: "owner",
});
return user;
};
const loginUser = async (payload) => {
// Find user
const { data: user, error } = await supabase
.from("users")
.select("*")
@ -48,82 +86,106 @@ const loginUser = async (payload) => {
.single();
if (error || !user) {
throw new AppError(httpStatus.BAD_REQUEST, "User does not exist");
throw new AppError(httpStatus.BAD_REQUEST, "Invalid credentials");
}
if (!user.is_verified) {
throw new AppError(
httpStatus.BAD_REQUEST,
"Please verify your email before logging in"
"Please verify your email first"
);
}
const isMatch = await compareValidPass(payload.password, user.password);
// Check password
const isMatch = await compareValidPass(payload.password, user.password_hash);
if (!isMatch) {
throw new AppError(httpStatus.BAD_REQUEST, "Password does not match");
throw new AppError(httpStatus.BAD_REQUEST, "Invalid credentials");
}
// Create token
const token = createToken({
email: user.email,
userId: user.id,
email: user.email,
role: user.role,
});
const { password, ...userData } = user;
const { password_hash, ...userData } = user;
return {
...userData,
user: userData,
accessToken: token,
};
};
const firebaseLogin = async ({ firebaseToken, email, name }) => {
if (!firebaseToken || !email || !name) {
throw new AppError(
httpStatus.BAD_REQUEST,
"firebaseToken, email, and name are required"
);
const firebaseLogin = async ({ firebaseToken, email, name, phone }) => {
// Verify Firebase token
const decodedToken = await admin.auth().verifyIdToken(firebaseToken);
if (decodedToken.email !== email) {
throw new AppError(httpStatus.UNAUTHORIZED, "Invalid Firebase token");
}
// 1. Verify Firebase Token
// const decodedFirebaseToken = await admin.auth().verifyIdToken(firebaseToken);
let { data: user } = await supabase
.from("users")
.select("*")
.eq("email", email)
.single();
// if (!decodedFirebaseToken || decodedFirebaseToken.email !== email) {
// throw new AppError(httpStatus.UNAUTHORIZED, "Invalid Firebase token");
// }
// 2. Check if user exists
let user = await UserService.findUserByEmail(email);
// 3. Create user if doesn't exist
if (!user) {
user = await UserService.createUser({
name,
email,
is_verified: true,
role: "user",
is_deleted: false,
needs_password_change: false,
const { data: newUser, error } = await supabase
.from("users")
.insert({
name,
email,
phone,
role: "admin",
is_verified: true,
})
.select()
.single();
if (error) {
throw new AppError(
httpStatus.INTERNAL_SERVER_ERROR,
"User creation failed"
);
}
const projectName = `${name}'s Store`;
const subdomain =
email.split("@")[0].toLowerCase() + Date.now().toString().slice(-4);
const { data: project, error: prerror } = await supabase
.from("projects")
.insert({
owner_id: newUser.id,
name: projectName,
sub_domain: subdomain,
status: "draft",
})
.select()
.single();
await supabase.from("project_members").insert({
project_id: project.id,
user_id: newUser.id,
role: "owner",
});
user = newUser;
}
// 4. Generate access & refresh tokens
const payload = {
const token = createToken({
userId: user.id,
email: user.email,
role: user.role,
};
});
const accessToken = createToken(payload);
// const refreshToken = createToken(
// payload,
// config.jwt.refresh_secret,
// config.jwt.refresh_expires_in
// );
const { password_hash, ...userData } = user;
return {
accessToken,
user,
user: userData,
accessToken: token,
};
};

View file

@ -1,31 +1,37 @@
//auth.validation.js
const z = require("zod");
const loginSchema = z.object({
email: z.string().email().min(1),
password: z.string().min(1),
const registerSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Valid email required"),
password: z.string().min(6, "Password must be at least 6 characters"),
phone: z.string().optional(),
});
const registerSchema = z.object({
name: z.string().min(1),
email: z.string().email().min(1),
password: z.string().min(1),
const loginSchema = z.object({
email: z.string().email("Valid email required"),
password: z.string().min(1, "Password is required"),
});
const firebaseLoginSchema = z.object({
firebaseToken: z.string().min(1, "Firebase token required"),
email: z.string().email("Valid email required"),
name: z.string().min(1, "Name is required"),
phone: z.string().optional(),
});
const resendVerificationSchema = z.object({
email: z
.string({ message: "Please enter a valid email address" })
.email()
.min(1),
email: z.string().email("Valid email required"),
});
const resetPasswordSchema = z.object({
newPassword: z.string().min(1),
token: z.string().min(1),
newPassword: z.string().min(6, "Password must be at least 6 characters"),
token: z.string().min(1, "Token is required"),
});
module.exports = {
loginSchema,
registerSchema,
loginSchema,
firebaseLoginSchema,
resendVerificationSchema,
resetPasswordSchema,
};

View file

@ -0,0 +1,293 @@
const catchAsync = require("../../utils/catchAsync");
const { createToken, verifyToken } = require("../../utils/jwt");
const sendResponse = require("../../utils/sendResponse");
const sendConfirmationEmail = require("../../utils/sendConfirmationEmail");
const sendResetPassEmail = require("../../utils/sendResetPassEmail");
const httpStatus = require("http-status");
const AppError = require("../../errors/AppError");
const { client_url } = require("../../config");
const supabase = require("../../config/supabaseClient");
const {
registerProjectCustomer,
loginProjectCustomer,
findUserByEmail,
updateUserPassword,
verifyUserEmail,
checkProjectMembership,
} = require("./projectAuth.service");
const register = catchAsync(async (req, res) => {
const { projectId } = req.params;
const result = await registerProjectCustomer(projectId, req.body);
if (!result.user.is_verified) {
const verificationToken = createToken(
{
userId: result.user.id,
email: result.user.email,
projectId: projectId,
type: "project_customer_verification",
},
"24h"
);
await sendConfirmationEmail(result.user.email, verificationToken);
}
const { password_hash, ...userResponse } = result.user;
let message;
if (result.isNewUser) {
message =
"Registration successful. Please check your email for verification.";
} else if (!result.user.is_verified) {
message =
"You've been added to this project. Please check your email for verification.";
} else {
message =
"You've been successfully registered for this project and can login now.";
}
sendResponse(res, {
success: true,
message,
data: {
user: userResponse,
project: result.project,
isNewUser: result.isNewUser,
needsVerification: !result.user.is_verified,
},
statusCode: httpStatus.CREATED,
});
});
// Login customer to a specific project
const login = catchAsync(async (req, res) => {
const { projectId } = req.params;
const result = await loginProjectCustomer(projectId, req.body);
sendResponse(res, {
success: true,
message: "Login successful",
data: result,
statusCode: httpStatus.OK,
});
});
// Verify customer email
const verifyEmail = catchAsync(async (req, res) => {
const { token } = req.query;
if (!token) {
throw new AppError(
httpStatus.BAD_REQUEST,
"Verification token is required"
);
}
const decoded = verifyToken(token);
if (decoded.type !== "project_customer_verification") {
throw new AppError(httpStatus.BAD_REQUEST, "Invalid verification token");
}
await verifyUserEmail(decoded.userId);
sendResponse(res, {
success: true,
message: "Email verified successfully. You can now login.",
data: null,
statusCode: httpStatus.OK,
});
});
// Resend verification email
const resendVerification = catchAsync(async (req, res) => {
const { projectId } = req.params;
const { email } = req.body;
const user = await findUserByEmail(email);
if (!user) {
throw new AppError(httpStatus.NOT_FOUND, "User not found");
}
// Check if user is member of this project
const membership = await checkProjectMembership(user.id, projectId);
if (!membership) {
throw new AppError(
httpStatus.NOT_FOUND,
"User not registered for this project"
);
}
if (user.is_verified) {
throw new AppError(httpStatus.BAD_REQUEST, "Email already verified");
}
const verificationToken = createToken(
{
userId: user.id,
email: user.email,
projectId: projectId,
type: "project_customer_verification",
},
"24h"
);
await sendConfirmationEmail(user.email, verificationToken);
sendResponse(res, {
success: true,
message: "Verification email sent successfully",
data: null,
statusCode: httpStatus.OK,
});
});
// Forgot password
const forgotPassword = catchAsync(async (req, res) => {
const { projectId } = req.params;
const { email } = req.body;
const user = await findUserByEmail(email);
if (!user) {
// Don't reveal if user exists for security
sendResponse(res, {
success: true,
message: "If the email exists, a password reset link has been sent",
data: null,
statusCode: httpStatus.OK,
});
return;
}
// Check if user is member of this project
const membership = await checkProjectMembership(user.id, projectId);
if (!membership) {
sendResponse(res, {
success: true,
message: "If the email exists, a password reset link has been sent",
data: null,
statusCode: httpStatus.OK,
});
return;
}
const resetToken = createToken(
{
userId: user.id,
email: user.email,
projectId: projectId,
type: "project_customer_reset",
},
"15m"
);
const resetLink = `${client_url}/projects/${projectId}/reset-password?token=${resetToken}`;
await sendResetPassEmail(user.email, resetLink);
sendResponse(res, {
success: true,
message: "Password reset email sent successfully",
data: null,
statusCode: httpStatus.OK,
});
});
// Reset password
const resetPassword = catchAsync(async (req, res) => {
const { newPassword, token } = req.body;
if (!token) {
throw new AppError(httpStatus.BAD_REQUEST, "Reset token is required");
}
const decoded = verifyToken(token);
if (decoded.type !== "project_customer_reset") {
throw new AppError(httpStatus.BAD_REQUEST, "Invalid reset token");
}
await updateUserPassword(decoded.email, newPassword);
sendResponse(res, {
success: true,
message: "Password reset successful. Please login with your new password.",
data: null,
statusCode: httpStatus.OK,
});
});
const getProfile = catchAsync(async (req, res) => {
const { userId, projectId } = req.user;
const { data: user, error } = await supabase
.from("users")
.select("id, name, email, phone, role, is_verified, created_at")
.eq("id", userId)
.single();
if (error || !user) {
throw new AppError(httpStatus.NOT_FOUND, "User not found");
}
const membership = await checkProjectMembership(userId, projectId);
sendResponse(res, {
success: true,
message: "Profile retrieved successfully",
data: {
...user,
projectRole: membership?.role,
},
statusCode: httpStatus.OK,
});
});
const updateProfile = catchAsync(async (req, res) => {
const { userId } = req.user;
const { name, phone } = req.body;
const updateData = {
updated_at: new Date().toISOString(),
};
if (name) updateData.name = name;
if (phone) updateData.phone = phone;
const { data: user, error } = await supabase
.from("users")
.update(updateData)
.eq("id", userId)
.select("id, name, email, phone, role, is_verified, created_at")
.single();
if (error) {
throw new AppError(
httpStatus.INTERNAL_SERVER_ERROR,
"Profile update failed"
);
}
sendResponse(res, {
success: true,
message: "Profile updated successfully",
data: user,
statusCode: httpStatus.OK,
});
});
const ProjectAuthController = {
register,
login,
verifyEmail,
resendVerification,
forgotPassword,
resetPassword,
getProfile,
updateProfile,
};
module.exports = ProjectAuthController;

View file

@ -0,0 +1,421 @@
// modules/ProjectAuth/projectAuth.routes.js - Subdomain-based routes
const router = require("express").Router();
const validateRequest = require("../../middleware/validateRequest");
const { projectCustomerAuth } = require("../../middleware/projectAuth");
const {
detectProjectFromSubdomain,
} = require("../../middleware/subdomainDetection");
const ProjectAuthController = require("./projectAuth.controller");
const {
projectCustomerRegisterSchema,
projectCustomerLoginSchema,
projectCustomerUpdateSchema,
emailSchema,
resetPasswordSchema,
} = require("./projectAuth.validation");
/**
* @swagger
* tags:
* name: Project Auth
* description: Authentication for ecommerce project customers (subdomain-based)
*/
/**
* @swagger
* components:
* schemas:
* ProjectUser:
* type: object
* properties:
* id:
* type: string
* format: uuid
* name:
* type: string
* email:
* type: string
* format: email
* phone:
* type: string
* role:
* type: string
* enum: [customer, admin]
* is_verified:
* type: boolean
* created_at:
* type: string
* format: date-time
* Project:
* type: object
* properties:
* id:
* type: string
* format: uuid
* name:
* type: string
* domain:
* type: string
* subdomain:
* type: string
* Error:
* type: object
* properties:
* success:
* type: boolean
* example: false
* message:
* type: string
* statusCode:
* type: integer
*/
/**
* @swagger
* /project-auth/register:
* post:
* summary: Register customer for current project (detected from subdomain)
* description: |
* Registers a new customer for the project identified by the subdomain/domain.
* Access via subdomain (shop.yourplatform.com) or custom domain (shop.com).
* If user exists globally, adds them to this project. If new user, creates account and adds to project.
* tags: [Project Auth]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* - email
* properties:
* name:
* type: string
* example: "John Doe"
* minLength: 1
* email:
* type: string
* format: email
* example: "john.doe@example.com"
* password:
* type: string
* example: "securePassword123"
* minLength: 6
* description: Required for new users, optional for existing users
* phone:
* type: string
* example: "+1-555-123-4567"
* responses:
* 201:
* description: Customer registered successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "Registration successful. Please check your email for verification."
* data:
* type: object
* properties:
* user:
* $ref: '#/components/schemas/ProjectUser'
* project:
* $ref: '#/components/schemas/Project'
* isNewUser:
* type: boolean
* needsVerification:
* type: boolean
* 400:
* description: Validation error or user already registered
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: No active project found for this domain
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
/**
* @swagger
* /project-auth/login:
* post:
* summary: Login customer to current project (detected from subdomain)
* description: Authenticates customer for the project identified by subdomain/domain
* tags: [Project Auth]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - email
* - password
* properties:
* email:
* type: string
* format: email
* example: "john.doe@example.com"
* password:
* type: string
* example: "securePassword123"
* responses:
* 200:
* description: Login successful
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "Login successful"
* data:
* type: object
* properties:
* user:
* $ref: '#/components/schemas/ProjectUser'
* projectRole:
* type: string
* example: "customer"
* accessToken:
* type: string
* description: Project-specific JWT token
* project:
* $ref: '#/components/schemas/Project'
* 400:
* description: Invalid credentials or account issues
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Project not found or user not registered for this project
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
/**
* @swagger
* /project-auth/verify-email:
* get:
* summary: Verify customer email address
* tags: [Project Auth]
* parameters:
* - in: query
* name: token
* required: true
* schema:
* type: string
* description: Email verification token
* responses:
* 200:
* description: Email verified successfully
* 400:
* description: Invalid or expired token
*/
/**
* @swagger
* /project-auth/resend-verification:
* post:
* summary: Resend email verification
* tags: [Project Auth]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - email
* properties:
* email:
* type: string
* format: email
* example: "john.doe@example.com"
* responses:
* 200:
* description: Verification email sent
* 400:
* description: Email already verified or validation error
* 404:
* description: User not found or not registered for this project
*/
/**
* @swagger
* /project-auth/forgot-password:
* post:
* summary: Request password reset
* tags: [Project Auth]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - email
* properties:
* email:
* type: string
* format: email
* example: "john.doe@example.com"
* responses:
* 200:
* description: Password reset email sent (if email exists)
* 400:
* description: Validation error
*/
/**
* @swagger
* /project-auth/reset-password:
* post:
* summary: Reset customer password
* tags: [Project Auth]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - token
* - newPassword
* properties:
* token:
* type: string
* description: Password reset token
* newPassword:
* type: string
* minLength: 6
* example: "newSecurePassword123"
* responses:
* 200:
* description: Password reset successful
* 400:
* description: Invalid token or validation error
*/
/**
* @swagger
* /project-auth/profile:
* get:
* summary: Get customer profile
* description: Get authenticated customer's profile for current project
* tags: [Project Auth]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Profile retrieved successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* data:
* allOf:
* - $ref: '#/components/schemas/ProjectUser'
* - type: object
* properties:
* projectRole:
* type: string
* example: "customer"
* 401:
* description: Unauthorized
* 404:
* description: User not found
* patch:
* summary: Update customer profile
* tags: [Project Auth]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* example: "John Updated"
* phone:
* type: string
* example: "+1-555-987-6543"
* responses:
* 200:
* description: Profile updated successfully
* 400:
* description: Validation error
* 401:
* description: Unauthorized
*/
router.use(detectProjectFromSubdomain());
router.post(
"/register",
validateRequest(projectCustomerRegisterSchema),
ProjectAuthController.register
);
router.post(
"/login",
validateRequest(projectCustomerLoginSchema),
ProjectAuthController.login
);
router.get("/verify-email", ProjectAuthController.verifyEmail);
router.post(
"/resend-verification",
validateRequest(emailSchema),
ProjectAuthController.resendVerification
);
router.post(
"/forgot-password",
validateRequest(emailSchema),
ProjectAuthController.forgotPassword
);
router.post(
"/reset-password",
validateRequest(resetPasswordSchema),
ProjectAuthController.resetPassword
);
// Protected routes
router.get("/profile", projectCustomerAuth(), ProjectAuthController.getProfile);
router.patch(
"/profile",
projectCustomerAuth(),
validateRequest(projectCustomerUpdateSchema),
ProjectAuthController.updateProfile
);
module.exports = router;

View file

@ -0,0 +1,315 @@
const httpStatus = require("http-status");
const AppError = require("../../errors/AppError");
const bcrypt = require("bcrypt");
const { createToken } = require("../../utils/jwt");
const supabase = require("../../config/supabaseClient");
const registerProjectCustomer = async (projectId, payload) => {
const { name, email, password, phone } = payload;
const { data: project, error: projectError } = await supabase
.from("projects")
.select("*")
.eq("id", projectId)
.eq("is_published", true)
.single();
if (projectError || !project) {
throw new AppError(
httpStatus.NOT_FOUND,
"Project not found or not available"
);
}
// Check if user already exists globally
const { data: existingUser } = await supabase
.from("users")
.select("*")
.eq("email", email)
.single();
let user;
let isNewUser = false;
if (existingUser) {
const { data: existingMember } = await supabase
.from("project_members")
.select("*")
.eq("project_id", projectId)
.eq("user_id", existingUser.id)
.single();
if (existingMember) {
throw new AppError(
httpStatus.BAD_REQUEST,
"You are already registered for this project"
);
}
if (password && existingUser.password_hash) {
throw new AppError(
httpStatus.BAD_REQUEST,
"You already have an account. Please login instead."
);
}
if (password && !existingUser.password_hash) {
const hashedPassword = await bcrypt.hash(password, 10);
const { error: updateError } = await supabase
.from("users")
.update({
password_hash: hashedPassword,
name: name || existingUser.name,
phone: phone || existingUser.phone,
updated_at: new Date().toISOString(),
})
.eq("id", existingUser.id);
if (updateError) {
throw new AppError(
httpStatus.INTERNAL_SERVER_ERROR,
"Failed to update user"
);
}
// Fetch updated user
const { data: updatedUser } = await supabase
.from("users")
.select("*")
.eq("id", existingUser.id)
.single();
user = updatedUser;
} else {
user = existingUser;
}
} else {
if (!password) {
throw new AppError(
httpStatus.BAD_REQUEST,
"Password is required for new users"
);
}
const hashedPassword = await bcrypt.hash(password, 10);
const { data: newUser, error: userError } = await supabase
.from("users")
.insert({
name,
email,
phone,
password_hash: hashedPassword,
role: "customer",
is_verified: true,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
})
.select()
.single();
if (userError) {
throw new AppError(
httpStatus.INTERNAL_SERVER_ERROR,
"User creation failed"
);
}
user = newUser;
isNewUser = true;
}
const { error: memberError } = await supabase.from("project_members").insert({
project_id: projectId,
user_id: user.id,
role: "customer",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
});
if (memberError) {
// If this was a new user and project_member creation failed, clean up
if (isNewUser) {
await supabase.from("users").delete().eq("id", user.id);
}
throw new AppError(
httpStatus.INTERNAL_SERVER_ERROR,
"Failed to add user to project"
);
}
return { user, project, isNewUser };
};
// Login customer to a specific project
const loginProjectCustomer = async (projectId, payload) => {
const { email, password } = payload;
// Check if project exists and is active
const { data: project, error: projectError } = await supabase
.from("projects")
.select("*")
.eq("id", projectId)
.eq("is_published", true)
.single();
if (projectError || !project) {
throw new AppError(
httpStatus.NOT_FOUND,
"Project not found or not available"
);
}
// Find user in users table
const { data: user, error: userError } = await supabase
.from("users")
.select("*")
.eq("email", email)
.single();
if (userError || !user) {
throw new AppError(httpStatus.BAD_REQUEST, "Invalid credentials");
}
// Check if user is member of this project
const { data: membership, error: memberError } = await supabase
.from("project_members")
.select("*")
.eq("project_id", projectId)
.eq("user_id", user.id)
.single();
if (memberError || !membership) {
throw new AppError(
httpStatus.BAD_REQUEST,
"You are not registered for this project"
);
}
if (!user.is_verified) {
throw new AppError(
httpStatus.BAD_REQUEST,
"Please verify your email before logging in"
);
}
if (user.status !== "active") {
throw new AppError(httpStatus.FORBIDDEN, "Account is not active");
}
// Verify password
const isPasswordValid = await bcrypt.compare(password, user.password_hash);
if (!isPasswordValid) {
throw new AppError(httpStatus.BAD_REQUEST, "Invalid credentials");
}
// Create project-specific token
const token = createToken({
userId: user.id,
email: user.email,
projectId: projectId,
projectRole: membership.role,
type: "project_member",
});
// Update last login
await supabase
.from("users")
.update({
updated_at: new Date().toISOString(),
})
.eq("id", user.id);
const { password_hash, ...userData } = user;
return {
user: userData,
projectRole: membership.role,
accessToken: token,
project: {
id: project.id,
name: project.name,
domain: project.domain,
subdomain: project.subdomain,
},
};
};
// Find user by email globally
const findUserByEmail = async (email) => {
const { data: user, error } = await supabase
.from("users")
.select("*")
.eq("email", email)
.single();
if (error || !user) {
return null;
}
return user;
};
// Update user password
const updateUserPassword = async (email, newPassword) => {
const hashedPassword = await bcrypt.hash(newPassword, 10);
const { error } = await supabase
.from("users")
.update({
password_hash: hashedPassword,
updated_at: new Date().toISOString(),
})
.eq("email", email);
if (error) {
throw new AppError(
httpStatus.INTERNAL_SERVER_ERROR,
"Password update failed"
);
}
return true;
};
// Verify user email
const verifyUserEmail = async (userId) => {
const { error } = await supabase
.from("users")
.update({
is_verified: true,
updated_at: new Date().toISOString(),
})
.eq("id", userId);
if (error) {
throw new AppError(httpStatus.BAD_REQUEST, "Email verification failed");
}
return true;
};
// Check if user is member of project
const checkProjectMembership = async (userId, projectId) => {
const { data: membership, error } = await supabase
.from("project_members")
.select("*")
.eq("project_id", projectId)
.eq("user_id", userId)
.single();
if (error || !membership) {
return null;
}
return membership;
};
module.exports = {
registerProjectCustomer,
loginProjectCustomer,
findUserByEmail,
updateUserPassword,
verifyUserEmail,
checkProjectMembership,
};

View file

@ -0,0 +1,38 @@
const z = require("zod");
const projectCustomerRegisterSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Valid email address required"),
password: z
.string()
.min(6, "Password must be at least 6 characters")
.optional(), // Optional for existing users
phone: z.string().optional(),
});
const projectCustomerLoginSchema = z.object({
email: z.string().email("Valid email address required"),
password: z.string().min(1, "Password is required"),
});
const projectCustomerUpdateSchema = z.object({
name: z.string().min(1, "Name cannot be empty").optional(),
phone: z.string().optional(),
});
const emailSchema = z.object({
email: z.string().email("Valid email address required"),
});
const resetPasswordSchema = z.object({
token: z.string().min(1, "Reset token is required"),
newPassword: z.string().min(6, "Password must be at least 6 characters"),
});
module.exports = {
projectCustomerRegisterSchema,
projectCustomerLoginSchema,
projectCustomerUpdateSchema,
emailSchema,
resetPasswordSchema,
};

View file

@ -1,6 +1,7 @@
const router = require("express").Router();
const authRoutes = require("../modules/Auth/auth.routes");
const userRoutes = require("../modules/User/user.routes");
const projectAuth = require("../modules/projectAuth/projectAuth.routes");
const { path } = require("../../app");
const moduleRoutes = [
{
@ -11,6 +12,10 @@ const moduleRoutes = [
path: "/users",
route: userRoutes,
},
{
path: "/project-auth",
route: projectAuth,
},
];
moduleRoutes.forEach((route) => router.use(route.path, route.route));

View file

@ -4,12 +4,12 @@ const options = {
definition: {
openapi: "3.0.0",
info: {
title: "Learnup Bangladesh API",
title: "Website Builder API",
version: "1.0.0",
description: "API documentation for Learnup Bangladesh backend",
description: "API documentation for Website Builder backend",
contact: {
name: "Learnup Bangladesh Dev Team",
email: "learnupbangladesh@gmail.com",
name: "Website Builder Team",
email: "websitebuilder@gmail.com",
},
},
servers: [