├── .env.example ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── erdiagram.png ├── package-lock.json ├── package.json ├── postman_collection.json ├── readme.md ├── src ├── app.ts ├── app │ ├── DB │ │ └── index.ts │ ├── builder │ │ └── QueryBuilder.ts │ ├── config │ │ └── index.ts │ ├── errors │ │ ├── AppError.ts │ │ ├── handleCastError.ts │ │ ├── handleDuplicateError.ts │ │ ├── handleValidationError.ts │ │ └── handleZodError.ts │ ├── interface │ │ ├── error.ts │ │ └── index.d.ts │ ├── middlewares │ │ ├── auth.ts │ │ ├── globalErrorhandler.ts │ │ ├── notFound.ts │ │ └── validateRequest.ts │ ├── modules │ │ ├── AcademicDepartment │ │ │ ├── academicDepartment.controller.ts │ │ │ ├── academicDepartment.interface.ts │ │ │ ├── academicDepartment.model.ts │ │ │ ├── academicDepartment.route.ts │ │ │ ├── academicDepartment.service.ts │ │ │ ├── academicDepartment.validation.ts │ │ │ └── academicDepartmets.constant.ts │ │ ├── AcademicFaculty │ │ │ ├── academicFaculty.constant.ts │ │ │ ├── academicFaculty.controller.ts │ │ │ ├── academicFaculty.interface.ts │ │ │ ├── academicFaculty.model.ts │ │ │ ├── academicFaculty.route.ts │ │ │ ├── academicFaculty.service.ts │ │ │ └── academicFaculty.validation.ts │ │ ├── AcademicSemester │ │ │ ├── academicSemester.constant.ts │ │ │ ├── academicSemester.controller.ts │ │ │ ├── academicSemester.interface.ts │ │ │ ├── academicSemester.model.ts │ │ │ ├── academicSemester.route.ts │ │ │ ├── academicSemester.service.ts │ │ │ └── academicSemester.validation.ts │ │ ├── Admin │ │ │ ├── admin.constant.ts │ │ │ ├── admin.controller.ts │ │ │ ├── admin.interface.ts │ │ │ ├── admin.model.ts │ │ │ ├── admin.route.ts │ │ │ ├── admin.service.ts │ │ │ └── admin.validation.ts │ │ ├── Auth │ │ │ ├── auth.controller.ts │ │ │ ├── auth.interface.ts │ │ │ ├── auth.route.ts │ │ │ ├── auth.service.ts │ │ │ ├── auth.utils.ts │ │ │ └── auth.validation.ts │ │ ├── Course │ │ │ ├── course.constant.ts │ │ │ ├── course.controller.ts │ │ │ ├── course.interface.ts │ │ │ ├── course.model.ts │ │ │ ├── course.route.ts │ │ │ ├── course.service.ts │ │ │ └── course.validation.ts │ │ ├── EnrolledCourse │ │ │ ├── enrolledCourse.constant.ts │ │ │ ├── enrolledCourse.controller.ts │ │ │ ├── enrolledCourse.interface.ts │ │ │ ├── enrolledCourse.model.ts │ │ │ ├── enrolledCourse.route.ts │ │ │ ├── enrolledCourse.service.ts │ │ │ ├── enrolledCourse.utils.ts │ │ │ └── enrolledCourse.validaton.ts │ │ ├── Faculty │ │ │ ├── faculty.constant.ts │ │ │ ├── faculty.controller.ts │ │ │ ├── faculty.interface.ts │ │ │ ├── faculty.model.ts │ │ │ ├── faculty.route.ts │ │ │ ├── faculty.service.ts │ │ │ └── faculty.validation.ts │ │ ├── OfferedCourse │ │ │ ├── OfferedCourse.constant.ts │ │ │ ├── OfferedCourse.controller.ts │ │ │ ├── OfferedCourse.interface.ts │ │ │ ├── OfferedCourse.model.ts │ │ │ ├── OfferedCourse.route.ts │ │ │ ├── OfferedCourse.service.ts │ │ │ ├── OfferedCourse.utils.ts │ │ │ └── OfferedCourse.validation.ts │ │ ├── SemesterRegistration │ │ │ ├── semesterRegistration.constant.ts │ │ │ ├── semesterRegistration.controller.ts │ │ │ ├── semesterRegistration.interface.ts │ │ │ ├── semesterRegistration.model.ts │ │ │ ├── semesterRegistration.route.ts │ │ │ ├── semesterRegistration.service.ts │ │ │ └── semesterRegistration.validation.ts │ │ ├── Student │ │ │ ├── student.constant.ts │ │ │ ├── student.controller.ts │ │ │ ├── student.interface.ts │ │ │ ├── student.model.ts │ │ │ ├── student.route.ts │ │ │ ├── student.service.ts │ │ │ └── student.validation.ts │ │ └── User │ │ │ ├── user.constant.ts │ │ │ ├── user.controller.ts │ │ │ ├── user.interface.ts │ │ │ ├── user.model.ts │ │ │ ├── user.route.ts │ │ │ ├── user.service.ts │ │ │ ├── user.utils.ts │ │ │ └── user.validation.ts │ ├── routes │ │ └── index.ts │ └── utils │ │ ├── catchAsync.ts │ │ ├── sendEmail.ts │ │ ├── sendImageToCloudinary.ts │ │ └── sendResponse.ts └── server.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV= development 2 | PORT=5000 3 | DATABASE_URL=put_your_atlas_url 4 | BCRYPT_SALT_ROUNDS=12 5 | DEFAULT_PASS=phuniversity!@# 6 | JWT_ACCESS_SECRET = 091b2c529dec033b5ff4531e622ea3f93170e045222963319662b7e4a34f0cdd 7 | JWT_REFRESH_SECRET = 41b991b21dc0a439cb45fed544992ba3fafa3f912d3c4dedebec3592d7d552fb74a86a4d69ea560bcf7bf988d173ddecaffa9815dd5a6661bcacd58c0cdb2dc5 8 | JWT_ACCESS_EXPIRES_IN=10d 9 | JWT_REFRESH_EXPIRES_IN=365d 10 | RESET_PASS_UI_LINK= http://localhost:5173/auth/reset-password 11 | CLOUDINARY_CLOUD_NAME=put_your_coudinary_cloud_name 12 | CLOUDINARY_API_KEY=put_your_coudinary_api_key 13 | CLOUDINARY_API_SECRET=put_your_coudinary_api_secret 14 | SUPER_ADMIN_PASSWORD=admin12345 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "prettier" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": "latest", 14 | "sourceType": "module" 15 | }, 16 | "plugins": ["@typescript-eslint"], 17 | "rules": { 18 | "no-unused-vars": "error", 19 | "no-unused-expressions": "error", 20 | "prefer-const": "error", 21 | "no-console": "warn", 22 | "no-undef": "error" 23 | }, 24 | "globals": { 25 | "process": "readonly" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /erdiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apollo-Level2-Web-Dev/L2B2-PH-university-server/32d268ebdbded17a1a3f9d4c6b5e4748ec2e9104/erdiagram.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "first-project", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start:prod": "node ./dist/server.js", 8 | "start:dev": "ts-node-dev --respawn --transpile-only src/server.ts", 9 | "build": "tsc", 10 | "lint": "eslint src --ignore-path .eslintignore --ext .ts", 11 | "lint:fix": "npx eslint src --fix", 12 | "prettier": "prettier --ignore-path .gitignore --write \"./src/**/*.+(js|ts|json)\"", 13 | "prettier:fix": "npx prettier --write src", 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "bcrypt": "^5.1.1", 21 | "cloudinary": "^1.41.3", 22 | "cookie-parser": "^1.4.6", 23 | "cors": "^2.8.5", 24 | "dotenv": "^16.3.1", 25 | "express": "^4.18.2", 26 | "http-status": "^1.7.3", 27 | "jsonwebtoken": "^9.0.2", 28 | "lint-staged": "^15.1.0", 29 | "mongoose": "^8.0.1", 30 | "multer": "^1.4.5-lts.1", 31 | "nodemailer": "^6.9.7", 32 | "zod": "^3.22.4" 33 | }, 34 | "devDependencies": { 35 | "@types/bcrypt": "^5.0.2", 36 | "@types/cookie-parser": "^1.4.6", 37 | "@types/cors": "^2.8.16", 38 | "@types/express": "^4.17.21", 39 | "@types/jsonwebtoken": "^9.0.5", 40 | "@types/multer": "^1.4.11", 41 | "@types/nodemailer": "^6.4.14", 42 | "@typescript-eslint/eslint-plugin": "^6.11.0", 43 | "@typescript-eslint/parser": "^6.11.0", 44 | "eslint": "^8.53.0", 45 | "eslint-config-prettier": "^9.0.0", 46 | "husky": "^8.0.0", 47 | "prettier": "^3.1.0", 48 | "ts-node-dev": "^2.0.0", 49 | "typescript": "^5.2.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ### Requirement Analysis 2 | 3 | [Link to Requirement Analysis Document](https://docs.google.com/document/d/10mkjS8boCQzW4xpsESyzwCCLJcM3hvLghyD_TeXPBx0/edit?usp=sharing) 4 | 5 | Description: This document outlines the detailed analysis of project requirements. 6 | 7 | --- 8 | 9 | ### Entity-Relationship Diagrams 10 | 11 | ![ER DIAGRAM](./erdiagram.png) 12 | 13 | Description: This is an updated diagram illustrates the relationships among User, Student, Admin, Faculty, Academic Semester, Academic Faculty, Academic Department , Course , Semester Registration , Offered Couse. 14 | 15 | --- 16 | 17 | ![POSTMAN COLLECTION](./postman_collection.json) 18 | 19 | Description: This is a postman collection of all the API endpoints.Download this , and import it in your postman if you needed. 20 | 21 | --- 22 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /* eslint-disable no-unused-vars */ 3 | /* eslint-disable @typescript-eslint/no-unused-vars */ 4 | /* eslint-disable @typescript-eslint/no-explicit-any */ 5 | import cookieParser from 'cookie-parser'; 6 | import cors from 'cors'; 7 | import express, { Application, Request, Response } from 'express'; 8 | import globalErrorHandler from './app/middlewares/globalErrorhandler'; 9 | import notFound from './app/middlewares/notFound'; 10 | import router from './app/routes'; 11 | 12 | const app: Application = express(); 13 | 14 | //parsers 15 | app.use(express.json()); 16 | app.use(cookieParser()); 17 | 18 | app.use(cors({ origin: ['http://localhost:5173'], credentials: true })); 19 | 20 | // application routes 21 | app.use('/api/v1', router); 22 | 23 | app.get('/', (req: Request, res: Response) => { 24 | res.send('Hi Next Level Developer !'); 25 | }); 26 | 27 | app.use(globalErrorHandler); 28 | 29 | //Not Found 30 | app.use(notFound); 31 | 32 | export default app; 33 | -------------------------------------------------------------------------------- /src/app/DB/index.ts: -------------------------------------------------------------------------------- 1 | import config from '../config'; 2 | import { USER_ROLE } from '../modules/User/user.constant'; 3 | import { User } from '../modules/User/user.model'; 4 | 5 | const superUser = { 6 | id: '0001', 7 | email: 'abedinforhan@gmail.com', 8 | password: config.super_admin_password, 9 | needsPasswordChange: false, 10 | role: USER_ROLE.superAdmin, 11 | status: 'in-progress', 12 | isDeleted: false, 13 | }; 14 | 15 | const seedSuperAdmin = async () => { 16 | //when database is connected, we will check is there any user who is super admin 17 | const isSuperAdminExits = await User.findOne({ role: USER_ROLE.superAdmin }); 18 | 19 | if (!isSuperAdminExits) { 20 | await User.create(superUser); 21 | } 22 | }; 23 | 24 | export default seedSuperAdmin; 25 | -------------------------------------------------------------------------------- /src/app/builder/QueryBuilder.ts: -------------------------------------------------------------------------------- 1 | import { FilterQuery, Query } from 'mongoose'; 2 | 3 | class QueryBuilder { 4 | public modelQuery: Query; 5 | public query: Record; 6 | 7 | constructor(modelQuery: Query, query: Record) { 8 | this.modelQuery = modelQuery; 9 | this.query = query; 10 | } 11 | 12 | search(searchableFields: string[]) { 13 | const searchTerm = this?.query?.searchTerm; 14 | if (searchTerm) { 15 | this.modelQuery = this.modelQuery.find({ 16 | $or: searchableFields.map( 17 | (field) => 18 | ({ 19 | [field]: { $regex: searchTerm, $options: 'i' }, 20 | }) as FilterQuery, 21 | ), 22 | }); 23 | } 24 | 25 | return this; 26 | } 27 | 28 | filter() { 29 | const queryObj = { ...this.query }; // copy 30 | 31 | // Filtering 32 | const excludeFields = ['searchTerm', 'sort', 'limit', 'page', 'fields']; 33 | 34 | excludeFields.forEach((el) => delete queryObj[el]); 35 | 36 | this.modelQuery = this.modelQuery.find(queryObj as FilterQuery); 37 | 38 | return this; 39 | } 40 | 41 | sort() { 42 | const sort = 43 | (this?.query?.sort as string)?.split(',')?.join(' ') || '-createdAt'; 44 | this.modelQuery = this.modelQuery.sort(sort as string); 45 | 46 | return this; 47 | } 48 | 49 | paginate() { 50 | const page = Number(this?.query?.page) || 1; 51 | const limit = Number(this?.query?.limit) || 10; 52 | const skip = (page - 1) * limit; 53 | 54 | this.modelQuery = this.modelQuery.skip(skip).limit(limit); 55 | 56 | return this; 57 | } 58 | 59 | fields() { 60 | const fields = 61 | (this?.query?.fields as string)?.split(',')?.join(' ') || '-__v'; 62 | 63 | this.modelQuery = this.modelQuery.select(fields); 64 | return this; 65 | } 66 | async countTotal() { 67 | const totalQueries = this.modelQuery.getFilter(); 68 | const total = await this.modelQuery.model.countDocuments(totalQueries); 69 | const page = Number(this?.query?.page) || 1; 70 | const limit = Number(this?.query?.limit) || 10; 71 | const totalPage = Math.ceil(total / limit); 72 | 73 | return { 74 | page, 75 | limit, 76 | total, 77 | totalPage, 78 | }; 79 | } 80 | } 81 | 82 | export default QueryBuilder; 83 | -------------------------------------------------------------------------------- /src/app/config/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import path from 'path'; 3 | 4 | dotenv.config({ path: path.join((process.cwd(), '.env')) }); 5 | 6 | export default { 7 | NODE_ENV: process.env.NODE_ENV, 8 | port: process.env.PORT, 9 | database_url: process.env.DATABASE_URL, 10 | bcrypt_salt_rounds: process.env.BCRYPT_SALT_ROUNDS, 11 | default_password: process.env.DEFAULT_PASS, 12 | jwt_access_secret: process.env.JWT_ACCESS_SECRET, 13 | jwt_refresh_secret: process.env.JWT_REFRESH_SECRET, 14 | jwt_access_expires_in: process.env.JWT_ACCESS_EXPIRES_IN, 15 | jwt_refresh_expires_in: process.env.JWT_REFRESH_EXPIRES_IN, 16 | reset_pass_ui_link: process.env.RESET_PASS_UI_LINK, 17 | cloudinary_cloud_name: process.env.CLOUDINARY_CLOUD_NAME, 18 | cloudinary_api_key: process.env.CLOUDINARY_API_KEY, 19 | cloudinary_api_secret: process.env.CLOUDINARY_API_SECRET, 20 | super_admin_password: process.env.SUPER_ADMIN_PASSWORD, 21 | }; 22 | -------------------------------------------------------------------------------- /src/app/errors/AppError.ts: -------------------------------------------------------------------------------- 1 | class AppError extends Error { 2 | public statusCode: number; 3 | 4 | constructor(statusCode: number, message: string, stack = '') { 5 | super(message); 6 | this.statusCode = statusCode; 7 | 8 | if (stack) { 9 | this.stack = stack; 10 | } else { 11 | Error.captureStackTrace(this, this.constructor); 12 | } 13 | } 14 | } 15 | 16 | export default AppError; 17 | -------------------------------------------------------------------------------- /src/app/errors/handleCastError.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { TErrorSources, TGenericErrorResponse } from '../interface/error'; 3 | 4 | const handleCastError = ( 5 | err: mongoose.Error.CastError, 6 | ): TGenericErrorResponse => { 7 | const errorSources: TErrorSources = [ 8 | { 9 | path: err.path, 10 | message: err.message, 11 | }, 12 | ]; 13 | 14 | const statusCode = 400; 15 | 16 | return { 17 | statusCode, 18 | message: 'Invalid ID', 19 | errorSources, 20 | }; 21 | }; 22 | 23 | export default handleCastError; 24 | -------------------------------------------------------------------------------- /src/app/errors/handleDuplicateError.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { TErrorSources, TGenericErrorResponse } from '../interface/error'; 3 | 4 | const handleDuplicateError = (err: any): TGenericErrorResponse => { 5 | // Extract value within double quotes using regex 6 | const match = err.message.match(/"([^"]*)"/); 7 | 8 | // The extracted value will be in the first capturing group 9 | const extractedMessage = match && match[1]; 10 | 11 | const errorSources: TErrorSources = [ 12 | { 13 | path: '', 14 | message: `${extractedMessage} is already exists`, 15 | }, 16 | ]; 17 | 18 | const statusCode = 400; 19 | 20 | return { 21 | statusCode, 22 | message: 'Invalid ID', 23 | errorSources, 24 | }; 25 | }; 26 | 27 | export default handleDuplicateError; 28 | -------------------------------------------------------------------------------- /src/app/errors/handleValidationError.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { TErrorSources, TGenericErrorResponse } from '../interface/error'; 3 | 4 | const handleValidationError = ( 5 | err: mongoose.Error.ValidationError, 6 | ): TGenericErrorResponse => { 7 | const errorSources: TErrorSources = Object.values(err.errors).map( 8 | (val: mongoose.Error.ValidatorError | mongoose.Error.CastError) => { 9 | return { 10 | path: val?.path, 11 | message: val?.message, 12 | }; 13 | }, 14 | ); 15 | 16 | const statusCode = 400; 17 | 18 | return { 19 | statusCode, 20 | message: 'Validation Error', 21 | errorSources, 22 | }; 23 | }; 24 | 25 | export default handleValidationError; 26 | -------------------------------------------------------------------------------- /src/app/errors/handleZodError.ts: -------------------------------------------------------------------------------- 1 | import { ZodError, ZodIssue } from 'zod'; 2 | import { TErrorSources, TGenericErrorResponse } from '../interface/error'; 3 | 4 | const handleZodError = (err: ZodError): TGenericErrorResponse => { 5 | const errorSources: TErrorSources = err.issues.map((issue: ZodIssue) => { 6 | return { 7 | path: issue?.path[issue.path.length - 1], 8 | message: issue.message, 9 | }; 10 | }); 11 | 12 | const statusCode = 400; 13 | 14 | return { 15 | statusCode, 16 | message: 'Validation Error', 17 | errorSources, 18 | }; 19 | }; 20 | 21 | export default handleZodError; 22 | -------------------------------------------------------------------------------- /src/app/interface/error.ts: -------------------------------------------------------------------------------- 1 | export type TErrorSources = { 2 | path: string | number; 3 | message: string; 4 | }[]; 5 | 6 | export type TGenericErrorResponse = { 7 | statusCode: number; 8 | message: string; 9 | errorSources: TErrorSources; 10 | }; 11 | -------------------------------------------------------------------------------- /src/app/interface/index.d.ts: -------------------------------------------------------------------------------- 1 | import { JwtPayload } from 'jsonwebtoken'; 2 | 3 | declare global { 4 | namespace Express { 5 | interface Request { 6 | user: JwtPayload; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/middlewares/auth.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import httpStatus from 'http-status'; 3 | import jwt, { JwtPayload } from 'jsonwebtoken'; 4 | import config from '../config'; 5 | import AppError from '../errors/AppError'; 6 | import { TUserRole } from '../modules/User/user.interface'; 7 | import { User } from '../modules/User/user.model'; 8 | import catchAsync from '../utils/catchAsync'; 9 | 10 | const auth = (...requiredRoles: TUserRole[]) => { 11 | return catchAsync(async (req: Request, res: Response, next: NextFunction) => { 12 | const token = req.headers.authorization; 13 | 14 | // checking if the token is missing 15 | if (!token) { 16 | throw new AppError(httpStatus.UNAUTHORIZED, 'You are not authorized!'); 17 | } 18 | 19 | // checking if the given token is valid 20 | const decoded = jwt.verify( 21 | token, 22 | config.jwt_access_secret as string, 23 | ) as JwtPayload; 24 | 25 | const { role, userId, iat } = decoded; 26 | 27 | // checking if the user is exist 28 | const user = await User.isUserExistsByCustomId(userId); 29 | 30 | if (!user) { 31 | throw new AppError(httpStatus.NOT_FOUND, 'This user is not found !'); 32 | } 33 | // checking if the user is already deleted 34 | 35 | const isDeleted = user?.isDeleted; 36 | 37 | if (isDeleted) { 38 | throw new AppError(httpStatus.FORBIDDEN, 'This user is deleted !'); 39 | } 40 | 41 | // checking if the user is blocked 42 | const userStatus = user?.status; 43 | 44 | if (userStatus === 'blocked') { 45 | throw new AppError(httpStatus.FORBIDDEN, 'This user is blocked ! !'); 46 | } 47 | 48 | if ( 49 | user.passwordChangedAt && 50 | User.isJWTIssuedBeforePasswordChanged( 51 | user.passwordChangedAt, 52 | iat as number, 53 | ) 54 | ) { 55 | throw new AppError(httpStatus.UNAUTHORIZED, 'You are not authorized !'); 56 | } 57 | 58 | if (requiredRoles && !requiredRoles.includes(role)) { 59 | throw new AppError( 60 | httpStatus.UNAUTHORIZED, 61 | 'You are not authorized hi!', 62 | ); 63 | } 64 | 65 | req.user = decoded as JwtPayload & { role: string }; 66 | next(); 67 | }); 68 | }; 69 | 70 | export default auth; 71 | -------------------------------------------------------------------------------- /src/app/middlewares/globalErrorhandler.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | /* eslint-disable no-unused-vars */ 3 | import { ErrorRequestHandler } from 'express'; 4 | import { ZodError } from 'zod'; 5 | import config from '../config'; 6 | import AppError from '../errors/AppError'; 7 | import handleCastError from '../errors/handleCastError'; 8 | import handleDuplicateError from '../errors/handleDuplicateError'; 9 | import handleValidationError from '../errors/handleValidationError'; 10 | import handleZodError from '../errors/handleZodError'; 11 | import { TErrorSources } from '../interface/error'; 12 | 13 | const globalErrorHandler: ErrorRequestHandler = (err, req, res, next) => { 14 | console.log(err.statusCode); 15 | //setting default values 16 | let statusCode = 500; 17 | let message = 'Something went wrong!'; 18 | let errorSources: TErrorSources = [ 19 | { 20 | path: '', 21 | message: 'Something went wrong', 22 | }, 23 | ]; 24 | 25 | if (err instanceof ZodError) { 26 | const simplifiedError = handleZodError(err); 27 | statusCode = simplifiedError?.statusCode; 28 | message = simplifiedError?.message; 29 | errorSources = simplifiedError?.errorSources; 30 | } else if (err?.name === 'ValidationError') { 31 | const simplifiedError = handleValidationError(err); 32 | statusCode = simplifiedError?.statusCode; 33 | message = simplifiedError?.message; 34 | errorSources = simplifiedError?.errorSources; 35 | } else if (err?.name === 'CastError') { 36 | const simplifiedError = handleCastError(err); 37 | statusCode = simplifiedError?.statusCode; 38 | message = simplifiedError?.message; 39 | errorSources = simplifiedError?.errorSources; 40 | } else if (err?.code === 11000) { 41 | const simplifiedError = handleDuplicateError(err); 42 | statusCode = simplifiedError?.statusCode; 43 | message = simplifiedError?.message; 44 | errorSources = simplifiedError?.errorSources; 45 | } else if (err instanceof AppError) { 46 | statusCode = err?.statusCode; 47 | message = err.message; 48 | errorSources = [ 49 | { 50 | path: '', 51 | message: err?.message, 52 | }, 53 | ]; 54 | } else if (err instanceof Error) { 55 | message = err.message; 56 | errorSources = [ 57 | { 58 | path: '', 59 | message: err?.message, 60 | }, 61 | ]; 62 | } 63 | 64 | //ultimate return 65 | return res.status(statusCode).json({ 66 | success: false, 67 | message, 68 | errorSources, 69 | err, 70 | stack: config.NODE_ENV === 'development' ? err?.stack : null, 71 | }); 72 | }; 73 | 74 | export default globalErrorHandler; 75 | 76 | //pattern 77 | /* 78 | success 79 | message 80 | errorSources:[ 81 | path:'', 82 | message:'' 83 | ] 84 | stack 85 | */ 86 | -------------------------------------------------------------------------------- /src/app/middlewares/notFound.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | 4 | import { NextFunction, Request, Response } from 'express'; 5 | import httpStatus from 'http-status'; 6 | 7 | const notFound = (req: Request, res: Response, next: NextFunction) => { 8 | return res.status(httpStatus.NOT_FOUND).json({ 9 | success: false, 10 | message: 'API Not Found !!', 11 | error: '', 12 | }); 13 | }; 14 | 15 | export default notFound; 16 | -------------------------------------------------------------------------------- /src/app/middlewares/validateRequest.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { AnyZodObject } from 'zod'; 3 | import catchAsync from '../utils/catchAsync'; 4 | 5 | const validateRequest = (schema: AnyZodObject) => { 6 | return catchAsync(async (req: Request, res: Response, next: NextFunction) => { 7 | await schema.parseAsync({ 8 | body: req.body, 9 | cookies: req.cookies, 10 | }); 11 | 12 | next(); 13 | }); 14 | }; 15 | 16 | export default validateRequest; 17 | -------------------------------------------------------------------------------- /src/app/modules/AcademicDepartment/academicDepartment.controller.ts: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import catchAsync from '../../utils/catchAsync'; 3 | import sendResponse from '../../utils/sendResponse'; 4 | import { AcademicDepartmentServices } from './academicDepartment.service'; 5 | 6 | const createAcademicDepartmemt = catchAsync(async (req, res) => { 7 | const result = 8 | await AcademicDepartmentServices.createAcademicDepartmentIntoDB(req.body); 9 | 10 | sendResponse(res, { 11 | statusCode: httpStatus.OK, 12 | success: true, 13 | message: 'Academic department is created successfully', 14 | data: result, 15 | }); 16 | }); 17 | 18 | const getAllAcademicDepartments = catchAsync(async (req, res) => { 19 | const result = 20 | await AcademicDepartmentServices.getAllAcademicDepartmentsFromDB(req.query); 21 | sendResponse(res, { 22 | statusCode: httpStatus.OK, 23 | success: true, 24 | message: 'Academic departments are retrieved successfully', 25 | meta: result.meta, 26 | data: result.result, 27 | }); 28 | }); 29 | 30 | const getSingleAcademicDepartment = catchAsync(async (req, res) => { 31 | const { departmentId } = req.params; 32 | const result = 33 | await AcademicDepartmentServices.getSingleAcademicDepartmentFromDB( 34 | departmentId, 35 | ); 36 | 37 | sendResponse(res, { 38 | statusCode: httpStatus.OK, 39 | success: true, 40 | message: 'Academic department is retrieved successfully', 41 | data: result, 42 | }); 43 | }); 44 | 45 | const updateAcademicDeartment = catchAsync(async (req, res) => { 46 | const { departmentId } = req.params; 47 | const result = 48 | await AcademicDepartmentServices.updateAcademicDepartmentIntoDB( 49 | departmentId, 50 | req.body, 51 | ); 52 | 53 | sendResponse(res, { 54 | statusCode: httpStatus.OK, 55 | success: true, 56 | message: 'Academic department is updated successfully', 57 | data: result, 58 | }); 59 | }); 60 | 61 | export const AcademicDepartmentControllers = { 62 | createAcademicDepartmemt, 63 | getAllAcademicDepartments, 64 | getSingleAcademicDepartment, 65 | updateAcademicDeartment, 66 | }; 67 | -------------------------------------------------------------------------------- /src/app/modules/AcademicDepartment/academicDepartment.interface.ts: -------------------------------------------------------------------------------- 1 | import { Types } from 'mongoose'; 2 | 3 | export type TAcademicDepartment = { 4 | name: string; 5 | academicFaculty: Types.ObjectId; 6 | }; 7 | -------------------------------------------------------------------------------- /src/app/modules/AcademicDepartment/academicDepartment.model.ts: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import { Schema, model } from 'mongoose'; 3 | import AppError from '../../errors/AppError'; 4 | import { TAcademicDepartment } from './academicDepartment.interface'; 5 | 6 | const academicDepartmentSchema = new Schema( 7 | { 8 | name: { 9 | type: String, 10 | required: true, 11 | unique: true, 12 | }, 13 | academicFaculty: { 14 | type: Schema.Types.ObjectId, 15 | ref: 'AcademicFaculty', 16 | }, 17 | }, 18 | { 19 | timestamps: true, 20 | }, 21 | ); 22 | 23 | academicDepartmentSchema.pre('save', async function (next) { 24 | const isDepartmentExist = await AcademicDepartment.findOne({ 25 | name: this.name, 26 | }); 27 | 28 | if (isDepartmentExist) { 29 | throw new AppError( 30 | httpStatus.NOT_FOUND, 31 | 'This department is already exist!', 32 | ); 33 | } 34 | 35 | next(); 36 | }); 37 | 38 | academicDepartmentSchema.pre('findOneAndUpdate', async function (next) { 39 | const query = this.getQuery(); 40 | const isDepartmentExist = await AcademicDepartment.findOne(query); 41 | 42 | if (!isDepartmentExist) { 43 | throw new AppError( 44 | httpStatus.NOT_FOUND, 45 | 'This department does not exist! ', 46 | ); 47 | } 48 | 49 | next(); 50 | }); 51 | 52 | export const AcademicDepartment = model( 53 | 'AcademicDepartment', 54 | academicDepartmentSchema, 55 | ); 56 | -------------------------------------------------------------------------------- /src/app/modules/AcademicDepartment/academicDepartment.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import auth from '../../middlewares/auth'; 3 | import validateRequest from '../../middlewares/validateRequest'; 4 | import { USER_ROLE } from '../User/user.constant'; 5 | import { AcademicDepartmentControllers } from './academicDepartment.controller'; 6 | import { AcademicDepartmentValidation } from './academicDepartment.validation'; 7 | 8 | const router = express.Router(); 9 | 10 | router.post( 11 | '/create-academic-department', 12 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 13 | validateRequest( 14 | AcademicDepartmentValidation.createAcademicDepartmentValidationSchema, 15 | ), 16 | AcademicDepartmentControllers.createAcademicDepartmemt, 17 | ); 18 | 19 | router.get( 20 | '/:departmentId', 21 | auth( 22 | USER_ROLE.superAdmin, 23 | USER_ROLE.admin, 24 | USER_ROLE.faculty, 25 | USER_ROLE.student, 26 | ), 27 | AcademicDepartmentControllers.getSingleAcademicDepartment, 28 | ); 29 | 30 | router.patch( 31 | '/:departmentId', 32 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 33 | validateRequest( 34 | AcademicDepartmentValidation.updateAcademicDepartmentValidationSchema, 35 | ), 36 | AcademicDepartmentControllers.updateAcademicDeartment, 37 | ); 38 | 39 | router.get( 40 | '/', 41 | auth( 42 | USER_ROLE.superAdmin, 43 | USER_ROLE.admin, 44 | USER_ROLE.faculty, 45 | USER_ROLE.student, 46 | ), 47 | AcademicDepartmentControllers.getAllAcademicDepartments, 48 | ); 49 | 50 | export const AcademicDepartmentRoutes = router; 51 | -------------------------------------------------------------------------------- /src/app/modules/AcademicDepartment/academicDepartment.service.ts: -------------------------------------------------------------------------------- 1 | import QueryBuilder from '../../builder/QueryBuilder'; 2 | import { TAcademicDepartment } from './academicDepartment.interface'; 3 | import { AcademicDepartment } from './academicDepartment.model'; 4 | import { AcademicDepartmentSearchableFields } from './academicDepartmets.constant'; 5 | 6 | const createAcademicDepartmentIntoDB = async (payload: TAcademicDepartment) => { 7 | const result = await AcademicDepartment.create(payload); 8 | return result; 9 | }; 10 | 11 | const getAllAcademicDepartmentsFromDB = async ( 12 | query: Record, 13 | ) => { 14 | const academicDepartmentQuery = new QueryBuilder( 15 | AcademicDepartment.find().populate('academicFaculty'), 16 | query, 17 | ) 18 | .search(AcademicDepartmentSearchableFields) 19 | .filter() 20 | .sort() 21 | .paginate() 22 | .fields(); 23 | 24 | const result = await academicDepartmentQuery.modelQuery; 25 | const meta = await academicDepartmentQuery.countTotal(); 26 | 27 | return { 28 | meta, 29 | result, 30 | }; 31 | }; 32 | 33 | const getSingleAcademicDepartmentFromDB = async (id: string) => { 34 | const result = 35 | await AcademicDepartment.findById(id).populate('academicFaculty'); 36 | 37 | return result; 38 | }; 39 | 40 | const updateAcademicDepartmentIntoDB = async ( 41 | id: string, 42 | payload: Partial, 43 | ) => { 44 | const result = await AcademicDepartment.findOneAndUpdate( 45 | { _id: id }, 46 | payload, 47 | { 48 | new: true, 49 | }, 50 | ); 51 | return result; 52 | }; 53 | 54 | export const AcademicDepartmentServices = { 55 | createAcademicDepartmentIntoDB, 56 | getAllAcademicDepartmentsFromDB, 57 | getSingleAcademicDepartmentFromDB, 58 | updateAcademicDepartmentIntoDB, 59 | }; 60 | -------------------------------------------------------------------------------- /src/app/modules/AcademicDepartment/academicDepartment.validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const createAcademicDepartmentValidationSchema = z.object({ 4 | body: z.object({ 5 | name: z.string({ 6 | invalid_type_error: 'Academic department must be string', 7 | required_error: 'Name is required', 8 | }), 9 | academicFaculty: z.string({ 10 | invalid_type_error: 'Academic faculty must be string', 11 | required_error: 'Faculty is required', 12 | }), 13 | }), 14 | }); 15 | 16 | const updateAcademicDepartmentValidationSchema = z.object({ 17 | body: z.object({ 18 | name: z 19 | .string({ 20 | invalid_type_error: 'Academic department must be string', 21 | required_error: 'Name is required', 22 | }) 23 | .optional(), 24 | academicFaculty: z 25 | .string({ 26 | invalid_type_error: 'Academic faculty must be string', 27 | required_error: 'Faculty is required', 28 | }) 29 | .optional(), 30 | }), 31 | }); 32 | 33 | export const AcademicDepartmentValidation = { 34 | createAcademicDepartmentValidationSchema, 35 | updateAcademicDepartmentValidationSchema, 36 | }; 37 | -------------------------------------------------------------------------------- /src/app/modules/AcademicDepartment/academicDepartmets.constant.ts: -------------------------------------------------------------------------------- 1 | export const AcademicDepartmentSearchableFields = ['name']; 2 | -------------------------------------------------------------------------------- /src/app/modules/AcademicFaculty/academicFaculty.constant.ts: -------------------------------------------------------------------------------- 1 | export const AcademicFacultySearchableFields = ['name']; 2 | -------------------------------------------------------------------------------- /src/app/modules/AcademicFaculty/academicFaculty.controller.ts: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import catchAsync from '../../utils/catchAsync'; 3 | import sendResponse from '../../utils/sendResponse'; 4 | import { AcademicFacultyServices } from './academicFaculty.service'; 5 | 6 | const createAcademicFaculty = catchAsync(async (req, res) => { 7 | const result = await AcademicFacultyServices.createAcademicFacultyIntoDB( 8 | req.body, 9 | ); 10 | 11 | sendResponse(res, { 12 | statusCode: httpStatus.OK, 13 | success: true, 14 | message: 'Academic faculty is created successfully', 15 | data: result, 16 | }); 17 | }); 18 | 19 | const getAllAcademicFaculties = catchAsync(async (req, res) => { 20 | const result = await AcademicFacultyServices.getAllAcademicFacultiesFromDB( 21 | req.query, 22 | ); 23 | 24 | sendResponse(res, { 25 | statusCode: httpStatus.OK, 26 | success: true, 27 | message: 'Academic faculties are retrieved successfully', 28 | meta: result.meta, 29 | data: result.result, 30 | }); 31 | }); 32 | 33 | const getSingleAcademicFaculty = catchAsync(async (req, res) => { 34 | const { facultyId } = req.params; 35 | 36 | const result = 37 | await AcademicFacultyServices.getSingleAcademicFacultyFromDB(facultyId); 38 | 39 | sendResponse(res, { 40 | statusCode: httpStatus.OK, 41 | success: true, 42 | message: 'Academic faculty is retrieved successfully', 43 | data: result, 44 | }); 45 | }); 46 | 47 | const updateAcademicFaculty = catchAsync(async (req, res) => { 48 | const { facultyId } = req.params; 49 | const result = await AcademicFacultyServices.updateAcademicFacultyIntoDB( 50 | facultyId, 51 | req.body, 52 | ); 53 | 54 | sendResponse(res, { 55 | statusCode: httpStatus.OK, 56 | success: true, 57 | message: 'Academic faculty is updated successfully', 58 | data: result, 59 | }); 60 | }); 61 | 62 | export const AcademicFacultyControllers = { 63 | createAcademicFaculty, 64 | getAllAcademicFaculties, 65 | getSingleAcademicFaculty, 66 | updateAcademicFaculty, 67 | }; 68 | -------------------------------------------------------------------------------- /src/app/modules/AcademicFaculty/academicFaculty.interface.ts: -------------------------------------------------------------------------------- 1 | export type TAcademicFaculty = { 2 | name: string; 3 | }; 4 | -------------------------------------------------------------------------------- /src/app/modules/AcademicFaculty/academicFaculty.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose'; 2 | import { TAcademicFaculty } from './academicFaculty.interface'; 3 | 4 | const academicFacultySchema = new Schema( 5 | { 6 | name: { 7 | type: String, 8 | required: true, 9 | unique: true, 10 | }, 11 | }, 12 | { 13 | timestamps: true, 14 | }, 15 | ); 16 | 17 | export const AcademicFaculty = model( 18 | 'AcademicFaculty', 19 | academicFacultySchema, 20 | ); 21 | -------------------------------------------------------------------------------- /src/app/modules/AcademicFaculty/academicFaculty.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import auth from '../../middlewares/auth'; 3 | import validateRequest from '../../middlewares/validateRequest'; 4 | import { USER_ROLE } from '../User/user.constant'; 5 | import { AcademicFacultyControllers } from './academicFaculty.controller'; 6 | import { AcademicFacultyValidation } from './academicFaculty.validation'; 7 | 8 | const router = express.Router(); 9 | 10 | router.post( 11 | '/create-academic-faculty', 12 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 13 | validateRequest( 14 | AcademicFacultyValidation.createAcademicFacultyValidationSchema, 15 | ), 16 | AcademicFacultyControllers.createAcademicFaculty, 17 | ); 18 | 19 | router.get( 20 | '/:id', 21 | auth( 22 | USER_ROLE.superAdmin, 23 | USER_ROLE.admin, 24 | USER_ROLE.faculty, 25 | USER_ROLE.student, 26 | ), 27 | AcademicFacultyControllers.getSingleAcademicFaculty, 28 | ); 29 | 30 | router.patch( 31 | '/:id', 32 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 33 | validateRequest( 34 | AcademicFacultyValidation.updateAcademicFacultyValidationSchema, 35 | ), 36 | AcademicFacultyControllers.updateAcademicFaculty, 37 | ); 38 | 39 | router.get( 40 | '/', 41 | auth( 42 | USER_ROLE.superAdmin, 43 | USER_ROLE.admin, 44 | USER_ROLE.faculty, 45 | USER_ROLE.student, 46 | ), 47 | AcademicFacultyControllers.getAllAcademicFaculties, 48 | ); 49 | 50 | export const AcademicFacultyRoutes = router; 51 | -------------------------------------------------------------------------------- /src/app/modules/AcademicFaculty/academicFaculty.service.ts: -------------------------------------------------------------------------------- 1 | import QueryBuilder from '../../builder/QueryBuilder'; 2 | import { AcademicFacultySearchableFields } from './academicFaculty.constant'; 3 | import { TAcademicFaculty } from './academicFaculty.interface'; 4 | import { AcademicFaculty } from './academicFaculty.model'; 5 | 6 | const createAcademicFacultyIntoDB = async (payload: TAcademicFaculty) => { 7 | const result = await AcademicFaculty.create(payload); 8 | return result; 9 | }; 10 | 11 | const getAllAcademicFacultiesFromDB = async ( 12 | query: Record, 13 | ) => { 14 | const academicFacultyQuery = new QueryBuilder(AcademicFaculty.find(), query) 15 | .search(AcademicFacultySearchableFields) 16 | .filter() 17 | .sort() 18 | .paginate() 19 | .fields(); 20 | 21 | const result = await academicFacultyQuery.modelQuery; 22 | const meta = await academicFacultyQuery.countTotal(); 23 | 24 | return { 25 | meta, 26 | result, 27 | }; 28 | }; 29 | 30 | const getSingleAcademicFacultyFromDB = async (id: string) => { 31 | const result = await AcademicFaculty.findById(id); 32 | return result; 33 | }; 34 | 35 | const updateAcademicFacultyIntoDB = async ( 36 | id: string, 37 | payload: Partial, 38 | ) => { 39 | const result = await AcademicFaculty.findOneAndUpdate({ _id: id }, payload, { 40 | new: true, 41 | }); 42 | return result; 43 | }; 44 | 45 | export const AcademicFacultyServices = { 46 | createAcademicFacultyIntoDB, 47 | getAllAcademicFacultiesFromDB, 48 | getSingleAcademicFacultyFromDB, 49 | updateAcademicFacultyIntoDB, 50 | }; 51 | -------------------------------------------------------------------------------- /src/app/modules/AcademicFaculty/academicFaculty.validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const createAcademicFacultyValidationSchema = z.object({ 4 | body: z.object({ 5 | name: z.string({ 6 | invalid_type_error: 'Academic faculty must be string', 7 | }), 8 | }), 9 | }); 10 | 11 | const updateAcademicFacultyValidationSchema = z.object({ 12 | body: z.object({ 13 | name: z.string({ 14 | invalid_type_error: 'Academic faculty must be string', 15 | }), 16 | }), 17 | }); 18 | 19 | export const AcademicFacultyValidation = { 20 | createAcademicFacultyValidationSchema, 21 | updateAcademicFacultyValidationSchema, 22 | }; 23 | -------------------------------------------------------------------------------- /src/app/modules/AcademicSemester/academicSemester.constant.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TAcademicSemesterCode, 3 | TAcademicSemesterName, 4 | TAcademicSemesterNameCodeMapper, 5 | TMonths, 6 | } from './academicSemester.interface'; 7 | 8 | export const Months: TMonths[] = [ 9 | 'January', 10 | 'February', 11 | 'March', 12 | 'April', 13 | 'May', 14 | 'June', 15 | 'July', 16 | 'August', 17 | 'September', 18 | 'October', 19 | 'November', 20 | 'December', 21 | ]; 22 | 23 | export const AcademicSemesterName: TAcademicSemesterName[] = [ 24 | 'Autumn', 25 | 'Summer', 26 | 'Fall', 27 | ]; 28 | 29 | export const AcademicSemesterCode: TAcademicSemesterCode[] = ['01', '02', '03']; 30 | 31 | export const academicSemesterNameCodeMapper: TAcademicSemesterNameCodeMapper = { 32 | Autumn: '01', 33 | Summer: '02', 34 | Fall: '03', 35 | }; 36 | 37 | export const AcademicSemesterSearchableFields = ['name', 'year']; 38 | -------------------------------------------------------------------------------- /src/app/modules/AcademicSemester/academicSemester.controller.ts: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import catchAsync from '../../utils/catchAsync'; 3 | import sendResponse from '../../utils/sendResponse'; 4 | import { AcademicSemesterServices } from './academicSemester.service'; 5 | 6 | const createAcademicSemester = catchAsync(async (req, res) => { 7 | const result = await AcademicSemesterServices.createAcademicSemesterIntoDB( 8 | req.body, 9 | ); 10 | 11 | sendResponse(res, { 12 | statusCode: httpStatus.OK, 13 | success: true, 14 | message: 'Academic semester is created successfully', 15 | data: result, 16 | }); 17 | }); 18 | 19 | const getAllAcademicSemesters = catchAsync(async (req, res) => { 20 | const result = await AcademicSemesterServices.getAllAcademicSemestersFromDB( 21 | req.query, 22 | ); 23 | 24 | sendResponse(res, { 25 | statusCode: httpStatus.OK, 26 | success: true, 27 | message: 'Academic semesters are retrieved successfully', 28 | meta: result.meta, 29 | data: result.result, 30 | }); 31 | }); 32 | 33 | const getSingleAcademicSemester = catchAsync(async (req, res) => { 34 | const { semesterId } = req.params; 35 | 36 | const result = 37 | await AcademicSemesterServices.getSingleAcademicSemesterFromDB(semesterId); 38 | 39 | sendResponse(res, { 40 | statusCode: httpStatus.OK, 41 | success: true, 42 | message: 'Academic semester is retrieved successfully', 43 | data: result, 44 | }); 45 | }); 46 | 47 | const updateAcademicSemester = catchAsync(async (req, res) => { 48 | const { semesterId } = req.params; 49 | const result = await AcademicSemesterServices.updateAcademicSemesterIntoDB( 50 | semesterId, 51 | req.body, 52 | ); 53 | 54 | sendResponse(res, { 55 | statusCode: httpStatus.OK, 56 | success: true, 57 | message: 'Academic semester is retrieved successfully', 58 | data: result, 59 | }); 60 | }); 61 | 62 | export const AcademicSemesterControllers = { 63 | createAcademicSemester, 64 | getAllAcademicSemesters, 65 | getSingleAcademicSemester, 66 | updateAcademicSemester, 67 | }; 68 | -------------------------------------------------------------------------------- /src/app/modules/AcademicSemester/academicSemester.interface.ts: -------------------------------------------------------------------------------- 1 | export type TMonths = 2 | | 'January' 3 | | 'February' 4 | | 'March' 5 | | 'April' 6 | | 'May' 7 | | 'June' 8 | | 'July' 9 | | 'August' 10 | | 'September' 11 | | 'October' 12 | | 'November' 13 | | 'December'; 14 | 15 | export type TAcademicSemesterName = 'Autumn' | 'Summer' | 'Fall'; 16 | export type TAcademicSemesterCode = '01' | '02' | '03'; 17 | 18 | export type TAcademicSemester = { 19 | name: TAcademicSemesterName; 20 | code: TAcademicSemesterCode; 21 | year: string; 22 | startMonth: TMonths; 23 | endMonth: TMonths; 24 | }; 25 | 26 | export type TAcademicSemesterNameCodeMapper = { 27 | [key: string]: string; 28 | }; 29 | -------------------------------------------------------------------------------- /src/app/modules/AcademicSemester/academicSemester.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose'; 2 | import { 3 | AcademicSemesterCode, 4 | AcademicSemesterName, 5 | Months, 6 | } from './academicSemester.constant'; 7 | import { TAcademicSemester } from './academicSemester.interface'; 8 | 9 | const acdemicSemesterSchema = new Schema( 10 | { 11 | name: { 12 | type: String, 13 | required: true, 14 | enum: AcademicSemesterName, 15 | }, 16 | year: { 17 | type: String, 18 | required: true, 19 | }, 20 | code: { 21 | type: String, 22 | required: true, 23 | enum: AcademicSemesterCode, 24 | }, 25 | startMonth: { 26 | type: String, 27 | required: true, 28 | enum: Months, 29 | }, 30 | endMonth: { 31 | type: String, 32 | required: true, 33 | enum: Months, 34 | }, 35 | }, 36 | { 37 | timestamps: true, 38 | }, 39 | ); 40 | 41 | acdemicSemesterSchema.pre('save', async function (next) { 42 | const isSemesterExists = await AcademicSemester.findOne({ 43 | year: this.year, 44 | name: this.name, 45 | }); 46 | 47 | if (isSemesterExists) { 48 | throw new Error('Semester is already exists !'); 49 | } 50 | next(); 51 | }); 52 | 53 | export const AcademicSemester = model( 54 | 'AcademicSemester', 55 | acdemicSemesterSchema, 56 | ); 57 | 58 | // Name Year 59 | //2030 Autumn => Created 60 | // 2031 Autumn 61 | //2030 Autumn => XXX 62 | //2030 Fall => Created 63 | 64 | // Autumn 01 65 | // Summer 02 66 | // Fall 03 67 | -------------------------------------------------------------------------------- /src/app/modules/AcademicSemester/academicSemester.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import auth from '../../middlewares/auth'; 3 | import validateRequest from '../../middlewares/validateRequest'; 4 | import { USER_ROLE } from '../User/user.constant'; 5 | import { AcademicSemesterControllers } from './academicSemester.controller'; 6 | import { AcademicSemesterValidations } from './academicSemester.validation'; 7 | 8 | const router = express.Router(); 9 | 10 | router.post( 11 | '/create-academic-semester', 12 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 13 | validateRequest( 14 | AcademicSemesterValidations.createAcdemicSemesterValidationSchema, 15 | ), 16 | AcademicSemesterControllers.createAcademicSemester, 17 | ); 18 | 19 | router.get( 20 | '/:semesterId', 21 | auth( 22 | USER_ROLE.superAdmin, 23 | USER_ROLE.admin, 24 | USER_ROLE.faculty, 25 | USER_ROLE.student, 26 | ), 27 | AcademicSemesterControllers.getSingleAcademicSemester, 28 | ); 29 | 30 | router.patch( 31 | '/:semesterId', 32 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 33 | validateRequest( 34 | AcademicSemesterValidations.updateAcademicSemesterValidationSchema, 35 | ), 36 | AcademicSemesterControllers.updateAcademicSemester, 37 | ); 38 | 39 | router.get( 40 | '/', 41 | auth( 42 | USER_ROLE.superAdmin, 43 | USER_ROLE.admin, 44 | USER_ROLE.faculty, 45 | USER_ROLE.student, 46 | ), 47 | AcademicSemesterControllers.getAllAcademicSemesters, 48 | ); 49 | 50 | export const AcademicSemesterRoutes = router; 51 | -------------------------------------------------------------------------------- /src/app/modules/AcademicSemester/academicSemester.service.ts: -------------------------------------------------------------------------------- 1 | import QueryBuilder from '../../builder/QueryBuilder'; 2 | import { 3 | AcademicSemesterSearchableFields, 4 | academicSemesterNameCodeMapper, 5 | } from './academicSemester.constant'; 6 | import { TAcademicSemester } from './academicSemester.interface'; 7 | import { AcademicSemester } from './academicSemester.model'; 8 | 9 | const createAcademicSemesterIntoDB = async (payload: TAcademicSemester) => { 10 | if (academicSemesterNameCodeMapper[payload.name] !== payload.code) { 11 | throw new Error('Invalid Semester Code'); 12 | } 13 | 14 | const result = await AcademicSemester.create(payload); 15 | return result; 16 | }; 17 | 18 | const getAllAcademicSemestersFromDB = async ( 19 | query: Record, 20 | ) => { 21 | const academicSemesterQuery = new QueryBuilder(AcademicSemester.find(), query) 22 | .search(AcademicSemesterSearchableFields) 23 | .filter() 24 | .sort() 25 | .paginate() 26 | .fields(); 27 | 28 | const result = await academicSemesterQuery.modelQuery; 29 | const meta = await academicSemesterQuery.countTotal(); 30 | 31 | return { 32 | meta, 33 | result, 34 | }; 35 | }; 36 | 37 | const getSingleAcademicSemesterFromDB = async (id: string) => { 38 | const result = await AcademicSemester.findById(id); 39 | return result; 40 | }; 41 | 42 | const updateAcademicSemesterIntoDB = async ( 43 | id: string, 44 | payload: Partial, 45 | ) => { 46 | if ( 47 | payload.name && 48 | payload.code && 49 | academicSemesterNameCodeMapper[payload.name] !== payload.code 50 | ) { 51 | throw new Error('Invalid Semester Code'); 52 | } 53 | 54 | const result = await AcademicSemester.findOneAndUpdate({ _id: id }, payload, { 55 | new: true, 56 | }); 57 | return result; 58 | }; 59 | 60 | export const AcademicSemesterServices = { 61 | createAcademicSemesterIntoDB, 62 | getAllAcademicSemestersFromDB, 63 | getSingleAcademicSemesterFromDB, 64 | updateAcademicSemesterIntoDB, 65 | }; 66 | -------------------------------------------------------------------------------- /src/app/modules/AcademicSemester/academicSemester.validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { 3 | AcademicSemesterCode, 4 | AcademicSemesterName, 5 | Months, 6 | } from './academicSemester.constant'; 7 | 8 | const createAcdemicSemesterValidationSchema = z.object({ 9 | body: z.object({ 10 | name: z.enum([...AcademicSemesterName] as [string, ...string[]]), 11 | year: z.string(), 12 | code: z.enum([...AcademicSemesterCode] as [string, ...string[]]), 13 | startMonth: z.enum([...Months] as [string, ...string[]]), 14 | endMonth: z.enum([...Months] as [string, ...string[]]), 15 | }), 16 | }); 17 | 18 | const updateAcademicSemesterValidationSchema = z.object({ 19 | body: z.object({ 20 | name: z.enum([...AcademicSemesterName] as [string, ...string[]]).optional(), 21 | year: z.string().optional(), 22 | code: z.enum([...AcademicSemesterCode] as [string, ...string[]]).optional(), 23 | startMonth: z.enum([...Months] as [string, ...string[]]).optional(), 24 | endMonth: z.enum([...Months] as [string, ...string[]]).optional(), 25 | }), 26 | }); 27 | 28 | export const AcademicSemesterValidations = { 29 | createAcdemicSemesterValidationSchema, 30 | updateAcademicSemesterValidationSchema, 31 | }; 32 | -------------------------------------------------------------------------------- /src/app/modules/Admin/admin.constant.ts: -------------------------------------------------------------------------------- 1 | import { TBloodGroup, TGender } from './admin.interface'; 2 | 3 | export const Gender: TGender[] = ['male', 'female', 'other']; 4 | 5 | export const BloodGroup: TBloodGroup[] = [ 6 | 'A+', 7 | 'A-', 8 | 'B+', 9 | 'B-', 10 | 'AB+', 11 | 'AB-', 12 | 'O+', 13 | 'O-', 14 | ]; 15 | 16 | export const AdminSearchableFields = [ 17 | 'email', 18 | 'id', 19 | 'contactNo', 20 | 'emergencyContactNo', 21 | 'name.firstName', 22 | 'name.lastName', 23 | 'name.middleName', 24 | ]; 25 | -------------------------------------------------------------------------------- /src/app/modules/Admin/admin.controller.ts: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import catchAsync from '../../utils/catchAsync'; 3 | import sendResponse from '../../utils/sendResponse'; 4 | import { AdminServices } from './admin.service'; 5 | 6 | const getSingleAdmin = catchAsync(async (req, res) => { 7 | const { id } = req.params; 8 | const result = await AdminServices.getSingleAdminFromDB(id); 9 | 10 | sendResponse(res, { 11 | statusCode: httpStatus.OK, 12 | success: true, 13 | message: 'Admin is retrieved successfully', 14 | data: result, 15 | }); 16 | }); 17 | 18 | const getAllAdmins = catchAsync(async (req, res) => { 19 | const result = await AdminServices.getAllAdminsFromDB(req.query); 20 | 21 | sendResponse(res, { 22 | statusCode: httpStatus.OK, 23 | success: true, 24 | message: 'Admins are retrieved successfully', 25 | meta: result.meta, 26 | data: result.result, 27 | }); 28 | }); 29 | 30 | const updateAdmin = catchAsync(async (req, res) => { 31 | const { id } = req.params; 32 | const { admin } = req.body; 33 | const result = await AdminServices.updateAdminIntoDB(id, admin); 34 | 35 | sendResponse(res, { 36 | statusCode: httpStatus.OK, 37 | success: true, 38 | message: 'Admin is updated successfully', 39 | data: result, 40 | }); 41 | }); 42 | 43 | const deleteAdmin = catchAsync(async (req, res) => { 44 | const { id } = req.params; 45 | const result = await AdminServices.deleteAdminFromDB(id); 46 | 47 | sendResponse(res, { 48 | statusCode: httpStatus.OK, 49 | success: true, 50 | message: 'Admin is deleted successfully', 51 | data: result, 52 | }); 53 | }); 54 | 55 | export const AdminControllers = { 56 | getAllAdmins, 57 | getSingleAdmin, 58 | deleteAdmin, 59 | updateAdmin, 60 | }; 61 | -------------------------------------------------------------------------------- /src/app/modules/Admin/admin.interface.ts: -------------------------------------------------------------------------------- 1 | import { Model, Types } from 'mongoose'; 2 | 3 | export type TGender = 'male' | 'female' | 'other'; 4 | export type TBloodGroup = 5 | | 'A+' 6 | | 'A-' 7 | | 'B+' 8 | | 'B-' 9 | | 'AB+' 10 | | 'AB-' 11 | | 'O+' 12 | | 'O-'; 13 | 14 | export type TUserName = { 15 | firstName: string; 16 | middleName: string; 17 | lastName: string; 18 | }; 19 | 20 | export type TAdmin = { 21 | id: string; 22 | user: Types.ObjectId; 23 | designation: string; 24 | name: TUserName; 25 | gender: TGender; 26 | dateOfBirth?: Date; 27 | email: string; 28 | contactNo: string; 29 | emergencyContactNo: string; 30 | bloogGroup?: TBloodGroup; 31 | presentAddress: string; 32 | permanentAddress: string; 33 | profileImg?: string; 34 | isDeleted: boolean; 35 | }; 36 | 37 | export interface AdminModel extends Model { 38 | // eslint-disable-next-line no-unused-vars 39 | isUserExists(id: string): Promise; 40 | } 41 | -------------------------------------------------------------------------------- /src/app/modules/Admin/admin.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose'; 2 | import { BloodGroup, Gender } from './admin.constant'; 3 | import { AdminModel, TAdmin, TUserName } from './admin.interface'; 4 | 5 | const userNameSchema = new Schema({ 6 | firstName: { 7 | type: String, 8 | required: [true, 'First Name is required'], 9 | trim: true, 10 | maxlength: [20, 'Name can not be more than 20 characters'], 11 | }, 12 | middleName: { 13 | type: String, 14 | trim: true, 15 | }, 16 | lastName: { 17 | type: String, 18 | trim: true, 19 | required: [true, 'Last Name is required'], 20 | maxlength: [20, 'Name can not be more than 20 characters'], 21 | }, 22 | }); 23 | 24 | const adminSchema = new Schema( 25 | { 26 | id: { 27 | type: String, 28 | required: [true, 'ID is required'], 29 | unique: true, 30 | }, 31 | user: { 32 | type: Schema.Types.ObjectId, 33 | required: [true, 'User id is required'], 34 | unique: true, 35 | ref: 'User', 36 | }, 37 | designation: { 38 | type: String, 39 | required: [true, 'Designation is required'], 40 | }, 41 | name: { 42 | type: userNameSchema, 43 | required: [true, 'Name is required'], 44 | }, 45 | gender: { 46 | type: String, 47 | enum: { 48 | values: Gender, 49 | message: '{VALUE} is not a valid gender', 50 | }, 51 | required: [true, 'Gender is required'], 52 | }, 53 | dateOfBirth: { type: Date }, 54 | email: { 55 | type: String, 56 | required: [true, 'Email is required'], 57 | unique: true, 58 | }, 59 | contactNo: { type: String, required: [true, 'Contact number is required'] }, 60 | emergencyContactNo: { 61 | type: String, 62 | required: [true, 'Emergency contact number is required'], 63 | }, 64 | bloogGroup: { 65 | type: String, 66 | enum: { 67 | values: BloodGroup, 68 | message: '{VALUE} is not a valid blood group', 69 | }, 70 | }, 71 | presentAddress: { 72 | type: String, 73 | required: [true, 'Present address is required'], 74 | }, 75 | permanentAddress: { 76 | type: String, 77 | required: [true, 'Permanent address is required'], 78 | }, 79 | profileImg: { type: String, default: '' }, 80 | isDeleted: { 81 | type: Boolean, 82 | default: false, 83 | }, 84 | }, 85 | { 86 | toJSON: { 87 | virtuals: true, 88 | }, 89 | }, 90 | ); 91 | 92 | // generating full name 93 | adminSchema.virtual('fullName').get(function () { 94 | return ( 95 | this?.name?.firstName + 96 | '' + 97 | this?.name?.middleName + 98 | '' + 99 | this?.name?.lastName 100 | ); 101 | }); 102 | 103 | // filter out deleted documents 104 | adminSchema.pre('find', function (next) { 105 | this.find({ isDeleted: { $ne: true } }); 106 | next(); 107 | }); 108 | 109 | adminSchema.pre('findOne', function (next) { 110 | this.find({ isDeleted: { $ne: true } }); 111 | next(); 112 | }); 113 | 114 | adminSchema.pre('aggregate', function (next) { 115 | this.pipeline().unshift({ $match: { isDeleted: { $ne: true } } }); 116 | next(); 117 | }); 118 | 119 | //checking if user is already exist! 120 | adminSchema.statics.isUserExists = async function (id: string) { 121 | const existingUser = await Admin.findOne({ id }); 122 | return existingUser; 123 | }; 124 | 125 | export const Admin = model('Admin', adminSchema); 126 | -------------------------------------------------------------------------------- /src/app/modules/Admin/admin.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import auth from '../../middlewares/auth'; 3 | import validateRequest from '../../middlewares/validateRequest'; 4 | import { USER_ROLE } from '../User/user.constant'; 5 | import { AdminControllers } from './admin.controller'; 6 | import { updateAdminValidationSchema } from './admin.validation'; 7 | 8 | const router = express.Router(); 9 | 10 | router.get( 11 | '/', 12 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 13 | AdminControllers.getAllAdmins, 14 | ); 15 | 16 | router.get( 17 | '/:id', 18 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 19 | AdminControllers.getSingleAdmin, 20 | ); 21 | 22 | router.patch( 23 | '/:id', 24 | auth(USER_ROLE.superAdmin), 25 | validateRequest(updateAdminValidationSchema), 26 | AdminControllers.updateAdmin, 27 | ); 28 | 29 | router.delete( 30 | '/:adminId', 31 | auth(USER_ROLE.superAdmin), 32 | AdminControllers.deleteAdmin, 33 | ); 34 | 35 | export const AdminRoutes = router; 36 | -------------------------------------------------------------------------------- /src/app/modules/Admin/admin.service.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import httpStatus from 'http-status'; 3 | import mongoose from 'mongoose'; 4 | import QueryBuilder from '../../builder/QueryBuilder'; 5 | import AppError from '../../errors/AppError'; 6 | import { User } from '../User/user.model'; 7 | import { AdminSearchableFields } from './admin.constant'; 8 | import { TAdmin } from './admin.interface'; 9 | import { Admin } from './admin.model'; 10 | 11 | const getAllAdminsFromDB = async (query: Record) => { 12 | const adminQuery = new QueryBuilder(Admin.find(), query) 13 | .search(AdminSearchableFields) 14 | .filter() 15 | .sort() 16 | .paginate() 17 | .fields(); 18 | 19 | const result = await adminQuery.modelQuery; 20 | const meta = await adminQuery.countTotal(); 21 | return { 22 | result, 23 | meta, 24 | }; 25 | }; 26 | 27 | const getSingleAdminFromDB = async (id: string) => { 28 | const result = await Admin.findById(id); 29 | return result; 30 | }; 31 | 32 | const updateAdminIntoDB = async (id: string, payload: Partial) => { 33 | const { name, ...remainingAdminData } = payload; 34 | 35 | const modifiedUpdatedData: Record = { 36 | ...remainingAdminData, 37 | }; 38 | 39 | if (name && Object.keys(name).length) { 40 | for (const [key, value] of Object.entries(name)) { 41 | modifiedUpdatedData[`name.${key}`] = value; 42 | } 43 | } 44 | 45 | const result = await Admin.findByIdAndUpdate({ id }, modifiedUpdatedData, { 46 | new: true, 47 | runValidators: true, 48 | }); 49 | return result; 50 | }; 51 | 52 | const deleteAdminFromDB = async (id: string) => { 53 | const session = await mongoose.startSession(); 54 | 55 | try { 56 | session.startTransaction(); 57 | 58 | const deletedAdmin = await Admin.findByIdAndUpdate( 59 | id, 60 | { isDeleted: true }, 61 | { new: true, session }, 62 | ); 63 | 64 | if (!deletedAdmin) { 65 | throw new AppError(httpStatus.BAD_REQUEST, 'Failed to delete student'); 66 | } 67 | 68 | // get user _id from deletedAdmin 69 | const userId = deletedAdmin.user; 70 | 71 | const deletedUser = await User.findOneAndUpdate( 72 | userId, 73 | { isDeleted: true }, 74 | { new: true, session }, 75 | ); 76 | 77 | if (!deletedUser) { 78 | throw new AppError(httpStatus.BAD_REQUEST, 'Failed to delete user'); 79 | } 80 | 81 | await session.commitTransaction(); 82 | await session.endSession(); 83 | 84 | return deletedAdmin; 85 | } catch (err: any) { 86 | await session.abortTransaction(); 87 | await session.endSession(); 88 | throw new Error(err); 89 | } 90 | }; 91 | 92 | export const AdminServices = { 93 | getAllAdminsFromDB, 94 | getSingleAdminFromDB, 95 | updateAdminIntoDB, 96 | deleteAdminFromDB, 97 | }; 98 | -------------------------------------------------------------------------------- /src/app/modules/Admin/admin.validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { BloodGroup, Gender } from './admin.constant'; 3 | 4 | const createUserNameValidationSchema = z.object({ 5 | firstName: z.string().min(1).max(20), 6 | middleName: z.string().max(20), 7 | lastName: z.string().max(20), 8 | }); 9 | 10 | export const createAdminValidationSchema = z.object({ 11 | body: z.object({ 12 | password: z.string().max(20).optional(), 13 | admin: z.object({ 14 | designation: z.string(), 15 | name: createUserNameValidationSchema, 16 | gender: z.enum([...Gender] as [string, ...string[]]), 17 | dateOfBirth: z.string().optional(), 18 | email: z.string().email(), 19 | contactNo: z.string(), 20 | emergencyContactNo: z.string(), 21 | bloogGroup: z.enum([...BloodGroup] as [string, ...string[]]), 22 | presentAddress: z.string(), 23 | permanentAddress: z.string(), 24 | // profileImg: z.string(), 25 | }), 26 | }), 27 | }); 28 | 29 | const updateUserNameValidationSchema = z.object({ 30 | firstName: z.string().min(3).max(20).optional(), 31 | middleName: z.string().min(3).max(20).optional(), 32 | lastName: z.string().min(3).max(20).optional(), 33 | }); 34 | 35 | export const updateAdminValidationSchema = z.object({ 36 | body: z.object({ 37 | admin: z.object({ 38 | name: updateUserNameValidationSchema, 39 | designation: z.string().max(30).optional(), 40 | gender: z.enum([...Gender] as [string, ...string[]]).optional(), 41 | dateOfBirth: z.string().optional(), 42 | email: z.string().email().optional(), 43 | contactNo: z.string().optional(), 44 | emergencyContactNo: z.string().optional(), 45 | bloogGroup: z.enum([...BloodGroup] as [string, ...string[]]).optional(), 46 | presentAddress: z.string().optional(), 47 | permanentAddress: z.string().optional(), 48 | // profileImg: z.string().optional(), 49 | }), 50 | }), 51 | }); 52 | 53 | export const AdminValidations = { 54 | createAdminValidationSchema, 55 | updateAdminValidationSchema, 56 | }; 57 | -------------------------------------------------------------------------------- /src/app/modules/Auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import config from '../../config'; 3 | import AppError from '../../errors/AppError'; 4 | import catchAsync from '../../utils/catchAsync'; 5 | import sendResponse from '../../utils/sendResponse'; 6 | import { AuthServices } from './auth.service'; 7 | 8 | const loginUser = catchAsync(async (req, res) => { 9 | const result = await AuthServices.loginUser(req.body); 10 | const { refreshToken, accessToken, needsPasswordChange } = result; 11 | 12 | res.cookie('refreshToken', refreshToken, { 13 | secure: config.NODE_ENV === 'production', 14 | httpOnly: true, 15 | sameSite: 'none', 16 | maxAge: 1000 * 60 * 60 * 24 * 365, 17 | }); 18 | 19 | sendResponse(res, { 20 | statusCode: httpStatus.OK, 21 | success: true, 22 | message: 'User is logged in successfully!', 23 | data: { 24 | accessToken, 25 | needsPasswordChange, 26 | }, 27 | }); 28 | }); 29 | 30 | const changePassword = catchAsync(async (req, res) => { 31 | const { ...passwordData } = req.body; 32 | 33 | const result = await AuthServices.changePassword(req.user, passwordData); 34 | sendResponse(res, { 35 | statusCode: httpStatus.OK, 36 | success: true, 37 | message: 'Password is updated successfully!', 38 | data: result, 39 | }); 40 | }); 41 | 42 | const refreshToken = catchAsync(async (req, res) => { 43 | const { refreshToken } = req.cookies; 44 | const result = await AuthServices.refreshToken(refreshToken); 45 | 46 | sendResponse(res, { 47 | statusCode: httpStatus.OK, 48 | success: true, 49 | message: 'Access token is retrieved successfully!', 50 | data: result, 51 | }); 52 | }); 53 | 54 | const forgetPassword = catchAsync(async (req, res) => { 55 | const userId = req.body.id; 56 | const result = await AuthServices.forgetPassword(userId); 57 | sendResponse(res, { 58 | statusCode: httpStatus.OK, 59 | success: true, 60 | message: 'Reset link is generated successfully!', 61 | data: result, 62 | }); 63 | }); 64 | 65 | const resetPassword = catchAsync(async (req, res) => { 66 | const token = req.headers.authorization; 67 | 68 | if (!token) { 69 | throw new AppError(httpStatus.BAD_REQUEST, 'Something went wrong !'); 70 | } 71 | 72 | const result = await AuthServices.resetPassword(req.body, token); 73 | sendResponse(res, { 74 | statusCode: httpStatus.OK, 75 | success: true, 76 | message: 'Password reset successfully!', 77 | data: result, 78 | }); 79 | }); 80 | 81 | export const AuthControllers = { 82 | loginUser, 83 | changePassword, 84 | refreshToken, 85 | forgetPassword, 86 | resetPassword, 87 | }; 88 | -------------------------------------------------------------------------------- /src/app/modules/Auth/auth.interface.ts: -------------------------------------------------------------------------------- 1 | export type TLoginUser = { 2 | id: string; 3 | password: string; 4 | }; 5 | -------------------------------------------------------------------------------- /src/app/modules/Auth/auth.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import auth from '../../middlewares/auth'; 3 | import validateRequest from '../../middlewares/validateRequest'; 4 | import { USER_ROLE } from '../User/user.constant'; 5 | import { AuthControllers } from './auth.controller'; 6 | import { AuthValidation } from './auth.validation'; 7 | 8 | const router = express.Router(); 9 | 10 | router.post( 11 | '/login', 12 | validateRequest(AuthValidation.loginValidationSchema), 13 | AuthControllers.loginUser, 14 | ); 15 | 16 | router.post( 17 | '/change-password', 18 | auth( 19 | USER_ROLE.superAdmin, 20 | USER_ROLE.admin, 21 | USER_ROLE.faculty, 22 | USER_ROLE.student, 23 | ), 24 | validateRequest(AuthValidation.changePasswordValidationSchema), 25 | AuthControllers.changePassword, 26 | ); 27 | 28 | router.post( 29 | '/refresh-token', 30 | validateRequest(AuthValidation.refreshTokenValidationSchema), 31 | AuthControllers.refreshToken, 32 | ); 33 | 34 | router.post( 35 | '/forget-password', 36 | validateRequest(AuthValidation.forgetPasswordValidationSchema), 37 | AuthControllers.forgetPassword, 38 | ); 39 | 40 | router.post( 41 | '/reset-password', 42 | validateRequest(AuthValidation.forgetPasswordValidationSchema), 43 | AuthControllers.resetPassword, 44 | ); 45 | 46 | export const AuthRoutes = router; 47 | -------------------------------------------------------------------------------- /src/app/modules/Auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | import httpStatus from 'http-status'; 3 | import jwt, { JwtPayload } from 'jsonwebtoken'; 4 | import config from '../../config'; 5 | import AppError from '../../errors/AppError'; 6 | import { sendEmail } from '../../utils/sendEmail'; 7 | import { User } from '../User/user.model'; 8 | import { TLoginUser } from './auth.interface'; 9 | import { createToken, verifyToken } from './auth.utils'; 10 | 11 | const loginUser = async (payload: TLoginUser) => { 12 | // checking if the user is exist 13 | const user = await User.isUserExistsByCustomId(payload.id); 14 | 15 | if (!user) { 16 | throw new AppError(httpStatus.NOT_FOUND, 'This user is not found !'); 17 | } 18 | // checking if the user is already deleted 19 | 20 | const isDeleted = user?.isDeleted; 21 | 22 | if (isDeleted) { 23 | throw new AppError(httpStatus.FORBIDDEN, 'This user is deleted !'); 24 | } 25 | 26 | // checking if the user is blocked 27 | 28 | const userStatus = user?.status; 29 | 30 | if (userStatus === 'blocked') { 31 | throw new AppError(httpStatus.FORBIDDEN, 'This user is blocked ! !'); 32 | } 33 | 34 | //checking if the password is correct 35 | 36 | if (!(await User.isPasswordMatched(payload?.password, user?.password))) 37 | throw new AppError(httpStatus.FORBIDDEN, 'Password do not matched'); 38 | 39 | //create token and sent to the client 40 | 41 | const jwtPayload = { 42 | userId: user.id, 43 | role: user.role, 44 | }; 45 | 46 | const accessToken = createToken( 47 | jwtPayload, 48 | config.jwt_access_secret as string, 49 | config.jwt_access_expires_in as string, 50 | ); 51 | 52 | const refreshToken = createToken( 53 | jwtPayload, 54 | config.jwt_refresh_secret as string, 55 | config.jwt_refresh_expires_in as string, 56 | ); 57 | 58 | return { 59 | accessToken, 60 | refreshToken, 61 | needsPasswordChange: user?.needsPasswordChange, 62 | }; 63 | }; 64 | 65 | const changePassword = async ( 66 | userData: JwtPayload, 67 | payload: { oldPassword: string; newPassword: string }, 68 | ) => { 69 | // checking if the user is exist 70 | const user = await User.isUserExistsByCustomId(userData.userId); 71 | 72 | if (!user) { 73 | throw new AppError(httpStatus.NOT_FOUND, 'This user is not found !'); 74 | } 75 | // checking if the user is already deleted 76 | 77 | const isDeleted = user?.isDeleted; 78 | 79 | if (isDeleted) { 80 | throw new AppError(httpStatus.FORBIDDEN, 'This user is deleted !'); 81 | } 82 | 83 | // checking if the user is blocked 84 | 85 | const userStatus = user?.status; 86 | 87 | if (userStatus === 'blocked') { 88 | throw new AppError(httpStatus.FORBIDDEN, 'This user is blocked ! !'); 89 | } 90 | 91 | //checking if the password is correct 92 | 93 | if (!(await User.isPasswordMatched(payload.oldPassword, user?.password))) 94 | throw new AppError(httpStatus.FORBIDDEN, 'Password do not matched'); 95 | 96 | //hash new password 97 | const newHashedPassword = await bcrypt.hash( 98 | payload.newPassword, 99 | Number(config.bcrypt_salt_rounds), 100 | ); 101 | 102 | await User.findOneAndUpdate( 103 | { 104 | id: userData.userId, 105 | role: userData.role, 106 | }, 107 | { 108 | password: newHashedPassword, 109 | needsPasswordChange: false, 110 | passwordChangedAt: new Date(), 111 | }, 112 | ); 113 | 114 | return null; 115 | }; 116 | 117 | const refreshToken = async (token: string) => { 118 | // checking if the given token is valid 119 | const decoded = verifyToken(token, config.jwt_refresh_secret as string); 120 | 121 | const { userId, iat } = decoded; 122 | 123 | // checking if the user is exist 124 | const user = await User.isUserExistsByCustomId(userId); 125 | 126 | if (!user) { 127 | throw new AppError(httpStatus.NOT_FOUND, 'This user is not found !'); 128 | } 129 | // checking if the user is already deleted 130 | const isDeleted = user?.isDeleted; 131 | 132 | if (isDeleted) { 133 | throw new AppError(httpStatus.FORBIDDEN, 'This user is deleted !'); 134 | } 135 | 136 | // checking if the user is blocked 137 | const userStatus = user?.status; 138 | 139 | if (userStatus === 'blocked') { 140 | throw new AppError(httpStatus.FORBIDDEN, 'This user is blocked ! !'); 141 | } 142 | 143 | if ( 144 | user.passwordChangedAt && 145 | User.isJWTIssuedBeforePasswordChanged(user.passwordChangedAt, iat as number) 146 | ) { 147 | throw new AppError(httpStatus.UNAUTHORIZED, 'You are not authorized !'); 148 | } 149 | 150 | const jwtPayload = { 151 | userId: user.id, 152 | role: user.role, 153 | }; 154 | 155 | const accessToken = createToken( 156 | jwtPayload, 157 | config.jwt_access_secret as string, 158 | config.jwt_access_expires_in as string, 159 | ); 160 | 161 | return { 162 | accessToken, 163 | }; 164 | }; 165 | 166 | const forgetPassword = async (userId: string) => { 167 | // checking if the user is exist 168 | const user = await User.isUserExistsByCustomId(userId); 169 | 170 | if (!user) { 171 | throw new AppError(httpStatus.NOT_FOUND, 'This user is not found !'); 172 | } 173 | // checking if the user is already deleted 174 | const isDeleted = user?.isDeleted; 175 | 176 | if (isDeleted) { 177 | throw new AppError(httpStatus.FORBIDDEN, 'This user is deleted !'); 178 | } 179 | 180 | // checking if the user is blocked 181 | const userStatus = user?.status; 182 | 183 | if (userStatus === 'blocked') { 184 | throw new AppError(httpStatus.FORBIDDEN, 'This user is blocked ! !'); 185 | } 186 | 187 | const jwtPayload = { 188 | userId: user.id, 189 | role: user.role, 190 | }; 191 | 192 | const resetToken = createToken( 193 | jwtPayload, 194 | config.jwt_access_secret as string, 195 | '10m', 196 | ); 197 | 198 | const resetUILink = `${config.reset_pass_ui_link}?id=${user.id}&token=${resetToken} `; 199 | 200 | sendEmail(user.email, resetUILink); 201 | 202 | console.log(resetUILink); 203 | }; 204 | 205 | const resetPassword = async ( 206 | payload: { id: string; newPassword: string }, 207 | token: string, 208 | ) => { 209 | // checking if the user is exist 210 | const user = await User.isUserExistsByCustomId(payload?.id); 211 | 212 | if (!user) { 213 | throw new AppError(httpStatus.NOT_FOUND, 'This user is not found !'); 214 | } 215 | // checking if the user is already deleted 216 | const isDeleted = user?.isDeleted; 217 | 218 | if (isDeleted) { 219 | throw new AppError(httpStatus.FORBIDDEN, 'This user is deleted !'); 220 | } 221 | 222 | // checking if the user is blocked 223 | const userStatus = user?.status; 224 | 225 | if (userStatus === 'blocked') { 226 | throw new AppError(httpStatus.FORBIDDEN, 'This user is blocked ! !'); 227 | } 228 | 229 | const decoded = jwt.verify( 230 | token, 231 | config.jwt_access_secret as string, 232 | ) as JwtPayload; 233 | 234 | //localhost:3000?id=A-0001&token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJBLTAwMDEiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MDI4NTA2MTcsImV4cCI6MTcwMjg1MTIxN30.-T90nRaz8-KouKki1DkCSMAbsHyb9yDi0djZU3D6QO4 235 | 236 | if (payload.id !== decoded.userId) { 237 | console.log(payload.id, decoded.userId); 238 | throw new AppError(httpStatus.FORBIDDEN, 'You are forbidden!'); 239 | } 240 | 241 | //hash new password 242 | const newHashedPassword = await bcrypt.hash( 243 | payload.newPassword, 244 | Number(config.bcrypt_salt_rounds), 245 | ); 246 | 247 | await User.findOneAndUpdate( 248 | { 249 | id: decoded.userId, 250 | role: decoded.role, 251 | }, 252 | { 253 | password: newHashedPassword, 254 | needsPasswordChange: false, 255 | passwordChangedAt: new Date(), 256 | }, 257 | ); 258 | }; 259 | 260 | export const AuthServices = { 261 | loginUser, 262 | changePassword, 263 | refreshToken, 264 | forgetPassword, 265 | resetPassword, 266 | }; 267 | -------------------------------------------------------------------------------- /src/app/modules/Auth/auth.utils.ts: -------------------------------------------------------------------------------- 1 | import jwt, { JwtPayload } from 'jsonwebtoken'; 2 | 3 | export const createToken = ( 4 | jwtPayload: { userId: string; role: string }, 5 | secret: string, 6 | expiresIn: string, 7 | ) => { 8 | return jwt.sign(jwtPayload, secret, { 9 | expiresIn, 10 | }); 11 | }; 12 | 13 | export const verifyToken = (token: string, secret: string) => { 14 | return jwt.verify(token, secret) as JwtPayload; 15 | }; 16 | -------------------------------------------------------------------------------- /src/app/modules/Auth/auth.validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const loginValidationSchema = z.object({ 4 | body: z.object({ 5 | id: z.string({ required_error: 'Id is required.' }), 6 | password: z.string({ required_error: 'Password is required' }), 7 | }), 8 | }); 9 | 10 | const changePasswordValidationSchema = z.object({ 11 | body: z.object({ 12 | oldPassword: z.string({ 13 | required_error: 'Old password is required', 14 | }), 15 | newPassword: z.string({ required_error: 'Password is required' }), 16 | }), 17 | }); 18 | 19 | const refreshTokenValidationSchema = z.object({ 20 | cookies: z.object({ 21 | refreshToken: z.string({ 22 | required_error: 'Refresh token is required!', 23 | }), 24 | }), 25 | }); 26 | 27 | const forgetPasswordValidationSchema = z.object({ 28 | body: z.object({ 29 | id: z.string({ 30 | required_error: 'User id is required!', 31 | }), 32 | }), 33 | }); 34 | 35 | const resetPasswordValidationSchema = z.object({ 36 | body: z.object({ 37 | id: z.string({ 38 | required_error: 'User id is required!', 39 | }), 40 | newPassword: z.string({ 41 | required_error: 'User password is required!', 42 | }), 43 | }), 44 | }); 45 | 46 | export const AuthValidation = { 47 | loginValidationSchema, 48 | changePasswordValidationSchema, 49 | refreshTokenValidationSchema, 50 | forgetPasswordValidationSchema, 51 | resetPasswordValidationSchema, 52 | }; 53 | -------------------------------------------------------------------------------- /src/app/modules/Course/course.constant.ts: -------------------------------------------------------------------------------- 1 | export const CourseSearchableFields = ['title', 'prefix']; 2 | -------------------------------------------------------------------------------- /src/app/modules/Course/course.controller.ts: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import catchAsync from '../../utils/catchAsync'; 3 | import sendResponse from '../../utils/sendResponse'; 4 | import { CourseServices } from './course.service'; 5 | 6 | const createCourse = catchAsync(async (req, res) => { 7 | const result = await CourseServices.createCourseIntoDB(req.body); 8 | 9 | sendResponse(res, { 10 | statusCode: httpStatus.OK, 11 | success: true, 12 | message: 'Course is created successfully', 13 | data: result, 14 | }); 15 | }); 16 | 17 | const getAllCourses = catchAsync(async (req, res) => { 18 | const result = await CourseServices.getAllCoursesFromDB(req.query); 19 | 20 | sendResponse(res, { 21 | statusCode: httpStatus.OK, 22 | success: true, 23 | message: 'Course are retrieved successfully', 24 | meta: result.meta, 25 | data: result.result, 26 | }); 27 | }); 28 | 29 | const getSingleCourse = catchAsync(async (req, res) => { 30 | const { id } = req.params; 31 | const result = await CourseServices.getSingleCourseFromDB(id); 32 | 33 | sendResponse(res, { 34 | statusCode: httpStatus.OK, 35 | success: true, 36 | message: 'Course is retrieved successfully', 37 | data: result, 38 | }); 39 | }); 40 | 41 | const updateCourse = catchAsync(async (req, res) => { 42 | const { id } = req.params; 43 | const result = await CourseServices.updateCourseIntoDB(id, req.body); 44 | 45 | sendResponse(res, { 46 | statusCode: httpStatus.OK, 47 | success: true, 48 | message: 'course is updated successfully', 49 | data: result, 50 | }); 51 | }); 52 | 53 | const deleteCourse = catchAsync(async (req, res) => { 54 | const { id } = req.params; 55 | const result = await CourseServices.deleteCourseFromDB(id); 56 | 57 | sendResponse(res, { 58 | statusCode: httpStatus.OK, 59 | success: true, 60 | message: 'Course is deleted successfully', 61 | data: result, 62 | }); 63 | }); 64 | 65 | const assignFacultiesWithCourse = catchAsync(async (req, res) => { 66 | const { courseId } = req.params; 67 | const { faculties } = req.body; 68 | 69 | const result = await CourseServices.assignFacultiesWithCourseIntoDB( 70 | courseId, 71 | faculties, 72 | ); 73 | 74 | sendResponse(res, { 75 | statusCode: httpStatus.OK, 76 | success: true, 77 | message: 'Faculties assigned successfully', 78 | data: result, 79 | }); 80 | }); 81 | 82 | const getFacultiesWithCourse = catchAsync(async (req, res) => { 83 | const { courseId } = req.params; 84 | 85 | const result = await CourseServices.getFacultiesWithCourseFromDB(courseId); 86 | 87 | sendResponse(res, { 88 | statusCode: httpStatus.OK, 89 | success: true, 90 | message: 'Faculties retrieved successfully', 91 | data: result, 92 | }); 93 | }); 94 | 95 | const removeFacultiesFromCourse = catchAsync(async (req, res) => { 96 | const { courseId } = req.params; 97 | const { faculties } = req.body; 98 | 99 | const result = await CourseServices.removeFacultiesFromCourseFromDB( 100 | courseId, 101 | faculties, 102 | ); 103 | 104 | sendResponse(res, { 105 | statusCode: httpStatus.OK, 106 | success: true, 107 | message: 'Faculties removed successfully', 108 | data: result, 109 | }); 110 | }); 111 | 112 | export const CourseControllers = { 113 | createCourse, 114 | getSingleCourse, 115 | getAllCourses, 116 | updateCourse, 117 | deleteCourse, 118 | assignFacultiesWithCourse, 119 | getFacultiesWithCourse, 120 | removeFacultiesFromCourse, 121 | }; 122 | -------------------------------------------------------------------------------- /src/app/modules/Course/course.interface.ts: -------------------------------------------------------------------------------- 1 | import { Types } from 'mongoose'; 2 | 3 | export type TPreRequisiteCourses = { 4 | course: Types.ObjectId; 5 | isDeleted: boolean; 6 | }; 7 | 8 | export type TCourse = { 9 | title: string; 10 | prefix: string; 11 | code: number; 12 | credits: number; 13 | isDeleted?: boolean; 14 | preRequisiteCourses: [TPreRequisiteCourses]; 15 | }; 16 | 17 | export type TCoursefaculty = { 18 | course: Types.ObjectId; 19 | faculties: [Types.ObjectId]; 20 | }; 21 | -------------------------------------------------------------------------------- /src/app/modules/Course/course.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose'; 2 | import { 3 | TCourse, 4 | TCoursefaculty, 5 | TPreRequisiteCourses, 6 | } from './course.interface'; 7 | 8 | const preRequisiteCoursesSchema = new Schema( 9 | { 10 | course: { 11 | type: Schema.Types.ObjectId, 12 | ref: 'Course', 13 | }, 14 | isDeleted: { 15 | type: Boolean, 16 | default: false, 17 | }, 18 | }, 19 | { 20 | _id: false, 21 | }, 22 | ); 23 | 24 | const courseSchema = new Schema({ 25 | title: { 26 | type: String, 27 | unique: true, 28 | trim: true, 29 | required: true, 30 | }, 31 | prefix: { 32 | type: String, 33 | trim: true, 34 | required: true, 35 | }, 36 | code: { 37 | type: Number, 38 | trim: true, 39 | required: true, 40 | }, 41 | credits: { 42 | type: Number, 43 | trim: true, 44 | required: true, 45 | }, 46 | preRequisiteCourses: [preRequisiteCoursesSchema], 47 | isDeleted: { 48 | type: Boolean, 49 | default: false, 50 | }, 51 | }); 52 | 53 | export const Course = model('Course', courseSchema); 54 | 55 | const courseFacultySchema = new Schema({ 56 | course: { 57 | type: Schema.Types.ObjectId, 58 | ref: 'Course', 59 | unique: true, 60 | }, 61 | faculties: [ 62 | { 63 | type: Schema.Types.ObjectId, 64 | ref: 'Faculty', 65 | }, 66 | ], 67 | }); 68 | 69 | export const CourseFaculty = model( 70 | 'CourseFaculty', 71 | courseFacultySchema, 72 | ); 73 | -------------------------------------------------------------------------------- /src/app/modules/Course/course.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import auth from '../../middlewares/auth'; 3 | import validateRequest from '../../middlewares/validateRequest'; 4 | import { USER_ROLE } from '../User/user.constant'; 5 | import { CourseControllers } from './course.controller'; 6 | import { CourseValidations } from './course.validation'; 7 | 8 | const router = express.Router(); 9 | 10 | router.post( 11 | '/create-course', 12 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 13 | validateRequest(CourseValidations.createCourseValidationSchema), 14 | CourseControllers.createCourse, 15 | ); 16 | 17 | router.get( 18 | '/:id', 19 | auth( 20 | USER_ROLE.superAdmin, 21 | USER_ROLE.admin, 22 | USER_ROLE.faculty, 23 | USER_ROLE.student, 24 | ), 25 | CourseControllers.getSingleCourse, 26 | ); 27 | 28 | router.patch( 29 | '/:id', 30 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 31 | validateRequest(CourseValidations.updateCourseValidationSchema), 32 | CourseControllers.updateCourse, 33 | ); 34 | 35 | router.delete( 36 | '/:id', 37 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 38 | CourseControllers.deleteCourse, 39 | ); 40 | 41 | router.put( 42 | '/:courseId/assign-faculties', 43 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 44 | validateRequest(CourseValidations.facultiesWithCourseValidationSchema), 45 | CourseControllers.assignFacultiesWithCourse, 46 | ); 47 | 48 | router.get( 49 | '/:courseId/get-faculties', 50 | auth( 51 | USER_ROLE.superAdmin, 52 | USER_ROLE.admin, 53 | USER_ROLE.faculty, 54 | USER_ROLE.student, 55 | ), 56 | CourseControllers.getFacultiesWithCourse, 57 | ); 58 | 59 | router.delete( 60 | '/:courseId/remove-faculties', 61 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 62 | validateRequest(CourseValidations.facultiesWithCourseValidationSchema), 63 | CourseControllers.removeFacultiesFromCourse, 64 | ); 65 | 66 | router.get( 67 | '/', 68 | auth( 69 | USER_ROLE.superAdmin, 70 | USER_ROLE.admin, 71 | USER_ROLE.faculty, 72 | USER_ROLE.student, 73 | ), 74 | CourseControllers.getAllCourses, 75 | ); 76 | 77 | export const CourseRoutes = router; 78 | -------------------------------------------------------------------------------- /src/app/modules/Course/course.service.ts: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import mongoose from 'mongoose'; 3 | import QueryBuilder from '../../builder/QueryBuilder'; 4 | import AppError from '../../errors/AppError'; 5 | import { CourseSearchableFields } from './course.constant'; 6 | import { TCourse, TCoursefaculty } from './course.interface'; 7 | import { Course, CourseFaculty } from './course.model'; 8 | 9 | const createCourseIntoDB = async (payload: TCourse) => { 10 | const result = await Course.create(payload); 11 | return result; 12 | }; 13 | 14 | const getAllCoursesFromDB = async (query: Record) => { 15 | const courseQuery = new QueryBuilder( 16 | Course.find().populate('preRequisiteCourses.course'), 17 | query, 18 | ) 19 | .search(CourseSearchableFields) 20 | .filter() 21 | .sort() 22 | .paginate() 23 | .fields(); 24 | 25 | const result = await courseQuery.modelQuery; 26 | const meta = await courseQuery.countTotal(); 27 | 28 | return { 29 | meta, 30 | result, 31 | }; 32 | }; 33 | 34 | const getSingleCourseFromDB = async (id: string) => { 35 | const result = await Course.findById(id).populate( 36 | 'preRequisiteCourses.course', 37 | ); 38 | return result; 39 | }; 40 | 41 | const updateCourseIntoDB = async (id: string, payload: Partial) => { 42 | const { preRequisiteCourses, ...courseRemainingData } = payload; 43 | 44 | const session = await mongoose.startSession(); 45 | 46 | try { 47 | session.startTransaction(); 48 | 49 | //step1: basic course info update 50 | const updatedBasicCourseInfo = await Course.findByIdAndUpdate( 51 | id, 52 | courseRemainingData, 53 | { 54 | new: true, 55 | runValidators: true, 56 | session, 57 | }, 58 | ); 59 | 60 | if (!updatedBasicCourseInfo) { 61 | throw new AppError(httpStatus.BAD_REQUEST, 'Failed to update course'); 62 | } 63 | 64 | // check if there is any pre requisite courses to update 65 | if (preRequisiteCourses && preRequisiteCourses.length > 0) { 66 | // filter out the deleted fields 67 | const deletedPreRequisites = preRequisiteCourses 68 | .filter((el) => el.course && el.isDeleted) 69 | .map((el) => el.course); 70 | 71 | const deletedPreRequisiteCourses = await Course.findByIdAndUpdate( 72 | id, 73 | { 74 | $pull: { 75 | preRequisiteCourses: { course: { $in: deletedPreRequisites } }, 76 | }, 77 | }, 78 | { 79 | new: true, 80 | runValidators: true, 81 | session, 82 | }, 83 | ); 84 | 85 | if (!deletedPreRequisiteCourses) { 86 | throw new AppError(httpStatus.BAD_REQUEST, 'Failed to update course'); 87 | } 88 | 89 | // filter out the new course fields 90 | const newPreRequisites = preRequisiteCourses?.filter( 91 | (el) => el.course && !el.isDeleted, 92 | ); 93 | 94 | const newPreRequisiteCourses = await Course.findByIdAndUpdate( 95 | id, 96 | { 97 | $addToSet: { preRequisiteCourses: { $each: newPreRequisites } }, 98 | }, 99 | { 100 | new: true, 101 | runValidators: true, 102 | session, 103 | }, 104 | ); 105 | 106 | if (!newPreRequisiteCourses) { 107 | throw new AppError(httpStatus.BAD_REQUEST, 'Failed to update course'); 108 | } 109 | } 110 | 111 | await session.commitTransaction(); 112 | await session.endSession(); 113 | 114 | const result = await Course.findById(id).populate( 115 | 'preRequisiteCourses.course', 116 | ); 117 | 118 | return result; 119 | } catch (err) { 120 | await session.abortTransaction(); 121 | await session.endSession(); 122 | throw new AppError(httpStatus.BAD_REQUEST, 'Failed to update course'); 123 | } 124 | }; 125 | 126 | const deleteCourseFromDB = async (id: string) => { 127 | const result = await Course.findByIdAndUpdate( 128 | id, 129 | { isDeleted: true }, 130 | { 131 | new: true, 132 | }, 133 | ); 134 | return result; 135 | }; 136 | 137 | const assignFacultiesWithCourseIntoDB = async ( 138 | id: string, 139 | payload: Partial, 140 | ) => { 141 | const result = await CourseFaculty.findByIdAndUpdate( 142 | id, 143 | { 144 | course: id, 145 | $addToSet: { faculties: { $each: payload } }, 146 | }, 147 | { 148 | upsert: true, 149 | new: true, 150 | }, 151 | ); 152 | return result; 153 | }; 154 | 155 | const getFacultiesWithCourseFromDB = async (courseId: string) => { 156 | const result = await CourseFaculty.findOne({ course: courseId }).populate( 157 | 'faculties', 158 | ); 159 | return result; 160 | }; 161 | 162 | const removeFacultiesFromCourseFromDB = async ( 163 | id: string, 164 | payload: Partial, 165 | ) => { 166 | const result = await CourseFaculty.findByIdAndUpdate( 167 | id, 168 | { 169 | $pull: { faculties: { $in: payload } }, 170 | }, 171 | { 172 | new: true, 173 | }, 174 | ); 175 | return result; 176 | }; 177 | 178 | export const CourseServices = { 179 | createCourseIntoDB, 180 | getAllCoursesFromDB, 181 | getSingleCourseFromDB, 182 | updateCourseIntoDB, 183 | deleteCourseFromDB, 184 | assignFacultiesWithCourseIntoDB, 185 | getFacultiesWithCourseFromDB, 186 | removeFacultiesFromCourseFromDB, 187 | }; 188 | -------------------------------------------------------------------------------- /src/app/modules/Course/course.validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const PreRequisiteCourseValidationSchema = z.object({ 4 | course: z.string(), 5 | isDeleted: z.boolean().optional(), 6 | }); 7 | 8 | const createCourseValidationSchema = z.object({ 9 | body: z.object({ 10 | title: z.string(), 11 | prefix: z.string(), 12 | code: z.number(), 13 | credits: z.number(), 14 | preRequisiteCourses: z.array(PreRequisiteCourseValidationSchema).optional(), 15 | isDeleted: z.boolean().optional(), 16 | }), 17 | }); 18 | 19 | const updatePreRequisiteCourseValidationSchema = z.object({ 20 | course: z.string(), 21 | isDeleted: z.boolean().optional(), 22 | }); 23 | 24 | const updateCourseValidationSchema = z.object({ 25 | body: z.object({ 26 | title: z.string().optional(), 27 | prefix: z.string().optional(), 28 | code: z.number().optional(), 29 | credits: z.number().optional(), 30 | preRequisiteCourses: z 31 | .array(updatePreRequisiteCourseValidationSchema) 32 | .optional(), 33 | isDeleted: z.boolean().optional(), 34 | }), 35 | }); 36 | 37 | const facultiesWithCourseValidationSchema = z.object({ 38 | body: z.object({ 39 | faculties: z.array(z.string()), 40 | }), 41 | }); 42 | 43 | export const CourseValidations = { 44 | createCourseValidationSchema, 45 | updateCourseValidationSchema, 46 | facultiesWithCourseValidationSchema, 47 | }; 48 | -------------------------------------------------------------------------------- /src/app/modules/EnrolledCourse/enrolledCourse.constant.ts: -------------------------------------------------------------------------------- 1 | export const Grade = ['A', 'B', 'C', 'D', 'F', 'NA']; 2 | -------------------------------------------------------------------------------- /src/app/modules/EnrolledCourse/enrolledCourse.controller.ts: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import catchAsync from '../../utils/catchAsync'; 3 | import sendResponse from '../../utils/sendResponse'; 4 | import { EnrolledCourseServices } from './enrolledCourse.service'; 5 | 6 | const createEnrolledCourse = catchAsync(async (req, res) => { 7 | const userId = req.user.userId; 8 | const result = await EnrolledCourseServices.createEnrolledCourseIntoDB( 9 | userId, 10 | req.body, 11 | ); 12 | 13 | sendResponse(res, { 14 | statusCode: httpStatus.OK, 15 | success: true, 16 | message: 'Student is enrolled successfully', 17 | data: result, 18 | }); 19 | }); 20 | 21 | const getAllEnrolledCourses = catchAsync(async (req, res) => { 22 | const facultyId = req.user.userId; 23 | 24 | const result = await EnrolledCourseServices.getAllEnrolledCoursesFromDB( 25 | facultyId, 26 | req.query, 27 | ); 28 | 29 | sendResponse(res, { 30 | statusCode: httpStatus.OK, 31 | success: true, 32 | message: 'Enrolled courses are retrieved successfully', 33 | meta: result.meta, 34 | data: result.result, 35 | }); 36 | }); 37 | 38 | const getMyEnrolledCourses = catchAsync(async (req, res) => { 39 | const studentId = req.user.userId; 40 | 41 | const result = await EnrolledCourseServices.getMyEnrolledCoursesFromDB( 42 | studentId, 43 | req.query, 44 | ); 45 | 46 | sendResponse(res, { 47 | statusCode: httpStatus.OK, 48 | success: true, 49 | message: 'Enrolled courses are retrieved successfully', 50 | meta: result.meta, 51 | data: result.result, 52 | }); 53 | }); 54 | 55 | const updateEnrolledCourseMarks = catchAsync(async (req, res) => { 56 | const facultyId = req.user.userId; 57 | const result = await EnrolledCourseServices.updateEnrolledCourseMarksIntoDB( 58 | facultyId, 59 | req.body, 60 | ); 61 | 62 | sendResponse(res, { 63 | statusCode: httpStatus.OK, 64 | success: true, 65 | message: 'Marks is updated successfully', 66 | data: result, 67 | }); 68 | }); 69 | 70 | export const EnrolledCourseControllers = { 71 | createEnrolledCourse, 72 | getAllEnrolledCourses, 73 | getMyEnrolledCourses, 74 | updateEnrolledCourseMarks, 75 | }; 76 | -------------------------------------------------------------------------------- /src/app/modules/EnrolledCourse/enrolledCourse.interface.ts: -------------------------------------------------------------------------------- 1 | import { Types } from 'mongoose'; 2 | 3 | export type TGrade = 'A' | 'B' | 'C' | 'D' | 'F' | 'NA'; 4 | 5 | export type TEnrolledCourseMarks = { 6 | classTest1: number; 7 | midTerm: number; 8 | classTest2: number; 9 | finalTerm: number; 10 | }; 11 | 12 | export type TEnrolledCourse = { 13 | semesterRegistration: Types.ObjectId; 14 | academicSemester: Types.ObjectId; 15 | academicFaculty: Types.ObjectId; 16 | academicDepartment: Types.ObjectId; 17 | offeredCourse: Types.ObjectId; 18 | course: Types.ObjectId; 19 | student: Types.ObjectId; 20 | faculty: Types.ObjectId; 21 | isEnrolled: boolean; 22 | courseMarks: TEnrolledCourseMarks; 23 | grade: TGrade; 24 | gradePoints: number; 25 | isCompleted: boolean; 26 | }; 27 | -------------------------------------------------------------------------------- /src/app/modules/EnrolledCourse/enrolledCourse.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import { Grade } from './enrolledCourse.constant'; 3 | import { 4 | TEnrolledCourse, 5 | TEnrolledCourseMarks, 6 | } from './enrolledCourse.interface'; 7 | 8 | const courseMarksSchema = new Schema( 9 | { 10 | classTest1: { 11 | type: Number, 12 | min: 0, 13 | max: 10, 14 | default: 0, 15 | }, 16 | midTerm: { 17 | type: Number, 18 | min: 0, 19 | max: 30, 20 | default: 0, 21 | }, 22 | classTest2: { 23 | type: Number, 24 | min: 0, 25 | max: 10, 26 | default: 0, 27 | }, 28 | finalTerm: { 29 | type: Number, 30 | min: 0, 31 | max: 50, 32 | default: 0, 33 | }, 34 | }, 35 | { 36 | _id: false, 37 | }, 38 | ); 39 | 40 | const enrolledCourseSchema = new Schema({ 41 | semesterRegistration: { 42 | type: Schema.Types.ObjectId, 43 | ref: 'SemesterRegistration', 44 | required: true, 45 | }, 46 | academicSemester: { 47 | type: Schema.Types.ObjectId, 48 | ref: 'AcademicSemester', 49 | required: true, 50 | }, 51 | academicFaculty: { 52 | type: Schema.Types.ObjectId, 53 | ref: 'AcademicFaculty', 54 | required: true, 55 | }, 56 | academicDepartment: { 57 | type: Schema.Types.ObjectId, 58 | ref: 'AcademicDepartment', 59 | required: true, 60 | }, 61 | offeredCourse: { 62 | type: Schema.Types.ObjectId, 63 | ref: 'OfferedCourse', 64 | required: true, 65 | }, 66 | course: { 67 | type: Schema.Types.ObjectId, 68 | ref: 'Course', 69 | required: true, 70 | }, 71 | student: { 72 | type: Schema.Types.ObjectId, 73 | ref: 'Student', 74 | required: true, 75 | }, 76 | faculty: { 77 | type: Schema.Types.ObjectId, 78 | ref: 'Faculty', 79 | required: true, 80 | }, 81 | isEnrolled: { 82 | type: Boolean, 83 | default: false, 84 | }, 85 | courseMarks: { 86 | type: courseMarksSchema, 87 | default: {}, 88 | }, 89 | grade: { 90 | type: String, 91 | enum: Grade, 92 | default: 'NA', 93 | }, 94 | gradePoints: { 95 | type: Number, 96 | min: 0, 97 | max: 4, 98 | default: 0, 99 | }, 100 | isCompleted: { 101 | type: Boolean, 102 | default: false, 103 | }, 104 | }); 105 | 106 | const EnrolledCourse = mongoose.model( 107 | 'EnrolledCourse', 108 | enrolledCourseSchema, 109 | ); 110 | 111 | export default EnrolledCourse; 112 | -------------------------------------------------------------------------------- /src/app/modules/EnrolledCourse/enrolledCourse.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import auth from '../../middlewares/auth'; 3 | import validateRequest from '../../middlewares/validateRequest'; 4 | 5 | import { USER_ROLE } from '../User/user.constant'; 6 | import { EnrolledCourseControllers } from './enrolledCourse.controller'; 7 | import { EnrolledCourseValidations } from './enrolledCourse.validaton'; 8 | 9 | const router = express.Router(); 10 | 11 | router.post( 12 | '/create-enrolled-course', 13 | auth(USER_ROLE.student), 14 | validateRequest( 15 | EnrolledCourseValidations.createEnrolledCourseValidationZodSchema, 16 | ), 17 | EnrolledCourseControllers.createEnrolledCourse, 18 | ); 19 | 20 | router.get( 21 | '/', 22 | auth(USER_ROLE.faculty), 23 | EnrolledCourseControllers.getAllEnrolledCourses, 24 | ); 25 | 26 | router.get( 27 | '/my-enrolled-courses', 28 | auth(USER_ROLE.student), 29 | EnrolledCourseControllers.getMyEnrolledCourses, 30 | ); 31 | 32 | router.patch( 33 | '/update-enrolled-course-marks', 34 | auth(USER_ROLE.superAdmin, USER_ROLE.admin, USER_ROLE.faculty), 35 | validateRequest( 36 | EnrolledCourseValidations.updateEnrolledCourseMarksValidationZodSchema, 37 | ), 38 | EnrolledCourseControllers.updateEnrolledCourseMarks, 39 | ); 40 | 41 | export const EnrolledCourseRoutes = router; 42 | -------------------------------------------------------------------------------- /src/app/modules/EnrolledCourse/enrolledCourse.service.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import httpStatus from 'http-status'; 3 | import mongoose from 'mongoose'; 4 | import QueryBuilder from '../../builder/QueryBuilder'; 5 | import AppError from '../../errors/AppError'; 6 | import { Course } from '../Course/course.model'; 7 | import { Faculty } from '../Faculty/faculty.model'; 8 | import { OfferedCourse } from '../OfferedCourse/OfferedCourse.model'; 9 | import { SemesterRegistration } from '../SemesterRegistration/semesterRegistration.model'; 10 | import { Student } from '../Student/student.model'; 11 | import { TEnrolledCourse } from './enrolledCourse.interface'; 12 | import EnrolledCourse from './enrolledCourse.model'; 13 | import { calculateGradeAndPoints } from './enrolledCourse.utils'; 14 | 15 | const createEnrolledCourseIntoDB = async ( 16 | userId: string, 17 | payload: TEnrolledCourse, 18 | ) => { 19 | /** 20 | * Step1: Check if the offered cousres is exists 21 | * Step2: Check if the student is already enrolled 22 | * Step3: Check if the max credits exceed 23 | * Step4: Create an enrolled course 24 | */ 25 | 26 | const { offeredCourse } = payload; 27 | 28 | const isOfferedCourseExists = await OfferedCourse.findById(offeredCourse); 29 | 30 | if (!isOfferedCourseExists) { 31 | throw new AppError(httpStatus.NOT_FOUND, 'Offered course not found !'); 32 | } 33 | 34 | if (isOfferedCourseExists.maxCapacity <= 0) { 35 | throw new AppError(httpStatus.BAD_GATEWAY, 'Room is full !'); 36 | } 37 | 38 | const student = await Student.findOne({ id: userId }, { _id: 1 }); 39 | 40 | if (!student) { 41 | throw new AppError(httpStatus.NOT_FOUND, 'Student not found !'); 42 | } 43 | const isStudentAlreadyEnrolled = await EnrolledCourse.findOne({ 44 | semesterRegistration: isOfferedCourseExists?.semesterRegistration, 45 | offeredCourse, 46 | student: student._id, 47 | }); 48 | 49 | if (isStudentAlreadyEnrolled) { 50 | throw new AppError(httpStatus.CONFLICT, 'Student is already enrolled !'); 51 | } 52 | 53 | // check total credits exceeds maxCredit 54 | const course = await Course.findById(isOfferedCourseExists.course); 55 | const currentCredit = course?.credits; 56 | 57 | const semesterRegistration = await SemesterRegistration.findById( 58 | isOfferedCourseExists.semesterRegistration, 59 | ).select('maxCredit'); 60 | 61 | const maxCredit = semesterRegistration?.maxCredit; 62 | 63 | const enrolledCourses = await EnrolledCourse.aggregate([ 64 | { 65 | $match: { 66 | semesterRegistration: isOfferedCourseExists.semesterRegistration, 67 | student: student._id, 68 | }, 69 | }, 70 | { 71 | $lookup: { 72 | from: 'courses', 73 | localField: 'course', 74 | foreignField: '_id', 75 | as: 'enrolledCourseData', 76 | }, 77 | }, 78 | { 79 | $unwind: '$enrolledCourseData', 80 | }, 81 | { 82 | $group: { 83 | _id: null, 84 | totalEnrolledCredits: { $sum: '$enrolledCourseData.credits' }, 85 | }, 86 | }, 87 | { 88 | $project: { 89 | _id: 0, 90 | totalEnrolledCredits: 1, 91 | }, 92 | }, 93 | ]); 94 | 95 | // total enrolled credits + new enrolled course credit > maxCredit 96 | const totalCredits = 97 | enrolledCourses.length > 0 ? enrolledCourses[0].totalEnrolledCredits : 0; 98 | 99 | if (totalCredits && maxCredit && totalCredits + currentCredit > maxCredit) { 100 | throw new AppError( 101 | httpStatus.BAD_REQUEST, 102 | 'You have exceeded maximum number of credits !', 103 | ); 104 | } 105 | 106 | const session = await mongoose.startSession(); 107 | 108 | try { 109 | session.startTransaction(); 110 | 111 | const result = await EnrolledCourse.create( 112 | [ 113 | { 114 | semesterRegistration: isOfferedCourseExists.semesterRegistration, 115 | academicSemester: isOfferedCourseExists.academicSemester, 116 | academicFaculty: isOfferedCourseExists.academicFaculty, 117 | academicDepartment: isOfferedCourseExists.academicDepartment, 118 | offeredCourse: offeredCourse, 119 | course: isOfferedCourseExists.course, 120 | student: student._id, 121 | faculty: isOfferedCourseExists.faculty, 122 | isEnrolled: true, 123 | }, 124 | ], 125 | { session }, 126 | ); 127 | 128 | if (!result) { 129 | throw new AppError( 130 | httpStatus.BAD_REQUEST, 131 | 'Failed to enroll in this cousre !', 132 | ); 133 | } 134 | 135 | const maxCapacity = isOfferedCourseExists.maxCapacity; 136 | await OfferedCourse.findByIdAndUpdate(offeredCourse, { 137 | maxCapacity: maxCapacity - 1, 138 | }); 139 | 140 | await session.commitTransaction(); 141 | await session.endSession(); 142 | 143 | return result; 144 | } catch (err: any) { 145 | await session.abortTransaction(); 146 | await session.endSession(); 147 | throw new Error(err); 148 | } 149 | }; 150 | 151 | const getAllEnrolledCoursesFromDB = async ( 152 | facultyId: string, 153 | query: Record, 154 | ) => { 155 | const faculty = await Faculty.findOne({ id: facultyId }); 156 | 157 | if (!faculty) { 158 | throw new AppError(httpStatus.NOT_FOUND, 'Faculty not found !'); 159 | } 160 | 161 | const enrolledCourseQuery = new QueryBuilder( 162 | EnrolledCourse.find({ 163 | faculty: faculty._id, 164 | }).populate( 165 | 'semesterRegistration academicSemester academicFaculty academicDepartment offeredCourse course student faculty', 166 | ), 167 | query, 168 | ) 169 | .filter() 170 | .sort() 171 | .paginate() 172 | .fields(); 173 | 174 | const result = await enrolledCourseQuery.modelQuery; 175 | const meta = await enrolledCourseQuery.countTotal(); 176 | 177 | return { 178 | meta, 179 | result, 180 | }; 181 | }; 182 | 183 | const getMyEnrolledCoursesFromDB = async ( 184 | studentId: string, 185 | query: Record, 186 | ) => { 187 | const student = await Student.findOne({ id: studentId }); 188 | 189 | if (!student) { 190 | throw new AppError(httpStatus.NOT_FOUND, 'Student not found !'); 191 | } 192 | 193 | const enrolledCourseQuery = new QueryBuilder( 194 | EnrolledCourse.find({ student: student._id }).populate( 195 | 'semesterRegistration academicSemester academicFaculty academicDepartment offeredCourse course student faculty', 196 | ), 197 | query, 198 | ) 199 | .filter() 200 | .sort() 201 | .paginate() 202 | .fields(); 203 | 204 | const result = await enrolledCourseQuery.modelQuery; 205 | const meta = await enrolledCourseQuery.countTotal(); 206 | 207 | return { 208 | meta, 209 | result, 210 | }; 211 | }; 212 | 213 | const updateEnrolledCourseMarksIntoDB = async ( 214 | facultyId: string, 215 | payload: Partial, 216 | ) => { 217 | const { semesterRegistration, offeredCourse, student, courseMarks } = payload; 218 | 219 | const isSemesterRegistrationExists = 220 | await SemesterRegistration.findById(semesterRegistration); 221 | 222 | if (!isSemesterRegistrationExists) { 223 | throw new AppError( 224 | httpStatus.NOT_FOUND, 225 | 'Semester registration not found !', 226 | ); 227 | } 228 | 229 | const isOfferedCourseExists = await OfferedCourse.findById(offeredCourse); 230 | 231 | if (!isOfferedCourseExists) { 232 | throw new AppError(httpStatus.NOT_FOUND, 'Offered course not found !'); 233 | } 234 | const isStudentExists = await Student.findById(student); 235 | 236 | if (!isStudentExists) { 237 | throw new AppError(httpStatus.NOT_FOUND, 'Student not found !'); 238 | } 239 | 240 | const faculty = await Faculty.findOne({ id: facultyId }, { _id: 1 }); 241 | 242 | if (!faculty) { 243 | throw new AppError(httpStatus.NOT_FOUND, 'Faculty not found !'); 244 | } 245 | 246 | const isCourseBelongToFaculty = await EnrolledCourse.findOne({ 247 | semesterRegistration, 248 | offeredCourse, 249 | student, 250 | faculty: faculty._id, 251 | }); 252 | 253 | if (!isCourseBelongToFaculty) { 254 | throw new AppError(httpStatus.FORBIDDEN, 'You are forbidden! !'); 255 | } 256 | 257 | const modifiedData: Record = { 258 | ...courseMarks, 259 | }; 260 | 261 | if (courseMarks?.finalTerm) { 262 | const { classTest1, classTest2, midTerm, finalTerm } = 263 | isCourseBelongToFaculty.courseMarks; 264 | 265 | const totalMarks = 266 | Math.ceil(classTest1) + 267 | Math.ceil(midTerm) + 268 | Math.ceil(classTest2) + 269 | Math.ceil(finalTerm); 270 | 271 | const result = calculateGradeAndPoints(totalMarks); 272 | 273 | modifiedData.grade = result.grade; 274 | modifiedData.gradePoints = result.gradePoints; 275 | modifiedData.isCompleted = true; 276 | } 277 | 278 | if (courseMarks && Object.keys(courseMarks).length) { 279 | for (const [key, value] of Object.entries(courseMarks)) { 280 | modifiedData[`courseMarks.${key}`] = value; 281 | } 282 | } 283 | 284 | const result = await EnrolledCourse.findByIdAndUpdate( 285 | isCourseBelongToFaculty._id, 286 | modifiedData, 287 | { 288 | new: true, 289 | }, 290 | ); 291 | 292 | return result; 293 | }; 294 | 295 | export const EnrolledCourseServices = { 296 | createEnrolledCourseIntoDB, 297 | getAllEnrolledCoursesFromDB, 298 | getMyEnrolledCoursesFromDB, 299 | updateEnrolledCourseMarksIntoDB, 300 | }; 301 | -------------------------------------------------------------------------------- /src/app/modules/EnrolledCourse/enrolledCourse.utils.ts: -------------------------------------------------------------------------------- 1 | export const calculateGradeAndPoints = (totalMarks: number) => { 2 | let result = { 3 | grade: 'NA', 4 | gradePoints: 0, 5 | }; 6 | 7 | /** 8 | * 0-19 F 9 | * 20-39 D 10 | * 40-59 C 11 | * 60-79 B 12 | * 80-100 A 13 | */ 14 | if (totalMarks >= 0 && totalMarks <= 19) { 15 | result = { 16 | grade: 'F', 17 | gradePoints: 0.0, 18 | }; 19 | } else if (totalMarks >= 20 && totalMarks <= 39) { 20 | result = { 21 | grade: 'D', 22 | gradePoints: 2.0, 23 | }; 24 | } else if (totalMarks >= 40 && totalMarks <= 59) { 25 | result = { 26 | grade: 'C', 27 | gradePoints: 3.0, 28 | }; 29 | } else if (totalMarks >= 60 && totalMarks <= 79) { 30 | result = { 31 | grade: 'B', 32 | gradePoints: 3.5, 33 | }; 34 | } else if (totalMarks >= 80 && totalMarks <= 100) { 35 | result = { 36 | grade: 'A', 37 | gradePoints: 4.0, 38 | }; 39 | } else { 40 | result = { 41 | grade: 'NA', 42 | gradePoints: 0, 43 | }; 44 | } 45 | 46 | return result; 47 | }; 48 | -------------------------------------------------------------------------------- /src/app/modules/EnrolledCourse/enrolledCourse.validaton.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const createEnrolledCourseValidationZodSchema = z.object({ 4 | body: z.object({ 5 | offeredCourse: z.string(), 6 | }), 7 | }); 8 | 9 | const updateEnrolledCourseMarksValidationZodSchema = z.object({ 10 | body: z.object({ 11 | semesterRegistration: z.string(), 12 | offeredCourse: z.string(), 13 | student: z.string(), 14 | courseMarks: z.object({ 15 | classTest1: z.number().optional(), 16 | midTerm: z.number().optional(), 17 | classTest2: z.number().optional(), 18 | finalTerm: z.number().optional(), 19 | }), 20 | }), 21 | }); 22 | 23 | export const EnrolledCourseValidations = { 24 | createEnrolledCourseValidationZodSchema, 25 | updateEnrolledCourseMarksValidationZodSchema, 26 | }; 27 | -------------------------------------------------------------------------------- /src/app/modules/Faculty/faculty.constant.ts: -------------------------------------------------------------------------------- 1 | import { TBloodGroup, TGender } from './faculty.interface'; 2 | 3 | export const Gender: TGender[] = ['male', 'female', 'other']; 4 | 5 | export const BloodGroup: TBloodGroup[] = [ 6 | 'A+', 7 | 'A-', 8 | 'B+', 9 | 'B-', 10 | 'AB+', 11 | 'AB-', 12 | 'O+', 13 | 'O-', 14 | ]; 15 | 16 | export const FacultySearchableFields = [ 17 | 'email', 18 | 'id', 19 | 'contactNo', 20 | 'emergencyContactNo', 21 | 'name.firstName', 22 | 'name.lastName', 23 | 'name.middleName', 24 | ]; 25 | -------------------------------------------------------------------------------- /src/app/modules/Faculty/faculty.controller.ts: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import catchAsync from '../../utils/catchAsync'; 3 | import sendResponse from '../../utils/sendResponse'; 4 | import { FacultyServices } from './faculty.service'; 5 | 6 | const getSingleFaculty = catchAsync(async (req, res) => { 7 | const { id } = req.params; 8 | const result = await FacultyServices.getSingleFacultyFromDB(id); 9 | 10 | sendResponse(res, { 11 | statusCode: httpStatus.OK, 12 | success: true, 13 | message: 'Faculty is retrieved successfully', 14 | data: result, 15 | }); 16 | }); 17 | 18 | const getAllFaculties = catchAsync(async (req, res) => { 19 | const result = await FacultyServices.getAllFacultiesFromDB(req.query); 20 | 21 | sendResponse(res, { 22 | statusCode: httpStatus.OK, 23 | success: true, 24 | message: 'Faculties are retrieved successfully', 25 | meta: result.meta, 26 | data: result.result, 27 | }); 28 | }); 29 | 30 | const updateFaculty = catchAsync(async (req, res) => { 31 | const { id } = req.params; 32 | const { faculty } = req.body; 33 | const result = await FacultyServices.updateFacultyIntoDB(id, faculty); 34 | 35 | sendResponse(res, { 36 | statusCode: httpStatus.OK, 37 | success: true, 38 | message: 'Faculty is updated successfully', 39 | data: result, 40 | }); 41 | }); 42 | 43 | const deleteFaculty = catchAsync(async (req, res) => { 44 | const { id } = req.params; 45 | const result = await FacultyServices.deleteFacultyFromDB(id); 46 | 47 | sendResponse(res, { 48 | statusCode: httpStatus.OK, 49 | success: true, 50 | message: 'Faculty is deleted successfully', 51 | data: result, 52 | }); 53 | }); 54 | 55 | export const FacultyControllers = { 56 | getAllFaculties, 57 | getSingleFaculty, 58 | deleteFaculty, 59 | updateFaculty, 60 | }; 61 | -------------------------------------------------------------------------------- /src/app/modules/Faculty/faculty.interface.ts: -------------------------------------------------------------------------------- 1 | import { Model, Types } from 'mongoose'; 2 | 3 | export type TGender = 'male' | 'female' | 'other'; 4 | export type TBloodGroup = 5 | | 'A+' 6 | | 'A-' 7 | | 'B+' 8 | | 'B-' 9 | | 'AB+' 10 | | 'AB-' 11 | | 'O+' 12 | | 'O-'; 13 | 14 | export type TUserName = { 15 | firstName: string; 16 | middleName: string; 17 | lastName: string; 18 | }; 19 | 20 | export type TFaculty = { 21 | id: string; 22 | user: Types.ObjectId; 23 | designation: string; 24 | name: TUserName; 25 | gender: TGender; 26 | dateOfBirth?: Date; 27 | email: string; 28 | contactNo: string; 29 | emergencyContactNo: string; 30 | bloogGroup?: TBloodGroup; 31 | presentAddress: string; 32 | permanentAddress: string; 33 | profileImg?: string; 34 | academicDepartment: Types.ObjectId; 35 | academicFaculty: Types.ObjectId; 36 | isDeleted: boolean; 37 | }; 38 | 39 | export interface FacultyModel extends Model { 40 | // eslint-disable-next-line no-unused-vars 41 | isUserExists(id: string): Promise; 42 | } 43 | -------------------------------------------------------------------------------- /src/app/modules/Faculty/faculty.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose'; 2 | import { BloodGroup, Gender } from './faculty.constant'; 3 | import { FacultyModel, TFaculty, TUserName } from './faculty.interface'; 4 | 5 | const userNameSchema = new Schema({ 6 | firstName: { 7 | type: String, 8 | required: [true, 'First Name is required'], 9 | trim: true, 10 | maxlength: [20, 'Name can not be more than 20 characters'], 11 | }, 12 | middleName: { 13 | type: String, 14 | trim: true, 15 | }, 16 | lastName: { 17 | type: String, 18 | trim: true, 19 | required: [true, 'Last Name is required'], 20 | maxlength: [20, 'Name can not be more than 20 characters'], 21 | }, 22 | }); 23 | 24 | const facultySchema = new Schema( 25 | { 26 | id: { 27 | type: String, 28 | required: [true, 'ID is required'], 29 | unique: true, 30 | }, 31 | user: { 32 | type: Schema.Types.ObjectId, 33 | required: [true, 'User id is required'], 34 | unique: true, 35 | ref: 'User', 36 | }, 37 | designation: { 38 | type: String, 39 | required: [true, 'Designation is required'], 40 | }, 41 | name: { 42 | type: userNameSchema, 43 | required: [true, 'Name is required'], 44 | }, 45 | gender: { 46 | type: String, 47 | enum: { 48 | values: Gender, 49 | message: '{VALUE} is not a valid gender', 50 | }, 51 | required: [true, 'Gender is required'], 52 | }, 53 | dateOfBirth: { type: Date }, 54 | email: { 55 | type: String, 56 | required: [true, 'Email is required'], 57 | unique: true, 58 | }, 59 | contactNo: { type: String, required: [true, 'Contact number is required'] }, 60 | emergencyContactNo: { 61 | type: String, 62 | required: [true, 'Emergency contact number is required'], 63 | }, 64 | bloogGroup: { 65 | type: String, 66 | enum: { 67 | values: BloodGroup, 68 | message: '{VALUE} is not a valid blood group', 69 | }, 70 | }, 71 | presentAddress: { 72 | type: String, 73 | required: [true, 'Present address is required'], 74 | }, 75 | permanentAddress: { 76 | type: String, 77 | required: [true, 'Permanent address is required'], 78 | }, 79 | profileImg: { type: String, default: '' }, 80 | academicDepartment: { 81 | type: Schema.Types.ObjectId, 82 | required: [true, 'Acadcemic Department is required'], 83 | ref: 'AcademicDepartment', 84 | }, 85 | academicFaculty: { 86 | type: Schema.Types.ObjectId, 87 | required: [true, 'Acadcemic Faculty is required'], 88 | ref: 'AcademicFaculty', 89 | }, 90 | isDeleted: { 91 | type: Boolean, 92 | default: false, 93 | }, 94 | }, 95 | { 96 | toJSON: { 97 | virtuals: true, 98 | }, 99 | }, 100 | ); 101 | 102 | // generating full name 103 | facultySchema.virtual('fullName').get(function () { 104 | return ( 105 | this?.name?.firstName + 106 | '' + 107 | this?.name?.middleName + 108 | '' + 109 | this?.name?.lastName 110 | ); 111 | }); 112 | 113 | // filter out deleted documents 114 | facultySchema.pre('find', function (next) { 115 | this.find({ isDeleted: { $ne: true } }); 116 | next(); 117 | }); 118 | 119 | facultySchema.pre('findOne', function (next) { 120 | this.find({ isDeleted: { $ne: true } }); 121 | next(); 122 | }); 123 | 124 | facultySchema.pre('aggregate', function (next) { 125 | this.pipeline().unshift({ $match: { isDeleted: { $ne: true } } }); 126 | next(); 127 | }); 128 | 129 | //checking if user is already exist! 130 | facultySchema.statics.isUserExists = async function (id: string) { 131 | const existingUser = await Faculty.findOne({ id }); 132 | return existingUser; 133 | }; 134 | 135 | export const Faculty = model('Faculty', facultySchema); 136 | -------------------------------------------------------------------------------- /src/app/modules/Faculty/faculty.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import auth from '../../middlewares/auth'; 3 | import validateRequest from '../../middlewares/validateRequest'; 4 | import { USER_ROLE } from '../User/user.constant'; 5 | import { FacultyControllers } from './faculty.controller'; 6 | import { updateFacultyValidationSchema } from './faculty.validation'; 7 | 8 | const router = express.Router(); 9 | 10 | router.get( 11 | '/:id', 12 | auth(USER_ROLE.superAdmin, USER_ROLE.admin, USER_ROLE.faculty), 13 | FacultyControllers.getSingleFaculty, 14 | ); 15 | 16 | router.patch( 17 | '/:id', 18 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 19 | validateRequest(updateFacultyValidationSchema), 20 | FacultyControllers.updateFaculty, 21 | ); 22 | 23 | router.delete( 24 | '/:id', 25 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 26 | FacultyControllers.deleteFaculty, 27 | ); 28 | 29 | router.get( 30 | '/', 31 | auth(USER_ROLE.superAdmin, USER_ROLE.admin, USER_ROLE.faculty), 32 | FacultyControllers.getAllFaculties, 33 | ); 34 | 35 | export const FacultyRoutes = router; 36 | -------------------------------------------------------------------------------- /src/app/modules/Faculty/faculty.service.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import httpStatus from 'http-status'; 3 | import mongoose from 'mongoose'; 4 | import QueryBuilder from '../../builder/QueryBuilder'; 5 | import AppError from '../../errors/AppError'; 6 | import { User } from '../User/user.model'; 7 | import { FacultySearchableFields } from './faculty.constant'; 8 | import { TFaculty } from './faculty.interface'; 9 | import { Faculty } from './faculty.model'; 10 | 11 | const getAllFacultiesFromDB = async (query: Record) => { 12 | const facultyQuery = new QueryBuilder( 13 | Faculty.find().populate('academicDepartment academicFaculty'), 14 | query, 15 | ) 16 | .search(FacultySearchableFields) 17 | .filter() 18 | .sort() 19 | .paginate() 20 | .fields(); 21 | 22 | const result = await facultyQuery.modelQuery; 23 | const meta = await facultyQuery.countTotal(); 24 | return { 25 | meta, 26 | result, 27 | }; 28 | }; 29 | 30 | const getSingleFacultyFromDB = async (id: string) => { 31 | const result = await Faculty.findById(id).populate( 32 | 'academicDepartment academicFaculty', 33 | ); 34 | 35 | return result; 36 | }; 37 | 38 | const updateFacultyIntoDB = async (id: string, payload: Partial) => { 39 | const { name, ...remainingFacultyData } = payload; 40 | 41 | const modifiedUpdatedData: Record = { 42 | ...remainingFacultyData, 43 | }; 44 | 45 | if (name && Object.keys(name).length) { 46 | for (const [key, value] of Object.entries(name)) { 47 | modifiedUpdatedData[`name.${key}`] = value; 48 | } 49 | } 50 | 51 | const result = await Faculty.findByIdAndUpdate(id, modifiedUpdatedData, { 52 | new: true, 53 | runValidators: true, 54 | }); 55 | return result; 56 | }; 57 | 58 | const deleteFacultyFromDB = async (id: string) => { 59 | const session = await mongoose.startSession(); 60 | 61 | try { 62 | session.startTransaction(); 63 | 64 | const deletedFaculty = await Faculty.findByIdAndUpdate( 65 | id, 66 | { isDeleted: true }, 67 | { new: true, session }, 68 | ); 69 | 70 | if (!deletedFaculty) { 71 | throw new AppError(httpStatus.BAD_REQUEST, 'Failed to delete faculty'); 72 | } 73 | 74 | // get user _id from deletedFaculty 75 | const userId = deletedFaculty.user; 76 | 77 | const deletedUser = await User.findByIdAndUpdate( 78 | userId, 79 | { isDeleted: true }, 80 | { new: true, session }, 81 | ); 82 | 83 | if (!deletedUser) { 84 | throw new AppError(httpStatus.BAD_REQUEST, 'Failed to delete user'); 85 | } 86 | 87 | await session.commitTransaction(); 88 | await session.endSession(); 89 | 90 | return deletedFaculty; 91 | } catch (err: any) { 92 | await session.abortTransaction(); 93 | await session.endSession(); 94 | throw new Error(err); 95 | } 96 | }; 97 | 98 | export const FacultyServices = { 99 | getAllFacultiesFromDB, 100 | getSingleFacultyFromDB, 101 | updateFacultyIntoDB, 102 | deleteFacultyFromDB, 103 | }; 104 | -------------------------------------------------------------------------------- /src/app/modules/Faculty/faculty.validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { BloodGroup, Gender } from './faculty.constant'; 3 | 4 | const createUserNameValidationSchema = z.object({ 5 | firstName: z 6 | .string() 7 | .min(1) 8 | .max(20) 9 | .refine((value) => /^[A-Z]/.test(value), { 10 | message: 'First Name must start with a capital letter', 11 | }), 12 | middleName: z.string(), 13 | lastName: z.string(), 14 | }); 15 | 16 | export const createFacultyValidationSchema = z.object({ 17 | body: z.object({ 18 | password: z.string().max(20), 19 | faculty: z.object({ 20 | designation: z.string(), 21 | name: createUserNameValidationSchema, 22 | gender: z.enum([...Gender] as [string, ...string[]]), 23 | dateOfBirth: z.string().optional(), 24 | email: z.string().email(), 25 | contactNo: z.string(), 26 | emergencyContactNo: z.string(), 27 | bloogGroup: z.enum([...BloodGroup] as [string, ...string[]]), 28 | presentAddress: z.string(), 29 | permanentAddress: z.string(), 30 | academicDepartment: z.string(), 31 | // profileImg: z.string(), 32 | }), 33 | }), 34 | }); 35 | 36 | const updateUserNameValidationSchema = z.object({ 37 | firstName: z.string().min(1).max(20).optional(), 38 | middleName: z.string().optional(), 39 | lastName: z.string().optional(), 40 | }); 41 | 42 | export const updateFacultyValidationSchema = z.object({ 43 | body: z.object({ 44 | faculty: z.object({ 45 | designation: z.string().optional(), 46 | name: updateUserNameValidationSchema, 47 | gender: z.enum([...Gender] as [string, ...string[]]).optional(), 48 | dateOfBirth: z.string().optional(), 49 | email: z.string().email().optional(), 50 | contactNo: z.string().optional(), 51 | emergencyContactNo: z.string().optional(), 52 | bloogGroup: z.enum([...BloodGroup] as [string, ...string[]]).optional(), 53 | presentAddress: z.string().optional(), 54 | permanentAddress: z.string().optional(), 55 | // profileImg: z.string().optional(), 56 | academicDepartment: z.string().optional(), 57 | }), 58 | }), 59 | }); 60 | 61 | export const studentValidations = { 62 | createFacultyValidationSchema, 63 | updateFacultyValidationSchema, 64 | }; 65 | -------------------------------------------------------------------------------- /src/app/modules/OfferedCourse/OfferedCourse.constant.ts: -------------------------------------------------------------------------------- 1 | export const Days = ['Sat', 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri']; 2 | -------------------------------------------------------------------------------- /src/app/modules/OfferedCourse/OfferedCourse.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import httpStatus from 'http-status'; 3 | import catchAsync from '../../utils/catchAsync'; 4 | import sendResponse from '../../utils/sendResponse'; 5 | import { OfferedCourseServices } from './OfferedCourse.service'; 6 | 7 | const createOfferedCourse = catchAsync(async (req: Request, res: Response) => { 8 | const result = await OfferedCourseServices.createOfferedCourseIntoDB( 9 | req.body, 10 | ); 11 | sendResponse(res, { 12 | statusCode: httpStatus.OK, 13 | success: true, 14 | message: 'Offered Course is created successfully !', 15 | data: result, 16 | }); 17 | }); 18 | 19 | const getAllOfferedCourses = catchAsync(async (req: Request, res: Response) => { 20 | const result = await OfferedCourseServices.getAllOfferedCoursesFromDB( 21 | req.query, 22 | ); 23 | 24 | sendResponse(res, { 25 | statusCode: httpStatus.OK, 26 | success: true, 27 | message: 'OfferedCourses retrieved successfully !', 28 | meta: result.meta, 29 | data: result.result, 30 | }); 31 | }); 32 | 33 | const getMyOfferedCourses = catchAsync(async (req: Request, res: Response) => { 34 | const userId = req.user.userId; 35 | const result = await OfferedCourseServices.getMyOfferedCoursesFromDB( 36 | userId, 37 | req.query, 38 | ); 39 | 40 | sendResponse(res, { 41 | statusCode: httpStatus.OK, 42 | success: true, 43 | message: 'OfferedCourses retrieved successfully !', 44 | meta: result.meta, 45 | data: result.result, 46 | }); 47 | }); 48 | 49 | const getSingleOfferedCourses = catchAsync( 50 | async (req: Request, res: Response) => { 51 | const { id } = req.params; 52 | const result = await OfferedCourseServices.getSingleOfferedCourseFromDB(id); 53 | 54 | sendResponse(res, { 55 | statusCode: httpStatus.OK, 56 | success: true, 57 | message: 'OfferedCourse fetched successfully', 58 | data: result, 59 | }); 60 | }, 61 | ); 62 | 63 | const updateOfferedCourse = catchAsync(async (req: Request, res: Response) => { 64 | const { id } = req.params; 65 | 66 | const result = await OfferedCourseServices.updateOfferedCourseIntoDB( 67 | id, 68 | req.body, 69 | ); 70 | sendResponse(res, { 71 | statusCode: httpStatus.OK, 72 | success: true, 73 | message: 'OfferedCourse updated successfully', 74 | data: result, 75 | }); 76 | }); 77 | 78 | const deleteOfferedCourseFromDB = catchAsync( 79 | async (req: Request, res: Response) => { 80 | const { id } = req.params; 81 | const result = await OfferedCourseServices.deleteOfferedCourseFromDB(id); 82 | sendResponse(res, { 83 | statusCode: httpStatus.OK, 84 | success: true, 85 | message: 'OfferedCourse deleted successfully', 86 | data: result, 87 | }); 88 | }, 89 | ); 90 | 91 | export const OfferedCourseControllers = { 92 | createOfferedCourse, 93 | getAllOfferedCourses, 94 | getMyOfferedCourses, 95 | getSingleOfferedCourses, 96 | updateOfferedCourse, 97 | deleteOfferedCourseFromDB, 98 | }; 99 | -------------------------------------------------------------------------------- /src/app/modules/OfferedCourse/OfferedCourse.interface.ts: -------------------------------------------------------------------------------- 1 | import { Types } from 'mongoose'; 2 | 3 | export type TDays = 'Sat' | 'Sun' | 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri'; 4 | 5 | export type TOfferedCourse = { 6 | semesterRegistration: Types.ObjectId; 7 | academicSemester?: Types.ObjectId; 8 | academicFaculty: Types.ObjectId; 9 | academicDepartment: Types.ObjectId; 10 | course: Types.ObjectId; 11 | faculty: Types.ObjectId; 12 | maxCapacity: number; 13 | section: number; 14 | days: TDays[]; 15 | startTime: string; 16 | endTime: string; 17 | }; 18 | 19 | export type TSchedule = { 20 | days: TDays[]; 21 | startTime: string; 22 | endTime: string; 23 | }; 24 | -------------------------------------------------------------------------------- /src/app/modules/OfferedCourse/OfferedCourse.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import { Days } from './OfferedCourse.constant'; 3 | import { TOfferedCourse } from './OfferedCourse.interface'; 4 | 5 | const offeredCourseSchema = new mongoose.Schema( 6 | { 7 | semesterRegistration: { 8 | type: Schema.Types.ObjectId, 9 | required: true, 10 | ref: 'SemesterRegistration', 11 | }, 12 | academicSemester: { 13 | type: Schema.Types.ObjectId, 14 | required: true, 15 | ref: 'AcademicSemester', 16 | }, 17 | academicFaculty: { 18 | type: Schema.Types.ObjectId, 19 | required: true, 20 | ref: 'AcademicFaculty', 21 | }, 22 | academicDepartment: { 23 | type: Schema.Types.ObjectId, 24 | required: true, 25 | ref: 'AcademicDepartment', 26 | }, 27 | course: { 28 | type: Schema.Types.ObjectId, 29 | required: true, 30 | ref: 'Course', 31 | }, 32 | faculty: { 33 | type: Schema.Types.ObjectId, 34 | required: true, 35 | ref: 'Faculty', 36 | }, 37 | maxCapacity: { 38 | type: Number, 39 | required: true, 40 | }, 41 | section: { 42 | type: Number, 43 | required: true, 44 | }, 45 | days: [ 46 | { 47 | type: String, 48 | enum: Days, 49 | }, 50 | ], 51 | startTime: { 52 | type: String, 53 | required: true, 54 | }, 55 | endTime: { 56 | type: String, 57 | required: true, 58 | }, 59 | }, 60 | { 61 | timestamps: true, 62 | }, 63 | ); 64 | 65 | export const OfferedCourse = mongoose.model( 66 | 'OfferedCourse', 67 | offeredCourseSchema, 68 | ); 69 | -------------------------------------------------------------------------------- /src/app/modules/OfferedCourse/OfferedCourse.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import auth from '../../middlewares/auth'; 3 | import validateRequest from '../../middlewares/validateRequest'; 4 | import { USER_ROLE } from '../User/user.constant'; 5 | import { OfferedCourseControllers } from './OfferedCourse.controller'; 6 | import { OfferedCourseValidations } from './OfferedCourse.validation'; 7 | 8 | const router = express.Router(); 9 | 10 | router.get( 11 | '/', 12 | auth(USER_ROLE.superAdmin, USER_ROLE.admin, USER_ROLE.faculty), 13 | OfferedCourseControllers.getAllOfferedCourses, 14 | ); 15 | 16 | router.get( 17 | '/my-offered-courses', 18 | auth(USER_ROLE.student), 19 | OfferedCourseControllers.getMyOfferedCourses, 20 | ); 21 | 22 | router.get( 23 | '/:id', 24 | auth( 25 | USER_ROLE.superAdmin, 26 | USER_ROLE.admin, 27 | USER_ROLE.faculty, 28 | USER_ROLE.student, 29 | ), 30 | OfferedCourseControllers.getSingleOfferedCourses, 31 | ); 32 | 33 | router.post( 34 | '/create-offered-course', 35 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 36 | validateRequest(OfferedCourseValidations.createOfferedCourseValidationSchema), 37 | OfferedCourseControllers.createOfferedCourse, 38 | ); 39 | 40 | router.patch( 41 | '/:id', 42 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 43 | validateRequest(OfferedCourseValidations.updateOfferedCourseValidationSchema), 44 | OfferedCourseControllers.updateOfferedCourse, 45 | ); 46 | 47 | router.delete( 48 | '/:id', 49 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 50 | OfferedCourseControllers.deleteOfferedCourseFromDB, 51 | ); 52 | 53 | export const offeredCourseRoutes = router; 54 | -------------------------------------------------------------------------------- /src/app/modules/OfferedCourse/OfferedCourse.service.ts: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import QueryBuilder from '../../builder/QueryBuilder'; 3 | import AppError from '../../errors/AppError'; 4 | import { AcademicDepartment } from '../AcademicDepartment/academicDepartment.model'; 5 | import { AcademicFaculty } from '../AcademicFaculty/academicFaculty.model'; 6 | import { Course } from '../Course/course.model'; 7 | import { Faculty } from '../Faculty/faculty.model'; 8 | import { SemesterRegistration } from '../SemesterRegistration/semesterRegistration.model'; 9 | import { Student } from '../Student/student.model'; 10 | import { TOfferedCourse } from './OfferedCourse.interface'; 11 | import { OfferedCourse } from './OfferedCourse.model'; 12 | import { hasTimeConflict } from './OfferedCourse.utils'; 13 | 14 | const createOfferedCourseIntoDB = async (payload: TOfferedCourse) => { 15 | const { 16 | semesterRegistration, 17 | academicFaculty, 18 | academicDepartment, 19 | course, 20 | section, 21 | faculty, 22 | days, 23 | startTime, 24 | endTime, 25 | } = payload; 26 | 27 | /** 28 | * Step 1: check if the semester registration id is exists! 29 | * Step 2: check if the academic faculty id is exists! 30 | * Step 3: check if the academic department id is exists! 31 | * Step 4: check if the course id is exists! 32 | * Step 5: check if the faculty id is exists! 33 | * Step 6: check if the department is belong to the faculty 34 | * Step 7: check if the same offered course same section in same registered semester exists 35 | * Step 8: get the schedules of the faculties 36 | * Step 9: check if the faculty is available at that time. If not then throw error 37 | * Step 10: create the offered course 38 | */ 39 | 40 | //check if the semester registration id is exists! 41 | const isSemesterRegistrationExits = 42 | await SemesterRegistration.findById(semesterRegistration); 43 | 44 | if (!isSemesterRegistrationExits) { 45 | throw new AppError( 46 | httpStatus.NOT_FOUND, 47 | 'Semester registration not found !', 48 | ); 49 | } 50 | 51 | const academicSemester = isSemesterRegistrationExits.academicSemester; 52 | 53 | const isAcademicFacultyExits = 54 | await AcademicFaculty.findById(academicFaculty); 55 | 56 | if (!isAcademicFacultyExits) { 57 | throw new AppError(httpStatus.NOT_FOUND, 'Academic Faculty not found !'); 58 | } 59 | 60 | const isAcademicDepartmentExits = 61 | await AcademicDepartment.findById(academicDepartment); 62 | 63 | if (!isAcademicDepartmentExits) { 64 | throw new AppError(httpStatus.NOT_FOUND, 'Academic Department not found !'); 65 | } 66 | 67 | const isCourseExits = await Course.findById(course); 68 | 69 | if (!isCourseExits) { 70 | throw new AppError(httpStatus.NOT_FOUND, 'Course not found !'); 71 | } 72 | 73 | const isFacultyExits = await Faculty.findById(faculty); 74 | 75 | if (!isFacultyExits) { 76 | throw new AppError(httpStatus.NOT_FOUND, 'Faculty not found !'); 77 | } 78 | 79 | // check if the department is belong to the faculty 80 | const isDepartmentBelongToFaculty = await AcademicDepartment.findOne({ 81 | _id: academicDepartment, 82 | academicFaculty, 83 | }); 84 | 85 | if (!isDepartmentBelongToFaculty) { 86 | throw new AppError( 87 | httpStatus.BAD_REQUEST, 88 | `This ${isAcademicDepartmentExits.name} is not belong to this ${isAcademicFacultyExits.name}`, 89 | ); 90 | } 91 | 92 | // check if the same offered course same section in same registered semester exists 93 | 94 | const isSameOfferedCourseExistsWithSameRegisteredSemesterWithSameSection = 95 | await OfferedCourse.findOne({ 96 | semesterRegistration, 97 | course, 98 | section, 99 | }); 100 | 101 | if (isSameOfferedCourseExistsWithSameRegisteredSemesterWithSameSection) { 102 | throw new AppError( 103 | httpStatus.BAD_REQUEST, 104 | `Offered course with same section is already exist!`, 105 | ); 106 | } 107 | 108 | // get the schedules of the faculties 109 | const assignedSchedules = await OfferedCourse.find({ 110 | semesterRegistration, 111 | faculty, 112 | days: { $in: days }, 113 | }).select('days startTime endTime'); 114 | 115 | const newSchedule = { 116 | days, 117 | startTime, 118 | endTime, 119 | }; 120 | 121 | if (hasTimeConflict(assignedSchedules, newSchedule)) { 122 | throw new AppError( 123 | httpStatus.CONFLICT, 124 | `This faculty is not available at that time ! Choose other time or day`, 125 | ); 126 | } 127 | 128 | const result = await OfferedCourse.create({ 129 | ...payload, 130 | academicSemester, 131 | }); 132 | return result; 133 | }; 134 | 135 | const getAllOfferedCoursesFromDB = async (query: Record) => { 136 | const offeredCourseQuery = new QueryBuilder(OfferedCourse.find(), query) 137 | .filter() 138 | .sort() 139 | .paginate() 140 | .fields(); 141 | 142 | const result = await offeredCourseQuery.modelQuery; 143 | const meta = await offeredCourseQuery.countTotal(); 144 | 145 | return { 146 | meta, 147 | result, 148 | }; 149 | }; 150 | 151 | const getMyOfferedCoursesFromDB = async ( 152 | userId: string, 153 | query: Record, 154 | ) => { 155 | //pagination setup 156 | 157 | const page = Number(query?.page) || 1; 158 | const limit = Number(query?.limit) || 10; 159 | const skip = (page - 1) * limit; 160 | 161 | const student = await Student.findOne({ id: userId }); 162 | // find the student 163 | if (!student) { 164 | throw new AppError(httpStatus.NOT_FOUND, 'User is noty found'); 165 | } 166 | 167 | //find current ongoing semester 168 | const currentOngoingRegistrationSemester = await SemesterRegistration.findOne( 169 | { 170 | status: 'ONGOING', 171 | }, 172 | ); 173 | 174 | if (!currentOngoingRegistrationSemester) { 175 | throw new AppError( 176 | httpStatus.NOT_FOUND, 177 | 'There is no ongoing semester registration!', 178 | ); 179 | } 180 | 181 | const aggregationQuery = [ 182 | { 183 | $match: { 184 | semesterRegistration: currentOngoingRegistrationSemester?._id, 185 | academicFaculty: student.academicFaculty, 186 | academicDepartment: student.academicDepartment, 187 | }, 188 | }, 189 | { 190 | $lookup: { 191 | from: 'courses', 192 | localField: 'course', 193 | foreignField: '_id', 194 | as: 'course', 195 | }, 196 | }, 197 | { 198 | $unwind: '$course', 199 | }, 200 | { 201 | $lookup: { 202 | from: 'enrolledcourses', 203 | let: { 204 | currentOngoingRegistrationSemester: 205 | currentOngoingRegistrationSemester._id, 206 | currentStudent: student._id, 207 | }, 208 | pipeline: [ 209 | { 210 | $match: { 211 | $expr: { 212 | $and: [ 213 | { 214 | $eq: [ 215 | '$semesterRegistration', 216 | '$$currentOngoingRegistrationSemester', 217 | ], 218 | }, 219 | { 220 | $eq: ['$student', '$$currentStudent'], 221 | }, 222 | { 223 | $eq: ['$isEnrolled', true], 224 | }, 225 | ], 226 | }, 227 | }, 228 | }, 229 | ], 230 | as: 'enrolledCourses', 231 | }, 232 | }, 233 | { 234 | $lookup: { 235 | from: 'enrolledcourses', 236 | let: { 237 | currentStudent: student._id, 238 | }, 239 | pipeline: [ 240 | { 241 | $match: { 242 | $expr: { 243 | $and: [ 244 | { 245 | $eq: ['$student', '$$currentStudent'], 246 | }, 247 | { 248 | $eq: ['$isCompleted', true], 249 | }, 250 | ], 251 | }, 252 | }, 253 | }, 254 | ], 255 | as: 'completedCourses', 256 | }, 257 | }, 258 | { 259 | $addFields: { 260 | completedCourseIds: { 261 | $map: { 262 | input: '$completedCourses', 263 | as: 'completed', 264 | in: '$$completed.course', 265 | }, 266 | }, 267 | }, 268 | }, 269 | { 270 | $addFields: { 271 | isPreRequisitesFulFilled: { 272 | $or: [ 273 | { $eq: ['$course.preRequisiteCourses', []] }, 274 | { 275 | $setIsSubset: [ 276 | '$course.preRequisiteCourses.course', 277 | '$completedCourseIds', 278 | ], 279 | }, 280 | ], 281 | }, 282 | 283 | isAlreadyEnrolled: { 284 | $in: [ 285 | '$course._id', 286 | { 287 | $map: { 288 | input: '$enrolledCourses', 289 | as: 'enroll', 290 | in: '$$enroll.course', 291 | }, 292 | }, 293 | ], 294 | }, 295 | }, 296 | }, 297 | { 298 | $match: { 299 | isAlreadyEnrolled: false, 300 | isPreRequisitesFulFilled: true, 301 | }, 302 | }, 303 | ]; 304 | 305 | const paginationQuery = [ 306 | { 307 | $skip: skip, 308 | }, 309 | { 310 | $limit: limit, 311 | }, 312 | ]; 313 | 314 | const result = await OfferedCourse.aggregate([ 315 | ...aggregationQuery, 316 | ...paginationQuery, 317 | ]); 318 | 319 | const total = (await OfferedCourse.aggregate(aggregationQuery)).length; 320 | 321 | const totalPage = Math.ceil(result.length / limit); 322 | 323 | return { 324 | meta: { 325 | page, 326 | limit, 327 | total, 328 | totalPage, 329 | }, 330 | result, 331 | }; 332 | }; 333 | 334 | const getSingleOfferedCourseFromDB = async (id: string) => { 335 | const offeredCourse = await OfferedCourse.findById(id); 336 | 337 | if (!offeredCourse) { 338 | throw new AppError(404, 'Offered Course not found'); 339 | } 340 | 341 | return offeredCourse; 342 | }; 343 | 344 | const updateOfferedCourseIntoDB = async ( 345 | id: string, 346 | payload: Pick, 347 | ) => { 348 | /** 349 | * Step 1: check if the offered course exists 350 | * Step 2: check if the faculty exists 351 | * Step 3: check if the semester registration status is upcoming 352 | * Step 4: check if the faculty is available at that time. If not then throw error 353 | * Step 5: update the offered course 354 | */ 355 | const { faculty, days, startTime, endTime } = payload; 356 | 357 | const isOfferedCourseExists = await OfferedCourse.findById(id); 358 | 359 | if (!isOfferedCourseExists) { 360 | throw new AppError(httpStatus.NOT_FOUND, 'Offered course not found !'); 361 | } 362 | 363 | const isFacultyExists = await Faculty.findById(faculty); 364 | 365 | if (!isFacultyExists) { 366 | throw new AppError(httpStatus.NOT_FOUND, 'Faculty not found !'); 367 | } 368 | 369 | const semesterRegistration = isOfferedCourseExists.semesterRegistration; 370 | // get the schedules of the faculties 371 | 372 | // Checking the status of the semester registration 373 | const semesterRegistrationStatus = 374 | await SemesterRegistration.findById(semesterRegistration); 375 | 376 | if (semesterRegistrationStatus?.status !== 'UPCOMING') { 377 | throw new AppError( 378 | httpStatus.BAD_REQUEST, 379 | `You can not update this offered course as it is ${semesterRegistrationStatus?.status}`, 380 | ); 381 | } 382 | 383 | // check if the faculty is available at that time. 384 | const assignedSchedules = await OfferedCourse.find({ 385 | semesterRegistration, 386 | faculty, 387 | days: { $in: days }, 388 | }).select('days startTime endTime'); 389 | 390 | const newSchedule = { 391 | days, 392 | startTime, 393 | endTime, 394 | }; 395 | 396 | if (hasTimeConflict(assignedSchedules, newSchedule)) { 397 | throw new AppError( 398 | httpStatus.CONFLICT, 399 | `This faculty is not available at that time ! Choose other time or day`, 400 | ); 401 | } 402 | 403 | const result = await OfferedCourse.findByIdAndUpdate(id, payload, { 404 | new: true, 405 | }); 406 | return result; 407 | }; 408 | 409 | const deleteOfferedCourseFromDB = async (id: string) => { 410 | /** 411 | * Step 1: check if the offered course exists 412 | * Step 2: check if the semester registration status is upcoming 413 | * Step 3: delete the offered course 414 | */ 415 | const isOfferedCourseExists = await OfferedCourse.findById(id); 416 | 417 | if (!isOfferedCourseExists) { 418 | throw new AppError(httpStatus.NOT_FOUND, 'Offered Course not found'); 419 | } 420 | 421 | const semesterRegistation = isOfferedCourseExists.semesterRegistration; 422 | 423 | const semesterRegistrationStatus = 424 | await SemesterRegistration.findById(semesterRegistation).select('status'); 425 | 426 | if (semesterRegistrationStatus?.status !== 'UPCOMING') { 427 | throw new AppError( 428 | httpStatus.BAD_REQUEST, 429 | `Offered course can not update ! because the semester ${semesterRegistrationStatus}`, 430 | ); 431 | } 432 | 433 | const result = await OfferedCourse.findByIdAndDelete(id); 434 | 435 | return result; 436 | }; 437 | 438 | export const OfferedCourseServices = { 439 | createOfferedCourseIntoDB, 440 | getAllOfferedCoursesFromDB, 441 | getMyOfferedCoursesFromDB, 442 | getSingleOfferedCourseFromDB, 443 | deleteOfferedCourseFromDB, 444 | updateOfferedCourseIntoDB, 445 | }; 446 | -------------------------------------------------------------------------------- /src/app/modules/OfferedCourse/OfferedCourse.utils.ts: -------------------------------------------------------------------------------- 1 | import { TSchedule } from './OfferedCourse.interface'; 2 | 3 | export const hasTimeConflict = ( 4 | assignedSchedules: TSchedule[], 5 | newSchedule: TSchedule, 6 | ) => { 7 | for (const schedule of assignedSchedules) { 8 | const existingStartTime = new Date(`1970-01-01T${schedule.startTime}`); 9 | const existingEndTime = new Date(`1970-01-01T${schedule.endTime}`); 10 | const newStartTime = new Date(`1970-01-01T${newSchedule.startTime}`); 11 | const newEndTime = new Date(`1970-01-01T${newSchedule.endTime}`); 12 | 13 | // 10:30 - 12:30 14 | // 11:30 - 1.30 15 | if (newStartTime < existingEndTime && newEndTime > existingStartTime) { 16 | return true; 17 | } 18 | } 19 | 20 | return false; 21 | }; 22 | -------------------------------------------------------------------------------- /src/app/modules/OfferedCourse/OfferedCourse.validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { Days } from './OfferedCourse.constant'; 3 | 4 | const timeStringSchema = z.string().refine( 5 | (time) => { 6 | const regex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/; // 00-09 10-19 20-23 7 | return regex.test(time); 8 | }, 9 | { 10 | message: 'Invalid time format , expected "HH:MM" in 24 hours format', 11 | }, 12 | ); 13 | 14 | const createOfferedCourseValidationSchema = z.object({ 15 | body: z 16 | .object({ 17 | semesterRegistration: z.string(), 18 | academicFaculty: z.string(), 19 | academicDepartment: z.string(), 20 | course: z.string(), 21 | faculty: z.string(), 22 | section: z.number(), 23 | maxCapacity: z.number(), 24 | days: z.array(z.enum([...Days] as [string, ...string[]])), 25 | startTime: timeStringSchema, // HH: MM 00-23: 00-59 26 | endTime: timeStringSchema, 27 | }) 28 | .refine( 29 | (body) => { 30 | // startTime : 10:30 => 1970-01-01T10:30 31 | //endTime : 12:30 => 1970-01-01T12:30 32 | 33 | const start = new Date(`1970-01-01T${body.startTime}:00`); 34 | const end = new Date(`1970-01-01T${body.endTime}:00`); 35 | 36 | return end > start; 37 | }, 38 | { 39 | message: 'Start time should be before End time ! ', 40 | }, 41 | ), 42 | }); 43 | 44 | const updateOfferedCourseValidationSchema = z.object({ 45 | body: z 46 | .object({ 47 | faculty: z.string(), 48 | maxCapacity: z.number(), 49 | days: z.array(z.enum([...Days] as [string, ...string[]])), 50 | startTime: timeStringSchema, // HH: MM 00-23: 00-59 51 | endTime: timeStringSchema, 52 | }) 53 | .refine( 54 | (body) => { 55 | // startTime : 10:30 => 1970-01-01T10:30 56 | //endTime : 12:30 => 1970-01-01T12:30 57 | 58 | const start = new Date(`1970-01-01T${body.startTime}:00`); 59 | const end = new Date(`1970-01-01T${body.endTime}:00`); 60 | 61 | return end > start; 62 | }, 63 | { 64 | message: 'Start time should be before End time ! ', 65 | }, 66 | ), 67 | }); 68 | 69 | export const OfferedCourseValidations = { 70 | createOfferedCourseValidationSchema, 71 | updateOfferedCourseValidationSchema, 72 | }; 73 | -------------------------------------------------------------------------------- /src/app/modules/SemesterRegistration/semesterRegistration.constant.ts: -------------------------------------------------------------------------------- 1 | export const SemesterRegistrationStatus = ['UPCOMING', 'ONGOING', 'ENDED']; 2 | 3 | export const RegistrationStatus = { 4 | UPCOMING: 'UPCOMING', 5 | ONGOING: 'ONGOING', 6 | ENDED: 'ENDED', 7 | } as const; 8 | -------------------------------------------------------------------------------- /src/app/modules/SemesterRegistration/semesterRegistration.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import httpStatus from 'http-status'; 3 | import catchAsync from '../../utils/catchAsync'; 4 | import sendResponse from '../../utils/sendResponse'; 5 | import { SemesterRegistrationService } from './semesterRegistration.service'; 6 | 7 | const createSemesterRegistration = catchAsync( 8 | async (req: Request, res: Response) => { 9 | const result = 10 | await SemesterRegistrationService.createSemesterRegistrationIntoDB( 11 | req.body, 12 | ); 13 | 14 | sendResponse(res, { 15 | statusCode: httpStatus.OK, 16 | success: true, 17 | message: 'Semester Registration is created successfully!', 18 | data: result, 19 | }); 20 | }, 21 | ); 22 | 23 | const getAllSemesterRegistrations = catchAsync( 24 | async (req: Request, res: Response) => { 25 | const result = 26 | await SemesterRegistrationService.getAllSemesterRegistrationsFromDB( 27 | req.query, 28 | ); 29 | 30 | sendResponse(res, { 31 | statusCode: httpStatus.OK, 32 | success: true, 33 | message: 'Semester Registration is retrieved successfully !', 34 | meta: result.meta, 35 | data: result.result, 36 | }); 37 | }, 38 | ); 39 | 40 | const getSingleSemesterRegistration = catchAsync( 41 | async (req: Request, res: Response) => { 42 | const { id } = req.params; 43 | 44 | const result = 45 | await SemesterRegistrationService.getSingleSemesterRegistrationsFromDB( 46 | id, 47 | ); 48 | 49 | sendResponse(res, { 50 | statusCode: httpStatus.OK, 51 | success: true, 52 | message: 'Semester Registration is retrieved successfully', 53 | data: result, 54 | }); 55 | }, 56 | ); 57 | 58 | const updateSemesterRegistration = catchAsync( 59 | async (req: Request, res: Response) => { 60 | const { id } = req.params; 61 | const result = 62 | await SemesterRegistrationService.updateSemesterRegistrationIntoDB( 63 | id, 64 | req.body, 65 | ); 66 | 67 | sendResponse(res, { 68 | statusCode: httpStatus.OK, 69 | success: true, 70 | message: 'Semester Registration is updated successfully', 71 | data: result, 72 | }); 73 | }, 74 | ); 75 | 76 | const deleteSemesterRegistration = catchAsync( 77 | async (req: Request, res: Response) => { 78 | const { id } = req.params; 79 | const result = 80 | await SemesterRegistrationService.deleteSemesterRegistrationFromDB(id); 81 | 82 | sendResponse(res, { 83 | statusCode: httpStatus.OK, 84 | success: true, 85 | message: 'Semester Registration is updated successfully', 86 | data: result, 87 | }); 88 | }, 89 | ); 90 | 91 | export const SemesterRegistrationController = { 92 | createSemesterRegistration, 93 | getAllSemesterRegistrations, 94 | getSingleSemesterRegistration, 95 | updateSemesterRegistration, 96 | deleteSemesterRegistration, 97 | }; 98 | -------------------------------------------------------------------------------- /src/app/modules/SemesterRegistration/semesterRegistration.interface.ts: -------------------------------------------------------------------------------- 1 | import { Types } from 'mongoose'; 2 | 3 | export type TSemesterRegistration = { 4 | academicSemester: Types.ObjectId; 5 | status: 'UPCOMING' | 'ONGOING' | 'ENDED'; 6 | startDate: Date; 7 | endDate: Date; 8 | minCredit: number; 9 | maxCredit: number; 10 | }; 11 | -------------------------------------------------------------------------------- /src/app/modules/SemesterRegistration/semesterRegistration.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import { SemesterRegistrationStatus } from './semesterRegistration.constant'; 3 | import { TSemesterRegistration } from './semesterRegistration.interface'; 4 | 5 | const semesterRegistrationSchema = new mongoose.Schema( 6 | { 7 | academicSemester: { 8 | type: Schema.Types.ObjectId, 9 | required: true, 10 | unique: true, 11 | ref: 'AcademicSemester', 12 | }, 13 | status: { 14 | type: String, 15 | enum: SemesterRegistrationStatus, 16 | default: 'UPCOMING', 17 | }, 18 | startDate: { 19 | type: Date, 20 | required: true, 21 | }, 22 | endDate: { 23 | type: Date, 24 | required: true, 25 | }, 26 | minCredit: { 27 | type: Number, 28 | default: 3, 29 | }, 30 | maxCredit: { 31 | type: Number, 32 | default: 15, 33 | }, 34 | }, 35 | { 36 | timestamps: true, 37 | }, 38 | ); 39 | 40 | export const SemesterRegistration = mongoose.model( 41 | 'SemesterRegistration', 42 | semesterRegistrationSchema, 43 | ); 44 | -------------------------------------------------------------------------------- /src/app/modules/SemesterRegistration/semesterRegistration.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import auth from '../../middlewares/auth'; 3 | import validateRequest from '../../middlewares/validateRequest'; 4 | import { USER_ROLE } from '../User/user.constant'; 5 | import { SemesterRegistrationController } from './semesterRegistration.controller'; 6 | import { SemesterRegistrationValidations } from './semesterRegistration.validation'; 7 | 8 | const router = express.Router(); 9 | 10 | router.post( 11 | '/create-semester-registration', 12 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 13 | validateRequest( 14 | SemesterRegistrationValidations.createSemesterRegistrationValidationSchema, 15 | ), 16 | SemesterRegistrationController.createSemesterRegistration, 17 | ); 18 | 19 | router.get( 20 | '/:id', 21 | auth( 22 | USER_ROLE.superAdmin, 23 | USER_ROLE.admin, 24 | USER_ROLE.faculty, 25 | USER_ROLE.student, 26 | ), 27 | SemesterRegistrationController.getSingleSemesterRegistration, 28 | ); 29 | 30 | router.patch( 31 | '/:id', 32 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 33 | validateRequest( 34 | SemesterRegistrationValidations.upadateSemesterRegistrationValidationSchema, 35 | ), 36 | SemesterRegistrationController.updateSemesterRegistration, 37 | ); 38 | 39 | router.delete( 40 | '/:id', 41 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 42 | SemesterRegistrationController.deleteSemesterRegistration, 43 | ); 44 | 45 | router.get( 46 | '/', 47 | auth( 48 | USER_ROLE.superAdmin, 49 | USER_ROLE.admin, 50 | USER_ROLE.faculty, 51 | USER_ROLE.student, 52 | ), 53 | SemesterRegistrationController.getAllSemesterRegistrations, 54 | ); 55 | 56 | export const semesterRegistrationRoutes = router; 57 | -------------------------------------------------------------------------------- /src/app/modules/SemesterRegistration/semesterRegistration.service.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import httpStatus from 'http-status'; 3 | import mongoose from 'mongoose'; 4 | import QueryBuilder from '../../builder/QueryBuilder'; 5 | import AppError from '../../errors/AppError'; 6 | import { AcademicSemester } from '../AcademicSemester/academicSemester.model'; 7 | import { OfferedCourse } from '../OfferedCourse/OfferedCourse.model'; 8 | import { RegistrationStatus } from './semesterRegistration.constant'; 9 | import { TSemesterRegistration } from './semesterRegistration.interface'; 10 | import { SemesterRegistration } from './semesterRegistration.model'; 11 | 12 | const createSemesterRegistrationIntoDB = async ( 13 | payload: TSemesterRegistration, 14 | ) => { 15 | /** 16 | * Step1: Check if there any registered semester that is already 'UPCOMING'|'ONGOING' 17 | * Step2: Check if the semester is exist 18 | * Step3: Check if the semester is already registered! 19 | * Step4: Create the semester registration 20 | */ 21 | 22 | const academicSemester = payload?.academicSemester; 23 | 24 | //check if there any registered semester that is already 'UPCOMING'|'ONGOING' 25 | const isThereAnyUpcomingOrOngoingSEmester = 26 | await SemesterRegistration.findOne({ 27 | $or: [ 28 | { status: RegistrationStatus.UPCOMING }, 29 | { status: RegistrationStatus.ONGOING }, 30 | ], 31 | }); 32 | 33 | if (isThereAnyUpcomingOrOngoingSEmester) { 34 | throw new AppError( 35 | httpStatus.BAD_REQUEST, 36 | `There is aready an ${isThereAnyUpcomingOrOngoingSEmester.status} registered semester !`, 37 | ); 38 | } 39 | // check if the semester is exist 40 | const isAcademicSemesterExists = 41 | await AcademicSemester.findById(academicSemester); 42 | 43 | if (!isAcademicSemesterExists) { 44 | throw new AppError( 45 | httpStatus.NOT_FOUND, 46 | 'This academic semester not found !', 47 | ); 48 | } 49 | 50 | // check if the semester is already registered! 51 | const isSemesterRegistrationExists = await SemesterRegistration.findOne({ 52 | academicSemester, 53 | }); 54 | 55 | if (isSemesterRegistrationExists) { 56 | throw new AppError( 57 | httpStatus.CONFLICT, 58 | 'This semester is already registered!', 59 | ); 60 | } 61 | 62 | const result = await SemesterRegistration.create(payload); 63 | return result; 64 | }; 65 | 66 | const getAllSemesterRegistrationsFromDB = async ( 67 | query: Record, 68 | ) => { 69 | const semesterRegistrationQuery = new QueryBuilder( 70 | SemesterRegistration.find().populate('academicSemester'), 71 | query, 72 | ) 73 | .filter() 74 | .sort() 75 | .paginate() 76 | .fields(); 77 | 78 | const result = await semesterRegistrationQuery.modelQuery; 79 | const meta = await semesterRegistrationQuery.countTotal(); 80 | return { 81 | result, 82 | meta, 83 | }; 84 | }; 85 | 86 | const getSingleSemesterRegistrationsFromDB = async (id: string) => { 87 | const result = 88 | await SemesterRegistration.findById(id).populate('academicSemester'); 89 | 90 | return result; 91 | }; 92 | 93 | const updateSemesterRegistrationIntoDB = async ( 94 | id: string, 95 | payload: Partial, 96 | ) => { 97 | /** 98 | * Step1: Check if the semester is exist 99 | * Step2: Check if the requested registered semester is exists 100 | * Step3: If the requested semester registration is ended, we will not update anything 101 | * Step4: If the requested semester registration is 'UPCOMING', we will let update everything. 102 | * Step5: If the requested semester registration is 'ONGOING', we will not update anything except status to 'ENDED' 103 | * Step6: If the requested semester registration is 'ENDED' , we will not update anything 104 | * 105 | * UPCOMING --> ONGOING --> ENDED 106 | * 107 | */ 108 | 109 | // check if the requested registered semester is exists 110 | // check if the semester is already registered! 111 | const isSemesterRegistrationExists = await SemesterRegistration.findById(id); 112 | 113 | if (!isSemesterRegistrationExists) { 114 | throw new AppError(httpStatus.NOT_FOUND, 'This semester is not found !'); 115 | } 116 | 117 | //if the requested semester registration is ended , we will not update anything 118 | const currentSemesterStatus = isSemesterRegistrationExists?.status; 119 | const requestedStatus = payload?.status; 120 | 121 | if (currentSemesterStatus === RegistrationStatus.ENDED) { 122 | throw new AppError( 123 | httpStatus.BAD_REQUEST, 124 | `This semester is already ${currentSemesterStatus}`, 125 | ); 126 | } 127 | 128 | // UPCOMING --> ONGOING --> ENDED 129 | if ( 130 | currentSemesterStatus === RegistrationStatus.UPCOMING && 131 | requestedStatus === RegistrationStatus.ENDED 132 | ) { 133 | throw new AppError( 134 | httpStatus.BAD_REQUEST, 135 | `You can not directly change status from ${currentSemesterStatus} to ${requestedStatus}`, 136 | ); 137 | } 138 | 139 | if ( 140 | currentSemesterStatus === RegistrationStatus.ONGOING && 141 | requestedStatus === RegistrationStatus.UPCOMING 142 | ) { 143 | throw new AppError( 144 | httpStatus.BAD_REQUEST, 145 | `You can not directly change status from ${currentSemesterStatus} to ${requestedStatus}`, 146 | ); 147 | } 148 | 149 | const result = await SemesterRegistration.findByIdAndUpdate(id, payload, { 150 | new: true, 151 | runValidators: true, 152 | }); 153 | 154 | return result; 155 | }; 156 | 157 | const deleteSemesterRegistrationFromDB = async (id: string) => { 158 | /** 159 | * Step1: Delete associated offered courses. 160 | * Step2: Delete semester registraton when the status is 161 | 'UPCOMING'. 162 | **/ 163 | 164 | // checking if the semester registration is exist 165 | const isSemesterRegistrationExists = await SemesterRegistration.findById(id); 166 | 167 | if (!isSemesterRegistrationExists) { 168 | throw new AppError( 169 | httpStatus.NOT_FOUND, 170 | 'This registered semester is not found !', 171 | ); 172 | } 173 | 174 | // checking if the status is still "UPCOMING" 175 | const semesterRegistrationStatus = isSemesterRegistrationExists.status; 176 | 177 | if (semesterRegistrationStatus !== 'UPCOMING') { 178 | throw new AppError( 179 | httpStatus.BAD_REQUEST, 180 | `You can not update as the registered semester is ${semesterRegistrationStatus}`, 181 | ); 182 | } 183 | 184 | const session = await mongoose.startSession(); 185 | 186 | //deleting associated offered courses 187 | 188 | try { 189 | session.startTransaction(); 190 | 191 | const deletedOfferedCourse = await OfferedCourse.deleteMany( 192 | { 193 | semesterRegistration: id, 194 | }, 195 | { 196 | session, 197 | }, 198 | ); 199 | 200 | if (!deletedOfferedCourse) { 201 | throw new AppError( 202 | httpStatus.BAD_REQUEST, 203 | 'Failed to delete semester registration !', 204 | ); 205 | } 206 | 207 | const deletedSemisterRegistration = 208 | await SemesterRegistration.findByIdAndDelete(id, { 209 | session, 210 | new: true, 211 | }); 212 | 213 | if (!deletedSemisterRegistration) { 214 | throw new AppError( 215 | httpStatus.BAD_REQUEST, 216 | 'Failed to delete semester registration !', 217 | ); 218 | } 219 | 220 | await session.commitTransaction(); 221 | await session.endSession(); 222 | 223 | return null; 224 | } catch (err: any) { 225 | await session.abortTransaction(); 226 | await session.endSession(); 227 | throw new Error(err); 228 | } 229 | }; 230 | 231 | export const SemesterRegistrationService = { 232 | createSemesterRegistrationIntoDB, 233 | getAllSemesterRegistrationsFromDB, 234 | getSingleSemesterRegistrationsFromDB, 235 | updateSemesterRegistrationIntoDB, 236 | deleteSemesterRegistrationFromDB, 237 | }; 238 | -------------------------------------------------------------------------------- /src/app/modules/SemesterRegistration/semesterRegistration.validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { SemesterRegistrationStatus } from './semesterRegistration.constant'; 3 | 4 | const createSemesterRegistrationValidationSchema = z.object({ 5 | body: z.object({ 6 | academicSemester: z.string(), 7 | status: z.enum([...(SemesterRegistrationStatus as [string, ...string[]])]), 8 | startDate: z.string().datetime(), 9 | endDate: z.string().datetime(), 10 | minCredit: z.number(), 11 | maxCredit: z.number(), 12 | }), 13 | }); 14 | 15 | const upadateSemesterRegistrationValidationSchema = z.object({ 16 | body: z.object({ 17 | academicSemester: z.string().optional(), 18 | status: z 19 | .enum([...(SemesterRegistrationStatus as [string, ...string[]])]) 20 | .optional(), 21 | startDate: z.string().datetime().optional(), 22 | endDate: z.string().datetime().optional(), 23 | minCredit: z.number().optional(), 24 | maxCredit: z.number().optional(), 25 | }), 26 | }); 27 | 28 | export const SemesterRegistrationValidations = { 29 | createSemesterRegistrationValidationSchema, 30 | upadateSemesterRegistrationValidationSchema, 31 | }; 32 | -------------------------------------------------------------------------------- /src/app/modules/Student/student.constant.ts: -------------------------------------------------------------------------------- 1 | export const studentSearchableFields = [ 2 | 'email', 3 | 'name.firstName', 4 | 'name.lastName', 5 | 'presentAddress', 6 | ]; 7 | -------------------------------------------------------------------------------- /src/app/modules/Student/student.controller.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | import httpStatus from 'http-status'; 3 | import catchAsync from '../../utils/catchAsync'; 4 | import sendResponse from '../../utils/sendResponse'; 5 | import { StudentServices } from './student.service'; 6 | 7 | const getSingleStudent = catchAsync(async (req, res) => { 8 | const { id } = req.params; 9 | const result = await StudentServices.getSingleStudentFromDB(id); 10 | 11 | sendResponse(res, { 12 | statusCode: httpStatus.OK, 13 | success: true, 14 | message: 'Student is retrieved successfully', 15 | data: result, 16 | }); 17 | }); 18 | 19 | const getAllStudents: RequestHandler = catchAsync(async (req, res) => { 20 | const result = await StudentServices.getAllStudentsFromDB(req.query); 21 | console.log({ result }); 22 | sendResponse(res, { 23 | statusCode: httpStatus.OK, 24 | success: true, 25 | message: 'Student are retrieved successfully', 26 | meta: result.meta, 27 | data: result.result, 28 | }); 29 | }); 30 | 31 | const updateStudent = catchAsync(async (req, res) => { 32 | const { id } = req.params; 33 | const { student } = req.body; 34 | const result = await StudentServices.updateStudentIntoDB(id, student); 35 | 36 | sendResponse(res, { 37 | statusCode: httpStatus.OK, 38 | success: true, 39 | message: 'Student is updated successfully', 40 | data: result, 41 | }); 42 | }); 43 | 44 | const deleteStudent = catchAsync(async (req, res) => { 45 | const { id } = req.params; 46 | const result = await StudentServices.deleteStudentFromDB(id); 47 | 48 | sendResponse(res, { 49 | statusCode: httpStatus.OK, 50 | success: true, 51 | message: 'Student is deleted successfully', 52 | data: result, 53 | }); 54 | }); 55 | 56 | export const StudentControllers = { 57 | getAllStudents, 58 | getSingleStudent, 59 | deleteStudent, 60 | updateStudent, 61 | }; 62 | -------------------------------------------------------------------------------- /src/app/modules/Student/student.interface.ts: -------------------------------------------------------------------------------- 1 | import { Model, Types } from 'mongoose'; 2 | 3 | export type TUserName = { 4 | firstName: string; 5 | middleName: string; 6 | lastName: string; 7 | }; 8 | 9 | export type TGuardian = { 10 | fatherName: string; 11 | fatherOccupation: string; 12 | fatherContactNo: string; 13 | motherName: string; 14 | motherOccupation: string; 15 | motherContactNo: string; 16 | }; 17 | 18 | export type TLocalGuardian = { 19 | name: string; 20 | occupation: string; 21 | contactNo: string; 22 | address: string; 23 | }; 24 | 25 | export type TStudent = { 26 | id: string; 27 | user: Types.ObjectId; 28 | name: TUserName; 29 | gender: 'male' | 'female' | 'other'; 30 | dateOfBirth?: Date; 31 | email: string; 32 | contactNo: string; 33 | emergencyContactNo: string; 34 | bloogGroup?: 'A+' | 'A-' | 'B+' | 'B-' | 'AB+' | 'AB-' | 'O+' | 'O-'; 35 | presentAddress: string; 36 | permanentAddress: string; 37 | guardian: TGuardian; 38 | localGuardian: TLocalGuardian; 39 | profileImg?: string; 40 | admissionSemester: Types.ObjectId; 41 | academicDepartment: Types.ObjectId; 42 | academicFaculty: Types.ObjectId; 43 | isDeleted: boolean; 44 | }; 45 | 46 | //for creating static 47 | export interface StudentModel extends Model { 48 | // eslint-disable-next-line no-unused-vars 49 | isUserExists(id: string): Promise; 50 | } 51 | -------------------------------------------------------------------------------- /src/app/modules/Student/student.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose'; 2 | import { 3 | StudentModel, 4 | TGuardian, 5 | TLocalGuardian, 6 | TStudent, 7 | TUserName, 8 | } from './student.interface'; 9 | 10 | const userNameSchema = new Schema({ 11 | firstName: { 12 | type: String, 13 | required: [true, 'First Name is required'], 14 | trim: true, 15 | maxlength: [20, 'Name can not be more than 20 characters'], 16 | }, 17 | middleName: { 18 | type: String, 19 | trim: true, 20 | }, 21 | lastName: { 22 | type: String, 23 | trim: true, 24 | required: [true, 'Last Name is required'], 25 | maxlength: [20, 'Name can not be more than 20 characters'], 26 | }, 27 | }); 28 | 29 | const guardianSchema = new Schema({ 30 | fatherName: { 31 | type: String, 32 | trim: true, 33 | required: [true, 'Father Name is required'], 34 | }, 35 | fatherOccupation: { 36 | type: String, 37 | trim: true, 38 | required: [true, 'Father occupation is required'], 39 | }, 40 | fatherContactNo: { 41 | type: String, 42 | required: [true, 'Father Contact No is required'], 43 | }, 44 | motherName: { 45 | type: String, 46 | required: [true, 'Mother Name is required'], 47 | }, 48 | motherOccupation: { 49 | type: String, 50 | required: [true, 'Mother occupation is required'], 51 | }, 52 | motherContactNo: { 53 | type: String, 54 | required: [true, 'Mother Contact No is required'], 55 | }, 56 | }); 57 | 58 | const localGuradianSchema = new Schema({ 59 | name: { 60 | type: String, 61 | required: [true, 'Name is required'], 62 | }, 63 | occupation: { 64 | type: String, 65 | required: [true, 'Occupation is required'], 66 | }, 67 | contactNo: { 68 | type: String, 69 | required: [true, 'Contact number is required'], 70 | }, 71 | address: { 72 | type: String, 73 | required: [true, 'Address is required'], 74 | }, 75 | }); 76 | 77 | const studentSchema = new Schema( 78 | { 79 | id: { 80 | type: String, 81 | required: [true, 'ID is required'], 82 | unique: true, 83 | }, 84 | user: { 85 | type: Schema.Types.ObjectId, 86 | required: [true, 'User id is required'], 87 | unique: true, 88 | ref: 'User', 89 | }, 90 | name: { 91 | type: userNameSchema, 92 | required: [true, 'Name is required'], 93 | }, 94 | gender: { 95 | type: String, 96 | enum: { 97 | values: ['male', 'female', 'other'], 98 | message: '{VALUE} is not a valid gender', 99 | }, 100 | required: [true, 'Gender is required'], 101 | }, 102 | dateOfBirth: { type: Date }, 103 | email: { 104 | type: String, 105 | required: [true, 'Email is required'], 106 | unique: true, 107 | }, 108 | contactNo: { type: String, required: [true, 'Contact number is required'] }, 109 | emergencyContactNo: { 110 | type: String, 111 | required: [true, 'Emergency contact number is required'], 112 | }, 113 | bloogGroup: { 114 | type: String, 115 | enum: { 116 | values: ['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-'], 117 | message: '{VALUE} is not a valid blood group', 118 | }, 119 | }, 120 | presentAddress: { 121 | type: String, 122 | required: [true, 'Present address is required'], 123 | }, 124 | permanentAddress: { 125 | type: String, 126 | required: [true, 'Permanent address is required'], 127 | }, 128 | guardian: { 129 | type: guardianSchema, 130 | required: [true, 'Guardian information is required'], 131 | }, 132 | localGuardian: { 133 | type: localGuradianSchema, 134 | required: [true, 'Local guardian information is required'], 135 | }, 136 | profileImg: { type: String, default: '' }, 137 | admissionSemester: { 138 | type: Schema.Types.ObjectId, 139 | ref: 'AcademicSemester', 140 | }, 141 | isDeleted: { 142 | type: Boolean, 143 | default: false, 144 | }, 145 | academicDepartment: { 146 | type: Schema.Types.ObjectId, 147 | ref: 'AcademicDepartment', 148 | }, 149 | academicFaculty: { 150 | type: Schema.Types.ObjectId, 151 | ref: 'AcademicFaculty', 152 | }, 153 | }, 154 | { 155 | toJSON: { 156 | virtuals: true, 157 | }, 158 | }, 159 | ); 160 | 161 | //virtual 162 | studentSchema.virtual('fullName').get(function () { 163 | return this?.name?.firstName + this?.name?.middleName + this?.name?.lastName; 164 | }); 165 | 166 | // Query Middleware 167 | studentSchema.pre('find', function (next) { 168 | this.find({ isDeleted: { $ne: true } }); 169 | next(); 170 | }); 171 | 172 | studentSchema.pre('findOne', function (next) { 173 | this.find({ isDeleted: { $ne: true } }); 174 | next(); 175 | }); 176 | 177 | studentSchema.pre('aggregate', function (next) { 178 | this.pipeline().unshift({ $match: { isDeleted: { $ne: true } } }); 179 | next(); 180 | }); 181 | 182 | //creating a custom static method 183 | studentSchema.statics.isUserExists = async function (id: string) { 184 | const existingUser = await Student.findOne({ id }); 185 | return existingUser; 186 | }; 187 | 188 | export const Student = model('Student', studentSchema); 189 | -------------------------------------------------------------------------------- /src/app/modules/Student/student.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import auth from '../../middlewares/auth'; 3 | import validateRequest from '../../middlewares/validateRequest'; 4 | import { USER_ROLE } from '../User/user.constant'; 5 | import { StudentControllers } from './student.controller'; 6 | import { updateStudentValidationSchema } from './student.validation'; 7 | 8 | const router = express.Router(); 9 | 10 | router.get( 11 | '/', 12 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 13 | StudentControllers.getAllStudents, 14 | ); 15 | 16 | router.get( 17 | '/:id', 18 | auth(USER_ROLE.superAdmin, USER_ROLE.admin, USER_ROLE.faculty), 19 | StudentControllers.getSingleStudent, 20 | ); 21 | 22 | router.patch( 23 | '/:id', 24 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 25 | validateRequest(updateStudentValidationSchema), 26 | StudentControllers.updateStudent, 27 | ); 28 | 29 | router.delete( 30 | '/:id', 31 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 32 | StudentControllers.deleteStudent, 33 | ); 34 | 35 | export const StudentRoutes = router; 36 | -------------------------------------------------------------------------------- /src/app/modules/Student/student.service.ts: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import mongoose from 'mongoose'; 3 | import QueryBuilder from '../../builder/QueryBuilder'; 4 | import AppError from '../../errors/AppError'; 5 | import { User } from '../User/user.model'; 6 | import { studentSearchableFields } from './student.constant'; 7 | import { TStudent } from './student.interface'; 8 | import { Student } from './student.model'; 9 | 10 | const getAllStudentsFromDB = async (query: Record) => { 11 | const studentQuery = new QueryBuilder( 12 | Student.find() 13 | .populate('user') 14 | .populate('admissionSemester') 15 | .populate('academicDepartment academicFaculty'), 16 | query, 17 | ) 18 | .search(studentSearchableFields) 19 | .filter() 20 | .sort() 21 | .paginate() 22 | .fields(); 23 | 24 | const meta = await studentQuery.countTotal(); 25 | const result = await studentQuery.modelQuery; 26 | 27 | return { 28 | meta, 29 | result, 30 | }; 31 | }; 32 | 33 | const getSingleStudentFromDB = async (id: string) => { 34 | const result = await Student.findById(id) 35 | .populate('admissionSemester') 36 | .populate('academicDepartment academicFaculty'); 37 | return result; 38 | }; 39 | 40 | const updateStudentIntoDB = async (id: string, payload: Partial) => { 41 | const { name, guardian, localGuardian, ...remainingStudentData } = payload; 42 | 43 | const modifiedUpdatedData: Record = { 44 | ...remainingStudentData, 45 | }; 46 | 47 | /* 48 | guardain: { 49 | fatherOccupation:"Teacher" 50 | } 51 | 52 | guardian.fatherOccupation = Teacher 53 | 54 | name.firstName = 'Mezba' 55 | name.lastName = 'Abedin' 56 | */ 57 | 58 | if (name && Object.keys(name).length) { 59 | for (const [key, value] of Object.entries(name)) { 60 | modifiedUpdatedData[`name.${key}`] = value; 61 | } 62 | } 63 | 64 | if (guardian && Object.keys(guardian).length) { 65 | for (const [key, value] of Object.entries(guardian)) { 66 | modifiedUpdatedData[`guardian.${key}`] = value; 67 | } 68 | } 69 | 70 | if (localGuardian && Object.keys(localGuardian).length) { 71 | for (const [key, value] of Object.entries(localGuardian)) { 72 | modifiedUpdatedData[`localGuardian.${key}`] = value; 73 | } 74 | } 75 | 76 | const result = await Student.findByIdAndUpdate(id, modifiedUpdatedData, { 77 | new: true, 78 | runValidators: true, 79 | }); 80 | return result; 81 | }; 82 | 83 | const deleteStudentFromDB = async (id: string) => { 84 | const session = await mongoose.startSession(); 85 | 86 | try { 87 | session.startTransaction(); 88 | 89 | const deletedStudent = await Student.findByIdAndUpdate( 90 | id, 91 | { isDeleted: true }, 92 | { new: true, session }, 93 | ); 94 | 95 | if (!deletedStudent) { 96 | throw new AppError(httpStatus.BAD_REQUEST, 'Failed to delete student'); 97 | } 98 | 99 | // get user _id from deletedStudent 100 | const userId = deletedStudent.user; 101 | 102 | const deletedUser = await User.findByIdAndUpdate( 103 | userId, 104 | { isDeleted: true }, 105 | { new: true, session }, 106 | ); 107 | 108 | if (!deletedUser) { 109 | throw new AppError(httpStatus.BAD_REQUEST, 'Failed to delete user'); 110 | } 111 | 112 | await session.commitTransaction(); 113 | await session.endSession(); 114 | 115 | return deletedStudent; 116 | } catch (err) { 117 | await session.abortTransaction(); 118 | await session.endSession(); 119 | throw new Error('Failed to delete student'); 120 | } 121 | }; 122 | 123 | export const StudentServices = { 124 | getAllStudentsFromDB, 125 | getSingleStudentFromDB, 126 | updateStudentIntoDB, 127 | deleteStudentFromDB, 128 | }; 129 | -------------------------------------------------------------------------------- /src/app/modules/Student/student.validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const createUserNameValidationSchema = z.object({ 4 | firstName: z 5 | .string() 6 | .min(1) 7 | .max(20) 8 | .refine((value) => /^[A-Z]/.test(value), { 9 | message: 'First Name must start with a capital letter', 10 | }), 11 | middleName: z.string(), 12 | lastName: z.string(), 13 | }); 14 | 15 | const createGuardianValidationSchema = z.object({ 16 | fatherName: z.string(), 17 | fatherOccupation: z.string(), 18 | fatherContactNo: z.string(), 19 | motherName: z.string(), 20 | motherOccupation: z.string(), 21 | motherContactNo: z.string(), 22 | }); 23 | 24 | const createLocalGuardianValidationSchema = z.object({ 25 | name: z.string(), 26 | occupation: z.string(), 27 | contactNo: z.string(), 28 | address: z.string(), 29 | }); 30 | 31 | export const createStudentValidationSchema = z.object({ 32 | body: z.object({ 33 | password: z.string().max(20).optional(), 34 | student: z.object({ 35 | name: createUserNameValidationSchema, 36 | gender: z.enum(['male', 'female', 'other']), 37 | dateOfBirth: z.string().optional(), 38 | email: z.string().email(), 39 | contactNo: z.string(), 40 | emergencyContactNo: z.string(), 41 | bloogGroup: z.enum(['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-']), 42 | presentAddress: z.string(), 43 | permanentAddress: z.string(), 44 | guardian: createGuardianValidationSchema, 45 | localGuardian: createLocalGuardianValidationSchema, 46 | admissionSemester: z.string(), 47 | academicDepartment: z.string(), 48 | }), 49 | }), 50 | }); 51 | 52 | const updateUserNameValidationSchema = z.object({ 53 | firstName: z.string().min(1).max(20).optional(), 54 | middleName: z.string().optional(), 55 | lastName: z.string().optional(), 56 | }); 57 | 58 | const updateGuardianValidationSchema = z.object({ 59 | fatherName: z.string().optional(), 60 | fatherOccupation: z.string().optional(), 61 | fatherContactNo: z.string().optional(), 62 | motherName: z.string().optional(), 63 | motherOccupation: z.string().optional(), 64 | motherContactNo: z.string().optional(), 65 | }); 66 | 67 | const updateLocalGuardianValidationSchema = z.object({ 68 | name: z.string().optional(), 69 | occupation: z.string().optional(), 70 | contactNo: z.string().optional(), 71 | address: z.string().optional(), 72 | }); 73 | 74 | export const updateStudentValidationSchema = z.object({ 75 | body: z.object({ 76 | student: z.object({ 77 | name: updateUserNameValidationSchema, 78 | gender: z.enum(['male', 'female', 'other']).optional(), 79 | dateOfBirth: z.string().optional(), 80 | email: z.string().email().optional(), 81 | contactNo: z.string().optional(), 82 | emergencyContactNo: z.string().optional(), 83 | bloogGroup: z 84 | .enum(['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-']) 85 | .optional(), 86 | presentAddress: z.string().optional(), 87 | permanentAddress: z.string().optional(), 88 | guardian: updateGuardianValidationSchema.optional(), 89 | localGuardian: updateLocalGuardianValidationSchema.optional(), 90 | admissionSemester: z.string().optional(), 91 | academicDepartment: z.string().optional(), 92 | }), 93 | }), 94 | }); 95 | 96 | export const studentValidations = { 97 | createStudentValidationSchema, 98 | updateStudentValidationSchema, 99 | }; 100 | -------------------------------------------------------------------------------- /src/app/modules/User/user.constant.ts: -------------------------------------------------------------------------------- 1 | export const USER_ROLE = { 2 | superAdmin: 'superAdmin', 3 | student: 'student', 4 | faculty: 'faculty', 5 | admin: 'admin', 6 | } as const; 7 | 8 | export const UserStatus = ['in-progress', 'blocked']; 9 | -------------------------------------------------------------------------------- /src/app/modules/User/user.controller.ts: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import catchAsync from '../../utils/catchAsync'; 3 | import sendResponse from '../../utils/sendResponse'; 4 | import { UserServices } from './user.service'; 5 | 6 | const createStudent = catchAsync(async (req, res) => { 7 | const { password, student: studentData } = req.body; 8 | 9 | const result = await UserServices.createStudentIntoDB( 10 | req.file, 11 | password, 12 | studentData, 13 | ); 14 | 15 | sendResponse(res, { 16 | statusCode: httpStatus.OK, 17 | success: true, 18 | message: 'Student is created successfully', 19 | data: result, 20 | }); 21 | }); 22 | 23 | const createFaculty = catchAsync(async (req, res) => { 24 | const { password, faculty: facultyData } = req.body; 25 | 26 | const result = await UserServices.createFacultyIntoDB( 27 | req.file, 28 | password, 29 | facultyData, 30 | ); 31 | 32 | sendResponse(res, { 33 | statusCode: httpStatus.OK, 34 | success: true, 35 | message: 'Faculty is created successfully', 36 | data: result, 37 | }); 38 | }); 39 | 40 | const createAdmin = catchAsync(async (req, res) => { 41 | const { password, admin: adminData } = req.body; 42 | 43 | const result = await UserServices.createAdminIntoDB( 44 | req.file, 45 | password, 46 | adminData, 47 | ); 48 | 49 | sendResponse(res, { 50 | statusCode: httpStatus.OK, 51 | success: true, 52 | message: 'Admin is created successfully', 53 | data: result, 54 | }); 55 | }); 56 | 57 | const getMe = catchAsync(async (req, res) => { 58 | const { userId, role } = req.user; 59 | const result = await UserServices.getMe(userId, role); 60 | 61 | sendResponse(res, { 62 | statusCode: httpStatus.OK, 63 | success: true, 64 | message: 'User is retrieved successfully', 65 | data: result, 66 | }); 67 | }); 68 | 69 | const changeStatus = catchAsync(async (req, res) => { 70 | const id = req.params.id; 71 | 72 | const result = await UserServices.changeStatus(id, req.body); 73 | 74 | sendResponse(res, { 75 | statusCode: httpStatus.OK, 76 | success: true, 77 | message: 'Status is updated successfully', 78 | data: result, 79 | }); 80 | }); 81 | export const UserControllers = { 82 | createStudent, 83 | createFaculty, 84 | createAdmin, 85 | getMe, 86 | changeStatus, 87 | }; 88 | -------------------------------------------------------------------------------- /src/app/modules/User/user.interface.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { Model } from 'mongoose'; 3 | import { USER_ROLE } from './user.constant'; 4 | 5 | export interface TUser { 6 | id: string; 7 | email: string; 8 | password: string; 9 | needsPasswordChange: boolean; 10 | passwordChangedAt?: Date; 11 | role: 'superAdmin' | 'admin' | 'student' | 'faculty'; 12 | status: 'in-progress' | 'blocked'; 13 | isDeleted: boolean; 14 | } 15 | 16 | export interface UserModel extends Model { 17 | //instance methods for checking if the user exist 18 | isUserExistsByCustomId(id: string): Promise; 19 | //instance methods for checking if passwords are matched 20 | isPasswordMatched( 21 | plainTextPassword: string, 22 | hashedPassword: string, 23 | ): Promise; 24 | isJWTIssuedBeforePasswordChanged( 25 | passwordChangedTimestamp: Date, 26 | jwtIssuedTimestamp: number, 27 | ): boolean; 28 | } 29 | 30 | export type TUserRole = keyof typeof USER_ROLE; 31 | -------------------------------------------------------------------------------- /src/app/modules/User/user.model.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-this-alias */ 2 | import bcrypt from 'bcrypt'; 3 | import { Schema, model } from 'mongoose'; 4 | import config from '../../config'; 5 | import { UserStatus } from './user.constant'; 6 | import { TUser, UserModel } from './user.interface'; 7 | 8 | const userSchema = new Schema( 9 | { 10 | id: { 11 | type: String, 12 | required: true, 13 | unique: true, 14 | }, 15 | email: { 16 | type: String, 17 | required: true, 18 | unique: true, 19 | }, 20 | password: { 21 | type: String, 22 | required: true, 23 | select: 0, 24 | }, 25 | needsPasswordChange: { 26 | type: Boolean, 27 | default: true, 28 | }, 29 | passwordChangedAt: { 30 | type: Date, 31 | }, 32 | role: { 33 | type: String, 34 | enum: ['superAdmin', 'student', 'faculty', 'admin'], 35 | }, 36 | status: { 37 | type: String, 38 | enum: UserStatus, 39 | default: 'in-progress', 40 | }, 41 | isDeleted: { 42 | type: Boolean, 43 | default: false, 44 | }, 45 | }, 46 | { 47 | timestamps: true, 48 | }, 49 | ); 50 | 51 | userSchema.pre('save', async function (next) { 52 | // eslint-disable-next-line @typescript-eslint/no-this-alias 53 | const user = this; // doc 54 | // hashing password and save into DB 55 | user.password = await bcrypt.hash( 56 | user.password, 57 | Number(config.bcrypt_salt_rounds), 58 | ); 59 | next(); 60 | }); 61 | 62 | // set '' after saving password 63 | userSchema.post('save', function (doc, next) { 64 | doc.password = ''; 65 | next(); 66 | }); 67 | 68 | userSchema.statics.isUserExistsByCustomId = async function (id: string) { 69 | return await User.findOne({ id }).select('+password'); 70 | }; 71 | 72 | userSchema.statics.isPasswordMatched = async function ( 73 | plainTextPassword, 74 | hashedPassword, 75 | ) { 76 | return await bcrypt.compare(plainTextPassword, hashedPassword); 77 | }; 78 | 79 | userSchema.statics.isJWTIssuedBeforePasswordChanged = function ( 80 | passwordChangedTimestamp: Date, 81 | jwtIssuedTimestamp: number, 82 | ) { 83 | const passwordChangedTime = 84 | new Date(passwordChangedTimestamp).getTime() / 1000; 85 | return passwordChangedTime > jwtIssuedTimestamp; 86 | }; 87 | 88 | export const User = model('User', userSchema); 89 | -------------------------------------------------------------------------------- /src/app/modules/User/user.route.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import express, { NextFunction, Request, Response } from 'express'; 3 | import auth from '../../middlewares/auth'; 4 | import validateRequest from '../../middlewares/validateRequest'; 5 | import { upload } from '../../utils/sendImageToCloudinary'; 6 | import { createAdminValidationSchema } from '../Admin/admin.validation'; 7 | import { createFacultyValidationSchema } from '../Faculty/faculty.validation'; 8 | import { createStudentValidationSchema } from '../Student/student.validation'; 9 | import { USER_ROLE } from './user.constant'; 10 | import { UserControllers } from './user.controller'; 11 | import { UserValidation } from './user.validation'; 12 | 13 | const router = express.Router(); 14 | 15 | router.post( 16 | '/create-student', 17 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 18 | upload.single('file'), 19 | (req: Request, res: Response, next: NextFunction) => { 20 | req.body = JSON.parse(req.body.data); 21 | next(); 22 | }, 23 | validateRequest(createStudentValidationSchema), 24 | UserControllers.createStudent, 25 | ); 26 | 27 | router.post( 28 | '/create-faculty', 29 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 30 | upload.single('file'), 31 | (req: Request, res: Response, next: NextFunction) => { 32 | req.body = JSON.parse(req.body.data); 33 | next(); 34 | }, 35 | validateRequest(createFacultyValidationSchema), 36 | UserControllers.createFaculty, 37 | ); 38 | 39 | router.post( 40 | '/create-admin', 41 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 42 | upload.single('file'), 43 | (req: Request, res: Response, next: NextFunction) => { 44 | req.body = JSON.parse(req.body.data); 45 | next(); 46 | }, 47 | validateRequest(createAdminValidationSchema), 48 | UserControllers.createAdmin, 49 | ); 50 | 51 | router.post( 52 | '/change-status/:id', 53 | auth(USER_ROLE.superAdmin, USER_ROLE.admin), 54 | validateRequest(UserValidation.changeStatusValidationSchema), 55 | UserControllers.changeStatus, 56 | ); 57 | 58 | router.get( 59 | '/me', 60 | auth( 61 | USER_ROLE.superAdmin, 62 | USER_ROLE.admin, 63 | USER_ROLE.faculty, 64 | USER_ROLE.student, 65 | ), 66 | UserControllers.getMe, 67 | ); 68 | 69 | export const UserRoutes = router; 70 | -------------------------------------------------------------------------------- /src/app/modules/User/user.service.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | import httpStatus from 'http-status'; 4 | import mongoose from 'mongoose'; 5 | import config from '../../config'; 6 | import AppError from '../../errors/AppError'; 7 | import { sendImageToCloudinary } from '../../utils/sendImageToCloudinary'; 8 | import { AcademicDepartment } from '../AcademicDepartment/academicDepartment.model'; 9 | import { AcademicSemester } from '../AcademicSemester/academicSemester.model'; 10 | import { TAdmin } from '../Admin/admin.interface'; 11 | import { Admin } from '../Admin/admin.model'; 12 | import { TFaculty } from '../Faculty/faculty.interface'; 13 | import { Faculty } from '../Faculty/faculty.model'; 14 | import { TStudent } from '../Student/student.interface'; 15 | import { Student } from '../Student/student.model'; 16 | import { TUser } from './user.interface'; 17 | import { User } from './user.model'; 18 | import { 19 | generateAdminId, 20 | generateFacultyId, 21 | generateStudentId, 22 | } from './user.utils'; 23 | 24 | const createStudentIntoDB = async ( 25 | file: any, 26 | password: string, 27 | payload: TStudent, 28 | ) => { 29 | // create a user object 30 | const userData: Partial = {}; 31 | 32 | //if password is not given , use default password 33 | userData.password = password || (config.default_password as string); 34 | 35 | //set student role 36 | userData.role = 'student'; 37 | // set student email 38 | userData.email = payload.email; 39 | 40 | // find academic semester info 41 | const admissionSemester = await AcademicSemester.findById( 42 | payload.admissionSemester, 43 | ); 44 | 45 | if (!admissionSemester) { 46 | throw new AppError(400, 'Admission semester not found'); 47 | } 48 | 49 | // find department 50 | const academicDepartment = await AcademicDepartment.findById( 51 | payload.academicDepartment, 52 | ); 53 | 54 | if (!academicDepartment) { 55 | throw new AppError(400, 'Aademic department not found'); 56 | } 57 | payload.academicFaculty = academicDepartment.academicFaculty; 58 | 59 | const session = await mongoose.startSession(); 60 | 61 | try { 62 | session.startTransaction(); 63 | //set generated id 64 | userData.id = await generateStudentId(admissionSemester); 65 | 66 | if (file) { 67 | const imageName = `${userData.id}${payload?.name?.firstName}`; 68 | const path = file?.path; 69 | 70 | //send image to cloudinary 71 | const { secure_url } = await sendImageToCloudinary(imageName, path); 72 | payload.profileImg = secure_url as string; 73 | } 74 | 75 | // create a user (transaction-1) 76 | const newUser = await User.create([userData], { session }); // array 77 | 78 | //create a student 79 | if (!newUser.length) { 80 | throw new AppError(httpStatus.BAD_REQUEST, 'Failed to create user'); 81 | } 82 | // set id , _id as user 83 | payload.id = newUser[0].id; 84 | payload.user = newUser[0]._id; //reference _id 85 | 86 | // create a student (transaction-2) 87 | const newStudent = await Student.create([payload], { session }); 88 | 89 | if (!newStudent.length) { 90 | throw new AppError(httpStatus.BAD_REQUEST, 'Failed to create student'); 91 | } 92 | 93 | await session.commitTransaction(); 94 | await session.endSession(); 95 | 96 | return newStudent; 97 | } catch (err: any) { 98 | await session.abortTransaction(); 99 | await session.endSession(); 100 | throw new Error(err); 101 | } 102 | }; 103 | 104 | const createFacultyIntoDB = async ( 105 | file: any, 106 | password: string, 107 | payload: TFaculty, 108 | ) => { 109 | // create a user object 110 | const userData: Partial = {}; 111 | 112 | //if password is not given , use deafult password 113 | userData.password = password || (config.default_password as string); 114 | 115 | //set faculty role 116 | userData.role = 'faculty'; 117 | //set faculty email 118 | userData.email = payload.email; 119 | 120 | // find academic department info 121 | const academicDepartment = await AcademicDepartment.findById( 122 | payload.academicDepartment, 123 | ); 124 | 125 | if (!academicDepartment) { 126 | throw new AppError(400, 'Academic department not found'); 127 | } 128 | 129 | payload.academicFaculty = academicDepartment?.academicFaculty; 130 | 131 | const session = await mongoose.startSession(); 132 | 133 | try { 134 | session.startTransaction(); 135 | //set generated id 136 | userData.id = await generateFacultyId(); 137 | 138 | if (file) { 139 | const imageName = `${userData.id}${payload?.name?.firstName}`; 140 | const path = file?.path; 141 | //send image to cloudinary 142 | const { secure_url } = await sendImageToCloudinary(imageName, path); 143 | payload.profileImg = secure_url as string; 144 | } 145 | 146 | // create a user (transaction-1) 147 | const newUser = await User.create([userData], { session }); // array 148 | 149 | //create a faculty 150 | if (!newUser.length) { 151 | throw new AppError(httpStatus.BAD_REQUEST, 'Failed to create user'); 152 | } 153 | // set id , _id as user 154 | payload.id = newUser[0].id; 155 | payload.user = newUser[0]._id; //reference _id 156 | 157 | // create a faculty (transaction-2) 158 | 159 | const newFaculty = await Faculty.create([payload], { session }); 160 | 161 | if (!newFaculty.length) { 162 | throw new AppError(httpStatus.BAD_REQUEST, 'Failed to create faculty'); 163 | } 164 | 165 | await session.commitTransaction(); 166 | await session.endSession(); 167 | 168 | return newFaculty; 169 | } catch (err: any) { 170 | await session.abortTransaction(); 171 | await session.endSession(); 172 | throw new Error(err); 173 | } 174 | }; 175 | 176 | const createAdminIntoDB = async ( 177 | file: any, 178 | password: string, 179 | payload: TAdmin, 180 | ) => { 181 | // create a user object 182 | const userData: Partial = {}; 183 | 184 | //if password is not given , use deafult password 185 | userData.password = password || (config.default_password as string); 186 | 187 | //set student role 188 | userData.role = 'admin'; 189 | //set admin email 190 | userData.email = payload.email; 191 | const session = await mongoose.startSession(); 192 | 193 | try { 194 | session.startTransaction(); 195 | //set generated id 196 | userData.id = await generateAdminId(); 197 | 198 | if (file) { 199 | const imageName = `${userData.id}${payload?.name?.firstName}`; 200 | const path = file?.path; 201 | //send image to cloudinary 202 | const { secure_url } = await sendImageToCloudinary(imageName, path); 203 | payload.profileImg = secure_url as string; 204 | } 205 | 206 | // create a user (transaction-1) 207 | const newUser = await User.create([userData], { session }); 208 | 209 | //create a admin 210 | if (!newUser.length) { 211 | throw new AppError(httpStatus.BAD_REQUEST, 'Failed to create admin'); 212 | } 213 | // set id , _id as user 214 | payload.id = newUser[0].id; 215 | payload.user = newUser[0]._id; //reference _id 216 | 217 | // create a admin (transaction-2) 218 | const newAdmin = await Admin.create([payload], { session }); 219 | 220 | if (!newAdmin.length) { 221 | throw new AppError(httpStatus.BAD_REQUEST, 'Failed to create admin'); 222 | } 223 | 224 | await session.commitTransaction(); 225 | await session.endSession(); 226 | 227 | return newAdmin; 228 | } catch (err: any) { 229 | await session.abortTransaction(); 230 | await session.endSession(); 231 | throw new Error(err); 232 | } 233 | }; 234 | 235 | const getMe = async (userId: string, role: string) => { 236 | let result = null; 237 | if (role === 'student') { 238 | result = await Student.findOne({ id: userId }).populate('user'); 239 | } 240 | if (role === 'admin') { 241 | result = await Admin.findOne({ id: userId }).populate('user'); 242 | } 243 | 244 | if (role === 'faculty') { 245 | result = await Faculty.findOne({ id: userId }).populate('user'); 246 | } 247 | 248 | return result; 249 | }; 250 | 251 | const changeStatus = async (id: string, payload: { status: string }) => { 252 | const result = await User.findByIdAndUpdate(id, payload, { 253 | new: true, 254 | }); 255 | return result; 256 | }; 257 | 258 | export const UserServices = { 259 | createStudentIntoDB, 260 | createFacultyIntoDB, 261 | createAdminIntoDB, 262 | getMe, 263 | changeStatus, 264 | }; 265 | -------------------------------------------------------------------------------- /src/app/modules/User/user.utils.ts: -------------------------------------------------------------------------------- 1 | import { TAcademicSemester } from '../AcademicSemester/academicSemester.interface'; 2 | import { User } from './user.model'; 3 | 4 | const findLastStudentId = async () => { 5 | const lastStudent = await User.findOne( 6 | { 7 | role: 'student', 8 | }, 9 | { 10 | id: 1, 11 | _id: 0, 12 | }, 13 | ) 14 | .sort({ 15 | createdAt: -1, 16 | }) 17 | .lean(); 18 | 19 | return lastStudent?.id ? lastStudent.id : undefined; 20 | }; 21 | 22 | export const generateStudentId = async (payload: TAcademicSemester) => { 23 | let currentId = (0).toString(); 24 | const lastStudentId = await findLastStudentId(); 25 | 26 | const lastStudentSemesterCode = lastStudentId?.substring(4, 6); 27 | const lastStudentYear = lastStudentId?.substring(0, 4); 28 | 29 | const currentSemesterCode = payload.code; 30 | const currentYear = payload.year; 31 | 32 | if ( 33 | lastStudentId && 34 | lastStudentSemesterCode === currentSemesterCode && 35 | lastStudentYear === currentYear 36 | ) { 37 | currentId = lastStudentId.substring(6); 38 | } 39 | 40 | let incrementId = (Number(currentId) + 1).toString().padStart(4, '0'); 41 | 42 | incrementId = `${payload.year}${payload.code}${incrementId}`; 43 | 44 | return incrementId; 45 | }; 46 | 47 | // Faculty ID 48 | export const findLastFacultyId = async () => { 49 | const lastFaculty = await User.findOne( 50 | { 51 | role: 'faculty', 52 | }, 53 | { 54 | id: 1, 55 | _id: 0, 56 | }, 57 | ) 58 | .sort({ 59 | createdAt: -1, 60 | }) 61 | .lean(); 62 | 63 | return lastFaculty?.id ? lastFaculty.id.substring(2) : undefined; 64 | }; 65 | 66 | export const generateFacultyId = async () => { 67 | let currentId = (0).toString(); 68 | const lastFacultyId = await findLastFacultyId(); 69 | 70 | if (lastFacultyId) { 71 | currentId = lastFacultyId.substring(2); 72 | } 73 | 74 | let incrementId = (Number(currentId) + 1).toString().padStart(4, '0'); 75 | 76 | incrementId = `F-${incrementId}`; 77 | 78 | return incrementId; 79 | }; 80 | 81 | // Admin ID 82 | export const findLastAdminId = async () => { 83 | const lastAdmin = await User.findOne( 84 | { 85 | role: 'admin', 86 | }, 87 | { 88 | id: 1, 89 | _id: 0, 90 | }, 91 | ) 92 | .sort({ 93 | createdAt: -1, 94 | }) 95 | .lean(); 96 | 97 | return lastAdmin?.id ? lastAdmin.id.substring(2) : undefined; 98 | }; 99 | 100 | export const generateAdminId = async () => { 101 | let currentId = (0).toString(); 102 | const lastAdminId = await findLastAdminId(); 103 | 104 | if (lastAdminId) { 105 | currentId = lastAdminId.substring(2); 106 | } 107 | 108 | let incrementId = (Number(currentId) + 1).toString().padStart(4, '0'); 109 | 110 | incrementId = `A-${incrementId}`; 111 | return incrementId; 112 | }; 113 | -------------------------------------------------------------------------------- /src/app/modules/User/user.validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { UserStatus } from './user.constant'; 3 | 4 | const userValidationSchema = z.object({ 5 | pasword: z 6 | .string({ 7 | invalid_type_error: 'Password must be string', 8 | }) 9 | .max(20, { message: 'Password can not be more than 20 characters' }) 10 | .optional(), 11 | }); 12 | 13 | const changeStatusValidationSchema = z.object({ 14 | body: z.object({ 15 | status: z.enum([...UserStatus] as [string, ...string[]]), 16 | }), 17 | }); 18 | export const UserValidation = { 19 | userValidationSchema, 20 | changeStatusValidationSchema, 21 | }; 22 | -------------------------------------------------------------------------------- /src/app/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { AdminRoutes } from '../modules/Admin/admin.route'; 3 | import { AuthRoutes } from '../modules/Auth/auth.route'; 4 | import { CourseRoutes } from '../modules/Course/course.route'; 5 | 6 | import { AcademicDepartmentRoutes } from '../modules/AcademicDepartment/academicDepartment.route'; 7 | import { AcademicFacultyRoutes } from '../modules/AcademicFaculty/academicFaculty.route'; 8 | import { AcademicSemesterRoutes } from '../modules/AcademicSemester/academicSemester.route'; 9 | import { EnrolledCourseRoutes } from '../modules/EnrolledCourse/enrolledCourse.route'; 10 | import { FacultyRoutes } from '../modules/Faculty/faculty.route'; 11 | import { offeredCourseRoutes } from '../modules/OfferedCourse/OfferedCourse.route'; 12 | import { semesterRegistrationRoutes } from '../modules/SemesterRegistration/semesterRegistration.route'; 13 | import { StudentRoutes } from '../modules/Student/student.route'; 14 | import { UserRoutes } from '../modules/User/user.route'; 15 | 16 | const router = Router(); 17 | 18 | const moduleRoutes = [ 19 | { 20 | path: '/users', 21 | route: UserRoutes, 22 | }, 23 | { 24 | path: '/students', 25 | route: StudentRoutes, 26 | }, 27 | { 28 | path: '/faculties', 29 | route: FacultyRoutes, 30 | }, 31 | { 32 | path: '/admins', 33 | route: AdminRoutes, 34 | }, 35 | { 36 | path: '/academic-semesters', 37 | route: AcademicSemesterRoutes, 38 | }, 39 | { 40 | path: '/academic-faculties', 41 | route: AcademicFacultyRoutes, 42 | }, 43 | { 44 | path: '/academic-departments', 45 | route: AcademicDepartmentRoutes, 46 | }, 47 | { 48 | path: '/courses', 49 | route: CourseRoutes, 50 | }, 51 | { 52 | path: '/semester-registrations', 53 | route: semesterRegistrationRoutes, 54 | }, 55 | { 56 | path: '/offered-courses', 57 | route: offeredCourseRoutes, 58 | }, 59 | { 60 | path: '/auth', 61 | route: AuthRoutes, 62 | }, 63 | { 64 | path: '/enrolled-courses', 65 | route: EnrolledCourseRoutes, 66 | }, 67 | ]; 68 | 69 | moduleRoutes.forEach((route) => router.use(route.path, route.route)); 70 | 71 | export default router; 72 | -------------------------------------------------------------------------------- /src/app/utils/catchAsync.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, RequestHandler, Response } from 'express'; 2 | 3 | const catchAsync = (fn: RequestHandler) => { 4 | return (req: Request, res: Response, next: NextFunction) => { 5 | Promise.resolve(fn(req, res, next)).catch((err) => next(err)); 6 | }; 7 | }; 8 | 9 | export default catchAsync; 10 | -------------------------------------------------------------------------------- /src/app/utils/sendEmail.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | import config from '../config'; 3 | 4 | export const sendEmail = async (to: string, html: string) => { 5 | const transporter = nodemailer.createTransport({ 6 | host: 'smtp.gmail.com.', 7 | port: 587, 8 | secure: config.NODE_ENV === 'production', 9 | auth: { 10 | // TODO: replace `user` and `pass` values from 11 | user: 'mezbaul@programming-hero.com', 12 | pass: 'xfqj dshz wdui ymtb', 13 | }, 14 | }); 15 | 16 | await transporter.sendMail({ 17 | from: 'mezbaul@programming-hero.com', // sender address 18 | to, // list of receivers 19 | subject: 'Reset your password within ten mins!', // Subject line 20 | text: '', // plain text body 21 | html, // html body 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/app/utils/sendImageToCloudinary.ts: -------------------------------------------------------------------------------- 1 | import { UploadApiResponse, v2 as cloudinary } from 'cloudinary'; 2 | import fs from 'fs'; 3 | import multer from 'multer'; 4 | import config from '../config'; 5 | 6 | cloudinary.config({ 7 | cloud_name: config.cloudinary_cloud_name, 8 | api_key: config.cloudinary_api_key, 9 | api_secret: config.cloudinary_api_secret, 10 | }); 11 | 12 | export const sendImageToCloudinary = ( 13 | imageName: string, 14 | path: string, 15 | ): Promise> => { 16 | return new Promise((resolve, reject) => { 17 | cloudinary.uploader.upload( 18 | path, 19 | { public_id: imageName.trim() }, 20 | function (error, result) { 21 | if (error) { 22 | reject(error); 23 | } 24 | resolve(result as UploadApiResponse); 25 | // delete a file asynchronously 26 | fs.unlink(path, (err) => { 27 | if (err) { 28 | console.log(err); 29 | } else { 30 | console.log('File is deleted.'); 31 | } 32 | }); 33 | }, 34 | ); 35 | }); 36 | }; 37 | 38 | const storage = multer.diskStorage({ 39 | destination: function (req, file, cb) { 40 | cb(null, process.cwd() + '/uploads/'); 41 | }, 42 | filename: function (req, file, cb) { 43 | const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); 44 | cb(null, file.fieldname + '-' + uniqueSuffix); 45 | }, 46 | }); 47 | 48 | export const upload = multer({ storage: storage }); 49 | -------------------------------------------------------------------------------- /src/app/utils/sendResponse.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express'; 2 | 3 | type TMeta = { 4 | limit: number; 5 | page: number; 6 | total: number; 7 | totalPage: number; 8 | }; 9 | 10 | type TResponse = { 11 | statusCode: number; 12 | success: boolean; 13 | message?: string; 14 | meta?: TMeta; 15 | data: T; 16 | }; 17 | 18 | const sendResponse = (res: Response, data: TResponse) => { 19 | res.status(data?.statusCode).json({ 20 | success: data.success, 21 | message: data.message, 22 | meta: data.meta, 23 | data: data.data, 24 | }); 25 | }; 26 | 27 | export default sendResponse; 28 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'http'; 2 | import mongoose from 'mongoose'; 3 | import app from './app'; 4 | import seedSuperAdmin from './app/DB'; 5 | import config from './app/config'; 6 | 7 | let server: Server; 8 | 9 | async function main() { 10 | try { 11 | await mongoose.connect(config.database_url as string); 12 | 13 | seedSuperAdmin(); 14 | server = app.listen(config.port, () => { 15 | console.log(`app is listening on port ${config.port}`); 16 | }); 17 | } catch (err) { 18 | console.log(err); 19 | } 20 | } 21 | 22 | main(); 23 | 24 | process.on('unhandledRejection', (err) => { 25 | console.log(`😈 unahandledRejection is detected , shutting down ...`, err); 26 | if (server) { 27 | server.close(() => { 28 | process.exit(1); 29 | }); 30 | } 31 | process.exit(1); 32 | }); 33 | 34 | process.on('uncaughtException', () => { 35 | console.log(`😈 uncaughtException is detected , shutting down ...`); 36 | process.exit(1); 37 | }); 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], // which files to compile 3 | "exclude": ["node_modules"], // which files to skip 4 | "compilerOptions": { 5 | /* Visit https://aka.ms/tsconfig to read more about this file */ 6 | 7 | /* Projects */ 8 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 9 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 10 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 11 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 12 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 13 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 14 | 15 | /* Language and Environment */ 16 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 17 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 18 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 19 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 20 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 21 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 22 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 23 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 24 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 25 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 26 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 27 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 28 | 29 | /* Modules */ 30 | "module": "commonjs" /* Specify what module code is generated. */, 31 | "rootDir": "./src" /* Specify the root folder within your source files. */, 32 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 33 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 34 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 35 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 36 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 37 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 38 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 39 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 40 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 41 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 42 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 43 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 44 | // "resolveJsonModule": true, /* Enable importing .json files. */ 45 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 46 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 47 | 48 | /* JavaScript Support */ 49 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 50 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 51 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 52 | 53 | /* Emit */ 54 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 55 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 56 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 57 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 60 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 61 | // "removeComments": true, /* Disable emitting comments. */ 62 | // "noEmit": true, /* Disable emitting files from a compilation. */ 63 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 64 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 65 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 66 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 67 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 68 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 69 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 70 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 71 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 72 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 73 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 74 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 75 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 76 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 77 | 78 | /* Interop Constraints */ 79 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 80 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 81 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 82 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 83 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 84 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 85 | 86 | /* Type Checking */ 87 | "strict": true /* Enable all strict type-checking options. */, 88 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 89 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 90 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 91 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 92 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 93 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 94 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 95 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 96 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 97 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 98 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 99 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 100 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 101 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 102 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 103 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 104 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 105 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 106 | 107 | /* Completeness */ 108 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 109 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 110 | } 111 | } 112 | --------------------------------------------------------------------------------