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) => {
|
app.get("/", (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
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 admin = require("firebase-admin");
|
||||||
const serviceAccount = require("../../../path/to/firebaseServiceAccount.json");
|
const serviceAccount = require("./website-builder.json");
|
||||||
|
|
||||||
admin.initializeApp({
|
admin.initializeApp({
|
||||||
credential: admin.credential.cert(serviceAccount),
|
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 catchAsync = require("../../utils/catchAsync");
|
||||||
const { createToken, verifyToken } = require("../../utils/jwt");
|
const { createToken, verifyToken } = require("../../utils/jwt");
|
||||||
const sendConfirmationEmail = require("../../utils/sendConfirmationEmail");
|
|
||||||
const sendResponse = require("../../utils/sendResponse");
|
const sendResponse = require("../../utils/sendResponse");
|
||||||
|
const sendConfirmationEmail = require("../../utils/sendConfirmationEmail");
|
||||||
|
const sendResetPassEmail = require("../../utils/sendResetPassEmail");
|
||||||
const {
|
const {
|
||||||
registerUser,
|
registerUser,
|
||||||
loginUser,
|
loginUser,
|
||||||
firebaseLogin: firebaseAuth,
|
firebaseLogin: firebaseAuth,
|
||||||
} = require("./auth.service");
|
} = require("./auth.service");
|
||||||
const httpStatus = require("http-status").default;
|
const httpStatus = require("http-status");
|
||||||
const AppError = require("../../errors/AppError");
|
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 { 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 register = catchAsync(async (req, res) => {
|
||||||
const registerData = req.body;
|
const user = await registerUser(req.body);
|
||||||
const user = await registerUser(registerData);
|
|
||||||
const verificationToken = createToken({ email: user.email, role: user.role });
|
const verificationToken = createToken(
|
||||||
// sendConfirmationEmail(user.email, verificationToken);
|
{ email: user.email, userId: user.id },
|
||||||
|
"24h"
|
||||||
|
);
|
||||||
|
|
||||||
|
await sendConfirmationEmail(user.email, verificationToken);
|
||||||
|
|
||||||
sendResponse(res, {
|
sendResponse(res, {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Registration successful.",
|
message:
|
||||||
data: user,
|
"Registration successful. Please check your email for verification.",
|
||||||
|
data: { id: user.id, name: user.name, email: user.email },
|
||||||
statusCode: httpStatus.CREATED,
|
statusCode: httpStatus.CREATED,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const verfiyEmail = catchAsync(async (req, res) => {
|
const login = catchAsync(async (req, res) => {
|
||||||
const token = req.query.token;
|
const result = await loginUser(req.body);
|
||||||
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();
|
|
||||||
sendResponse(res, {
|
sendResponse(res, {
|
||||||
success: true,
|
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,
|
data: null,
|
||||||
statusCode: httpStatus.OK,
|
statusCode: httpStatus.OK,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const resendVerification = catchAsync(async (req, res) => {
|
const resendVerification = catchAsync(async (req, res) => {
|
||||||
const user = await UserModel.findOne({ email: req.body.email });
|
const { email } = req.body;
|
||||||
if (!user) {
|
|
||||||
|
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");
|
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");
|
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, {
|
sendResponse(res, {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Verification email sent",
|
message: "Verification email sent",
|
||||||
|
|
@ -95,11 +116,27 @@ const resendVerification = catchAsync(async (req, res) => {
|
||||||
|
|
||||||
const forgotPassword = catchAsync(async (req, res) => {
|
const forgotPassword = catchAsync(async (req, res) => {
|
||||||
const { email } = req.body;
|
const { email } = req.body;
|
||||||
const user = await UserModel.findOne({ email });
|
|
||||||
if (!user) return res.status(404).json({ message: "User not found" });
|
const { data: user } = await supabase
|
||||||
const token = createToken({ email: user.email }, "15m");
|
.from("users")
|
||||||
const resetLink = `${client_url}/reset-password?token=${token}`;
|
.select("*")
|
||||||
await sendResetPassEmail(user.email, resetLink);
|
.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, {
|
sendResponse(res, {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -111,14 +148,20 @@ const forgotPassword = catchAsync(async (req, res) => {
|
||||||
|
|
||||||
const resetPassword = catchAsync(async (req, res) => {
|
const resetPassword = catchAsync(async (req, res) => {
|
||||||
const { newPassword, token } = req.body;
|
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(
|
const decoded = verifyToken(token);
|
||||||
{ email: user.email },
|
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||||
{ $set: { password: await hashPassword(newPassword) } }
|
|
||||||
);
|
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, {
|
sendResponse(res, {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Password reset successful",
|
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 = {
|
const AuthController = {
|
||||||
login,
|
|
||||||
register,
|
register,
|
||||||
verfiyEmail,
|
login,
|
||||||
|
firebaseLogin,
|
||||||
|
verfiyEmail: verifyEmail,
|
||||||
resendVerification,
|
resendVerification,
|
||||||
forgotPassword,
|
forgotPassword,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
firebaseLogin,
|
getProfile,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = AuthController;
|
module.exports = AuthController;
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,6 @@ router.post(
|
||||||
AuthController.resetPassword
|
AuthController.resetPassword
|
||||||
);
|
);
|
||||||
|
|
||||||
const authRoutes = router;
|
|
||||||
module.exports = authRoutes;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /auth/login:
|
* /auth/login:
|
||||||
|
|
@ -166,6 +163,8 @@ module.exports = authRoutes;
|
||||||
* type: string
|
* type: string
|
||||||
* name:
|
* name:
|
||||||
* type: string
|
* type: string
|
||||||
|
* phone:
|
||||||
|
* type: string
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: Successful login
|
* description: Successful login
|
||||||
|
|
@ -174,3 +173,6 @@ module.exports = authRoutes;
|
||||||
* 401:
|
* 401:
|
||||||
* description: Invalid Firebase token
|
* description: Invalid Firebase token
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const authRoutes = router;
|
||||||
|
module.exports = authRoutes;
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,10 @@ const bcrypt = require("bcrypt");
|
||||||
const compareValidPass = require("../../utils/validPass");
|
const compareValidPass = require("../../utils/validPass");
|
||||||
const { createToken } = require("../../utils/jwt");
|
const { createToken } = require("../../utils/jwt");
|
||||||
const supabase = require("../../config/supabaseClient");
|
const supabase = require("../../config/supabaseClient");
|
||||||
const UserService = require("../User/user.service");
|
const admin = require("../../config/firebase");
|
||||||
|
|
||||||
const registerUser = async (payload) => {
|
const registerUser = async (payload) => {
|
||||||
|
// Check if user exists
|
||||||
const { data: existingUser } = await supabase
|
const { data: existingUser } = await supabase
|
||||||
.from("users")
|
.from("users")
|
||||||
.select("*")
|
.select("*")
|
||||||
|
|
@ -17,30 +18,67 @@ const registerUser = async (payload) => {
|
||||||
throw new AppError(httpStatus.BAD_REQUEST, "User already exists");
|
throw new AppError(httpStatus.BAD_REQUEST, "User already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
const hashedPassword = await bcrypt.hash(payload.password, 10);
|
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")
|
.from("users")
|
||||||
.insert({
|
.insert({
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
email: payload.email,
|
email: payload.email,
|
||||||
phone: payload.phone,
|
phone: payload.phone,
|
||||||
password: hashedPassword,
|
password_hash: hashedPassword,
|
||||||
|
role: "admin",
|
||||||
|
is_verified: false,
|
||||||
})
|
})
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) {
|
if (userError) {
|
||||||
throw new AppError(
|
throw new AppError(
|
||||||
httpStatus.INTERNAL_SERVER_ERROR,
|
httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
"User creation failed"
|
"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) => {
|
const loginUser = async (payload) => {
|
||||||
|
// Find user
|
||||||
const { data: user, error } = await supabase
|
const { data: user, error } = await supabase
|
||||||
.from("users")
|
.from("users")
|
||||||
.select("*")
|
.select("*")
|
||||||
|
|
@ -48,82 +86,106 @@ const loginUser = async (payload) => {
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error || !user) {
|
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) {
|
if (!user.is_verified) {
|
||||||
throw new AppError(
|
throw new AppError(
|
||||||
httpStatus.BAD_REQUEST,
|
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) {
|
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({
|
const token = createToken({
|
||||||
email: user.email,
|
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { password, ...userData } = user;
|
const { password_hash, ...userData } = user;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...userData,
|
user: userData,
|
||||||
accessToken: token,
|
accessToken: token,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const firebaseLogin = async ({ firebaseToken, email, name }) => {
|
const firebaseLogin = async ({ firebaseToken, email, name, phone }) => {
|
||||||
if (!firebaseToken || !email || !name) {
|
// Verify Firebase token
|
||||||
throw new AppError(
|
const decodedToken = await admin.auth().verifyIdToken(firebaseToken);
|
||||||
httpStatus.BAD_REQUEST,
|
|
||||||
"firebaseToken, email, and name are required"
|
if (decodedToken.email !== email) {
|
||||||
);
|
throw new AppError(httpStatus.UNAUTHORIZED, "Invalid Firebase token");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Verify Firebase Token
|
let { data: user } = await supabase
|
||||||
// const decodedFirebaseToken = await admin.auth().verifyIdToken(firebaseToken);
|
.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) {
|
if (!user) {
|
||||||
user = await UserService.createUser({
|
const { data: newUser, error } = await supabase
|
||||||
name,
|
.from("users")
|
||||||
email,
|
.insert({
|
||||||
is_verified: true,
|
name,
|
||||||
role: "user",
|
email,
|
||||||
is_deleted: false,
|
phone,
|
||||||
needs_password_change: false,
|
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 token = createToken({
|
||||||
const payload = {
|
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
};
|
});
|
||||||
|
|
||||||
const accessToken = createToken(payload);
|
const { password_hash, ...userData } = user;
|
||||||
// const refreshToken = createToken(
|
|
||||||
// payload,
|
|
||||||
// config.jwt.refresh_secret,
|
|
||||||
// config.jwt.refresh_expires_in
|
|
||||||
// );
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken,
|
user: userData,
|
||||||
user,
|
accessToken: token,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,37 @@
|
||||||
//auth.validation.js
|
|
||||||
|
|
||||||
const z = require("zod");
|
const z = require("zod");
|
||||||
|
|
||||||
const loginSchema = z.object({
|
const registerSchema = z.object({
|
||||||
email: z.string().email().min(1),
|
name: z.string().min(1, "Name is required"),
|
||||||
password: z.string().min(1),
|
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({
|
const loginSchema = z.object({
|
||||||
name: z.string().min(1),
|
email: z.string().email("Valid email required"),
|
||||||
email: z.string().email().min(1),
|
password: z.string().min(1, "Password is required"),
|
||||||
password: z.string().min(1),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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({
|
const resendVerificationSchema = z.object({
|
||||||
email: z
|
email: z.string().email("Valid email required"),
|
||||||
.string({ message: "Please enter a valid email address" })
|
|
||||||
.email()
|
|
||||||
.min(1),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const resetPasswordSchema = z.object({
|
const resetPasswordSchema = z.object({
|
||||||
newPassword: z.string().min(1),
|
newPassword: z.string().min(6, "Password must be at least 6 characters"),
|
||||||
token: z.string().min(1),
|
token: z.string().min(1, "Token is required"),
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
loginSchema,
|
|
||||||
registerSchema,
|
registerSchema,
|
||||||
|
loginSchema,
|
||||||
|
firebaseLoginSchema,
|
||||||
resendVerificationSchema,
|
resendVerificationSchema,
|
||||||
resetPasswordSchema,
|
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 router = require("express").Router();
|
||||||
const authRoutes = require("../modules/Auth/auth.routes");
|
const authRoutes = require("../modules/Auth/auth.routes");
|
||||||
const userRoutes = require("../modules/User/user.routes");
|
const userRoutes = require("../modules/User/user.routes");
|
||||||
|
const projectAuth = require("../modules/projectAuth/projectAuth.routes");
|
||||||
const { path } = require("../../app");
|
const { path } = require("../../app");
|
||||||
const moduleRoutes = [
|
const moduleRoutes = [
|
||||||
{
|
{
|
||||||
|
|
@ -11,6 +12,10 @@ const moduleRoutes = [
|
||||||
path: "/users",
|
path: "/users",
|
||||||
route: userRoutes,
|
route: userRoutes,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/project-auth",
|
||||||
|
route: projectAuth,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
moduleRoutes.forEach((route) => router.use(route.path, route.route));
|
moduleRoutes.forEach((route) => router.use(route.path, route.route));
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ const options = {
|
||||||
definition: {
|
definition: {
|
||||||
openapi: "3.0.0",
|
openapi: "3.0.0",
|
||||||
info: {
|
info: {
|
||||||
title: "Learnup Bangladesh API",
|
title: "Website Builder API",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
description: "API documentation for Learnup Bangladesh backend",
|
description: "API documentation for Website Builder backend",
|
||||||
contact: {
|
contact: {
|
||||||
name: "Learnup Bangladesh Dev Team",
|
name: "Website Builder Team",
|
||||||
email: "learnupbangladesh@gmail.com",
|
email: "websitebuilder@gmail.com",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
servers: [
|
servers: [
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue