project setup

This commit is contained in:
S M Fahim Hossen 2025-09-07 12:21:36 +06:00
parent 7b7977a4c5
commit c114c9756a
41 changed files with 6072 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/node_modules

3943
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

35
package.json Normal file
View file

@ -0,0 +1,35 @@
{
"name": "learnup-server",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node src/server.js",
"dev": "nodemon src/server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@supabase/supabase-js": "^2.53.0",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^5.1.0",
"firebase-admin": "^13.4.0",
"http-status": "^2.1.0",
"jsonwebtoken": "^9.0.2",
"moment": "^2.30.1",
"multer": "^1.4.5-lts.2",
"nodemailer": "^6.10.0",
"slugify": "^1.6.6",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"uuid": "^11.1.0",
"zod": "^3.24.2"
},
"devDependencies": {
"nodemon": "^3.1.9"
}
}

25
src/app.js Normal file
View file

@ -0,0 +1,25 @@
const express = require("express");
const cors = require("cors");
const app = express();
const path = require("path");
const routes = require("./app/routes");
const notFound = require("./app/middleware/notFound");
const globalErrorHandler = require("./app/middleware/globalError");
app.use(cors({ origin: "*" }));
app.use(express.json());
const setupSwaggerDocs = require("./app/swagger/swaggerConfig");
setupSwaggerDocs(app);
app.use("/api/v1", routes);
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",
});
});
app.use(globalErrorHandler);
app.use(notFound);
module.exports = app;

View file

@ -0,0 +1,106 @@
class QueryBuilder {
constructor(baseQuery, queryParams = {}) {
this.query = baseQuery; // Supabase query builder
this.queryParams = queryParams;
this.filters = [];
this.searchFields = [];
this.totalCount = null;
this.selectedFields = "*";
}
// Add search using ILIKE
search(fields = []) {
this.searchFields = fields;
const keyword = this.queryParams.search;
if (keyword) {
const orConditions = fields.map((field) => `${field}.ilike.%${keyword}%`);
this.query = this.query.or(`(${orConditions.join(",")})`);
}
return this;
}
// Add filtering on exact matches
filter(fields = []) {
fields.forEach((field) => {
if (this.queryParams[field] !== undefined) {
this.query = this.query.eq(field, this.queryParams[field]);
}
});
return this;
}
// Add sorting
sort() {
const sortBy = this.queryParams.sortBy || "created_at";
const sortOrder = this.queryParams.sortOrder || "desc";
this.query = this.query.order(sortBy, { ascending: sortOrder === "asc" });
return this;
}
// Add pagination
paginate(defaultLimit = 10, maxLimit = 50) {
const page = parseInt(this.queryParams.page || "1", 10);
const limit = Math.min(
parseInt(this.queryParams.limit || defaultLimit, 10),
maxLimit
);
const from = (page - 1) * limit;
const to = from + limit - 1;
this.query = this.query.range(from, to, { count: "exact" });
this.paginationMeta = {
page,
limit,
};
return this;
}
// Field selection (comma separated: name,email,role)
fields() {
const fieldsParam = this.queryParams.fields;
if (fieldsParam) {
const fieldsArray = fieldsParam
.split(",")
.map((f) => f.trim())
.filter(Boolean);
if (fieldsArray.length > 0) {
this.selectedFields = fieldsArray.join(",");
}
}
this.query = this.query.select(this.selectedFields, { count: "exact" });
return this;
}
// Execute query and return response + pagination meta
async exec() {
console.log("finding all user");
const { data, count, error } = await this.query;
console.log(data, error, count);
if (error) throw new Error(error.message);
const total = count ?? data.length;
const totalPages = Math.ceil(total / (this.paginationMeta?.limit || total));
return {
data,
count: total,
meta: {
total,
page: this.paginationMeta?.page || 1,
limit: this.paginationMeta?.limit || total,
totalPages,
},
};
}
}
module.exports = QueryBuilder;

View file

@ -0,0 +1,8 @@
const admin = require("firebase-admin");
const serviceAccount = require("../../../path/to/firebaseServiceAccount.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
module.exports = admin;

22
src/app/config/index.js Normal file
View file

@ -0,0 +1,22 @@
const dotenv = require("dotenv");
const path = require("path");
dotenv.config({ path: path.join(process.cwd(), ".env") });
module.exports = {
port: process.env.PORT || 5000,
database_url: process.env.DATABASE_URL,
supabase_url: process.env.SUPABASE_URL,
supabase_service_role_key: process.env.SUPABASE_SERVICE_ROLE_KEY,
bcrypt_salt_rounds: process.env.BCRYPT_SALT_ROUNDS,
jwt_access_secret: process.env.JWT_ACCESS_SECRET,
jwt_refresh_secret: process.env.JWT_REFRESH_SECRET,
jwt_access_expires_in: process.env.JWT_ACCESS_EXPIRES_IN,
jwt_refresh_expires_in: process.env.JWT_REFRESH_EXPIRES_IN,
NODE_ENV: process.env.NODE_ENV,
client_url: process.env.CLIENT_URL,
backend_url: process.env.BACKEND_URL,
mail_host: process.env.MAIL_HOST,
mail_port: process.env.MAIL_PORT,
mail_user: process.env.MAIL_USER,
mail_pass: process.env.MAIL_PASS,
};

View file

@ -0,0 +1,6 @@
const { createClient } = require("@supabase/supabase-js");
const { supabase_url, supabase_service_role_key } = require(".");
const supabase = createClient(supabase_url, supabase_service_role_key);
module.exports = supabase;

View file

@ -0,0 +1,14 @@
class AppError extends Error {
constructor(statusCode, message, stack = '') {
super(message);
this.statusCode = statusCode;
if (stack) {
this.stack = stack;
} else {
Error.captureStackTrace(this, this.constructor);
}
}
}
module.exports = AppError;

View file

@ -0,0 +1,20 @@
const handleCastError = (err) => {
const errorSources = [
{
path: err.path,
message: err.message,
},
];
const statusCode = 400;
return {
statusCode,
message: 'Invalid ID',
errorSources,
};
};
module.exports = handleCastError;

View file

@ -0,0 +1,19 @@
/* eslint-disable no-unused-vars */
const handleDuplicateError = (err) => {
const match = err.message.match(/"([^"]*)"/);
const extractedMessage = match && match[1];
const errorSources = [
{
path: '',
message: `${extractedMessage} is already exists`,
},
];
const statusCode = 400;
return {
statusCode,
message: 'Invalid ID',
errorSources,
};
};
module.exports = handleDuplicateError;

View file

@ -0,0 +1,22 @@
const handleValidationError = (err) => {
const errorSources = Object.values(err.errors).map(
(val) => {
return {
path: val.path,
message: val.message,
};
},
);
const statusCode = 400;
return {
statusCode,
message: 'Validation Error',
errorSources,
};
};
module.exports = handleValidationError;

View file

@ -0,0 +1,19 @@
const handleZodError = (err) => {
const errorSources = err.issues.map((issue) => {
return {
path: issue?.path[issue.path.length - 1],
message: issue.message,
};
});
const statusCode = 400;
return {
statusCode,
message: 'Validation Error',
errorSources,
};
};
module.exports = handleZodError;

View file

@ -0,0 +1,48 @@
const httpStatus = require("http-status").default;
const supabase = require("../config/supabaseClient");
const AppError = require("../errors/AppError");
const catchAsync = require("../utils/catchAsync");
const { verifyToken } = require("../utils/jwt");
const auth = (...requiredRoles) => {
return catchAsync(async (req, res, next) => {
const headers = req.headers.authorization;
const token = headers?.split(" ")[1];
if (!token) {
throw new AppError(httpStatus.UNAUTHORIZED, "You are not authorized!");
}
// Verify JWT
const decoded = verifyToken(token);
const { role, userId, email } = decoded;
// Fetch user from Supabase
const { data: user, error } = await supabase
.from("users")
.select("*")
.eq("email", email)
.single();
if (error || !user) {
throw new AppError(httpStatus.NOT_FOUND, "This user is not found!");
}
if (user.is_deleted) {
throw new AppError(httpStatus.FORBIDDEN, "This user is deleted!");
}
if (user.status === "blocked") {
throw new AppError(httpStatus.FORBIDDEN, "This user is blocked!");
}
if (requiredRoles.length && !requiredRoles.includes(user.role)) {
throw new AppError(httpStatus.UNAUTHORIZED, "You are not authorized!");
}
req.user = { ...decoded, role: user.role, userId: user.id };
next();
});
};
module.exports = auth;

View file

@ -0,0 +1,68 @@
const { ZodError } = require("zod");
const config = require("../config");
const AppError = require("../errors/AppError");
const handleCastError = require("../errors/handleCastError");
const handleDuplicateError = require("../errors/handleDuplicateError");
const handleValidationError = require("../errors/handleValidationError");
const handleZodError = require("../errors/handleZodError");
const globalErrorHandler = (err, req, res, next) => {
let statusCode = 500;
let message = 'Something went wrong!';
let errorSources = [
{
path: '',
message: 'Something went wrong',
},
];
if (err instanceof ZodError) {
const simplifiedError = handleZodError(err);
statusCode = simplifiedError?.statusCode;
message = simplifiedError?.message;
errorSources = simplifiedError?.errorSources;
} else if (err?.name === 'ValidationError') {
const simplifiedError = handleValidationError(err);
statusCode = simplifiedError?.statusCode;
message = simplifiedError?.message;
errorSources = simplifiedError?.errorSources;
} else if (err?.name === 'CastError') {
const simplifiedError = handleCastError(err);
statusCode = simplifiedError?.statusCode;
message = simplifiedError?.message;
errorSources = simplifiedError?.errorSources;
} else if (err?.code === 11000) {
const simplifiedError = handleDuplicateError(err);
statusCode = simplifiedError?.statusCode;
message = simplifiedError?.message;
errorSources = simplifiedError?.errorSources;
} else if (err instanceof AppError) {
statusCode = err?.statusCode;
message = err.message;
errorSources = [
{
path: '',
message: err?.message,
},
];
} else if (err instanceof Error) {
message = err.message;
errorSources = [
{
path: '',
message: err?.message,
},
];
}
// return
return res.status(statusCode).json({
success: false,
message,
errorSources,
err,
stack: config.NODE_ENV === 'development' ? err?.stack : null,
});
};
module.exports = globalErrorHandler;

View file

@ -0,0 +1,68 @@
const { ZodError } = require("zod");
const config = require("../config");
const AppError = require("../errors/AppError");
const handleCastError = require("../errors/handleCastError");
const handleDuplicateError = require("../errors/handleDuplicateError");
const handleValidationError = require("../errors/handleValidationError");
const handleZodError = require("../errors/handleZodError");
const globalErrorHandler = (err, req, res, next) => {
let statusCode = 500;
let message = 'Something went wrong!';
let errorSources = [
{
path: '',
message: 'Something went wrong',
},
];
if (err instanceof ZodError) {
const simplifiedError = handleZodError(err);
statusCode = simplifiedError?.statusCode;
message = simplifiedError?.message;
errorSources = simplifiedError?.errorSources;
} else if (err?.name === 'ValidationError') {
const simplifiedError = handleValidationError(err);
statusCode = simplifiedError?.statusCode;
message = simplifiedError?.message;
errorSources = simplifiedError?.errorSources;
} else if (err?.name === 'CastError') {
const simplifiedError = handleCastError(err);
statusCode = simplifiedError?.statusCode;
message = simplifiedError?.message;
errorSources = simplifiedError?.errorSources;
} else if (err?.code === 11000) {
const simplifiedError = handleDuplicateError(err);
statusCode = simplifiedError?.statusCode;
message = simplifiedError?.message;
errorSources = simplifiedError?.errorSources;
} else if (err instanceof AppError) {
statusCode = err?.statusCode;
message = err.message;
errorSources = [
{
path: '',
message: err?.message,
},
];
} else if (err instanceof Error) {
message = err.message;
errorSources = [
{
path: '',
message: err?.message,
},
];
}
// return
return res.status(statusCode).json({
success: false,
message,
errorSources,
err,
stack: config.NODE_ENV === 'development' ? err?.stack : null,
});
};
module.exports = globalErrorHandler;

View file

@ -0,0 +1,63 @@
const multer = require("multer");
const path = require("path");
const fs = require("fs");
// Base folder where files are physically stored
const BASE_DIR = path.join(__dirname, "../local/store");
// Multer storage configuration
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const subfolder = file.mimetype.startsWith("image") ? "images" : "videos";
const uploadDir = path.join(BASE_DIR, subfolder);
// Ensure the directory exists
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
const ext = path.extname(file.originalname);
const fileName = `${file.fieldname}-${uniqueSuffix}${ext}`;
// Set req._relativePath so we can reassign it below
const subfolder = file.mimetype.startsWith("image") ? "images" : "videos";
req._relativePath = `store/${subfolder}/${fileName}`;
cb(null, fileName);
},
});
// Middleware to override req.file.path with relative path
const setRelativePath = (req, res, next) => {
const processFile = (file) => {
const subfolder = file.mimetype.startsWith("image") ? "images" : "videos";
file.path = `store/${subfolder}/${file.filename}`;
};
if (req.file) {
processFile(req.file);
}
if (req.files && Array.isArray(req.files)) {
req.files.forEach(processFile);
}
next();
};
const uploadMedia = multer({
storage,
limits: {
fileSize: 10 * 1024 * 1024, // 10MB
},
});
module.exports = {
uploadMedia,
setRelativePath,
};

View file

@ -0,0 +1,10 @@
const httpStatus = require('http-status').default;
const notFound = (req, res, next) => {
return res.status(httpStatus.NOT_FOUND).json({
success: false,
message: 'API Not Found !!',
error: '',
});
};
module.exports = notFound;

View file

@ -0,0 +1,11 @@
const catchAsync = require('../utils/catchAsync');
const validateRequest = (schema) => {
return catchAsync(async (req, res, next) => {
await schema.parseAsync(req.body);
next();
});
};
module.exports = validateRequest;

View file

@ -0,0 +1,139 @@
//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 {
registerUser,
loginUser,
firebaseLogin: firebaseAuth,
} = require("./auth.service");
const httpStatus = require("http-status").default;
const AppError = require("../../errors/AppError");
const hashPassword = require("../../utils/hashedPassword");
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);
sendResponse(res, {
success: true,
message: "Registration successful.",
data: user,
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();
sendResponse(res, {
success: true,
message: "Email verified successfully and account activated",
data: null,
statusCode: httpStatus.OK,
});
});
const resendVerification = catchAsync(async (req, res) => {
const user = await UserModel.findOne({ email: req.body.email });
if (!user) {
throw new AppError(httpStatus.NOT_FOUND, "User not found");
}
if (user.isVerified) {
throw new AppError(httpStatus.BAD_REQUEST, "User already verified");
}
const verificationToken = createToken({ email: user.email, role: user.role });
sendConfirmationEmail(user.email, verificationToken);
sendResponse(res, {
success: true,
message: "Verification email sent",
data: null,
statusCode: httpStatus.OK,
});
});
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);
sendResponse(res, {
success: true,
message: "Password reset email sent",
data: null,
statusCode: httpStatus.OK,
});
});
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) } }
);
sendResponse(res, {
success: true,
message: "Password reset successful",
data: null,
statusCode: httpStatus.OK,
});
});
const AuthController = {
login,
register,
verfiyEmail,
resendVerification,
forgotPassword,
resetPassword,
firebaseLogin,
};
module.exports = AuthController;

View file

@ -0,0 +1,176 @@
//auth.routes.js
const validateRequest = require("../../middleware/validateRequest");
const AuthController = require("./auth.controller");
const {
loginSchema,
registerSchema,
resendVerificationSchema,
resetPasswordSchema,
} = require("./auth.validation");
const router = require("express").Router();
router.post("/login", validateRequest(loginSchema), AuthController.login);
router.post("/firebase", AuthController.firebaseLogin);
router.post(
"/register",
validateRequest(registerSchema),
AuthController.register
);
router.get("/verify-email", AuthController.verfiyEmail);
router.post(
"/resend-verification",
validateRequest(resendVerificationSchema),
AuthController.resendVerification
);
router.post(
"/forgot-password",
validateRequest(resendVerificationSchema),
AuthController.forgotPassword
);
router.post(
"/reset-password",
validateRequest(resetPasswordSchema),
AuthController.resetPassword
);
const authRoutes = router;
module.exports = authRoutes;
/**
* @swagger
* /auth/login:
* post:
* summary: Login user
* tags: [Auth]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - email
* - password
* properties:
* email:
* type: string
* format: email
* example: "johndoe1@example.com"
* password:
* type: string
* example: StrongP@ssw0rd
* responses:
* 200:
* description: Login successful
* 401:
* description: Invalid credentials
*/
/**
* @swagger
* /auth/register:
* post:
* summary: Register a new user
* tags: [Auth]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* - email
* - password
* properties:
* name:
* type: string
* example: John Doe
* email:
* type: string
* format: email
* example: johndoe@example.com
* password:
* type: string
* format: password
* example: StrongP@ssw0rd
* phone:
* type: string
* example: "017XXXXXXXX"
* dob:
* type: string
* format: date
* example: "2000-01-01"
* division:
* type: string
* example: Dhaka
* district:
* type: string
* example: Gazipur
* upazila:
* type: string
* example: Sreepur
* institution:
* type: string
* example: LearnUp High School
* responses:
* 201:
* description: User registered successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: User registered successfully
* data:
* type: object
* properties:
* id:
* type: string
* example: "uuid-or-id"
* name:
* type: string
* email:
* type: string
* 400:
* description: User already exists or validation error
* 500:
* description: Server error
*/
/**
* @swagger
* /auth/firebase:
* post:
* summary: Login or register using Firebase token
* tags: [Auth]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - firebaseToken
* - email
* - name
* properties:
* firebaseToken:
* type: string
* email:
* type: string
* name:
* type: string
* responses:
* 200:
* description: Successful login
* 400:
* description: Missing or invalid data
* 401:
* description: Invalid Firebase token
*/

View file

@ -0,0 +1,134 @@
const httpStatus = require("http-status").default;
const AppError = require("../../errors/AppError");
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 registerUser = async (payload) => {
const { data: existingUser } = await supabase
.from("users")
.select("*")
.eq("email", payload.email)
.single();
if (existingUser) {
throw new AppError(httpStatus.BAD_REQUEST, "User already exists");
}
const hashedPassword = await bcrypt.hash(payload.password, 10);
const { data: createdUser, error } = await supabase
.from("users")
.insert({
name: payload.name,
email: payload.email,
phone: payload.phone,
password: hashedPassword,
})
.select()
.single();
if (error) {
throw new AppError(
httpStatus.INTERNAL_SERVER_ERROR,
"User creation failed"
);
}
return createdUser;
};
const loginUser = async (payload) => {
const { data: user, error } = await supabase
.from("users")
.select("*")
.eq("email", payload.email)
.single();
if (error || !user) {
throw new AppError(httpStatus.BAD_REQUEST, "User does not exist");
}
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");
}
const token = createToken({
email: user.email,
userId: user.id,
role: user.role,
});
const { password, ...userData } = user;
return {
...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,
firebaseLogin,
};

View file

@ -0,0 +1,31 @@
//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),
email: z.string().email().min(1),
password: z.string().min(1),
});
const resendVerificationSchema = z.object({
email: z
.string({ message: "Please enter a valid email address" })
.email()
.min(1),
});
const resetPasswordSchema = z.object({
newPassword: z.string().min(1),
token: z.string().min(1),
});
module.exports = {
loginSchema,
registerSchema,
resendVerificationSchema,
resetPasswordSchema,
};

View file

@ -0,0 +1,8 @@
const searchableFields = ['name', 'email', 'phone', 'address', 'role', 'status']
const filterableFields = ['searchTerm', 'sort', 'limit', 'page']
module.exports = {
searchableFields,
filterableFields
}

View file

@ -0,0 +1,141 @@
const catchAsync = require("../../utils/catchAsync");
const sendResponse = require("../../utils/sendResponse");
const UserService = require("./user.service");
const httpStatus = require("http-status").default;
const users = catchAsync(async (req, res) => {
const adminId = req.user.userId;
console.log("admin id", adminId);
const result = await UserService.users(req.query, adminId);
sendResponse(res, {
success: true,
message: "Users fetched successfully",
data: result,
statusCode: httpStatus.OK,
});
});
const getUserByID = catchAsync(async (req, res) => {
const id = req.user.userId;
const result = await UserService.getUserByID(id);
sendResponse(res, {
success: true,
message: "User fetched successfully",
data: result,
statusCode: httpStatus.OK,
});
});
const checkUserRoleStatus = catchAsync(async (req, res) => {
const id = req.user.userId;
const result = await UserService.getUserByID(id);
sendResponse(res, {
success: true,
message: "User role checked successfully",
data: { role: result.role },
statusCode: httpStatus.OK,
});
});
const createUser = catchAsync(async (req, res) => {
const result = await UserService.createUser(req.body);
sendResponse(res, {
success: true,
message: "User created successfully",
data: result,
statusCode: httpStatus.CREATED,
});
});
const updateUserByAdmin = catchAsync(async (req, res) => {
const { id } = req.params;
const result = await UserService.updateUser(id, req.body);
sendResponse(res, {
success: true,
message: "User updated successfully",
data: result,
statusCode: httpStatus.OK,
});
});
const updateUser = catchAsync(async (req, res) => {
const id = req.user.userId;
console.log(id);
console.log(req.body);
const result = await UserService.updateUser(id, req.body);
sendResponse(res, {
success: true,
message: "User updated successfully",
data: result,
statusCode: httpStatus.OK,
});
});
const deleteUser = catchAsync(async (req, res) => {
const { id } = req.params;
const result = await UserService.deleteUser(id);
sendResponse(res, {
success: true,
message: "User deleted successfully",
data: result,
statusCode: httpStatus.OK,
});
});
const updateAccountStatus = catchAsync(async (req, res) => {
const { id } = req.params;
const status = req.body.status;
const result = await UserService.updateAccountStatus(id, status);
sendResponse(res, {
success: true,
message: "Account disabled successfully",
data: result,
statusCode: httpStatus.OK,
});
});
const updatePreferences = async (req, res) => {
try {
const { userId, preference } = req.body;
if (!Array.isArray(preference)) {
return res.status(400).json({
success: false,
message: "Preference must be an array of strings",
});
}
const updatedUser = await UserService.setUserPreferences(
userId,
preference
);
res.json({
success: true,
message: "Preferences updated successfully",
data: updatedUser,
});
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to update preferences",
error: error.message,
});
}
};
const UserController = {
users,
createUser,
updateUser,
deleteUser,
updateAccountStatus,
updateUserByAdmin,
getUserByID,
updatePreferences,
checkUserRoleStatus,
};
module.exports = UserController;

View file

@ -0,0 +1,248 @@
const auth = require("../../middleware/auth");
const validateRequest = require("../../middleware/validateRequest");
const UserController = require("./user.controller");
const router = require("express").Router();
const {
createUserValidation,
updateAccountStatusValidation,
} = require("./user.validation");
router.get("/", auth("superAdmin", "admin"), UserController.users);
router.post(
"/",
auth("superAdmin"),
validateRequest(createUserValidation),
UserController.createUser
);
router.patch(
"/me",
auth("user", "admin", "superAdmin"),
UserController.updateUser
);
router.patch("/me/preferences", UserController.updatePreferences);
router.patch("/:id", auth("superAdmin"), UserController.updateUserByAdmin);
router.patch(
"/:id/status",
auth("superAdmin"),
validateRequest(updateAccountStatusValidation),
UserController.updateAccountStatus
);
router.delete("/:id", auth("superAdmin"), UserController.deleteUser);
router.get(
"/me",
auth("user", "admin", "superAdmin"),
UserController.getUserByID
);
router.get(
"/me/role-check",
auth("user", "admin", "superAdmin"),
UserController.checkUserRoleStatus
);
const userRoutes = router;
module.exports = userRoutes;
/**
* @swagger
* tags:
* name: Users
* description: User management endpoints
*/
/**
* @swagger
* /users:
* get:
* summary: Get a list of users
* tags: [Users]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: number
* description: Page number for pagination
* - in: query
* name: limit
* schema:
* type: number
* description: Number of results per page
* - in: query
* name: search
* schema:
* type: string
* description: Search by name or email
* responses:
* 200:
* description: List of users
* 401:
* description: Unauthorized
*/
/**
* @swagger
* /users:
* post:
* summary: Create a new user (Admin only)
* tags: [Users]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UserInput'
* responses:
* 201:
* description: User created
* 400:
* description: Validation error
*/
/**
* @swagger
* /users/me:
* patch:
* summary: Update own profile
* tags: [Users]
* security:
* - bearerAuth: []
* requestBody:
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UserUpdate'
* responses:
* 200:
* description: Profile updated
* 404:
* description: User not found
*/
/**
* @swagger
* /users/me/preferences:
* patch:
* summary: Update user preferences
* tags: [Users]
* security:
* - bearerAuth: []
* requestBody:
* content:
* application/json:
* schema:
* type: object
* example:
* theme: dark
* notifications: true
* responses:
* 200:
* description: Preferences updated
*/
/**
* @swagger
* /users/{id}:
* patch:
* summary: Update user by admin
* tags: [Users]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: User ID
* requestBody:
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UserUpdate'
* responses:
* 200:
* description: User updated
* 404:
* description: User not found
*/
/**
* @swagger
* /users/{id}/status:
* patch:
* summary: Update account status
* tags: [Users]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: User ID
* requestBody:
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* enum: [active, blocked]
* example: blocked
* responses:
* 200:
* description: Status updated
*/
/**
* @swagger
* /users/{id}:
* delete:
* summary: Soft delete a user
* tags: [Users]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: User deleted
* 404:
* description: User not found
*/
/**
* @swagger
* /users/me:
* get:
* summary: Get own user info
* tags: [Users]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: User info
*/
/**
* @swagger
* /users/me/role-check:
* get:
* summary: Check user's role
* tags: [Users]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Role status
*/

