complete authentication for platform level and website level both
This commit is contained in:
parent
4417d1eba3
commit
41221b786a
15 changed files with 1541 additions and 143 deletions
|
|
@ -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",
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
13
src/app/config/website-builder.json
Normal file
13
src/app/config/website-builder.json
Normal 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"
|
||||
}
|
||||
99
src/app/middleware/projectAuth.js
Normal file
99
src/app/middleware/projectAuth.js
Normal 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,
|
||||
};
|
||||
78
src/app/middleware/subdomainDetection.js
Normal file
78
src/app/middleware/subdomainDetection.js
Normal 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,
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,15 +18,128 @@ 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 (userError) {
|
||||
throw new AppError(
|
||||
httpStatus.INTERNAL_SERVER_ERROR,
|
||||
"User creation failed"
|
||||
);
|
||||
}
|
||||
|
||||
// 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("*")
|
||||
.eq("email", payload.email)
|
||||
.single();
|
||||
|
||||
if (error || !user) {
|
||||
throw new AppError(httpStatus.BAD_REQUEST, "Invalid credentials");
|
||||
}
|
||||
|
||||
if (!user.is_verified) {
|
||||
throw new AppError(
|
||||
httpStatus.BAD_REQUEST,
|
||||
"Please verify your email first"
|
||||
);
|
||||
}
|
||||
|
||||
// Check password
|
||||
const isMatch = await compareValidPass(payload.password, user.password_hash);
|
||||
if (!isMatch) {
|
||||
throw new AppError(httpStatus.BAD_REQUEST, "Invalid credentials");
|
||||
}
|
||||
|
||||
// Create token
|
||||
const token = createToken({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
});
|
||||
|
||||
const { password_hash, ...userData } = user;
|
||||
|
||||
return {
|
||||
user: userData,
|
||||
accessToken: token,
|
||||
};
|
||||
};
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
let { data: user } = await supabase
|
||||
.from("users")
|
||||
.select("*")
|
||||
.eq("email", email)
|
||||
.single();
|
||||
|
||||
if (!user) {
|
||||
const { data: newUser, error } = await supabase
|
||||
.from("users")
|
||||
.insert({
|
||||
name,
|
||||
email,
|
||||
phone,
|
||||
role: "admin",
|
||||
is_verified: true,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
|
@ -37,96 +151,44 @@ const registerUser = async (payload) => {
|
|||
);
|
||||
}
|
||||
|
||||
return createdUser;
|
||||
};
|
||||
const projectName = `${name}'s Store`;
|
||||
const subdomain =
|
||||
email.split("@")[0].toLowerCase() + Date.now().toString().slice(-4);
|
||||
|
||||
const loginUser = async (payload) => {
|
||||
const { data: user, error } = await supabase
|
||||
.from("users")
|
||||
.select("*")
|
||||
.eq("email", payload.email)
|
||||
const { data: project, error: prerror } = await supabase
|
||||
.from("projects")
|
||||
.insert({
|
||||
owner_id: newUser.id,
|
||||
name: projectName,
|
||||
sub_domain: subdomain,
|
||||
status: "draft",
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error || !user) {
|
||||
throw new AppError(httpStatus.BAD_REQUEST, "User does not exist");
|
||||
}
|
||||
await supabase.from("project_members").insert({
|
||||
project_id: project.id,
|
||||
user_id: newUser.id,
|
||||
role: "owner",
|
||||
});
|
||||
|
||||
if (!user.is_verified) {
|
||||
throw new AppError(
|
||||
httpStatus.BAD_REQUEST,
|
||||
"Please verify your email before logging in"
|
||||
);
|
||||
}
|
||||
|
||||
const isMatch = await compareValidPass(payload.password, user.password);
|
||||
if (!isMatch) {
|
||||
throw new AppError(httpStatus.BAD_REQUEST, "Password does not match");
|
||||
user = newUser;
|
||||
}
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
// 1. Verify Firebase Token
|
||||
// const decodedFirebaseToken = await admin.auth().verifyIdToken(firebaseToken);
|
||||
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Generate access & refresh tokens
|
||||
const payload = {
|
||||
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
|
||||
// );
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
user,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
registerUser,
|
||||
loginUser,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
293
src/app/modules/projectAuth/projectAuth.controller.js
Normal file
293
src/app/modules/projectAuth/projectAuth.controller.js
Normal 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;
|
||||
421
src/app/modules/projectAuth/projectAuth.routes.js
Normal file
421
src/app/modules/projectAuth/projectAuth.routes.js
Normal 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;
|
||||
315
src/app/modules/projectAuth/projectAuth.service.js
Normal file
315
src/app/modules/projectAuth/projectAuth.service.js
Normal 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,
|
||||
};
|
||||
38
src/app/modules/projectAuth/projectAuth.validation.js
Normal file
38
src/app/modules/projectAuth/projectAuth.validation.js
Normal 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,
|
||||
};
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue