project setup
This commit is contained in:
parent
7b7977a4c5
commit
c114c9756a
41 changed files with 6072 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/node_modules
|
||||
3943
package-lock.json
generated
Normal file
3943
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
35
package.json
Normal file
35
package.json
Normal 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
25
src/app.js
Normal 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;
|
||||
106
src/app/builder/QueryBuilder.js
Normal file
106
src/app/builder/QueryBuilder.js
Normal 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;
|
||||
8
src/app/config/firebase.js
Normal file
8
src/app/config/firebase.js
Normal 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
22
src/app/config/index.js
Normal 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,
|
||||
};
|
||||
6
src/app/config/supabaseClient.js
Normal file
6
src/app/config/supabaseClient.js
Normal 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;
|
||||
14
src/app/errors/AppError.js
Normal file
14
src/app/errors/AppError.js
Normal 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;
|
||||
20
src/app/errors/handleCastError.js
Normal file
20
src/app/errors/handleCastError.js
Normal 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;
|
||||
|
||||
19
src/app/errors/handleDuplicateError.js
Normal file
19
src/app/errors/handleDuplicateError.js
Normal 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;
|
||||
22
src/app/errors/handleValidationError.js
Normal file
22
src/app/errors/handleValidationError.js
Normal 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;
|
||||
19
src/app/errors/handleZodError.js
Normal file
19
src/app/errors/handleZodError.js
Normal 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;
|
||||
48
src/app/middleware/auth.js
Normal file
48
src/app/middleware/auth.js
Normal 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;
|
||||
68
src/app/middleware/globalError.js
Normal file
68
src/app/middleware/globalError.js
Normal 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;
|
||||
68
src/app/middleware/globalErrorhandler.js
Normal file
68
src/app/middleware/globalErrorhandler.js
Normal 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;
|
||||
63
src/app/middleware/multerConfig.js
Normal file
63
src/app/middleware/multerConfig.js
Normal 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,
|
||||
};
|
||||
10
src/app/middleware/notFound.js
Normal file
10
src/app/middleware/notFound.js
Normal 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;
|
||||
11
src/app/middleware/validateRequest.js
Normal file
11
src/app/middleware/validateRequest.js
Normal 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;
|
||||
139
src/app/modules/Auth/auth.controller.js
Normal file
139
src/app/modules/Auth/auth.controller.js
Normal 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;
|
||||
176
src/app/modules/Auth/auth.routes.js
Normal file
176
src/app/modules/Auth/auth.routes.js
Normal 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
|
||||
*/
|
||||
134
src/app/modules/Auth/auth.service.js
Normal file
134
src/app/modules/Auth/auth.service.js
Normal 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,
|
||||
};
|
||||
31
src/app/modules/Auth/auth.validation.js
Normal file
31
src/app/modules/Auth/auth.validation.js
Normal 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,
|
||||
};
|
||||
8
src/app/modules/User/user.constants.js
Normal file
8
src/app/modules/User/user.constants.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
const searchableFields = ['name', 'email', 'phone', 'address', 'role', 'status']
|
||||
const filterableFields = ['searchTerm', 'sort', 'limit', 'page']
|
||||
|
||||
module.exports = {
|
||||
searchableFields,
|
||||
filterableFields
|
||||
}
|
||||
141
src/app/modules/User/user.controller.js
Normal file
141
src/app/modules/User/user.controller.js
Normal 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;
|
||||
248
src/app/modules/User/user.routes.js
Normal file
248
src/app/modules/User/user.routes.js
Normal 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
|
||||
*/
|
||||
196
src/app/modules/User/user.service.js
Normal file
196
src/app/modules/User/user.service.js
Normal 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;
|
||||
17
src/app/modules/User/user.validation.js
Normal file
17
src/app/modules/User/user.validation.js
Normal 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
18
src/app/routes/index.js
Normal 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;
|
||||
77
src/app/swagger/swaggerConfig.js
Normal file
77
src/app/swagger/swaggerConfig.js
Normal 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;
|
||||
7
src/app/utils/catchAsync.js
Normal file
7
src/app/utils/catchAsync.js
Normal 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;
|
||||
14
src/app/utils/hashedPassword.js
Normal file
14
src/app/utils/hashedPassword.js
Normal 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
32
src/app/utils/jwt.js
Normal 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
15
src/app/utils/response.js
Normal 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
|
||||
});
|
||||
};
|
||||
97
src/app/utils/sendConfirmationEmail.js
Normal file
97
src/app/utils/sendConfirmationEmail.js
Normal 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">
|
||||
© ${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;
|
||||
95
src/app/utils/sendResetPassEmail.js
Normal file
95
src/app/utils/sendResetPassEmail.js
Normal 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">
|
||||
© ${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;
|
||||
20
src/app/utils/sendResponse.js
Normal file
20
src/app/utils/sendResponse.js
Normal 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;
|
||||
12
src/app/utils/transporterMail.js
Normal file
12
src/app/utils/transporterMail.js
Normal 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
|
||||
7
src/app/utils/validPass.js
Normal file
7
src/app/utils/validPass.js
Normal 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
14
src/server.js
Normal 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
66
structure.txt
Normal 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
|
||||
|
||||
Loading…
Add table
Reference in a new issue