View file

@ -0,0 +1,196 @@
const supabase = require("../../config/supabaseClient");
const AppError = require("../../errors/AppError");
const hashPassword = require("../../utils/hashedPassword");
const httpStatus = require("http-status").default;
const QueryBuilder = require("../../builder/QueryBuilder");
const users = async (userId, queryParams) => {
const queryBuilder = new QueryBuilder("users");
queryBuilder
.filter("id", "neq", userId)
.filter("is_deleted", "eq", false)
.paginate(queryParams.page, queryParams.limit)
.sort("created_at", "desc");
// Execute the query properly
const { data, error, count } = await queryBuilder.query;
if (error) {
throw new AppError(500, "Failed to fetch users", error.message);
}
return {
meta: {
total: count,
page: Number(queryParams.page),
limit: Number(queryParams.limit),
},
data,
};
};
const getSuperAdminEmails = async () => {
const { data, error } = await supabase
.from("users")
.select("email")
.eq("role", "superAdmin")
.eq("is_deleted", false);
if (error)
throw new AppError(httpStatus.INTERNAL_SERVER_ERROR, error.message);
return data.map((u) => u.email);
};
const getUserByID = async (id) => {
const { data: user, error } = await supabase
.from("users")
.select("*")
.eq("id", id)
.eq("is_deleted", false)
.single();
if (!user || error)
throw new AppError(httpStatus.NOT_FOUND, "User not found");
return user;
};
const createUser = async (payload) => {
const { data: existingUser } = await supabase
.from("users")
.select("id")
.eq("email", payload.email)
.maybeSingle();
if (existingUser) {
throw new AppError(httpStatus.BAD_REQUEST, "User already exists");
}
const { data: result, error } = await supabase
.from("users")
.insert([payload])
.select()
.single();
if (error)
throw new AppError(httpStatus.INTERNAL_SERVER_ERROR, error.message);
return result;
};
const updateUser = async (id, payload) => {
const { data: user } = await supabase
.from("users")
.select("*")
.eq("id", id)
.single();
if (!user) throw new AppError(httpStatus.NOT_FOUND, "User not found");
if (payload.password) {
payload.needs_password_change = true;
payload.password = await hashPassword(payload.password);
}
const { data: updated, error } = await supabase
.from("users")
.update(payload)
.eq("id", id)
.select()
.single();
if (error)
throw new AppError(httpStatus.INTERNAL_SERVER_ERROR, error.message);
return updated;
};
const deleteUser = async (id) => {
const { data: user } = await supabase
.from("users")
.select("id")
.eq("id", id)
.single();
if (!user) throw new AppError(httpStatus.NOT_FOUND, "User not found");
const { data: updated, error } = await supabase
.from("users")
.update({ is_deleted: true })
.eq("id", id)
.select()
.single();
if (error)
throw new AppError(httpStatus.INTERNAL_SERVER_ERROR, error.message);
return updated;
};
const updateAccountStatus = async (id, status) => {
const { data: user } = await supabase
.from("users")
.select("*")
.eq("id", id)
.single();
if (!user) throw new AppError(httpStatus.NOT_FOUND, "User not found");
const is_deleted = status === "active" ? false : user.is_deleted;
const { data: updated, error } = await supabase
.from("users")
.update({ status, is_deleted })
.eq("id", id)
.select()
.single();
if (error)
throw new AppError(httpStatus.INTERNAL_SERVER_ERROR, error.message);
return updated;
};
const setUserPreferences = async (userId, preferences) => {
const { data: updatedUser, error } = await supabase
.from("users")
.update({ preferences })
.eq("id", userId)
.select("name, email, preferences")
.single();
if (!updatedUser || error)
throw new AppError(httpStatus.NOT_FOUND, "User not found");
return updatedUser;
};
const findUserByEmail = async (email) => {
const { data, error } = await supabase
.from("users")
.select("*")
.eq("email", email)
.single();
if (error && error.code !== "PGRST116") {
throw error;
}
return data || null;
};
const UserService = {
users,
createUser,
updateUser,
deleteUser,
updateAccountStatus,
getUserByID,
setUserPreferences,
getSuperAdminEmails,
findUserByEmail,
};
module.exports = UserService;

View file

@ -0,0 +1,17 @@
const { z } = require("zod");
const { registerSchema } = require("../Auth/auth.validation");
const createUserValidation = registerSchema.extend({
phone: z.string().regex(/^0?[1-9]\d{1,14}$/).optional(),
role: z.string(['admin', 'user']).min(1),
});
const updateUserValidation = registerSchema.deepPartial();
const updateAccountStatusValidation = z.object({
status: z.string(['active', 'disabled']).min(1),
})
module.exports = {
createUserValidation,
updateUserValidation,
updateAccountStatusValidation
}

18
src/app/routes/index.js Normal file
View file

@ -0,0 +1,18 @@
const router = require("express").Router();
const authRoutes = require("../modules/Auth/auth.routes");
const userRoutes = require("../modules/User/user.routes");
const { path } = require("../../app");
const moduleRoutes = [
{
path: "/auth",
route: authRoutes,
},
{
path: "/users",
route: userRoutes,
},
];
moduleRoutes.forEach((route) => router.use(route.path, route.route));
module.exports = router;

View file

@ -0,0 +1,77 @@
const swaggerJSDoc = require("swagger-jsdoc");
const swaggerUi = require("swagger-ui-express");
const options = {
definition: {
openapi: "3.0.0",
info: {
title: "Learnup Bangladesh API",
version: "1.0.0",
description: "API documentation for Learnup Bangladesh backend",
contact: {
name: "Learnup Bangladesh Dev Team",
email: "learnupbangladesh@gmail.com",
},
},
servers: [
{
url: "http://localhost:5000/api/v1",
description: "Development server",
},
],
components: {
securitySchemes: {
bearerAuth: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
},
},
schemas: {
UserInput: {
type: "object",
required: ["name", "email", "password"],
properties: {
name: { type: "string" },
email: { type: "string", format: "email" },
password: { type: "string" },
password: { type: "string" },
role: { type: "string" },
dob: { type: "string", format: "date" },
division: { type: "string" },
district: { type: "string" },
upazila: { type: "string" },
institution: { type: "string" },
},
},
UserUpdate: {
type: "object",
properties: {
name: { type: "string" },
email: { type: "string" },
password: { type: "string" },
role: { type: "string" },
phone: { type: "string" },
division: { type: "string" },
district: { type: "string" },
upazila: { type: "string" },
institution: { type: "string" },
},
},
},
},
security: [
{
bearerAuth: [],
},
],
},
apis: ["./src/app/modules/**/*.js"],
};
const swaggerSpec = swaggerJSDoc(options);
const setupSwaggerDocs = (app) => {
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
};
module.exports = setupSwaggerDocs;

View file

@ -0,0 +1,7 @@
const catchAsync = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch((err) => next(err));
};
};
module.exports = catchAsync;

View file

@ -0,0 +1,14 @@
const bcrypt = require('bcrypt');
async function hashPassword(plainPassword) {
const saltRounds = 10;
try {
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
return hashedPassword;
} catch (err) {
console.error('Error hashing password:', err);
throw err;
}
}
module.exports = hashPassword;

32
src/app/utils/jwt.js Normal file
View file

@ -0,0 +1,32 @@
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const config = require('../config');
// Generate Access Token
const createToken = (payload , expiresIn = config.jwt_access_expires_in) => {
// Generate a random secret for HS256 algorithm
const secret = config.jwt_access_secret
return jwt.sign(payload, secret, {
algorithm: 'HS256',
expiresIn: expiresIn
});
};
// Verify Token
const verifyToken = (token) => {
try {
// Verify the token with the stored secret
return jwt.verify(token, config.jwt_access_secret);
} catch (error) {
throw new Error('Invalid token');
}
};
module.exports = {
createToken,
verifyToken
};

15
src/app/utils/response.js Normal file
View file

@ -0,0 +1,15 @@
exports.successResponse = (res, message, data = {}) => {
return res.status(200).json({
success: true,
message,
data
});
};
exports.errorResponse = (res, message, statusCode = 500, error = "") => {
return res.status(statusCode).json({
success: false,
message,
error
});
};

View file

@ -0,0 +1,97 @@
const config = require("../config");
const transporter = require("./transporterMail");
const sendConfirmationEmail = (userEmail, token) => {
const confirmationLink = `${config.client_url}/verify-email?token=${token}`;
const mailOptions = {
from: config.mail_user,
to: userEmail,
subject: "Confirm Your Email Address - Learnup Bangladesh",
html: `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Confirm Your Email</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f4f6f8;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 40px auto;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
padding: 30px;
color: #333333;
}
h1 {
color: #2c3e50;
font-weight: 700;
margin-bottom: 10px;
}
p {
font-size: 16px;
line-height: 1.6;
margin-bottom: 20px;
}
a.button {
display: inline-block;
background-color: #1e88e5;
color: #ffffff !important;
text-decoration: none;
padding: 12px 25px;
border-radius: 5px;
font-weight: 600;
transition: background-color 0.3s ease;
}
a.button:hover {
background-color: #0d6efd;
}
.footer {
font-size: 12px;
color: #888888;
margin-top: 30px;
text-align: center;
}
.brand {
font-weight: 700;
color: #1e88e5;
}
</style>
</head>
<body>
<div class="container">
<h1>Welcome to <span class="brand">Learnup Bangladesh</span>!</h1>
<p>Thank you for registering with us. Please confirm your email address by clicking the button below:</p>
<p style="text-align:center;">
<a href="${confirmationLink}" class="button" target="_blank" rel="noopener noreferrer">Confirm Email Address</a>
</p>
<p>If the button above doesn't work, copy and paste the following URL into your browser:</p>
<p style="word-break: break-word; color:#1e88e5;">${confirmationLink}</p>
<p>If you didn't register, please ignore this email.</p>
<div class="footer">
&copy; ${new Date().getFullYear()} Learnup Bangladesh. All rights reserved.
</div>
</div>
</body>
</html>
`,
};
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
console.error("Error sending email:", error);
} else {
console.log("Confirmation email sent:", info.response);
}
});
};
module.exports = sendConfirmationEmail;

View file

@ -0,0 +1,95 @@
const config = require("../config");
const transporter = require("./transporterMail");
const sendResetPassEmail = (userEmail, resetLink) => {
const mailOptions = {
from: config.mail_user,
to: userEmail,
subject: "Reset your password - Learnup Bangladesh",
html: `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Confirm Your Email</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f4f6f8;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 40px auto;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
padding: 30px;
color: #333333;
}
h1 {
color: #2c3e50;
font-weight: 700;
margin-bottom: 10px;
}
p {
font-size: 16px;
line-height: 1.6;
margin-bottom: 20px;
}
a.button {
display: inline-block;
background-color: #1e88e5;
color: #ffffff !important;
text-decoration: none;
padding: 12px 25px;
border-radius: 5px;
font-weight: 600;
transition: background-color 0.3s ease;
}
a.button:hover {
background-color: #0d6efd;
}
.footer {
font-size: 12px;
color: #888888;
margin-top: 30px;
text-align: center;
}
.brand {
font-weight: 700;
color: #1e88e5;
}
</style>
</head>
<body>
<div class="container">
<h1>Welcome to <span class="brand">Learnup Bangladesh</span>!</h1>
<p>We have recived a request to reset you password. Please confirm your email address by clicking the button below:</p>
<p style="text-align:center;">
<a href="${resetLink}" class="button" target="_blank" rel="noopener noreferrer">Reset Password</a>
</p>
<p>This link will expire in 15 minutes. If the button above doesn't work, copy and paste the following URL into your browser:</p>
<p style="word-break: break-word; color:#1e88e5;">${resetLink}</p>
<p>If you didn't register, please ignore this email.</p>
<div class="footer">
&copy; ${new Date().getFullYear()} Learnup Bangladesh. All rights reserved.
</div>
</div>
</body>
</html>
`,
};
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
console.error("Error sending email:", error);
} else {
console.log("Password reset email sent:", info.response);
}
});
};
module.exports = sendResetPassEmail;

View file

@ -0,0 +1,20 @@
/**
* @description - This function sends a JSON response with the given data
* @param {Object} res - The ExpressJS response object
* @param {Object} data - An object containing the following properties:
* - success: A boolean indicating if the request was successful
* - message: A string with a message to be sent to the client
* - meta: An object containing any additional metadata
* - data: An object containing the data to be sent to the client
* @returns {undefined}
*/
const sendResponse = (res, data) => {
res.status(data?.statusCode || 200).json({
success: data.success,
message: data.message,
meta: data.meta,
data: data.data,
});
};
module.exports = sendResponse;

View file

@ -0,0 +1,12 @@
const config = require("../config");
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
host: config.mail_host,
port: config.mail_port,
auth: {
user: config.mail_user,
pass: config.mail_pass,
},
});
module.exports = transporter

View file

@ -0,0 +1,7 @@
const bcrypt = require('bcrypt');
const compareValidPass = async (payloadPass, hashedPass) => {
const isValidPass = await bcrypt.compare(payloadPass, hashedPass);
return isValidPass;
};
module.exports = compareValidPass;

14
src/server.js Normal file
View file

@ -0,0 +1,14 @@
const app = require("./app");
const config = require("./app/config/index");
async function main() {
try {
app.listen(config.port, () => {
console.log(`app is listening on port ${config.port}`);
});
} catch (err) {
console.log(err);
}
}
main();

66
structure.txt Normal file
View file

@ -0,0 +1,66 @@
| .env
| .env.example
| .gitignore
| package-lock.json
| package.json
| structure.txt
|
\---src
| app.js
| server.js
|
\---app
+---builder
| QueryBuilder.js
|
+---config
| index.js
| supabaseClient.js
|
+---errors
| AppError.js
| handleCastError.js
| handleDuplicateError.js
| handleValidationError.js
| handleZodError.js
|
+---middleware
| auth.js
| globalError.js
| globalErrorhandler.js
| multerConfig.js
| notFound.js
| uploadMinio.js
| validateRequest.js
|
+---modules
| +---Auth
| | auth.controller.js
| | auth.routes.js
| | auth.service.js
| | auth.validation.js
| |
| \---User
| user.constants.js
| user.controller.js
| user.routes.js
| user.service.js
| user.validation.js
|
+---routes
| index.js
|
+---swagger
| swaggerConfig.js
|
\---utils
catchAsync.js
hashedPassword.js
jwt.js
response.js
sendConfirmationEmail.js
sendResetPassEmail.js
sendResponse.js
transporterMail.js
validPass.js