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