├── .gitignore ├── .dockerignore ├── src ├── templates │ ├── certificateTemplate.pdf │ ├── emailTemplate.ejs │ └── passwordresetEmailTemplate.ejs ├── schemas │ ├── email-routes.schema.ts │ ├── admin │ │ ├── admin.category-routes.schema.ts │ │ ├── admin.mentee-routes.schema.ts │ │ └── admin.mentor-routes.schema.ts │ ├── common │ │ └── pagination-request.schema.ts │ ├── auth-routes.schems.ts │ ├── profile-routes.schema.ts │ ├── mentor-routes.schema.ts │ └── mentee-routes.schemas.ts ├── routes │ ├── country │ │ └── country.route.ts │ ├── category │ │ └── category.route.ts │ ├── admin │ │ ├── admin.route.ts │ │ ├── user │ │ │ └── user.route.ts │ │ ├── category │ │ │ └── category.route.ts │ │ ├── mentee │ │ │ └── mentee.route.ts │ │ └── mentor │ │ │ └── mentor.route.ts │ ├── emails │ │ └── emails.route.ts │ ├── profile │ │ └── profile.route.ts │ ├── auth │ │ └── auth.route.ts │ ├── mentor │ │ └── mentor.route.ts │ └── mentee │ │ └── mentee.route.ts ├── services │ ├── admin.service.ts │ ├── country.service.ts │ ├── admin │ │ ├── user.service.ts │ │ ├── generateCertificate.ts │ │ ├── user.service.test.ts │ │ ├── category.service.ts │ │ ├── email.service.ts │ │ ├── category.service.test.ts │ │ ├── mentee.service.test.ts │ │ ├── mentor_admin.service.test.ts │ │ ├── mentee.service.ts │ │ └── mentor.service.ts │ ├── category.service.ts │ ├── country.service.test.ts │ ├── admin.service.test.ts │ ├── category.service.test.ts │ ├── profile.service.ts │ ├── monthlyChecking.service.ts │ ├── mentee.service.ts │ ├── profile.service.test.ts │ ├── auth.service.test.ts │ ├── auth.service.ts │ ├── mentor.service.test.ts │ └── mentor.service.ts ├── server.ts ├── entities │ ├── category.entity.ts │ ├── country.entity.ts │ ├── email.entity.ts │ ├── baseEntity.ts │ ├── mentor.entity.ts │ ├── profile.entity.ts │ ├── mentee.entity.ts │ └── checkin.entity.ts ├── configs │ ├── dbConfig.ts │ ├── envConfig.ts │ ├── google-passport.ts │ └── linkedin-passport.ts ├── migrations │ ├── 1727636762101-monthlychecking.ts │ ├── 1733854881851-monthly-checking-rename.ts │ ├── 1720590807560-profile-lastName-nullable.ts │ ├── 1726930041488-UpdateMentorTableWithCountry.ts │ ├── 1726849469636-CreateCountryTable.ts │ ├── 1722051742722-RemoveUniqueConstraintFromProfileUuid.ts │ └── 1722749907154-AddNewApplicationStates.ts ├── enums │ └── index.ts ├── controllers │ ├── category.controller.ts │ ├── country.controller.ts │ ├── admin │ │ ├── email.controller.ts │ │ ├── user.controller.ts │ │ ├── category.controller.ts │ │ └── mentee.controller.ts │ ├── monthlyChecking.controller.ts │ ├── mentee.controller.ts │ ├── profile.controller.ts │ ├── mentor.controller.ts │ └── auth.controller.ts ├── types.ts ├── middlewares │ └── requestValidator.ts ├── app.ts └── scripts │ ├── seed-db.ts │ └── countries.json ├── .prettierrc ├── jest.config.ts ├── tsconfig.json ├── init-db.sh ├── .github ├── ISSUE_TEMPLATE │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── CI.yml ├── .env.example ├── .eslintrc.json ├── Dockerfile ├── docker-compose.yml ├── LICENSE ├── mocks.ts └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | dist/ 4 | .idea 5 | uploads/ 6 | certificates/ 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | dist 4 | .git 5 | *.md 6 | .gitignore 7 | .env 8 | -------------------------------------------------------------------------------- /src/templates/certificateTemplate.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sef-global/scholarx-backend/HEAD/src/templates/certificateTemplate.pdf -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "endOfLine": "auto" 7 | } 8 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | modulePathIgnorePatterns: ['/dist/'] 5 | } 6 | -------------------------------------------------------------------------------- /src/schemas/email-routes.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const sendEmailSchema = z.object({ 4 | subject: z.string(), 5 | to: z.string(), 6 | text: z.string() 7 | }) 8 | -------------------------------------------------------------------------------- /src/routes/country/country.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { getCountries } from '../../controllers/country.controller' 3 | 4 | const countryRouter = express.Router() 5 | 6 | countryRouter.get('/', getCountries) 7 | 8 | export default countryRouter 9 | -------------------------------------------------------------------------------- /src/schemas/admin/admin.category-routes.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const addCategorySchema = z.object({ 4 | categoryName: z.string() 5 | }) 6 | 7 | export const updateCategorySchema = z.object({ 8 | categoryName: z.string() 9 | }) 10 | -------------------------------------------------------------------------------- /src/routes/category/category.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { getCategories } from '../../controllers/category.controller' 3 | 4 | const categoryRouter = express.Router() 5 | 6 | categoryRouter.get('/', getCategories) 7 | 8 | export default categoryRouter 9 | -------------------------------------------------------------------------------- /src/schemas/common/pagination-request.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const paginationSchema = z.object({ 4 | pageNumber: z.coerce.number().int().positive(), 5 | pageSize: z.coerce.number().int().positive().max(100) 6 | }) 7 | 8 | export type PaginationRequest = z.infer 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true 10 | }, 11 | "include": ["src/**/*.ts", "test/", "jest.config.ts","mocks.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /src/schemas/auth-routes.schems.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const registerSchema = z.object({ 4 | email: z.string().email(), 5 | password: z.string(), 6 | first_name: z.string(), 7 | last_name: z.string() 8 | }) 9 | 10 | export const loginSchema = z.object({ 11 | email: z.string().email(), 12 | password: z.string() 13 | }) 14 | -------------------------------------------------------------------------------- /src/services/admin.service.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../configs/dbConfig' 2 | import Profile from '../entities/profile.entity' 3 | 4 | export const getAllUsers = async (): Promise => { 5 | const profileRepository = dataSource.getRepository(Profile) 6 | const allUsers = await profileRepository.find() 7 | return allUsers 8 | } 9 | -------------------------------------------------------------------------------- /src/schemas/profile-routes.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const updateProfileSchema = z.object({ 4 | primary_email: z.string().email().optional(), 5 | first_name: z.string().optional(), 6 | last_name: z.string().optional(), 7 | image_url: z.string().url().optional() 8 | }) 9 | 10 | export const getApplicationsSchema = z.object({ 11 | type: z.enum(['mentor', 'mentee']) 12 | }) 13 | -------------------------------------------------------------------------------- /src/schemas/mentor-routes.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { MentorApplicationStatus } from '../enums' 3 | 4 | export const mentorApplicationSchema = z.object({ 5 | application: z.record(z.unknown()), 6 | categoryId: z.string(), 7 | countryId: z.string().optional() 8 | }) 9 | 10 | export const getMenteesByMentorSchema = z.object({ 11 | status: z.nativeEnum(MentorApplicationStatus).or(z.undefined()) 12 | }) 13 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import startServer from './app' 2 | import { SERVER_PORT } from './configs/envConfig' 3 | 4 | const port = SERVER_PORT as number 5 | 6 | async function start(): Promise { 7 | try { 8 | await startServer(port) 9 | console.log('Server started!') 10 | } catch (err) { 11 | console.error('Something went wrong!', err) 12 | } 13 | } 14 | 15 | start().catch((err) => { 16 | console.error(err) 17 | }) 18 | -------------------------------------------------------------------------------- /init-db.sh: -------------------------------------------------------------------------------- 1 | # This is a script to initialize the database for the first time when the container is started. 2 | # It will wait for the database to be ready before running the migrations. 3 | # Wait for the 4 | 5 | echo "Database is ready. Running migrations..." 6 | 7 | # Run the migrations 8 | npm run sync:db 9 | npm run seed 10 | 11 | echo "Migrations complete. Database is ready." 12 | 13 | # Start the application 14 | 15 | npm run dev 16 | -------------------------------------------------------------------------------- /src/schemas/admin/admin.mentee-routes.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { MenteeApplicationStatus } from '../../enums' 3 | 4 | export const getAllMenteeEmailsSchema = z.object({ 5 | status: z.nativeEnum(MenteeApplicationStatus) 6 | }) 7 | 8 | export const getMenteesSchema = z.object({ 9 | state: z.nativeEnum(MenteeApplicationStatus).optional() 10 | }) 11 | 12 | export const updateMenteeStatusSchema = z.object({ 13 | state: z.nativeEnum(MenteeApplicationStatus) 14 | }) 15 | -------------------------------------------------------------------------------- /src/services/country.service.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../configs/dbConfig' 2 | import Country from '../entities/country.entity' 3 | 4 | export const getAllCountries = async (): Promise => { 5 | const countryRepository = dataSource.getRepository(Country) 6 | const countries: Country[] = await countryRepository.find({ 7 | select: ['uuid', 'code', 'name'] 8 | }) 9 | if (countries && countries.length > 0) { 10 | return countries 11 | } 12 | return null 13 | } 14 | -------------------------------------------------------------------------------- /src/entities/category.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, OneToMany } from 'typeorm' 2 | import BaseEntity from './baseEntity' 3 | import Mentor from './mentor.entity' 4 | @Entity('category') 5 | class Category extends BaseEntity { 6 | @Column({ type: 'varchar', length: 255 }) 7 | category: string 8 | 9 | @OneToMany(() => Mentor, (mentor) => mentor.category) 10 | mentors?: Mentor[] 11 | 12 | constructor(category: string) { 13 | super() 14 | this.category = category 15 | } 16 | } 17 | 18 | export default Category 19 | -------------------------------------------------------------------------------- /src/routes/admin/admin.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import userRouter from './user/user.route' 3 | import mentorRouter from './mentor/mentor.route' 4 | import categoryRouter from './category/category.route' 5 | import menteeRouter from './mentee/mentee.route' 6 | 7 | const adminRouter = express() 8 | 9 | adminRouter.use('/users', userRouter) 10 | adminRouter.use('/mentors', mentorRouter) 11 | adminRouter.use('/mentees', menteeRouter) 12 | adminRouter.use('/categories', categoryRouter) 13 | 14 | export default adminRouter 15 | -------------------------------------------------------------------------------- /src/configs/dbConfig.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { DataSource } from 'typeorm' 3 | import { DB_HOST, DB_NAME, DB_PASSWORD, DB_PORT, DB_USER } from './envConfig' 4 | 5 | export const dataSource = new DataSource({ 6 | type: 'postgres', 7 | host: DB_HOST, 8 | port: Number(DB_PORT) ?? 5432, 9 | username: DB_USER, 10 | password: DB_PASSWORD, 11 | database: DB_NAME, 12 | entities: [path.join(__dirname, '..', '**', '*.entity{.ts,.js}')], 13 | migrations: ['dist/src/migrations/*.js'], 14 | logging: false, 15 | synchronize: false 16 | }) 17 | -------------------------------------------------------------------------------- /src/entities/country.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, OneToMany } from 'typeorm' 2 | import BaseEntity from './baseEntity' 3 | import Mentor from './mentor.entity' 4 | 5 | @Entity() 6 | export class Country extends BaseEntity { 7 | @Column() 8 | code: string 9 | 10 | @Column() 11 | name: string 12 | 13 | @OneToMany(() => Mentor, (mentor) => mentor.country) 14 | mentors?: Mentor[] 15 | 16 | constructor(code: string, name: string) { 17 | super() 18 | this.code = code 19 | this.name = name 20 | } 21 | } 22 | 23 | export default Country 24 | -------------------------------------------------------------------------------- /src/migrations/1727636762101-monthlychecking.ts: -------------------------------------------------------------------------------- 1 | import { type MigrationInterface, type QueryRunner } from 'typeorm' 2 | 3 | export class Monthlychecking1727636762101 implements MigrationInterface { 4 | name = 'Monthlychecking1727636762101' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "monthly-check-in" DROP COLUMN "tags"`) 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "monthly-check-in" ADD "tags" json`) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/routes/emails/emails.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { sendEmailController } from '../../controllers/admin/email.controller' 3 | import { requireAuth } from '../../controllers/auth.controller' 4 | import { requestBodyValidator } from '../../middlewares/requestValidator' 5 | import { sendEmailSchema } from '../../schemas/email-routes.schema' 6 | 7 | const emailRouter = express.Router() 8 | 9 | emailRouter.post( 10 | '/send', 11 | [requireAuth, requestBodyValidator(sendEmailSchema)], 12 | sendEmailController 13 | ) 14 | 15 | export default emailRouter 16 | -------------------------------------------------------------------------------- /src/routes/admin/user/user.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { getAllUsersHandler } from '../../../controllers/admin/user.controller' 3 | import { requireAuth } from '../../../controllers/auth.controller' 4 | import { requestQueryValidator } from '../../../middlewares/requestValidator' 5 | import { paginationSchema } from '../../../schemas/common/pagination-request.schema' 6 | 7 | const userRouter = express.Router() 8 | 9 | userRouter.get( 10 | '/', 11 | [requireAuth, requestQueryValidator(paginationSchema)], 12 | getAllUsersHandler 13 | ) 14 | 15 | export default userRouter 16 | -------------------------------------------------------------------------------- /src/migrations/1733854881851-monthly-checking-rename.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class MonthlyCheckingRename1733854881851 implements MigrationInterface { 4 | name = 'MonthlyCheckingRename1733854881851' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "monthly-check-in" RENAME COLUMN "title" TO "month"`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "monthly-check-in" RENAME COLUMN "month" TO "title"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Description:** 11 | A clear and concise description of the implementation. 12 | 13 | **Tasks:** 14 | Add the tasks need to complete to achieve the implementation. 15 | 16 | **Acceptance Criteria:** 17 | Implementation should be able to perform these functionalities. 18 | 19 | **Additional Information:** 20 | Add additional information 21 | 22 | **Related Dependencies or References:** 23 | Add related dependencies and references relating to the implementation. 24 | -------------------------------------------------------------------------------- /src/enums/index.ts: -------------------------------------------------------------------------------- 1 | export enum ProfileTypes { 2 | DEFAULT = 'default', 3 | ADMIN = 'admin' 4 | } 5 | 6 | export enum EmailStatusTypes { 7 | SENT = 'sent', 8 | DELIVERED = 'delivered', 9 | FAILED = 'failed' 10 | } 11 | 12 | export enum MentorApplicationStatus { 13 | PENDING = 'pending', 14 | REJECTED = 'rejected', 15 | APPROVED = 'approved' 16 | } 17 | 18 | export enum MenteeApplicationStatus { 19 | PENDING = 'pending', 20 | REJECTED = 'rejected', 21 | APPROVED = 'approved', 22 | COMPLETED = 'completed', 23 | REVOKED = 'revoked' 24 | } 25 | 26 | export enum StatusUpdatedBy { 27 | ADMIN = 'admin', 28 | MENTOR = 'mentor' 29 | } 30 | -------------------------------------------------------------------------------- /src/migrations/1720590807560-profile-lastName-nullable.ts: -------------------------------------------------------------------------------- 1 | import { type MigrationInterface, type QueryRunner } from 'typeorm' 2 | 3 | export class ProfileLastNameNullable1720590807560 4 | implements MigrationInterface 5 | { 6 | name = 'ProfileLastNameNullable1720590807560' 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "profile" ALTER COLUMN "last_name" DROP NOT NULL` 11 | ) 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "profile" ALTER COLUMN "last_name" SET NOT NULL` 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DB_USER=your_db_user 2 | DB_HOST=your_db_host 3 | DB_NAME=your_db_name 4 | DB_PASSWORD=your_db_password 5 | DB_PORT=your_db_port_number 6 | SERVER_PORT=your_server_port_number 7 | JWT_SECRET=your_jwt_secret_key 8 | GOOGLE_CLIENT_ID='your_google_client_id' 9 | GOOGLE_CLIENT_SECRET='your_google_client_secret' 10 | GOOGLE_REDIRECT_URL=http://localhost:${SERVER_PORT}/api/auth/google/callback 11 | CLIENT_URL=http://localhost:5173 12 | IMG_HOST=http://localhost:${SERVER_PORT} 13 | SMTP_MAIL=your_smtp_mail 14 | SMTP_PASSWORD=your_smtp_password 15 | LINKEDIN_CLIENT_ID=your_linkedin_client_id 16 | LINKEDIN_CLIENT_SECRET=your_linkedin_client_secret 17 | LINKEDIN_REDIRECT_URL=http://localhost:${SERVER_PORT}/api/auth/linkedin/callback 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "standard-with-typescript", 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:prettier/recommended" 11 | ], 12 | "plugins": ["@typescript-eslint", "prettier", "unused-imports"], 13 | "parserOptions": { 14 | "ecmaVersion": "latest", 15 | "sourceType": "module", 16 | "project": "./tsconfig.json" 17 | }, 18 | "rules": { 19 | "@typescript-eslint/naming-convention": 0, 20 | "@typescript-eslint/no-misused-promises": 0, 21 | "@typescript-eslint/restrict-plus-operands": 0, 22 | "@typescript-eslint/strict-boolean-expressions": 0, 23 | "unused-imports/no-unused-imports": "error" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/services/admin/user.service.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../../configs/dbConfig' 2 | import Profile from '../../entities/profile.entity' 3 | import { type PaginatedApiResponse } from '../../types' 4 | 5 | export const getAllUsers = async ({ 6 | pageNumber, 7 | pageSize 8 | }: { 9 | pageNumber: number 10 | pageSize: number 11 | }): Promise> => { 12 | const profileRepository = dataSource.getRepository(Profile) 13 | const [users, count] = await profileRepository.findAndCount({ 14 | skip: (pageNumber - 1) * pageSize, 15 | take: pageSize 16 | }) 17 | return { 18 | pageNumber, 19 | pageSize, 20 | totalItemCount: count, 21 | items: users, 22 | statusCode: 200, 23 | message: 'Users retrieved successfully' 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/routes/admin/category/category.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { 3 | addCategory, 4 | updateCategory 5 | } from '../../../controllers/admin/category.controller' 6 | import { requireAuth } from '../../../controllers/auth.controller' 7 | import { requestBodyValidator } from '../../../middlewares/requestValidator' 8 | import { 9 | addCategorySchema, 10 | updateCategorySchema 11 | } from '../../../schemas/admin/admin.category-routes.schema' 12 | 13 | const categoryRouter = express.Router() 14 | 15 | categoryRouter.post( 16 | '/', 17 | [requireAuth, requestBodyValidator(addCategorySchema)], 18 | addCategory 19 | ) 20 | categoryRouter.put( 21 | '/:categoryId', 22 | [requireAuth, requestBodyValidator(updateCategorySchema)], 23 | updateCategory 24 | ) 25 | 26 | export default categoryRouter 27 | -------------------------------------------------------------------------------- /src/controllers/category.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from 'express' 2 | import { getAllCategories } from '../services/category.service' 3 | import type { ApiResponse } from '../types' 4 | import type Category from '../entities/category.entity' 5 | 6 | export const getCategories = async ( 7 | req: Request, 8 | res: Response 9 | ): Promise> => { 10 | try { 11 | const { statusCode, categories, message } = await getAllCategories() 12 | 13 | return res.status(statusCode).json({ categories, message }) 14 | } catch (err) { 15 | if (err instanceof Error) { 16 | console.error('Error executing query', err) 17 | return res 18 | .status(500) 19 | .json({ error: 'Internal server error', message: err.message }) 20 | } 21 | 22 | throw err 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/entities/email.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm' 2 | import { EmailStatusTypes } from '../enums' 3 | import BaseEntity from './baseEntity' 4 | 5 | @Entity('email') 6 | class Email extends BaseEntity { 7 | @Column({ type: 'varchar', length: 255 }) 8 | recipient: string 9 | 10 | @Column({ type: 'varchar', length: 255 }) 11 | subject: string 12 | 13 | @Column('varchar') 14 | content: string 15 | 16 | @Column({ type: 'enum', enum: EmailStatusTypes }) 17 | state: EmailStatusTypes 18 | 19 | constructor( 20 | recipient: string, 21 | subject: string, 22 | content: string, 23 | state: EmailStatusTypes 24 | ) { 25 | super() 26 | this.recipient = recipient 27 | this.subject = subject 28 | this.content = content 29 | this.state = state 30 | } 31 | } 32 | 33 | export default Email 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Node.js runtime as the base image 2 | FROM node:18 3 | 4 | # Set the working directory in the Docker container to /app 5 | WORKDIR /app/src 6 | 7 | # Copy package.json and package-lock.json to the working directory 8 | COPY package*.json ./ 9 | 10 | # Install any needed packages specified in package.json 11 | RUN npm install 12 | 13 | # Bundle the app source inside the Docker image 14 | # (assuming your app is in the "src" directory of your project) 15 | COPY . . 16 | 17 | # Copy the init-db.sh script to the working directory 18 | COPY init-db.sh /app/src/init-db.sh 19 | 20 | # Make the init-db.sh script executable 21 | RUN chmod +x /app/src/init-db.sh 22 | # Make port 8080 available to the world outside this container 23 | EXPOSE 8080 24 | 25 | # Run the app when the container launches 26 | 27 | CMD [ "sh", "init-db.sh"] 28 | -------------------------------------------------------------------------------- /src/entities/baseEntity.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'crypto' 2 | import { 3 | BeforeInsert, 4 | BeforeUpdate, 5 | Column, 6 | PrimaryGeneratedColumn 7 | } from 'typeorm' 8 | 9 | class BaseEntity { 10 | @PrimaryGeneratedColumn('uuid') 11 | uuid!: string 12 | 13 | @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) 14 | created_at: Date | undefined 15 | 16 | @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) 17 | updated_at: Date | undefined 18 | 19 | @BeforeInsert() 20 | @BeforeUpdate() 21 | updateTimestamps(): void { 22 | this.updated_at = new Date() 23 | if (!this.uuid) { 24 | this.created_at = new Date() 25 | } 26 | } 27 | 28 | @BeforeInsert() 29 | async generateUuid(): Promise { 30 | if (!this.uuid) { 31 | this.uuid = randomUUID() 32 | } 33 | } 34 | } 35 | 36 | export default BaseEntity 37 | -------------------------------------------------------------------------------- /src/controllers/country.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from 'express' 2 | import type { ApiResponse } from '../types' 3 | import type Country from '../entities/country.entity' 4 | import { getAllCountries } from '../services/country.service' 5 | 6 | export const getCountries = async ( 7 | req: Request, 8 | res: Response 9 | ): Promise> => { 10 | try { 11 | const data = await getAllCountries() 12 | if (!data) { 13 | return res.status(404).json({ message: 'Countries Not Found' }) 14 | } 15 | return res.status(200).json({ data, message: 'Countries Found' }) 16 | } catch (err) { 17 | if (err instanceof Error) { 18 | console.error('Error executing query', err) 19 | return res 20 | .status(500) 21 | .json({ error: 'Internal server error', message: err.message }) 22 | } 23 | throw err 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/migrations/1726930041488-UpdateMentorTableWithCountry.ts: -------------------------------------------------------------------------------- 1 | import { type MigrationInterface, type QueryRunner } from 'typeorm' 2 | 3 | export class UpdateMentorTableWithCountry1726930041488 4 | implements MigrationInterface 5 | { 6 | name = 'UpdateMentorTableWithCountry1726930041488' 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query(`ALTER TABLE "mentor" ADD "countryUuid" uuid`) 10 | await queryRunner.query( 11 | `ALTER TABLE "mentor" ADD CONSTRAINT "FK_3302c22eb1636f239d605eb61c3" FOREIGN KEY ("countryUuid") REFERENCES "country"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION` 12 | ) 13 | } 14 | 15 | public async down(queryRunner: QueryRunner): Promise { 16 | await queryRunner.query( 17 | `ALTER TABLE "mentor" DROP CONSTRAINT "FK_3302c22eb1636f239d605eb61c3"` 18 | ) 19 | await queryRunner.query(`ALTER TABLE "mentor" DROP COLUMN "countryUuid"`) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/schemas/admin/admin.mentor-routes.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { MentorApplicationStatus } from '../../enums' 3 | import { updateProfileSchema } from '../profile-routes.schema' 4 | 5 | export const mentorStatusSchema = z.object({ 6 | state: z.nativeEnum(MentorApplicationStatus) 7 | }) 8 | 9 | export const getAllMentorsByStatusSchema = z.object({ 10 | status: z.nativeEnum(MentorApplicationStatus).or(z.undefined()) 11 | }) 12 | 13 | export const getAllMentorEmailsSchema = z.object({ 14 | status: z.nativeEnum(MentorApplicationStatus).or(z.undefined()) 15 | }) 16 | 17 | export const updateMentorAvailabilitySchema = z.object({ 18 | availability: z.boolean() 19 | }) 20 | 21 | export const searchMentorsSchema = z.object({ 22 | q: z.string().or(z.undefined()) 23 | }) 24 | 25 | export const mentorUpdateSchema = z.object({ 26 | availability: z.boolean().optional(), 27 | application: z.record(z.string(), z.any()).optional(), 28 | category: z.string().uuid().optional(), 29 | profile: updateProfileSchema.optional() 30 | }) 31 | -------------------------------------------------------------------------------- /src/routes/profile/profile.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { requireAuth } from '../../controllers/auth.controller' 3 | import { 4 | deleteProfileHandler, 5 | getApplicationsHandler, 6 | getProfileHandler, 7 | updateProfileHandler 8 | } from '../../controllers/profile.controller' 9 | import { 10 | requestBodyValidator, 11 | requestQueryValidator 12 | } from '../../middlewares/requestValidator' 13 | import { 14 | getApplicationsSchema, 15 | updateProfileSchema 16 | } from '../../schemas/profile-routes.schema' 17 | 18 | const profileRouter = express.Router() 19 | 20 | profileRouter.get('/profile', requireAuth, getProfileHandler) 21 | profileRouter.put( 22 | '/profile', 23 | [requireAuth, requestBodyValidator(updateProfileSchema)], 24 | updateProfileHandler 25 | ) 26 | profileRouter.delete('/profile', requireAuth, deleteProfileHandler) 27 | profileRouter.get( 28 | '/applications', 29 | [requireAuth, requestQueryValidator(getApplicationsSchema)], 30 | getApplicationsHandler 31 | ) 32 | 33 | export default profileRouter 34 | -------------------------------------------------------------------------------- /src/services/category.service.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../configs/dbConfig' 2 | import Category from '../entities/category.entity' 3 | 4 | export const getAllCategories = async (): Promise<{ 5 | statusCode: number 6 | categories?: Array> | null 7 | message: string 8 | }> => { 9 | try { 10 | const categoryRepository = dataSource.getRepository(Category) 11 | const allCategories: Category[] = await categoryRepository.find({ 12 | select: ['category', 'uuid'] 13 | }) 14 | 15 | const categories = allCategories.map((category) => { 16 | return { category: category.category, uuid: category.uuid } 17 | }) 18 | 19 | if (!categories) { 20 | return { 21 | statusCode: 404, 22 | message: 'Categories not found' 23 | } 24 | } 25 | 26 | return { 27 | statusCode: 200, 28 | categories, 29 | message: 'All Categories found' 30 | } 31 | } catch (err) { 32 | console.error('Error getting mentor', err) 33 | throw new Error('Error getting mentor') 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: . 5 | ports: 6 | - "${SERVER_PORT}:3000" 7 | depends_on: 8 | - db 9 | environment: 10 | - DB_HOST=db 11 | - DB_PORT=5432 12 | - DB_USER=${DB_USER} 13 | - DB_PASSWORD=${DB_PASSWORD} 14 | - DB_NAME=${DB_NAME} 15 | - JWT_SECRET=${JWT_SECRET} 16 | - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} 17 | - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} 18 | - GOOGLE_REDIRECT_URL=${GOOGLE_REDIRECT_URL} 19 | - LINKEDIN_CLIENT_ID=${LINKEDIN_CLIENT_ID} 20 | - LINKEDIN_CLIENT_SECRET=${LINKEDIN_CLIENT_SECRET} 21 | - LINKEDIN_REDIRECT_URL=${LINKEDIN_REDIRECT_URL} 22 | - CLIENT_URL=${CLIENT_URL} 23 | - IMG_HOST=${IMG_HOST} 24 | - SMTP_MAIL=${SMTP_MAIL} 25 | - SMTP_PASSWORD=${SMTP_PASSWORD} 26 | command: ["sh", "/app/src/init-db.sh"] 27 | db: 28 | image: postgres:15 29 | ports: 30 | - "5432:5432" 31 | environment: 32 | - POSTGRES_USER=${DB_USER} 33 | - POSTGRES_PASSWORD=${DB_PASSWORD} 34 | - POSTGRES_DB=${DB_NAME} -------------------------------------------------------------------------------- /src/configs/envConfig.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | 3 | dotenv.config() 4 | 5 | export const DB_USER = process.env.DB_USER 6 | export const DB_HOST = process.env.DB_HOST 7 | export const DB_NAME = process.env.DB_NAME 8 | export const DB_PASSWORD = process.env.DB_PASSWORD 9 | export const DB_PORT = process.env.DB_PORT 10 | export const SERVER_PORT = process.env.SERVER_PORT ?? 3000 11 | export const JWT_SECRET = process.env.JWT_SECRET ?? '' 12 | export const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID ?? '' 13 | export const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET ?? '' 14 | export const GOOGLE_REDIRECT_URL = process.env.GOOGLE_REDIRECT_URL ?? '' 15 | export const CLIENT_URL = process.env.CLIENT_URL ?? '' 16 | export const IMG_HOST = process.env.IMG_HOST ?? '' 17 | export const SMTP_MAIL = process.env.SMTP_MAIL ?? '' 18 | export const SMTP_PASSWORD = process.env.SMTP_PASSWORD ?? '' 19 | export const LINKEDIN_CLIENT_ID = process.env.LINKEDIN_CLIENT_ID ?? '' 20 | export const LINKEDIN_CLIENT_SECRET = process.env.LINKEDIN_CLIENT_SECRET ?? '' 21 | export const LINKEDIN_REDIRECT_URL = process.env.LINKEDIN_REDIRECT_URL ?? '' 22 | -------------------------------------------------------------------------------- /src/services/admin/generateCertificate.ts: -------------------------------------------------------------------------------- 1 | import { PDFDocument, rgb } from 'pdf-lib' 2 | import fs from 'fs' 3 | 4 | export async function generateCertificate( 5 | menteeName: string, 6 | sourcePdfPath: string, 7 | outputPath: string 8 | ): Promise { 9 | try { 10 | const existingPdfBytes = fs.readFileSync(sourcePdfPath) 11 | const pdfDoc = await PDFDocument.load(existingPdfBytes) 12 | const page = pdfDoc.getPage(0) 13 | const fontSize = 24 14 | const datezFontSize = 18 15 | 16 | page.drawText(menteeName, { 17 | x: 66, 18 | y: page.getHeight() - 220, 19 | size: fontSize, 20 | color: rgb(0, 0, 0) 21 | }) 22 | 23 | const issueDate = new Date().toLocaleDateString() 24 | 25 | page.drawText(issueDate, { 26 | x: 160, 27 | y: page.getHeight() - 476, 28 | size: datezFontSize, 29 | color: rgb(0, 0, 0) 30 | }) 31 | 32 | const pdfBytes = await pdfDoc.save() 33 | 34 | fs.writeFileSync(outputPath, pdfBytes) 35 | return outputPath 36 | } catch (error) { 37 | console.error('Failed to modify the PDF:', error) 38 | throw error 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sustainable Education Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/controllers/admin/email.controller.ts: -------------------------------------------------------------------------------- 1 | import { type Request, type Response } from 'express' 2 | import type Profile from '../../entities/profile.entity' 3 | import { type ApiResponse } from '../../types' 4 | import type Email from '../../entities/email.entity' 5 | import { ProfileTypes } from '../../enums' 6 | import { sendEmail } from '../../services/admin/email.service' 7 | 8 | export const sendEmailController = async ( 9 | req: Request, 10 | res: Response 11 | ): Promise> => { 12 | const { to, subject, text } = req.body 13 | 14 | try { 15 | const user = req.user as Profile 16 | 17 | if (user.type !== ProfileTypes.ADMIN) { 18 | return res.status(403).json({ message: 'Only Admins are allowed' }) 19 | } 20 | 21 | const { statusCode, message } = await sendEmail(to, subject, text) 22 | return res.status(statusCode).json({ message }) 23 | } catch (err) { 24 | if (err instanceof Error) { 25 | console.error('Error executing query', err) 26 | return res 27 | .status(500) 28 | .json({ error: 'Internal server error', message: err.message }) 29 | } 30 | throw err 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type Mentee from './entities/mentee.entity' 2 | 3 | export interface ApiResponse { 4 | statusCode: number 5 | message?: string 6 | data?: T | null 7 | } 8 | 9 | export interface PaginatedApiResponse { 10 | pageNumber: number 11 | pageSize: number 12 | totalItemCount: number 13 | items: TItem[] 14 | statusCode: number 15 | message: string 16 | } 17 | 18 | export interface User extends Express.User { 19 | primary_email: string 20 | } 21 | 22 | export interface CreateProfile { 23 | primary_email: string 24 | id: string 25 | first_name: string 26 | last_name: string 27 | image_url: string 28 | } 29 | 30 | export interface LinkedInProfile { 31 | id: string 32 | givenName: string 33 | familyName: string 34 | picture: string 35 | email: string 36 | } 37 | 38 | export interface MonthlyCheckInResponse { 39 | uuid: string 40 | month: string 41 | generalUpdatesAndFeedback: string 42 | progressTowardsGoals: string 43 | mediaContentLinks: string[] 44 | mentorFeedback: string | null 45 | isCheckedByMentor: boolean 46 | mentorCheckedDate: Date | null 47 | checkInDate: Date 48 | mentee: Mentee 49 | } 50 | -------------------------------------------------------------------------------- /src/schemas/mentee-routes.schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { MenteeApplicationStatus } from '../enums' 3 | 4 | export const menteeApplicationSchema = z.object({ 5 | application: z.record(z.unknown()), 6 | mentorId: z.string() 7 | }) 8 | 9 | export const updateMenteeStatusSchema = z.object({ 10 | state: z.nativeEnum(MenteeApplicationStatus) 11 | }) 12 | 13 | export const postMonthlyCheckInSchema = z.object({ 14 | menteeId: z.string(), 15 | month: z.enum([ 16 | 'January', 17 | 'February', 18 | 'March', 19 | 'April', 20 | 'May', 21 | 'June', 22 | 'July', 23 | 'August', 24 | 'September', 25 | 'October', 26 | 'November', 27 | 'December' 28 | ]), 29 | generalUpdatesAndFeedback: z 30 | .string() 31 | .min(1, 'Please provide a valid feedback'), 32 | progressTowardsGoals: z 33 | .string() 34 | .min(1, 'Please provide a valid progress report'), 35 | mediaContentLinks: z.array(z.string()).optional() 36 | }) 37 | 38 | export const addFeedbackMonthlyCheckInSchema = z.object({ 39 | menteeId: z.string(), 40 | checkInId: z.string(), 41 | mentorFeedback: z.string().optional(), 42 | isCheckedByMentor: z.boolean() 43 | }) 44 | -------------------------------------------------------------------------------- /src/routes/auth/auth.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import passport from 'passport' 3 | import { 4 | googleRedirect, 5 | linkedinRedirect, 6 | login, 7 | logout, 8 | passwordReset, 9 | passwordResetRequest, 10 | register 11 | } from '../../controllers/auth.controller' 12 | import { requestBodyValidator } from '../../middlewares/requestValidator' 13 | import { loginSchema, registerSchema } from '../../schemas/auth-routes.schems' 14 | 15 | const authRouter = express.Router() 16 | 17 | authRouter.post('/register', requestBodyValidator(registerSchema), register) 18 | authRouter.post('/login', requestBodyValidator(loginSchema), login) 19 | authRouter.get('/logout', logout) 20 | 21 | authRouter.get( 22 | '/google', 23 | passport.authenticate('google', { 24 | scope: ['profile', 'email'] 25 | }) 26 | ) 27 | 28 | authRouter.get( 29 | '/linkedin', 30 | passport.authenticate('linkedin', { 31 | scope: ['openid', 'email', 'profile'] 32 | }) 33 | ) 34 | 35 | authRouter.get('/google/callback', googleRedirect) 36 | authRouter.get('/linkedin/callback', linkedinRedirect) 37 | authRouter.post('/password-reset-request', passwordResetRequest) 38 | authRouter.put('/passwordreset', passwordReset) 39 | export default authRouter 40 | -------------------------------------------------------------------------------- /src/controllers/admin/user.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from 'express' 2 | import type Profile from '../../entities/profile.entity' 3 | import { ProfileTypes } from '../../enums' 4 | import { getAllUsers } from '../../services/admin/user.service' 5 | import type { PaginatedApiResponse } from '../../types' 6 | 7 | export const getAllUsersHandler = async ( 8 | req: Request, 9 | res: Response 10 | ): Promise>> => { 11 | try { 12 | const user = req.user as Profile 13 | 14 | const pageNumber = parseInt(req.query.pageNumber as string) 15 | const pageSize = parseInt(req.query.pageSize as string) 16 | 17 | if (user.type !== ProfileTypes.ADMIN) { 18 | return res.status(403).json({ message: 'Only Admins are allowed' }) 19 | } 20 | 21 | const { items, message, statusCode, totalItemCount } = await getAllUsers({ 22 | pageNumber, 23 | pageSize 24 | }) 25 | 26 | return res.status(statusCode).json({ 27 | items, 28 | message, 29 | pageNumber, 30 | pageSize, 31 | totalItemCount 32 | }) 33 | } catch (err) { 34 | console.error('Error executing query', err) 35 | return res.status(500).json({ error: err }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/routes/mentor/mentor.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { 3 | requestBodyValidator, 4 | requestQueryValidator 5 | } from '../../middlewares/requestValidator' 6 | import { paginationSchema } from '../../schemas/common/pagination-request.schema' 7 | import { 8 | getMenteesByMentorSchema, 9 | mentorApplicationSchema 10 | } from '../../schemas/mentor-routes.schema' 11 | import { requireAuth } from './../../controllers/auth.controller' 12 | import { 13 | getAllMentorsHandler, 14 | getMenteesByMentor, 15 | mentorApplicationHandler, 16 | mentorAvailabilityHandler, 17 | mentorDetailsHandler 18 | } from './../../controllers/mentor.controller' 19 | 20 | const mentorRouter = express.Router() 21 | 22 | mentorRouter.post( 23 | '/', 24 | [requireAuth, requestBodyValidator(mentorApplicationSchema)], 25 | mentorApplicationHandler 26 | ) 27 | mentorRouter.get( 28 | '/mentees', 29 | [requireAuth, requestQueryValidator(getMenteesByMentorSchema)], 30 | getMenteesByMentor 31 | ) 32 | mentorRouter.put( 33 | '/:mentorId/availability', 34 | requireAuth, 35 | mentorAvailabilityHandler 36 | ) 37 | mentorRouter.get('/:mentorId', mentorDetailsHandler) 38 | mentorRouter.get( 39 | '/', 40 | requestQueryValidator(paginationSchema), 41 | getAllMentorsHandler 42 | ) 43 | 44 | export default mentorRouter 45 | -------------------------------------------------------------------------------- /src/migrations/1726849469636-CreateCountryTable.ts: -------------------------------------------------------------------------------- 1 | import { type MigrationInterface, type QueryRunner } from 'typeorm' 2 | 3 | export class CreateCountryTable1726849469636 implements MigrationInterface { 4 | name = 'CreateCountryTable1726849469636' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(` 8 | CREATE TABLE IF NOT EXISTS "country" ( 9 | "uuid" uuid NOT NULL DEFAULT uuid_generate_v4(), 10 | "created_at" TIMESTAMP NOT NULL DEFAULT now(), 11 | "updated_at" TIMESTAMP NOT NULL DEFAULT now(), 12 | "code" character varying NOT NULL, 13 | "name" character varying NOT NULL, 14 | CONSTRAINT "PK_4e06beff3ecfb1a974312fe536d" PRIMARY KEY ("uuid") 15 | ) 16 | `) 17 | 18 | const columnExists = await queryRunner.query(` 19 | SELECT column_name 20 | FROM information_schema.columns 21 | WHERE table_name='mentor' AND column_name='countryUuid' 22 | `) 23 | 24 | if (columnExists.length === 0) { 25 | await queryRunner.query(`ALTER TABLE "mentor" ADD "countryUuid" uuid`) 26 | } 27 | } 28 | 29 | public async down(queryRunner: QueryRunner): Promise { 30 | await queryRunner.query(`ALTER TABLE "mentor" DROP COLUMN "countryUuid"`) 31 | await queryRunner.query(`DROP TABLE "country"`) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/routes/admin/mentee/mentee.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { 3 | getAllMenteeEmails, 4 | getMenteeDetails, 5 | getMentees, 6 | updateMenteeStatus 7 | } from '../../../controllers/admin/mentee.controller' 8 | import { requireAuth } from '../../../controllers/auth.controller' 9 | import { 10 | requestBodyValidator, 11 | requestQueryValidator 12 | } from '../../../middlewares/requestValidator' 13 | import { 14 | getAllMenteeEmailsSchema, 15 | getMenteesSchema, 16 | updateMenteeStatusSchema 17 | } from '../../../schemas/admin/admin.mentee-routes.schema' 18 | import { paginationSchema } from '../../../schemas/common/pagination-request.schema' 19 | 20 | const menteeRouter = express.Router() 21 | 22 | menteeRouter.get( 23 | '/emails', 24 | [ 25 | requireAuth, 26 | requestQueryValidator(getAllMenteeEmailsSchema.merge(paginationSchema)) 27 | ], 28 | getAllMenteeEmails 29 | ) 30 | menteeRouter.get( 31 | '/applications', 32 | [ 33 | requireAuth, 34 | requestQueryValidator(getMenteesSchema.merge(paginationSchema)) 35 | ], 36 | getMentees 37 | ) 38 | menteeRouter.get('/:menteeId', requireAuth, getMenteeDetails) 39 | menteeRouter.put( 40 | '/:menteeId/state', 41 | [requireAuth, requestBodyValidator(updateMenteeStatusSchema)], 42 | updateMenteeStatus 43 | ) 44 | 45 | export default menteeRouter 46 | -------------------------------------------------------------------------------- /src/services/country.service.test.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../configs/dbConfig' 2 | import type Country from '../entities/country.entity' 3 | import { getAllCountries } from './country.service' 4 | 5 | jest.mock('../configs/dbConfig', () => ({ 6 | dataSource: { 7 | getRepository: jest.fn() 8 | } 9 | })) 10 | 11 | describe('Country Service - getAllCountries', () => { 12 | it('should get all countries successfully', async () => { 13 | const mockCountries = [ 14 | { 15 | uuid: 'mock-uuid-1', 16 | code: 'C1', 17 | name: 'Country 1' 18 | }, 19 | { 20 | uuid: 'mock-uuid-2', 21 | code: 'C2', 22 | name: 'Country 2' 23 | } 24 | ] as Country[] 25 | 26 | const mockCountryRepository = { 27 | find: jest.fn().mockResolvedValue(mockCountries) 28 | } 29 | 30 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 31 | mockCountryRepository 32 | ) 33 | 34 | const result = await getAllCountries() 35 | expect(result?.length).toBe(2) 36 | }) 37 | 38 | it('should handle when countries not found', async () => { 39 | const mockCountryRepository = { 40 | find: jest.fn().mockResolvedValue([]) 41 | } 42 | 43 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 44 | mockCountryRepository 45 | ) 46 | 47 | const result = await getAllCountries() 48 | expect(result).toBe(null) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/services/admin/user.service.test.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../../configs/dbConfig' 2 | import Profile from '../../entities/profile.entity' 3 | import { ProfileTypes } from '../../enums' 4 | import { getAllUsers } from './user.service' 5 | 6 | jest.mock('../../configs/dbConfig', () => ({ 7 | dataSource: { 8 | getRepository: jest.fn() 9 | } 10 | })) 11 | 12 | describe('getAllUsers', () => { 13 | it('should retrieve all users successfully', async () => { 14 | const mockUser1 = new Profile( 15 | 'user1@example.com', 16 | 'contact1@example.com', 17 | 'User1', 18 | 'Last1', 19 | 'image1.jpg', 20 | ProfileTypes.DEFAULT, 21 | 'hashedPassword1' 22 | ) 23 | 24 | const mockUser2 = new Profile( 25 | 'user2@example.com', 26 | 'contact2@example.com', 27 | 'User2', 28 | 'Last2', 29 | 'image2.jpg', 30 | ProfileTypes.ADMIN, 31 | 'hashedPassword2' 32 | ) 33 | 34 | const mockProfileRepository = { 35 | find: jest.fn().mockResolvedValue([mockUser1, mockUser2]), 36 | findAndCount: jest.fn().mockResolvedValue([[mockUser1, mockUser2], 2]) 37 | } 38 | 39 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 40 | mockProfileRepository 41 | ) 42 | 43 | const result = await getAllUsers({ 44 | pageNumber: 1, 45 | pageSize: 2 46 | }) 47 | 48 | expect(result.items).toEqual([mockUser1, mockUser2]) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | The purpose of this PR is to fix # 4 | 5 | ## Goals 6 | 7 | 8 | ## Approach 9 | 10 | 11 | ### Screenshots 12 | 13 | 14 | ## Checklist 15 | - [x] This PR doesn't commit any keys, passwords, tokens, usernames, or other secrets. 16 | - [ ] I have read and understood the development best practices guidelines ( http://bit.ly/sef-best-practices ) 17 | - [ ] My code follows the style guidelines of this project 18 | - [ ] I have performed a self-review of my own code 19 | - [ ] I have commented my code, particularly in hard-to-understand areas 20 | - [ ] I have made corresponding changes to the documentation 21 | 22 | ## Related PRs 23 | 24 | 25 | ## Test environment 26 | 27 | 28 | ## Learning 29 | 30 | -------------------------------------------------------------------------------- /src/middlewares/requestValidator.ts: -------------------------------------------------------------------------------- 1 | import { type NextFunction, type Request, type Response } from 'express' 2 | import { ZodError, type ZodSchema } from 'zod' 3 | 4 | export const requestBodyValidator = (schema: T) => { 5 | return (req: Request, res: Response, next: NextFunction) => { 6 | try { 7 | if (req.body.data) { 8 | schema.parse(JSON.parse(req.body.data)) 9 | } else { 10 | schema.parse(req.body) 11 | } 12 | next() 13 | } catch (err) { 14 | console.log(err) 15 | if (err instanceof ZodError) { 16 | const errorMessages = err.errors.map((issue) => ({ 17 | message: `${issue.path.join('.')} is ${issue.message}` 18 | })) 19 | return res 20 | .status(400) 21 | .json({ error: 'Invalid data', details: errorMessages }) 22 | } 23 | return res.status(500).json({ error: 'Internal server error' }) 24 | } 25 | } 26 | } 27 | 28 | export const requestQueryValidator = (schema: T) => { 29 | return (req: Request, res: Response, next: NextFunction) => { 30 | try { 31 | schema.parse(req.query) 32 | next() 33 | } catch (err) { 34 | if (err instanceof ZodError) { 35 | const errorMessages = err.errors.map((issue) => ({ 36 | message: `${issue.path.join('.')} is ${issue.message}` 37 | })) 38 | return res 39 | .status(400) 40 | .json({ error: 'Invalid data', details: errorMessages }) 41 | } 42 | return res.status(500).json({ error: 'Internal server error' }) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/services/admin.service.test.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../configs/dbConfig' 2 | import Profile from '../entities/profile.entity' 3 | import { ProfileTypes } from '../enums' 4 | import { getAllUsers } from './admin.service' 5 | 6 | jest.mock('../configs/dbConfig', () => ({ 7 | dataSource: { 8 | getRepository: jest.fn() 9 | } 10 | })) 11 | 12 | describe('getAllUsers', () => { 13 | it('should get all users successfully', async () => { 14 | const mockUser1 = new Profile( 15 | 'user1@example.com', 16 | 'contact1@example.com', 17 | 'User1', 18 | 'Last1', 19 | 'image1.jpg', 20 | ProfileTypes.DEFAULT, 21 | 'hashedPassword1' 22 | ) 23 | 24 | const mockUser2 = new Profile( 25 | 'user2@example.com', 26 | 'contact2@example.com', 27 | 'User2', 28 | 'Last2', 29 | 'image2.jpg', 30 | ProfileTypes.ADMIN, 31 | 'hashedPassword2' 32 | ) 33 | const mockProfileRepository = { 34 | find: jest.fn().mockResolvedValue([mockUser1, mockUser2]) 35 | } 36 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 37 | mockProfileRepository 38 | ) 39 | const result = await getAllUsers() 40 | expect(result).toEqual([mockUser1, mockUser2]) 41 | }) 42 | 43 | it('should handle an empty user list', async () => { 44 | const mockProfileRepository = { 45 | find: jest.fn().mockResolvedValue([]) 46 | } 47 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 48 | mockProfileRepository 49 | ) 50 | const result = await getAllUsers() 51 | expect(result).toEqual([]) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /src/entities/mentor.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm' 2 | import Profile from './profile.entity' 3 | import Mentee from './mentee.entity' 4 | import Category from './category.entity' 5 | import { MentorApplicationStatus } from '../enums' 6 | import BaseEntity from './baseEntity' 7 | import { Country } from './country.entity' 8 | 9 | @Entity('mentor') 10 | class Mentor extends BaseEntity { 11 | @Column({ 12 | type: 'enum', 13 | enum: MentorApplicationStatus, 14 | default: MentorApplicationStatus.PENDING 15 | }) 16 | state: MentorApplicationStatus 17 | 18 | @ManyToOne(() => Category, (category) => category.mentors) 19 | @JoinColumn() 20 | category: Category 21 | 22 | @Column({ type: 'json' }) 23 | application: Record 24 | 25 | @Column({ type: 'boolean' }) 26 | availability: boolean 27 | 28 | @ManyToOne(() => Profile) 29 | @JoinColumn() 30 | profile: Profile 31 | 32 | @ManyToOne(() => Country, (country) => country.mentors) 33 | @JoinColumn() 34 | country: Country 35 | 36 | @OneToMany(() => Mentee, (mentee) => mentee.mentor) 37 | mentees?: Mentee[] 38 | 39 | constructor( 40 | state: MentorApplicationStatus, 41 | category: Category, 42 | application: Record, 43 | availability: boolean, 44 | profile: Profile, 45 | country: Country 46 | ) { 47 | super() 48 | this.state = state 49 | this.category = category 50 | this.application = application 51 | this.availability = availability 52 | this.profile = profile 53 | this.country = country 54 | } 55 | } 56 | 57 | export default Mentor 58 | -------------------------------------------------------------------------------- /src/services/category.service.test.ts: -------------------------------------------------------------------------------- 1 | import { getAllCategories } from './category.service' 2 | import { dataSource } from '../configs/dbConfig' 3 | import type Category from '../entities/category.entity' 4 | 5 | jest.mock('../configs/dbConfig', () => ({ 6 | dataSource: { 7 | getRepository: jest.fn() 8 | } 9 | })) 10 | 11 | describe('Category Service - getAllCategories', () => { 12 | it('should get all categories successfully', async () => { 13 | const mockCategories = [ 14 | { 15 | uuid: 'mock-uuid-1', 16 | category: 'Category 1' 17 | }, 18 | { 19 | uuid: 'mock-uuid-2', 20 | category: 'Category 2' 21 | } 22 | ] as Category[] 23 | 24 | const mockCategoryRepository = { 25 | find: jest.fn().mockResolvedValue(mockCategories) 26 | } 27 | 28 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 29 | mockCategoryRepository 30 | ) 31 | 32 | const result = await getAllCategories() 33 | 34 | expect(result.statusCode).toBe(200) 35 | expect(result.categories?.length).toBe(2) 36 | expect(result.categories).toEqual([ 37 | { uuid: 'mock-uuid-1', category: 'Category 1' }, 38 | { uuid: 'mock-uuid-2', category: 'Category 2' } 39 | ]) 40 | expect(result.message).toBe('All Categories found') 41 | }) 42 | 43 | it('should handle categories not found', async () => { 44 | const mockCategoryRepository = { 45 | find: jest.fn().mockResolvedValue([]) 46 | } 47 | 48 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 49 | mockCategoryRepository 50 | ) 51 | 52 | const result = await getAllCategories() 53 | 54 | expect(result.categories?.length).toBe(0) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /src/routes/mentee/mentee.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { requireAuth } from '../../controllers/auth.controller' 3 | import { 4 | getMenteeDetails, 5 | getPublicMenteeDetails, 6 | menteeApplicationHandler, 7 | revokeApplication, 8 | updateMenteeStatus 9 | } from '../../controllers/mentee.controller' 10 | import { requestBodyValidator } from '../../middlewares/requestValidator' 11 | import { 12 | addFeedbackMonthlyCheckInSchema, 13 | menteeApplicationSchema, 14 | postMonthlyCheckInSchema, 15 | updateMenteeStatusSchema 16 | } from '../../schemas/mentee-routes.schemas' 17 | import { 18 | addFeedbackMonthlyCheckIn, 19 | getMonthlyCheckIns, 20 | postMonthlyCheckIn 21 | } from '../../controllers/monthlyChecking.controller' 22 | 23 | const menteeRouter = express.Router() 24 | 25 | menteeRouter.post( 26 | '/', 27 | [requireAuth, requestBodyValidator(menteeApplicationSchema)], 28 | menteeApplicationHandler 29 | ) 30 | menteeRouter.get('/:menteeId', requireAuth, getMenteeDetails) 31 | menteeRouter.get('/public/:menteeId', getPublicMenteeDetails) 32 | menteeRouter.put( 33 | '/:menteeId/status/', 34 | [requireAuth, requestBodyValidator(updateMenteeStatusSchema)], 35 | updateMenteeStatus 36 | ) 37 | menteeRouter.put('/revoke-application', requireAuth, revokeApplication) 38 | 39 | menteeRouter.post( 40 | '/checkin', 41 | [requireAuth, requestBodyValidator(postMonthlyCheckInSchema)], 42 | postMonthlyCheckIn 43 | ) 44 | 45 | menteeRouter.get('/checkin/:menteeId', requireAuth, getMonthlyCheckIns) 46 | 47 | menteeRouter.put( 48 | '/checking/feedback', 49 | [requireAuth, requestBodyValidator(addFeedbackMonthlyCheckInSchema)], 50 | addFeedbackMonthlyCheckIn 51 | ) 52 | 53 | export default menteeRouter 54 | -------------------------------------------------------------------------------- /src/entities/profile.entity.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt' 2 | import { Entity, Column, OneToMany } from 'typeorm' 3 | import { ProfileTypes } from '../enums' 4 | import BaseEntity from './baseEntity' 5 | import Mentor from './mentor.entity' 6 | import Mentee from './mentee.entity' 7 | 8 | @Entity({ name: 'profile' }) 9 | class Profile extends BaseEntity { 10 | @Column({ type: 'varchar', length: 255, unique: true }) 11 | primary_email: string 12 | 13 | @Column({ type: 'varchar', length: 255 }) 14 | first_name: string 15 | 16 | @Column({ type: 'varchar', length: 255, nullable: true }) 17 | last_name: string 18 | 19 | @Column({ type: 'varchar', length: 255 }) 20 | image_url: string 21 | 22 | @Column({ type: 'enum', enum: ProfileTypes, default: ProfileTypes.DEFAULT }) 23 | type: ProfileTypes 24 | 25 | @Column({ type: 'varchar', length: 255, select: false }) 26 | password: string 27 | 28 | @OneToMany(() => Mentor, (mentor) => mentor.profile) 29 | mentor?: Mentor[] 30 | 31 | @OneToMany(() => Mentee, (mentee) => mentee.profile) 32 | mentee?: Mentee[] 33 | 34 | constructor( 35 | primary_email: string, 36 | contact_email: string, 37 | first_name: string, 38 | last_name: string, 39 | image_uri: string, 40 | type: ProfileTypes, 41 | password: string 42 | ) { 43 | super() 44 | this.primary_email = primary_email 45 | this.first_name = first_name 46 | this.last_name = last_name 47 | this.image_url = image_uri 48 | this.type = type || ProfileTypes.DEFAULT 49 | this.password = password 50 | } 51 | 52 | async comparePassword(candidatePassword: string): Promise { 53 | return await bcrypt.compare(candidatePassword, this.password) 54 | } 55 | } 56 | 57 | export default Profile 58 | -------------------------------------------------------------------------------- /src/services/admin/category.service.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../../configs/dbConfig' 2 | import Category from '../../entities/category.entity' 3 | 4 | export const createCategory = async ( 5 | categoryName: string 6 | ): Promise<{ 7 | statusCode: number 8 | category?: Category | null 9 | message: string 10 | }> => { 11 | try { 12 | const categoryRepository = dataSource.getRepository(Category) 13 | 14 | const newCategory = new Category(categoryName) 15 | 16 | const saveCategory = await categoryRepository.save(newCategory) 17 | 18 | return { 19 | statusCode: 201, 20 | category: saveCategory, 21 | message: 'Category created successfully' 22 | } 23 | } catch (err) { 24 | console.error('Error creating category', err) 25 | throw new Error('Error creating category') 26 | } 27 | } 28 | 29 | export const changeCategory = async ( 30 | categoryId: string, 31 | categoryName: string 32 | ): Promise<{ 33 | statusCode: number 34 | category?: Category | null 35 | message: string 36 | }> => { 37 | try { 38 | const categoryRepository = dataSource.getRepository(Category) 39 | 40 | const category = await categoryRepository.findOne({ 41 | where: { uuid: categoryId } 42 | }) 43 | 44 | if (!category) { 45 | return { 46 | statusCode: 404, 47 | message: 'Category not found' 48 | } 49 | } 50 | 51 | await categoryRepository.update( 52 | { uuid: categoryId }, 53 | { category: categoryName } 54 | ) 55 | 56 | return { 57 | statusCode: 201, 58 | category, 59 | message: 'Category updated successfully' 60 | } 61 | } catch (err) { 62 | console.error('Error updating category', err) 63 | throw new Error('Error updating category') 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/migrations/1722051742722-RemoveUniqueConstraintFromProfileUuid.ts: -------------------------------------------------------------------------------- 1 | import { type MigrationInterface, type QueryRunner } from 'typeorm' 2 | 3 | export class RemoveUniqueConstraintFromProfileUuid1722051742722 4 | implements MigrationInterface 5 | { 6 | name = 'RemoveUniqueConstraintFromProfileUuid1722051742722' 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | // Check if the constraint exists before attempting to drop it 10 | const constraintExists = await queryRunner.query(` 11 | SELECT 1 12 | FROM information_schema.table_constraints 13 | WHERE constraint_name = 'REL_f671cf2220d1bd0621a1a5e92e' 14 | AND table_name = 'mentee' 15 | `) 16 | 17 | if (constraintExists.length > 0) { 18 | await queryRunner.query( 19 | `ALTER TABLE "mentee" DROP CONSTRAINT "REL_f671cf2220d1bd0621a1a5e92e"` 20 | ) 21 | } 22 | 23 | await queryRunner.query( 24 | `ALTER TABLE "mentee" DROP CONSTRAINT "FK_f671cf2220d1bd0621a1a5e92e7"` 25 | ) 26 | await queryRunner.query( 27 | `ALTER TABLE "mentee" ADD CONSTRAINT "FK_f671cf2220d1bd0621a1a5e92e7" FOREIGN KEY ("profileUuid") REFERENCES "profile"("uuid") ON DELETE CASCADE ON UPDATE NO ACTION` 28 | ) 29 | } 30 | 31 | public async down(queryRunner: QueryRunner): Promise { 32 | await queryRunner.query( 33 | `ALTER TABLE "mentee" DROP CONSTRAINT "FK_f671cf2220d1bd0621a1a5e92e7"` 34 | ) 35 | await queryRunner.query( 36 | `ALTER TABLE "mentee" ADD CONSTRAINT "REL_f671cf2220d1bd0621a1a5e92e" UNIQUE ("profileUuid")` 37 | ) 38 | await queryRunner.query( 39 | `ALTER TABLE "mentee" ADD CONSTRAINT "FK_f671cf2220d1bd0621a1a5e92e7" FOREIGN KEY ("profileUuid") REFERENCES "profile"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION` 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/controllers/admin/category.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from 'express' 2 | import type Profile from '../../entities/profile.entity' 3 | import { ProfileTypes } from '../../enums' 4 | import type Category from '../../entities/category.entity' 5 | import type { ApiResponse } from '../../types' 6 | import { 7 | changeCategory, 8 | createCategory 9 | } from '../../services/admin/category.service' 10 | 11 | export const addCategory = async ( 12 | req: Request, 13 | res: Response 14 | ): Promise> => { 15 | try { 16 | const user = req.user as Profile 17 | const { categoryName } = req.body 18 | 19 | if (user.type !== ProfileTypes.ADMIN) { 20 | return res.status(403).json({ message: 'Only Admins are allowed' }) 21 | } 22 | 23 | const { category, statusCode, message } = await createCategory(categoryName) 24 | return res.status(statusCode).json({ category, message }) 25 | } catch (err) { 26 | console.error('Error executing query', err) 27 | return res.status(500).json({ error: err }) 28 | } 29 | } 30 | 31 | export const updateCategory = async ( 32 | req: Request, 33 | res: Response 34 | ): Promise> => { 35 | try { 36 | const user = req.user as Profile 37 | const { categoryName } = req.body 38 | const { categoryId } = req.params 39 | 40 | if (user.type !== ProfileTypes.ADMIN) { 41 | return res.status(403).json({ message: 'Only Admins are allowed' }) 42 | } 43 | 44 | const { category, statusCode, message } = await changeCategory( 45 | categoryId, 46 | categoryName 47 | ) 48 | return res.status(statusCode).json({ category, message }) 49 | } catch (err) { 50 | console.error('Error executing query', err) 51 | return res.status(500).json({ error: err }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/routes/admin/mentor/mentor.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { 3 | getAllMentorEmails, 4 | getAllMentorsByStatus, 5 | mentorDetailsHandler, 6 | mentorStatusHandler, 7 | searchMentors, 8 | updateMentorAvailability, 9 | updateMentorHandler 10 | } from '../../../controllers/admin/mentor.controller' 11 | import { requireAuth } from '../../../controllers/auth.controller' 12 | import { 13 | requestBodyValidator, 14 | requestQueryValidator 15 | } from '../../../middlewares/requestValidator' 16 | import { 17 | getAllMentorEmailsSchema, 18 | getAllMentorsByStatusSchema, 19 | mentorStatusSchema, 20 | searchMentorsSchema, 21 | updateMentorAvailabilitySchema 22 | } from '../../../schemas/admin/admin.mentor-routes.schema' 23 | import { paginationSchema } from '../../../schemas/common/pagination-request.schema' 24 | 25 | const mentorRouter = express.Router() 26 | 27 | mentorRouter.put('/:mentorId', requireAuth, updateMentorHandler) 28 | mentorRouter.put( 29 | '/:mentorId/state', 30 | [requireAuth, requestBodyValidator(mentorStatusSchema)], 31 | mentorStatusHandler 32 | ) 33 | mentorRouter.get( 34 | '/', 35 | [ 36 | requireAuth, 37 | requestQueryValidator(getAllMentorsByStatusSchema.merge(paginationSchema)) 38 | ], 39 | getAllMentorsByStatus 40 | ) 41 | mentorRouter.get( 42 | '/emails', 43 | [requireAuth, requestQueryValidator(getAllMentorEmailsSchema)], 44 | getAllMentorEmails 45 | ) 46 | mentorRouter.get('/:mentorId', requireAuth, mentorDetailsHandler) 47 | mentorRouter.put( 48 | '/:mentorId/availability', 49 | [requireAuth, requestBodyValidator(updateMentorAvailabilitySchema)], 50 | updateMentorAvailability 51 | ) 52 | mentorRouter.get( 53 | '/search', 54 | [requireAuth, requestQueryValidator(searchMentorsSchema)], 55 | searchMentors 56 | ) 57 | 58 | export default mentorRouter 59 | -------------------------------------------------------------------------------- /src/entities/mentee.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, ManyToOne, OneToMany } from 'typeorm' 2 | import Mentor from './mentor.entity' 3 | import profileEntity from './profile.entity' 4 | import { MenteeApplicationStatus, StatusUpdatedBy } from '../enums' 5 | import BaseEntity from './baseEntity' 6 | import { UUID } from 'typeorm/driver/mongodb/bson.typings' 7 | import MonthlyCheckIn from './checkin.entity' 8 | 9 | @Entity('mentee') 10 | class Mentee extends BaseEntity { 11 | @Column({ 12 | type: 'enum', 13 | enum: MenteeApplicationStatus, 14 | default: MenteeApplicationStatus.PENDING 15 | }) 16 | state: MenteeApplicationStatus 17 | 18 | @Column({ type: 'enum', enum: StatusUpdatedBy, nullable: true }) 19 | status_updated_by!: StatusUpdatedBy 20 | 21 | @Column({ type: 'timestamp', nullable: true }) 22 | status_updated_date!: Date 23 | 24 | @Column({ type: 'json' }) 25 | application: Record 26 | 27 | @Column({ type: 'uuid', nullable: true, default: null }) 28 | certificate_id!: UUID 29 | 30 | @Column({ default: null, nullable: true }) 31 | journal!: string 32 | 33 | @ManyToOne(() => profileEntity, (profile) => profile.mentee) 34 | profile: profileEntity 35 | 36 | @ManyToOne(() => Mentor, (mentor) => mentor.mentees) 37 | mentor: Mentor 38 | 39 | @OneToMany(() => MonthlyCheckIn, (checkIn) => checkIn.mentee) 40 | checkIns?: MonthlyCheckIn[] 41 | 42 | constructor( 43 | state: MenteeApplicationStatus, 44 | application: Record, 45 | profile: profileEntity, 46 | mentor: Mentor, 47 | checkIns?: MonthlyCheckIn[] 48 | ) { 49 | super() 50 | this.state = state || MenteeApplicationStatus.PENDING 51 | this.application = application 52 | this.profile = profile 53 | this.mentor = mentor 54 | this.checkIns = checkIns 55 | } 56 | } 57 | 58 | export default Mentee 59 | -------------------------------------------------------------------------------- /src/entities/checkin.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm' 2 | import BaseEntity from './baseEntity' 3 | import Mentee from './mentee.entity' 4 | 5 | @Entity('monthly-check-in') 6 | class MonthlyCheckIn extends BaseEntity { 7 | @Column({ type: 'text' }) 8 | month: string 9 | 10 | @Column({ type: 'text' }) 11 | generalUpdatesAndFeedback: string 12 | 13 | @Column({ type: 'text' }) 14 | progressTowardsGoals: string 15 | 16 | @Column({ type: 'json', nullable: true }) 17 | mediaContentLinks: string[] 18 | 19 | @Column({ type: 'text', nullable: true }) 20 | mentorFeedback: string 21 | 22 | @Column({ type: 'boolean', default: false }) 23 | isCheckedByMentor: boolean 24 | 25 | @Column({ type: 'timestamp', nullable: true }) 26 | mentorCheckedDate: Date 27 | 28 | @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) 29 | checkInDate: Date 30 | 31 | @ManyToOne(() => Mentee, (mentee) => mentee.checkIns) 32 | @JoinColumn({ name: 'menteeId' }) 33 | mentee: Mentee 34 | 35 | constructor( 36 | month: string, 37 | generalUpdatesAndFeedback: string, 38 | progressTowardsGoals: string, 39 | mediaContentLinks: string[], 40 | mentorFeedback: string, 41 | isCheckedByMentor: boolean, 42 | mentorCheckedDate: Date, 43 | checkInDate: Date, 44 | mentee: Mentee 45 | ) { 46 | super() 47 | this.month = month 48 | this.generalUpdatesAndFeedback = generalUpdatesAndFeedback 49 | this.progressTowardsGoals = progressTowardsGoals 50 | this.mediaContentLinks = mediaContentLinks 51 | this.mentorFeedback = mentorFeedback 52 | this.isCheckedByMentor = isCheckedByMentor 53 | this.mentorCheckedDate = mentorCheckedDate 54 | this.checkInDate = checkInDate 55 | this.mentee = mentee 56 | } 57 | 58 | validate(): boolean { 59 | return this.mediaContentLinks.length >= 3 60 | } 61 | } 62 | 63 | export default MonthlyCheckIn 64 | -------------------------------------------------------------------------------- /mocks.ts: -------------------------------------------------------------------------------- 1 | const randomString = Math.random().toString(36) 2 | 3 | export const mockMentor = { 4 | email: `mentor${randomString}@gmail.com`, 5 | password: '123' 6 | } 7 | 8 | export const mockAdmin = { 9 | email: `admin${randomString}@gmail.com`, 10 | password: 'admin123' 11 | } 12 | 13 | export const mockUser = { 14 | email: `user${randomString}@gmail.com`, 15 | password: '123' 16 | } 17 | 18 | export const mentorApplicationInfo = { 19 | application: [ 20 | { 21 | question: 'What is your country?', 22 | answers: 'Singapore' 23 | }, 24 | { 25 | question: 'What is your expertise?', 26 | answers: 'Software Engineering' 27 | }, 28 | { 29 | question: 'What is your mentoring startegy?', 30 | answers: 'I will provide my insights' 31 | } 32 | ], 33 | categoryId: '60b5b847-99a2-4e47-b35b-81b4284311dd' 34 | } 35 | 36 | export const platformInfo = { 37 | description: 'This is a sample description.', 38 | mentor_questions: [ 39 | { 40 | question: 'What are your career goals?', 41 | question_type: 'text', 42 | options: [] 43 | }, 44 | { 45 | question: 'How do you handle challenges?', 46 | question_type: 'text', 47 | options: [] 48 | }, 49 | { 50 | question: 'Tell me about a time when you demonstrated leadership skills.', 51 | question_type: 'text', 52 | options: [] 53 | } 54 | ], 55 | image_url: 'https://example.com/images/sample.jpg', 56 | landing_page_url: 'https://example.com/landing-page', 57 | email_templates: { 58 | template1: { 59 | subject: 'Welcome to our mentoring program!', 60 | body: 'Dear {{mentor_name}},\n\nWe are excited to have you join our mentoring program...' 61 | }, 62 | template2: { 63 | subject: 'Follow-up on your mentoring session', 64 | body: 'Dear {{mentee_name}},\n\nI wanted to follow up on our recent mentoring session...' 65 | } 66 | }, 67 | title: 'Sample Mentoring Program' 68 | } 69 | 70 | export const emailTemplateInfo = { 71 | subject: 'Follow-up on your mentoring session', 72 | content: 'Sample content' 73 | } 74 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | services: 16 | postgres: 17 | image: postgres:latest 18 | env: 19 | POSTGRES_USER: postgres 20 | POSTGRES_PASSWORD: password 21 | POSTGRES_DB: mydatabase 22 | ports: 23 | - 5432:5432 24 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 25 | 26 | steps: 27 | - name: Checkout code 28 | uses: actions/checkout@v3 29 | 30 | - name: Use Node.js 31 | uses: actions/setup-node@v3 32 | with: 33 | node-version: '18' 34 | 35 | - name: Install dependencies 36 | run: npm install 37 | 38 | - name: ESLint check 39 | run: npm run lint 40 | 41 | - name: Set up environment variables 42 | run: | 43 | echo "DB_USER=postgres" >> .env 44 | echo "DB_HOST=localhost" >> .env 45 | echo "DB_NAME=mydatabase" >> .env 46 | echo "DB_PASSWORD=password" >> .env 47 | echo "DB_PORT=5432" >> .env 48 | echo "SERVER_PORT=4000" >> .env 49 | echo "JWT_SECRET=your_jwt_secret_key" >> .env 50 | echo "GOOGLE_CLIENT_ID=your_google_client_id" >> .env 51 | echo "GOOGLE_CLIENT_SECRET=your_google_client_secret" >> .env 52 | echo "GOOGLE_REDIRECT_URL=http://localhost:${SERVER_PORT}/api/auth/google/callback" >> .env 53 | echo "CLIENT_URL=http://localhost:5173" >> .env 54 | echo "IMG_HOST=http://localhost:${SERVER_PORT}" >> .env 55 | echo "SMTP_MAIL=your_smtp_mail" >> .env 56 | echo "SMTP_PASSWORD=your_smtp_password" >> .env 57 | echo "LINKEDIN_CLIENT_ID=your_linkedin_client_id" >> .env 58 | echo "LINKEDIN_CLIENT_SECRET=your_linkedin_client_secret" >> .env 59 | echo "LINKEDIN_REDIRECT_URL=http://localhost:${SERVER_PORT}/api/auth/linkedin/callback" >> .env 60 | 61 | - name: Run tests 62 | run: npm run test 63 | 64 | - name: Build 65 | run: npm run build 66 | -------------------------------------------------------------------------------- /src/migrations/1722749907154-AddNewApplicationStates.ts: -------------------------------------------------------------------------------- 1 | import { type MigrationInterface, type QueryRunner } from 'typeorm' 2 | 3 | export class AddNewApplicationStates1722749907154 4 | implements MigrationInterface 5 | { 6 | name = 'AddNewApplicationStates1722749907154' 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TYPE "public"."mentee_state_enum" RENAME TO "mentee_state_enum_old"` 11 | ) 12 | await queryRunner.query( 13 | `CREATE TYPE "public"."mentee_state_enum" AS ENUM('pending', 'rejected', 'approved', 'completed', 'revoked')` 14 | ) 15 | await queryRunner.query( 16 | `ALTER TABLE "mentee" ALTER COLUMN "state" DROP DEFAULT` 17 | ) 18 | await queryRunner.query( 19 | `ALTER TABLE "mentee" ALTER COLUMN "state" TYPE "public"."mentee_state_enum" USING "state"::"text"::"public"."mentee_state_enum"` 20 | ) 21 | await queryRunner.query( 22 | `ALTER TABLE "mentee" ALTER COLUMN "state" SET DEFAULT 'pending'` 23 | ) 24 | await queryRunner.query(`DROP TYPE "public"."mentee_state_enum_old"`) 25 | await queryRunner.query(`ALTER TABLE "mentee" DROP COLUMN "certificate_id"`) 26 | await queryRunner.query(`ALTER TABLE "mentee" ADD "certificate_id" uuid`) 27 | } 28 | 29 | public async down(queryRunner: QueryRunner): Promise { 30 | await queryRunner.query(`ALTER TABLE "mentee" DROP COLUMN "certificate_id"`) 31 | await queryRunner.query(`ALTER TABLE "mentee" ADD "certificate_id" bigint`) 32 | await queryRunner.query( 33 | `CREATE TYPE "public"."mentee_state_enum_old" AS ENUM('pending', 'rejected', 'approved')` 34 | ) 35 | await queryRunner.query( 36 | `ALTER TABLE "mentee" ALTER COLUMN "state" DROP DEFAULT` 37 | ) 38 | await queryRunner.query( 39 | `ALTER TABLE "mentee" ALTER COLUMN "state" TYPE "public"."mentee_state_enum_old" USING "state"::"text"::"public"."mentee_state_enum_old"` 40 | ) 41 | await queryRunner.query( 42 | `ALTER TABLE "mentee" ALTER COLUMN "state" SET DEFAULT 'pending'` 43 | ) 44 | await queryRunner.query(`DROP TYPE "public"."mentee_state_enum"`) 45 | await queryRunner.query( 46 | `ALTER TYPE "public"."mentee_state_enum_old" RENAME TO "mentee_state_enum"` 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/services/admin/email.service.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../../configs/dbConfig' 2 | import { EmailStatusTypes } from '../../enums' 3 | import nodemailer from 'nodemailer' 4 | import Email from '../../entities/email.entity' 5 | import { SMTP_MAIL, SMTP_PASSWORD } from '../../configs/envConfig' 6 | import { loadTemplate } from '../../utils' 7 | 8 | const transporter = nodemailer.createTransport({ 9 | host: 'smtp.gmail.com', 10 | port: 587, 11 | secure: false, 12 | auth: { 13 | user: SMTP_MAIL, 14 | pass: SMTP_PASSWORD 15 | } 16 | }) 17 | 18 | export const sendEmail = async ( 19 | to: string, 20 | subject: string, 21 | message: string, 22 | attachments?: Array<{ filename: string; path: string }> 23 | ): Promise<{ 24 | statusCode: number 25 | message: string 26 | }> => { 27 | const emailRepository = dataSource.getRepository(Email) 28 | 29 | try { 30 | const html = await loadTemplate('emailTemplate', { 31 | subject, 32 | message 33 | }) 34 | 35 | await transporter.sendMail({ 36 | from: `"Sustainable Education Foundation" <${SMTP_MAIL}>`, 37 | to, 38 | subject, 39 | html, 40 | attachments 41 | }) 42 | 43 | const email = new Email(to, subject, message, EmailStatusTypes.SENT) 44 | 45 | await emailRepository.save(email) 46 | 47 | return { statusCode: 200, message: 'Email sent and saved successfully' } 48 | } catch (error) { 49 | console.error('Error sending email:', error) 50 | throw new Error('Error sending email') 51 | } 52 | } 53 | 54 | export const sendResetPasswordEmail = async ( 55 | to: string, 56 | subject: string, 57 | message: string 58 | ): Promise<{ 59 | statusCode: number 60 | message: string 61 | }> => { 62 | const emailRepository = dataSource.getRepository(Email) 63 | 64 | try { 65 | const html = await loadTemplate('passwordresetEmailTemplate', { 66 | subject, 67 | message 68 | }) 69 | 70 | await transporter.sendMail({ 71 | from: `"Sustainable Education Foundation" <${SMTP_MAIL}>`, 72 | to, 73 | subject, 74 | html 75 | }) 76 | 77 | const email = new Email(to, subject, message, EmailStatusTypes.SENT) 78 | 79 | await emailRepository.save(email) 80 | 81 | return { statusCode: 200, message: 'Email sent and saved successfully' } 82 | } catch (error) { 83 | console.error('Error sending email:', error) 84 | throw new Error('Error sending email') 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import bodyParser from 'body-parser' 2 | import cookieParser from 'cookie-parser' 3 | import cors from 'cors' 4 | import type { Express } from 'express' 5 | import express from 'express' 6 | import fs from 'fs' 7 | import passport from 'passport' 8 | import { dataSource } from './configs/dbConfig' 9 | import { CLIENT_URL } from './configs/envConfig' 10 | import './configs/google-passport' 11 | import './configs/linkedin-passport' 12 | import adminRouter from './routes/admin/admin.route' 13 | import authRouter from './routes/auth/auth.route' 14 | import categoryRouter from './routes/category/category.route' 15 | import emailRouter from './routes/emails/emails.route' 16 | import menteeRouter from './routes/mentee/mentee.route' 17 | import mentorRouter from './routes/mentor/mentor.route' 18 | import profileRouter from './routes/profile/profile.route' 19 | import path from 'path' 20 | import countryRouter from './routes/country/country.route' 21 | 22 | const app = express() 23 | const staticFolder = 'uploads' 24 | export const certificatesDir = path.join(__dirname, 'certificates') 25 | 26 | app.use(cookieParser()) 27 | app.use(bodyParser.json()) 28 | app.use(express.static(staticFolder)) 29 | app.use(passport.initialize()) 30 | app.use( 31 | cors({ 32 | origin: CLIENT_URL, 33 | methods: 'GET, HEAD, PUT, PATCH, DELETE', 34 | credentials: true 35 | }) 36 | ) 37 | 38 | app.get('/', (req, res) => { 39 | res.send('ScholarX Backend') 40 | }) 41 | 42 | app.use('/api/auth', authRouter) 43 | app.use('/api/me', profileRouter) 44 | app.use('/api/admin', adminRouter) 45 | app.use('/api/mentors', mentorRouter) 46 | app.use('/api/mentees', menteeRouter) 47 | app.use('/api/categories', categoryRouter) 48 | app.use('/api/emails', emailRouter) 49 | app.use('/api/countries', countryRouter) 50 | 51 | if (!fs.existsSync(staticFolder)) { 52 | fs.mkdirSync(staticFolder, { recursive: true }) 53 | console.log('Directory created successfully!') 54 | } else { 55 | console.log('Directory already exists.') 56 | } 57 | 58 | if (!fs.existsSync(certificatesDir)) { 59 | fs.mkdirSync(certificatesDir) 60 | } 61 | 62 | export const startServer = async (port: number): Promise => { 63 | try { 64 | await dataSource.initialize() 65 | console.log('DB connection is successful') 66 | app.listen(port, () => { 67 | console.log(`Server is running on http://localhost:${port}`) 68 | }) 69 | 70 | return app 71 | } catch (err) { 72 | console.log('DB connection was not successful', err) 73 | throw err 74 | } 75 | } 76 | 77 | export default startServer 78 | -------------------------------------------------------------------------------- /src/controllers/monthlyChecking.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from 'express' 2 | import { type ApiResponse } from '../types' 3 | import type MonthlyCheckIn from '../entities/checkin.entity' 4 | 5 | import { 6 | addFeedbackByMentor, 7 | addMonthlyCheckInByMentee, 8 | fetchMonthlyCheckIns 9 | } from '../services/monthlyChecking.service' 10 | 11 | export const postMonthlyCheckIn = async ( 12 | req: Request, 13 | res: Response 14 | ): Promise>> => { 15 | try { 16 | const { 17 | menteeId, 18 | month, 19 | generalUpdatesAndFeedback, 20 | progressTowardsGoals, 21 | mediaContentLinks 22 | } = req.body 23 | 24 | const newCheckIn = await addMonthlyCheckInByMentee( 25 | menteeId, 26 | month, 27 | generalUpdatesAndFeedback, 28 | progressTowardsGoals, 29 | mediaContentLinks 30 | ) 31 | 32 | return res 33 | .status(201) 34 | .json({ checkIn: newCheckIn, message: 'Check-in added successfully' }) 35 | } catch (err) { 36 | if (err instanceof Error) { 37 | console.error('Error executing query', err) 38 | return res 39 | .status(500) 40 | .json({ error: 'Internal server error', message: err.message }) 41 | } 42 | throw err 43 | } 44 | } 45 | 46 | export const getMonthlyCheckIns = async ( 47 | req: Request, 48 | res: Response 49 | ): Promise>> => { 50 | try { 51 | const { menteeId } = req.params 52 | 53 | const { statusCode, checkIns, message } = await fetchMonthlyCheckIns( 54 | menteeId 55 | ) 56 | 57 | return res.status(statusCode).json({ checkIns, message }) 58 | } catch (err) { 59 | if (err instanceof Error) { 60 | console.error('Error executing query', err) 61 | return res 62 | .status(500) 63 | .json({ error: 'Internal server error', message: err.message }) 64 | } 65 | throw err 66 | } 67 | } 68 | 69 | export const addFeedbackMonthlyCheckIn = async ( 70 | req: Request, 71 | res: Response 72 | ): Promise => { 73 | try { 74 | const { checkInId, menteeId, mentorFeedback, isCheckedByMentor } = req.body 75 | 76 | const newMentorFeedbackCheckIn = await addFeedbackByMentor( 77 | menteeId, 78 | checkInId, 79 | mentorFeedback, 80 | isCheckedByMentor 81 | ) 82 | 83 | res.status(201).json({ 84 | feedbackCheckIn: newMentorFeedbackCheckIn 85 | }) 86 | } catch (err) { 87 | if (err instanceof Error) { 88 | console.error('Error executing query', err) 89 | res 90 | .status(500) 91 | .json({ error: 'Internal server error', message: err.message }) 92 | } 93 | throw err 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/configs/google-passport.ts: -------------------------------------------------------------------------------- 1 | import type { Request } from 'express' 2 | import passport from 'passport' 3 | import { Strategy as JwtStrategy } from 'passport-jwt' 4 | import Profile from '../entities/profile.entity' 5 | import { dataSource } from './dbConfig' 6 | import { 7 | GOOGLE_CLIENT_ID, 8 | GOOGLE_CLIENT_SECRET, 9 | GOOGLE_REDIRECT_URL, 10 | JWT_SECRET 11 | } from './envConfig' 12 | 13 | import { Strategy as GoogleStrategy } from 'passport-google-oauth20' 14 | import { findOrCreateUser } from '../services/auth.service' 15 | import { type CreateProfile, type User } from '../types' 16 | 17 | passport.use( 18 | new GoogleStrategy( 19 | { 20 | clientID: GOOGLE_CLIENT_ID, 21 | clientSecret: GOOGLE_CLIENT_SECRET, 22 | callbackURL: GOOGLE_REDIRECT_URL, 23 | scope: ['profile', 'email'], 24 | passReqToCallback: true 25 | }, 26 | async function ( 27 | req: Request, 28 | accessToken: string, 29 | refreshToken: string, 30 | profile: passport.Profile, 31 | done: (err: Error | null, user?: Profile) => void 32 | ) { 33 | try { 34 | const createProfile: CreateProfile = { 35 | id: profile.id, 36 | primary_email: profile.emails?.[0]?.value ?? '', 37 | first_name: profile.name?.givenName ?? '', 38 | last_name: profile.name?.familyName ?? '', 39 | image_url: profile.photos?.[0]?.value ?? '' 40 | } 41 | const user = await findOrCreateUser(createProfile) 42 | done(null, user) 43 | } catch (err) { 44 | done(err as Error) 45 | } 46 | } 47 | ) 48 | ) 49 | 50 | passport.serializeUser((user: Express.User, done) => { 51 | done(null, (user as User).primary_email) 52 | }) 53 | 54 | passport.deserializeUser(async (primary_email: string, done) => { 55 | try { 56 | const profileRepository = dataSource.getRepository(Profile) 57 | const user = await profileRepository.findOne({ 58 | where: { primary_email }, 59 | relations: ['mentor', 'mentee'] 60 | }) 61 | done(null, user) 62 | } catch (err) { 63 | done(err) 64 | } 65 | }) 66 | 67 | const cookieExtractor = (req: Request): string => { 68 | let token = null 69 | if (req?.cookies) { 70 | token = req.cookies.jwt 71 | } 72 | return token 73 | } 74 | 75 | const options = { 76 | jwtFromRequest: cookieExtractor, 77 | secretOrKey: JWT_SECRET 78 | } 79 | 80 | passport.use( 81 | new JwtStrategy(options, async (jwtPayload, done) => { 82 | try { 83 | const profileRepository = dataSource.getRepository(Profile) 84 | const profile = await profileRepository.findOne({ 85 | where: { uuid: jwtPayload.userId }, 86 | relations: ['mentor', 'mentee'] 87 | }) 88 | 89 | if (!profile) { 90 | done(null, false) 91 | } else { 92 | done(null, profile) 93 | } 94 | } catch (error) { 95 | done(error, false) 96 | } 97 | }) 98 | ) 99 | 100 | export default passport 101 | -------------------------------------------------------------------------------- /src/configs/linkedin-passport.ts: -------------------------------------------------------------------------------- 1 | import type { Request } from 'express' 2 | import passport from 'passport' 3 | import { Strategy as JwtStrategy } from 'passport-jwt' 4 | import Profile from '../entities/profile.entity' 5 | import { dataSource } from './dbConfig' 6 | import { 7 | JWT_SECRET, 8 | LINKEDIN_CLIENT_ID, 9 | LINKEDIN_CLIENT_SECRET, 10 | LINKEDIN_REDIRECT_URL 11 | } from './envConfig' 12 | 13 | import { Strategy as LinkedInStrategy } from 'passport-linkedin-oauth2' 14 | import { findOrCreateUser } from '../services/auth.service' 15 | import { type CreateProfile, type LinkedInProfile, type User } from '../types' 16 | 17 | passport.use( 18 | new LinkedInStrategy( 19 | { 20 | clientID: LINKEDIN_CLIENT_ID, 21 | clientSecret: LINKEDIN_CLIENT_SECRET, 22 | callbackURL: LINKEDIN_REDIRECT_URL, 23 | scope: ['openid', 'email', 'profile'], 24 | passReqToCallback: true 25 | }, 26 | async function ( 27 | req: Request, 28 | accessToken: string, 29 | refreshToken: string, 30 | profile: passport.Profile, 31 | done: (err: Error | null, user?: Profile) => void 32 | ) { 33 | try { 34 | const data = profile as unknown as LinkedInProfile 35 | const createProfile: CreateProfile = { 36 | id: data.id, 37 | primary_email: data?.email ?? '', 38 | first_name: data?.givenName ?? '', 39 | last_name: data?.familyName ?? '', 40 | image_url: data?.picture ?? '' 41 | } 42 | const user = await findOrCreateUser(createProfile) 43 | done(null, user) 44 | } catch (err) { 45 | done(err as Error) 46 | } 47 | } 48 | ) 49 | ) 50 | 51 | passport.serializeUser((user: Express.User, done) => { 52 | done(null, (user as User).primary_email) 53 | }) 54 | 55 | passport.deserializeUser(async (primary_email: string, done) => { 56 | try { 57 | const profileRepository = dataSource.getRepository(Profile) 58 | const user = await profileRepository.findOne({ 59 | where: { primary_email }, 60 | relations: ['mentor', 'mentee'] 61 | }) 62 | done(null, user) 63 | } catch (err) { 64 | done(err) 65 | } 66 | }) 67 | 68 | const cookieExtractor = (req: Request): string => { 69 | let token = null 70 | if (req?.cookies) { 71 | token = req.cookies.jwt 72 | } 73 | return token 74 | } 75 | 76 | const options = { 77 | jwtFromRequest: cookieExtractor, 78 | secretOrKey: JWT_SECRET 79 | } 80 | 81 | passport.use( 82 | new JwtStrategy(options, async (jwtPayload, done) => { 83 | try { 84 | const profileRepository = dataSource.getRepository(Profile) 85 | const profile = await profileRepository.findOne({ 86 | where: { uuid: jwtPayload.userId }, 87 | relations: ['mentor', 'mentee'] 88 | }) 89 | 90 | if (!profile) { 91 | done(null, false) 92 | } else { 93 | done(null, profile) 94 | } 95 | } catch (error) { 96 | done(error, false) 97 | } 98 | }) 99 | ) 100 | 101 | export default passport 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scholarx-backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon --watch 'src/**/*.ts' --exec ts-node-dev --respawn src/server.ts", 8 | "start": "ts-node src/server.ts", 9 | "build": "tsc", 10 | "test": "jest", 11 | "lint": "eslint . --ext .ts --ignore-pattern /src/migrations", 12 | "lint:fix": "eslint . --ext .ts --fix", 13 | "format": "prettier --write \"src/**/*.ts\"", 14 | "debug": "nodemon --watch 'src/**/*.ts' --exec ts-node-dev --inspect --respawn src/server.ts", 15 | "typeorm:db": "npm run build && npx typeorm -d dist/src/configs/dbConfig.js", 16 | "migration:generate": "npm run typeorm:db -- migration:generate", 17 | "migration:run": "npm run typeorm:db -- migration:run", 18 | "migration:revert": "npm run typeorm:db -- migration:revert", 19 | "sync:db": "npm run typeorm:db schema:sync", 20 | "seed": "npx ts-node src/scripts/seed-db.ts" 21 | }, 22 | "author": "", 23 | "license": "ISC", 24 | "dependencies": { 25 | "@faker-js/faker": "^8.4.1", 26 | "@tsed/passport": "^7.57.1", 27 | "bcrypt": "^5.1.0", 28 | "body-parser": "^1.20.2", 29 | "cookie-parser": "^1.4.6", 30 | "cors": "^2.8.5", 31 | "dotenv": "^16.3.1", 32 | "ejs": "^3.1.10", 33 | "express": "^4.18.2", 34 | "jsonwebtoken": "^9.0.2", 35 | "multer": "^1.4.5-lts.1", 36 | "nodemailer": "^6.9.13", 37 | "passport": "^0.6.0", 38 | "passport-google-oauth20": "^2.0.0", 39 | "passport-jwt": "^4.0.1", 40 | "passport-linkedin-oauth2": "github:auth0/passport-linkedin-oauth2#v3.0.0", 41 | "pdf-lib": "^1.17.1", 42 | "pg": "^8.10.0", 43 | "reflect-metadata": "^0.1.13", 44 | "ts-node": "^10.9.1", 45 | "typeorm": "^0.3.16", 46 | "zod": "^3.23.8" 47 | }, 48 | "devDependencies": { 49 | "@types/bcrypt": "^5.0.0", 50 | "@types/body-parser": "^1.19.2", 51 | "@types/cookie-parser": "^1.4.3", 52 | "@types/cors": "^2.8.13", 53 | "@types/ejs": "^3.1.5", 54 | "@types/express": "^4.17.17", 55 | "@types/jest": "^29.5.3", 56 | "@types/jsonwebtoken": "^9.0.2", 57 | "@types/multer": "^1.4.11", 58 | "@types/node": "^20.1.4", 59 | "@types/nodemailer": "^6.4.15", 60 | "@types/passport": "^1.0.12", 61 | "@types/passport-google-oauth20": "^2.0.14", 62 | "@types/passport-jwt": "^3.0.9", 63 | "@types/passport-linkedin-oauth2": "^1.5.6", 64 | "@types/pg": "^8.10.1", 65 | "@types/prettier": "^2.7.2", 66 | "@types/supertest": "^2.0.12", 67 | "@typescript-eslint/eslint-plugin": "^5.62.0", 68 | "@typescript-eslint/parser": "^5.59.5", 69 | "eslint": "^8.46.0", 70 | "eslint-config-prettier": "^8.10.0", 71 | "eslint-config-standard-with-typescript": "^37.0.0", 72 | "eslint-plugin-import": "^2.28.0", 73 | "eslint-plugin-n": "^16.0.1", 74 | "eslint-plugin-prettier": "^5.0.0", 75 | "eslint-plugin-promise": "^6.1.1", 76 | "eslint-plugin-unused-imports": "^2.0.0", 77 | "jest": "^29.7.0", 78 | "nodemon": "^3.0.1", 79 | "prettier": "^3.0.0", 80 | "supertest": "^6.3.3", 81 | "ts-jest": "^29.1.0", 82 | "ts-node-dev": "^2.0.0", 83 | "typescript": "^5.3.0" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/services/profile.service.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../configs/dbConfig' 2 | import Mentee from '../entities/mentee.entity' 3 | import Mentor from '../entities/mentor.entity' 4 | import Profile from '../entities/profile.entity' 5 | import { type CreateProfile } from '../types' 6 | import { getMentorPublicData } from '../utils' 7 | 8 | export const updateProfile = async ( 9 | user: Profile, 10 | updateData: Partial 11 | ): Promise<{ 12 | statusCode: number 13 | profile?: Profile | null 14 | message: string 15 | }> => { 16 | try { 17 | const profileRepository = dataSource.getRepository(Profile) 18 | 19 | const { primary_email, first_name, last_name, image_url } = updateData 20 | 21 | const updatedFields: Partial = { 22 | primary_email, 23 | first_name, 24 | last_name, 25 | image_url 26 | } 27 | 28 | await profileRepository.update( 29 | { uuid: user.uuid }, 30 | updatedFields as CreateProfile 31 | ) 32 | 33 | const savedProfile = await profileRepository.findOneBy({ 34 | uuid: user.uuid 35 | }) 36 | 37 | return { 38 | statusCode: 200, 39 | profile: savedProfile, 40 | message: 'Successfully updated the profile' 41 | } 42 | } catch (error) { 43 | console.error('Error executing login', error) 44 | return { statusCode: 500, message: 'Internal server error' } 45 | } 46 | } 47 | 48 | export const deleteProfile = async (userId: string): Promise => { 49 | const profileRepository = dataSource.getRepository(Profile) 50 | 51 | await profileRepository 52 | .createQueryBuilder() 53 | .delete() 54 | .from(Profile) 55 | .where('uuid = :uuid', { uuid: userId }) 56 | .execute() 57 | } 58 | 59 | export const getAllApplications = async ( 60 | userId: string, 61 | type: 'mentor' | 'mentee' 62 | ): Promise<{ 63 | statusCode: number 64 | applications?: Mentor[] | Mentee[] | null | undefined 65 | message: string 66 | }> => { 67 | try { 68 | let applications = [] 69 | if (type === 'mentor') { 70 | const mentorRepository = dataSource.getRepository(Mentor) 71 | 72 | const mentorApplications = await mentorRepository.find({ 73 | where: { profile: { uuid: userId } }, 74 | relations: ['category', 'profile'] 75 | }) 76 | 77 | applications = mentorApplications 78 | } else { 79 | const menteeRepository = dataSource.getRepository(Mentee) 80 | 81 | const menteeApplications = await menteeRepository.find({ 82 | where: { profile: { uuid: userId } }, 83 | relations: ['profile', 'mentor', 'mentor.profile'] 84 | }) 85 | 86 | applications = menteeApplications.map((application) => { 87 | const mentee = { 88 | ...application, 89 | mentor: getMentorPublicData(application.mentor) 90 | } 91 | 92 | return mentee as Mentee 93 | }) 94 | } 95 | 96 | if (applications?.length === 0) { 97 | return { 98 | statusCode: 200, 99 | applications, 100 | message: `No ${type} applications found for the user` 101 | } 102 | } 103 | 104 | return { 105 | statusCode: 200, 106 | applications, 107 | message: `${type} applications found` 108 | } 109 | } catch (error) { 110 | console.error('Error executing query', error) 111 | return { statusCode: 500, message: 'Internal server error' } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/services/monthlyChecking.service.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../configs/dbConfig' 2 | import MonthlyCheckIn from '../entities/checkin.entity' 3 | import Mentee from '../entities/mentee.entity' 4 | import { type MonthlyCheckInResponse } from '../types' 5 | 6 | export const addFeedbackByMentor = async ( 7 | menteeId: string, 8 | checkInId: string, 9 | mentorfeedback: string, 10 | isCheckedByMentor: boolean 11 | ): Promise<{ 12 | statusCode: number 13 | message: string 14 | }> => { 15 | try { 16 | const menteeRepository = dataSource.getRepository(Mentee) 17 | const checkInRepository = dataSource.getRepository(MonthlyCheckIn) 18 | 19 | const mentee = await menteeRepository.findOne({ 20 | where: { uuid: menteeId } 21 | }) 22 | 23 | if (!mentee) { 24 | return { statusCode: 404, message: 'Mentee not found' } 25 | } 26 | 27 | const checkIn = await checkInRepository.findOne({ 28 | where: { uuid: checkInId, mentee: { uuid: menteeId } } 29 | }) 30 | 31 | if (!checkIn) { 32 | return { statusCode: 404, message: 'Check-in not found' } 33 | } 34 | 35 | checkIn.mentorFeedback = mentorfeedback 36 | checkIn.isCheckedByMentor = isCheckedByMentor 37 | checkIn.mentorCheckedDate = new Date() 38 | 39 | await checkInRepository.save(checkIn) 40 | 41 | return { statusCode: 200, message: 'feedback added' } 42 | } catch (err) { 43 | console.error('Error in addFeedbackToMonthlyCheckIn', err) 44 | return { statusCode: 500, message: 'Internal server error' } 45 | } 46 | } 47 | 48 | export const addMonthlyCheckInByMentee = async ( 49 | menteeId: string, 50 | month: string, 51 | generalUpdatesAndFeedback: string, 52 | progressTowardsGoals: string, 53 | mediaContentLinks: string[] 54 | ): Promise<{ 55 | statusCode: number 56 | message: string 57 | }> => { 58 | try { 59 | const menteeRepository = dataSource.getRepository(Mentee) 60 | const checkInRepository = dataSource.getRepository(MonthlyCheckIn) 61 | 62 | const mentee = await menteeRepository.findOne({ 63 | where: { 64 | uuid: menteeId 65 | } 66 | }) 67 | 68 | if (!mentee) { 69 | return { statusCode: 404, message: 'Mentee not found' } 70 | } 71 | 72 | const newCheckIn = checkInRepository.create({ 73 | month, 74 | generalUpdatesAndFeedback, 75 | progressTowardsGoals, 76 | mediaContentLinks: mediaContentLinks || null, 77 | checkInDate: new Date(), 78 | mentee 79 | }) 80 | 81 | await checkInRepository.save(newCheckIn) 82 | 83 | return { statusCode: 200, message: 'monthly checking inserted' } 84 | } catch (err) { 85 | console.error('Error in addMonthlyCheckIn', err) 86 | throw new Error('Error in addMonthlyCheckIn') 87 | } 88 | } 89 | 90 | export const fetchMonthlyCheckIns = async ( 91 | menteeId: string 92 | ): Promise<{ 93 | statusCode: number 94 | checkIns: MonthlyCheckInResponse[] 95 | message: string 96 | }> => { 97 | try { 98 | const checkInRepository = dataSource.getRepository(MonthlyCheckIn) 99 | 100 | const mentee = await dataSource.getRepository(Mentee).findOne({ 101 | where: { uuid: menteeId } 102 | }) 103 | 104 | if (!mentee) { 105 | return { statusCode: 404, checkIns: [], message: 'Mentee not found' } 106 | } 107 | 108 | const checkIns = await checkInRepository.find({ 109 | where: { mentee: { uuid: menteeId } }, 110 | relations: ['mentee'], 111 | order: { checkInDate: 'DESC' } 112 | }) 113 | 114 | return { 115 | statusCode: 200, 116 | checkIns, 117 | message: 'Check-ins found' 118 | } 119 | } catch (err) { 120 | console.error('Error in fetchMonthlyCheckIns', err) 121 | return { statusCode: 500, checkIns: [], message: 'Internal server error' } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/controllers/mentee.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from 'express' 2 | import { type ApiResponse } from '../types' 3 | import type Mentee from '../entities/mentee.entity' 4 | import type Profile from '../entities/profile.entity' 5 | import { 6 | getMentee, 7 | revoke, 8 | updateStatus 9 | } from '../services/admin/mentee.service' 10 | import { MentorApplicationStatus, StatusUpdatedBy } from '../enums' 11 | import { addMentee, getPublicMentee } from '../services/mentee.service' 12 | 13 | export const menteeApplicationHandler = async ( 14 | req: Request, 15 | res: Response 16 | ): Promise> => { 17 | try { 18 | const user = req.user as Profile 19 | const { application, mentorId } = req.body 20 | const { mentee, statusCode, message } = await addMentee( 21 | user, 22 | application, 23 | mentorId 24 | ) 25 | return res.status(statusCode).json({ mentee, message }) 26 | } catch (err) { 27 | if (err instanceof Error) { 28 | console.error('Error executing query', err) 29 | return res 30 | .status(500) 31 | .json({ error: 'Internal server error', message: err.message }) 32 | } 33 | 34 | throw err 35 | } 36 | } 37 | 38 | export const updateMenteeStatus = async ( 39 | req: Request, 40 | res: Response 41 | ): Promise> => { 42 | try { 43 | const user = req.user as Profile 44 | const { state } = req.body 45 | const { menteeId } = req.params 46 | 47 | if ( 48 | !user.mentor?.filter( 49 | (mentor) => mentor.state === MentorApplicationStatus.APPROVED 50 | ) 51 | ) { 52 | return res.status(403).json({ message: 'Only mentors are allowed' }) 53 | } 54 | 55 | const { statusCode, message } = await updateStatus( 56 | menteeId, 57 | state, 58 | StatusUpdatedBy.MENTOR 59 | ) 60 | return res.status(statusCode).json({ message }) 61 | } catch (err) { 62 | if (err instanceof Error) { 63 | console.error('Error executing query', err) 64 | return res 65 | .status(500) 66 | .json({ error: 'Internal server error', message: err.message }) 67 | } 68 | throw err 69 | } 70 | } 71 | 72 | export const revokeApplication = async ( 73 | req: Request, 74 | res: Response 75 | ): Promise> => { 76 | try { 77 | const user = req.user as Profile 78 | 79 | const { statusCode, message } = await revoke(user.uuid) 80 | return res.status(statusCode).json({ message }) 81 | } catch (err) { 82 | if (err instanceof Error) { 83 | console.error('Error executing query', err) 84 | return res 85 | .status(500) 86 | .json({ error: 'Internal server error', message: err.message }) 87 | } 88 | throw err 89 | } 90 | } 91 | 92 | export const getMenteeDetails = async ( 93 | req: Request, 94 | res: Response 95 | ): Promise> => { 96 | try { 97 | const { menteeId } = req.params 98 | const { statusCode, message, mentee } = await getMentee(menteeId) 99 | return res.status(statusCode).json({ mentee, message }) 100 | } catch (err) { 101 | if (err instanceof Error) { 102 | console.error('Error executing query', err) 103 | return res 104 | .status(500) 105 | .json({ error: 'Internal server error', message: err.message }) 106 | } 107 | throw err 108 | } 109 | } 110 | 111 | export const getPublicMenteeDetails = async ( 112 | req: Request, 113 | res: Response 114 | ): Promise> => { 115 | try { 116 | const { menteeId } = req.params 117 | const { statusCode, message, mentee } = await getPublicMentee(menteeId) 118 | return res.status(statusCode).json({ mentee, message }) 119 | } catch (err) { 120 | if (err instanceof Error) { 121 | console.error('Error executing query', err) 122 | return res 123 | .status(500) 124 | .json({ error: 'Internal server error', message: err.message }) 125 | } 126 | throw err 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/controllers/profile.controller.ts: -------------------------------------------------------------------------------- 1 | import { type Request, type Response } from 'express' 2 | import { 3 | updateProfile, 4 | deleteProfile, 5 | getAllApplications 6 | } from '../services/profile.service' 7 | import type Profile from '../entities/profile.entity' 8 | import type { ApiResponse } from '../types' 9 | import type Mentor from '../entities/mentor.entity' 10 | import { upload } from '../utils' 11 | import { IMG_HOST } from '../configs/envConfig' 12 | 13 | export const getProfileHandler = async ( 14 | req: Request, 15 | res: Response 16 | ): Promise> => { 17 | try { 18 | const { user } = req 19 | if (!user) { 20 | return res.status(404).json({ message: 'Profile not found' }) 21 | } 22 | 23 | return res.status(200).json(user) 24 | } catch (err) { 25 | if (err instanceof Error) { 26 | console.error('Error executing query', err) 27 | return res 28 | .status(500) 29 | .json({ error: 'Internal server error', message: err.message }) 30 | } 31 | 32 | throw err 33 | } 34 | } 35 | 36 | export const updateProfileHandler = async ( 37 | req: Request, 38 | res: Response 39 | ): Promise> => { 40 | try { 41 | const user = req.user as Profile 42 | if (!user) { 43 | return res.status(404).json({ message: 'Profile not found' }) 44 | } 45 | 46 | return await new Promise>((resolve, reject) => { 47 | upload(req, res, async (err) => { 48 | if (err) { 49 | reject(err) 50 | } else { 51 | try { 52 | const updateData: Partial = { ...req.body } 53 | 54 | if (req.file) { 55 | updateData.image_url = IMG_HOST + '/' + req.file.filename 56 | } 57 | 58 | const { statusCode, profile, message } = await updateProfile( 59 | user, 60 | updateData 61 | ) 62 | return res.status(statusCode).json({ profile, message }) 63 | } catch (error) { 64 | reject(error) 65 | } 66 | } 67 | }) 68 | }) 69 | } catch (error) { 70 | if (error instanceof Error) { 71 | console.error('Error executing query', error) 72 | return res 73 | .status(500) 74 | .json({ error: 'Internal server error', message: error.message }) 75 | } 76 | throw error 77 | } 78 | } 79 | export const deleteProfileHandler = async ( 80 | req: Request, 81 | res: Response 82 | ): Promise> => { 83 | try { 84 | const user = req.user as Profile 85 | if (!user) { 86 | return res.status(404).json({ message: 'Profile not found' }) 87 | } 88 | 89 | await deleteProfile(user.uuid) 90 | return res.status(200).json({ message: 'Profile deleted' }) 91 | } catch (err) { 92 | if (err instanceof Error) { 93 | console.error('Error executing query', err) 94 | return res 95 | .status(500) 96 | .json({ error: 'Internal server error', message: err.message }) 97 | } 98 | 99 | throw err 100 | } 101 | } 102 | 103 | export const getApplicationsHandler = async ( 104 | req: Request, 105 | res: Response 106 | ): Promise> => { 107 | try { 108 | const user = req.user as Profile 109 | const applicationType = req.query.type 110 | if (applicationType === 'mentor' || applicationType === 'mentee') { 111 | const { applications, statusCode, message } = await getAllApplications( 112 | user.uuid, 113 | applicationType 114 | ) 115 | 116 | return res.status(statusCode).json({ 117 | applications, 118 | message 119 | }) 120 | } else { 121 | return res.status(400).json({ message: 'Invalid application type' }) 122 | } 123 | } catch (error) { 124 | if (error instanceof Error) { 125 | console.error('Error executing query', error) 126 | return res 127 | .status(500) 128 | .json({ error: 'Internal server errorrrr', message: error.message }) 129 | } 130 | 131 | throw error 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/services/admin/category.service.test.ts: -------------------------------------------------------------------------------- 1 | import { createCategory, changeCategory } from './category.service' 2 | import { dataSource } from '../../configs/dbConfig' 3 | 4 | jest.mock('../../configs/dbConfig', () => ({ 5 | dataSource: { 6 | getRepository: jest.fn() 7 | } 8 | })) 9 | 10 | describe('Category Service', () => { 11 | describe('createCategory', () => { 12 | it('should create a category successfully', async () => { 13 | const categoryName = 'Test Category' 14 | const mockCategoryRepository = { 15 | save: jest.fn().mockResolvedValue({ 16 | uuid: 'mock-uuid', 17 | category: categoryName, 18 | mentors: [], 19 | created_at: new Date(), 20 | updated_at: new Date(), 21 | updateTimestamps: jest.fn(), 22 | generateUuid: jest.fn() 23 | } as const) 24 | } 25 | 26 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 27 | mockCategoryRepository 28 | ) 29 | 30 | const result = await createCategory(categoryName) 31 | 32 | expect(result.statusCode).toBe(201) 33 | expect(result.category?.category).toBe(categoryName) 34 | expect(result.message).toBe('Category created successfully') 35 | }) 36 | 37 | it('should handle error during category creation', async () => { 38 | const categoryName = 'Test Category' 39 | const mockCategoryRepository = { 40 | save: jest.fn().mockRejectedValue(new Error('Test repository error')) 41 | } 42 | 43 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 44 | mockCategoryRepository 45 | ) 46 | 47 | await expect(createCategory(categoryName)).rejects.toThrowError( 48 | 'Error creating category' 49 | ) 50 | }) 51 | }) 52 | 53 | describe('changeCategory', () => { 54 | it('should update a category successfully', async () => { 55 | const categoryId = 'mock-uuid' 56 | const categoryName = 'Updated Category' 57 | const mockCategory = { 58 | uuid: 'mock-uuid', 59 | category: categoryName, 60 | mentors: [], 61 | created_at: new Date(), 62 | updated_at: new Date(), 63 | updateTimestamps: jest.fn(), 64 | generateUuid: jest.fn() 65 | } as const 66 | 67 | const mockCategoryRepository = { 68 | findOne: jest.fn().mockResolvedValue(mockCategory), 69 | update: jest.fn().mockResolvedValue({ affected: 1 }) 70 | } 71 | 72 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 73 | mockCategoryRepository 74 | ) 75 | 76 | const result = await changeCategory(categoryId, categoryName) 77 | 78 | expect(result.statusCode).toBe(201) 79 | expect(result.category?.category).toBe(categoryName) 80 | expect(result.message).toBe('Category updated successfully') 81 | }) 82 | 83 | it('should handle category not found during update', async () => { 84 | const categoryId = 'nonexistent-uuid' 85 | const categoryName = 'Updated Category' 86 | const mockCategoryRepository = { 87 | findOne: jest.fn().mockResolvedValue(null) 88 | } 89 | 90 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 91 | mockCategoryRepository 92 | ) 93 | 94 | const result = await changeCategory(categoryId, categoryName) 95 | 96 | expect(result.statusCode).toBe(404) 97 | expect(result.message).toBe('Category not found') 98 | }) 99 | 100 | it('should handle error during category update', async () => { 101 | const categoryId = 'mock-uuid' 102 | const categoryName = 'Updated Category' 103 | const mockCategoryRepository = { 104 | findOne: jest.fn().mockResolvedValue({}), 105 | update: jest.fn().mockRejectedValue(new Error('Test repository error')) 106 | } 107 | 108 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 109 | mockCategoryRepository 110 | ) 111 | 112 | await expect( 113 | changeCategory(categoryId, categoryName) 114 | ).rejects.toThrowError('Error updating category') 115 | }) 116 | }) 117 | }) 118 | -------------------------------------------------------------------------------- /src/services/mentee.service.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../configs/dbConfig' 2 | import Mentee from '../entities/mentee.entity' 3 | import Mentor from '../entities/mentor.entity' 4 | import type Profile from '../entities/profile.entity' 5 | import { MenteeApplicationStatus } from '../enums' 6 | import { 7 | getEmailContent, 8 | getMentorNotifyEmailContent, 9 | getMenteePublicData, 10 | capitalizeFirstLetter 11 | } from '../utils' 12 | import { sendEmail } from './admin/email.service' 13 | 14 | export const addMentee = async ( 15 | user: Profile, 16 | application: Record, 17 | mentorId: string 18 | ): Promise<{ 19 | statusCode: number 20 | mentee?: Mentee 21 | message: string 22 | }> => { 23 | try { 24 | const menteeRepository = dataSource.getRepository(Mentee) 25 | const mentorRepository = dataSource.getRepository(Mentor) 26 | 27 | const mentor = await mentorRepository.findOne({ 28 | where: { uuid: mentorId } 29 | }) 30 | 31 | if (mentor === null || mentor === undefined) { 32 | return { 33 | statusCode: 404, 34 | message: 'Mentor not found' 35 | } 36 | } 37 | 38 | if (!mentor.availability) { 39 | return { 40 | statusCode: 403, 41 | message: 'Mentor is not currently available' 42 | } 43 | } 44 | 45 | const userMentorProfile = await mentorRepository.findOne({ 46 | where: { 47 | profile: { 48 | uuid: user.uuid 49 | } 50 | } 51 | }) 52 | 53 | if (userMentorProfile) { 54 | return { 55 | statusCode: 409, 56 | message: 57 | 'A mentor cannot become a mentee, Please contact sustainableeducationfoundation@gmail.com' 58 | } 59 | } 60 | 61 | const existingMentees: Mentee[] = await menteeRepository.find({ 62 | where: { profile: { uuid: user.uuid } } 63 | }) 64 | 65 | for (const mentee of existingMentees) { 66 | switch (mentee.state) { 67 | case MenteeApplicationStatus.PENDING: 68 | return { 69 | statusCode: 409, 70 | message: 'The mentee application is pending' 71 | } 72 | case MenteeApplicationStatus.APPROVED: 73 | return { 74 | statusCode: 409, 75 | message: 'The user is already a mentee' 76 | } 77 | default: 78 | break 79 | } 80 | } 81 | 82 | application.firstName = capitalizeFirstLetter( 83 | application.firstName as string 84 | ) 85 | application.lastName = capitalizeFirstLetter(application.lastName as string) 86 | 87 | const newMentee = new Mentee( 88 | MenteeApplicationStatus.PENDING, 89 | application, 90 | user, 91 | mentor 92 | ) 93 | 94 | await menteeRepository.save(newMentee) 95 | 96 | const content = await getEmailContent( 97 | 'mentee', 98 | MenteeApplicationStatus.PENDING, 99 | application.firstName as string 100 | ) 101 | 102 | const mentorContent = getMentorNotifyEmailContent( 103 | mentor.application.firstName as string 104 | ) 105 | 106 | if (content) { 107 | await sendEmail( 108 | application.email as string, 109 | content.subject, 110 | content.message 111 | ) 112 | 113 | await sendEmail( 114 | mentor.application.email as string, 115 | mentorContent.subject, 116 | mentorContent.message 117 | ) 118 | } 119 | 120 | return { 121 | statusCode: 200, 122 | mentee: newMentee, 123 | message: 'New mentee created' 124 | } 125 | } catch (err) { 126 | throw new Error('Error adding mentee') 127 | } 128 | } 129 | 130 | export const getPublicMentee = async ( 131 | menteeId: string 132 | ): Promise<{ 133 | statusCode: number 134 | mentee: Mentee | null 135 | message: string 136 | }> => { 137 | try { 138 | const menteeRepository = dataSource.getRepository(Mentee) 139 | 140 | const mentee = await menteeRepository.findOne({ 141 | where: { uuid: menteeId }, 142 | relations: ['profile', 'mentor', 'mentor.profile'] 143 | }) 144 | 145 | if (!mentee) { 146 | return { 147 | statusCode: 404, 148 | mentee: null, 149 | message: 'Mentee not found' 150 | } 151 | } 152 | 153 | const publicMentee = getMenteePublicData(mentee) 154 | 155 | return { 156 | statusCode: 200, 157 | mentee: publicMentee, 158 | message: 'Mentees found' 159 | } 160 | } catch (err) { 161 | throw new Error('Error getting mentees') 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/controllers/admin/mentee.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from 'express' 2 | import type Mentee from '../../entities/mentee.entity' 3 | import type Profile from '../../entities/profile.entity' 4 | import { 5 | MenteeApplicationStatus, 6 | ProfileTypes, 7 | StatusUpdatedBy 8 | } from '../../enums' 9 | import { 10 | getAllMenteeEmailsService, 11 | getAllMentees, 12 | getMentee, 13 | updateStatus 14 | } from '../../services/admin/mentee.service' 15 | import type { ApiResponse, PaginatedApiResponse } from '../../types' 16 | 17 | export const getMentees = async ( 18 | req: Request, 19 | res: Response 20 | ): Promise>> => { 21 | try { 22 | const pageNumber = parseInt(req.query.pageNumber as string) 23 | const pageSize = parseInt(req.query.pageSize as string) 24 | 25 | const user = req.user as Profile 26 | const status: MenteeApplicationStatus | undefined = req.query.status as 27 | | MenteeApplicationStatus 28 | | undefined 29 | 30 | if (user.type !== ProfileTypes.ADMIN) { 31 | return res.status(403).json({ message: 'Only Admins are allowed' }) 32 | } 33 | 34 | if (status && !(status.toUpperCase() in MenteeApplicationStatus)) { 35 | return res.status(400).json({ message: 'Please provide a valid status' }) 36 | } 37 | 38 | const { items, totalItemCount, statusCode, message } = await getAllMentees({ 39 | status, 40 | pageNumber, 41 | pageSize 42 | }) 43 | return res.status(statusCode).json({ 44 | items, 45 | totalItemCount, 46 | pageNumber, 47 | pageSize, 48 | message 49 | }) 50 | } catch (err) { 51 | if (err instanceof Error) { 52 | console.error('Error executing query', err) 53 | return res 54 | .status(500) 55 | .json({ error: 'Internal server error', message: err.message }) 56 | } 57 | throw err 58 | } 59 | } 60 | 61 | export const updateMenteeStatus = async ( 62 | req: Request, 63 | res: Response 64 | ): Promise> => { 65 | try { 66 | const user = req.user as Profile 67 | const { state } = req.body 68 | const { menteeId } = req.params 69 | 70 | if (user.type !== ProfileTypes.ADMIN) { 71 | return res.status(403).json({ message: 'Only Admins are allowed' }) 72 | } 73 | 74 | const { statusCode, message } = await updateStatus( 75 | menteeId, 76 | state, 77 | StatusUpdatedBy.ADMIN 78 | ) 79 | return res.status(statusCode).json({ message }) 80 | } catch (err) { 81 | if (err instanceof Error) { 82 | console.error('Error executing query', err) 83 | return res 84 | .status(500) 85 | .json({ error: 'Internal server error', message: err.message }) 86 | } 87 | throw err 88 | } 89 | } 90 | 91 | export const getMenteeDetails = async ( 92 | req: Request, 93 | res: Response 94 | ): Promise> => { 95 | try { 96 | const user = req.user as Profile 97 | const { menteeId } = req.params 98 | 99 | if (user.type !== ProfileTypes.ADMIN) { 100 | return res.status(403).json({ message: 'Only Admins are allowed' }) 101 | } 102 | 103 | const { statusCode, message, mentee } = await getMentee(menteeId) 104 | return res.status(statusCode).json({ mentee, message }) 105 | } catch (err) { 106 | if (err instanceof Error) { 107 | console.error('Error executing query', err) 108 | return res 109 | .status(500) 110 | .json({ error: 'Internal server error', message: err.message }) 111 | } 112 | throw err 113 | } 114 | } 115 | 116 | export const getAllMenteeEmails = async ( 117 | req: Request, 118 | res: Response 119 | ): Promise>> => { 120 | try { 121 | const pageNumber = parseInt(req.query.pageNumber as string) 122 | const pageSize = parseInt(req.query.pageSize as string) 123 | 124 | const status: MenteeApplicationStatus | undefined = req.query.status as 125 | | MenteeApplicationStatus 126 | | undefined 127 | if (status && status.toUpperCase() in MenteeApplicationStatus) { 128 | const { items, statusCode, message, totalItemCount } = 129 | await getAllMenteeEmailsService({ 130 | status, 131 | pageNumber, 132 | pageSize 133 | }) 134 | return res.status(statusCode).json({ 135 | items, 136 | pageNumber, 137 | pageSize, 138 | totalItemCount, 139 | message 140 | }) 141 | } else { 142 | return res.status(400).json({ message: 'Invalid Status' }) 143 | } 144 | } catch (err) { 145 | console.error(err) 146 | return res.status(500).json({ error: err || 'Internal Server Error' }) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/controllers/mentor.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from 'express' 2 | import type Mentee from '../entities/mentee.entity' 3 | import type Mentor from '../entities/mentor.entity' 4 | import type Profile from '../entities/profile.entity' 5 | import { MenteeApplicationStatus } from '../enums' 6 | import { getAllMenteesByMentor } from '../services/admin/mentee.service' 7 | import { 8 | createMentor, 9 | getAllMentors, 10 | getMentor, 11 | updateAvailability 12 | } from '../services/mentor.service' 13 | import type { ApiResponse, PaginatedApiResponse } from '../types' 14 | 15 | export const mentorApplicationHandler = async ( 16 | req: Request, 17 | res: Response 18 | ): Promise> => { 19 | try { 20 | const user = req.user as Profile 21 | const { application, categoryId, countryId } = req.body 22 | 23 | const { mentor, statusCode, message } = await createMentor( 24 | user, 25 | application, 26 | categoryId, 27 | countryId 28 | ) 29 | 30 | return res.status(statusCode).json({ mentor, message }) 31 | } catch (err) { 32 | if (err instanceof Error) { 33 | console.error('Error executing query', err) 34 | return res 35 | .status(500) 36 | .json({ error: 'Internal server error', message: err.message }) 37 | } 38 | 39 | throw err 40 | } 41 | } 42 | 43 | export const mentorAvailabilityHandler = async ( 44 | req: Request, 45 | res: Response 46 | ): Promise> => { 47 | try { 48 | const { availability } = req.body 49 | const { mentorId } = req.params 50 | 51 | const { statusCode, message } = await updateAvailability( 52 | mentorId, 53 | availability 54 | ) 55 | 56 | return res.status(statusCode).json({ message }) 57 | } catch (err) { 58 | if (err instanceof Error) { 59 | console.error('Error executing query', err) 60 | return res 61 | .status(500) 62 | .json({ error: 'Internal server error', message: err.message }) 63 | } 64 | 65 | throw err 66 | } 67 | } 68 | 69 | export const mentorDetailsHandler = async ( 70 | req: Request, 71 | res: Response 72 | ): Promise> => { 73 | try { 74 | const mentorId = req.params.mentorId 75 | const { mentor, statusCode, message } = await getMentor(mentorId) 76 | 77 | if (!mentor) { 78 | return res.status(statusCode).json({ error: message }) 79 | } 80 | 81 | return res.status(statusCode).json({ mentor, message }) 82 | } catch (err) { 83 | if (err instanceof Error) { 84 | console.error('Error executing query', err) 85 | return res 86 | .status(500) 87 | .json({ error: 'Internal server error', message: err.message }) 88 | } 89 | 90 | throw err 91 | } 92 | } 93 | 94 | export const getAllMentorsHandler = async ( 95 | req: Request, 96 | res: Response 97 | ): Promise>> => { 98 | try { 99 | const pageNumber = parseInt(req.query.pageNumber as string) 100 | const pageSize = parseInt(req.query.pageSize as string) 101 | 102 | const categoryId: string = req.query.categoryId as string 103 | const { items, totalItemCount, statusCode, message } = await getAllMentors({ 104 | categoryId, 105 | pageNumber, 106 | pageSize 107 | }) 108 | 109 | return res.status(statusCode).json({ 110 | pageNumber, 111 | pageSize, 112 | totalItemCount, 113 | items, 114 | message 115 | }) 116 | } catch (err) { 117 | if (err instanceof Error) { 118 | console.error('Error executing query', err) 119 | return res 120 | .status(500) 121 | .json({ error: 'Internal server error', message: err.message }) 122 | } 123 | 124 | throw err 125 | } 126 | } 127 | 128 | export const getMenteesByMentor = async ( 129 | req: Request, 130 | res: Response 131 | ): Promise> => { 132 | try { 133 | const user = req.user as Profile 134 | const status: MenteeApplicationStatus | undefined = req.query.status as 135 | | MenteeApplicationStatus 136 | | undefined 137 | 138 | if (status && !(status.toUpperCase() in MenteeApplicationStatus)) { 139 | return res.status(400).json({ message: 'Please provide a valid status' }) 140 | } 141 | 142 | const { mentees, statusCode, message } = await getAllMenteesByMentor( 143 | status, 144 | user.uuid 145 | ) 146 | return res.status(statusCode).json({ mentees, message }) 147 | } catch (err) { 148 | if (err instanceof Error) { 149 | console.error('Error executing query', err) 150 | return res 151 | .status(500) 152 | .json({ error: 'Internal server error', message: err.message }) 153 | } 154 | 155 | throw err 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/services/profile.service.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | updateProfile, 3 | deleteProfile, 4 | getAllApplications 5 | } from './profile.service' 6 | import { dataSource } from '../configs/dbConfig' 7 | import Profile from '../entities/profile.entity' 8 | 9 | jest.mock('../configs/dbConfig', () => ({ 10 | dataSource: { 11 | getRepository: jest.fn() 12 | } 13 | })) 14 | 15 | describe('Profile Service', () => { 16 | describe('updateProfile', () => { 17 | it('should update the profile successfully', async () => { 18 | const user = { uuid: 'mock-uuid' } as unknown as Profile 19 | const partialProfile = { 20 | primary_email: 'new@example.com', 21 | first_name: 'John' 22 | } 23 | 24 | const mockProfileRepository = { 25 | update: jest.fn().mockResolvedValue({ affected: 1 }), 26 | findOneBy: jest.fn().mockResolvedValue({ 27 | uuid: 'mock-uuid', 28 | ...partialProfile 29 | } as const) 30 | } 31 | 32 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 33 | mockProfileRepository 34 | ) 35 | 36 | const result = await updateProfile(user, partialProfile) 37 | 38 | expect(result.statusCode).toBe(200) 39 | expect(result.profile?.uuid).toBe('mock-uuid') 40 | expect(result.profile?.primary_email).toBe('new@example.com') 41 | expect(result.profile?.first_name).toBe('John') 42 | expect(result.message).toBe('Successfully updated the profile') 43 | 44 | expect(mockProfileRepository.update).toHaveBeenCalledWith( 45 | { uuid: user.uuid }, 46 | partialProfile 47 | ) 48 | 49 | expect(mockProfileRepository.findOneBy).toHaveBeenCalledWith({ 50 | uuid: user.uuid 51 | }) 52 | }) 53 | 54 | it('should handle error during profile update', async () => { 55 | const user = { uuid: 'mock-uuid' } as unknown as Profile 56 | const partialProfile = { 57 | primary_email: 'new@example.com', 58 | first_name: 'John' 59 | } 60 | 61 | const mockProfileRepository = { 62 | update: jest.fn().mockRejectedValue(new Error('Test repository error')) 63 | } 64 | 65 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 66 | mockProfileRepository 67 | ) 68 | 69 | const result = await updateProfile(user, partialProfile) 70 | 71 | expect(result.statusCode).toBe(500) 72 | expect(result.message).toBe('Internal server error') 73 | }) 74 | }) 75 | 76 | describe('deleteProfile', () => { 77 | it('should delete the profile successfully', async () => { 78 | const userId = 'mock-uuid' 79 | 80 | const mockProfileRepository = { 81 | createQueryBuilder: jest.fn().mockReturnThis(), 82 | delete: jest.fn().mockReturnThis(), 83 | from: jest.fn().mockReturnThis(), 84 | where: jest.fn().mockReturnThis(), 85 | execute: jest.fn().mockResolvedValue({ affected: 1 }) 86 | } 87 | 88 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 89 | mockProfileRepository 90 | ) 91 | 92 | await deleteProfile(userId) 93 | 94 | expect(mockProfileRepository.createQueryBuilder).toHaveBeenCalled() 95 | expect(mockProfileRepository.delete).toHaveBeenCalled() 96 | expect(mockProfileRepository.from).toHaveBeenCalledWith(Profile) 97 | expect(mockProfileRepository.where).toHaveBeenCalledWith('uuid = :uuid', { 98 | uuid: userId 99 | }) 100 | expect(mockProfileRepository.execute).toHaveBeenCalled() 101 | }) 102 | 103 | it('should handle error during profile deletion', async () => { 104 | const userId = 'mock-uuid' 105 | 106 | const mockProfileRepository = { 107 | createQueryBuilder: jest.fn().mockReturnThis(), 108 | delete: jest.fn().mockReturnThis(), 109 | from: jest.fn().mockReturnThis(), 110 | where: jest.fn().mockReturnThis(), 111 | execute: jest 112 | .fn() 113 | .mockRejectedValue(new Error('Error executing delete query')) 114 | } 115 | 116 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 117 | mockProfileRepository 118 | ) 119 | 120 | await expect(deleteProfile(userId)).rejects.toThrowError( 121 | 'Error executing delete query' 122 | ) 123 | }) 124 | }) 125 | 126 | describe('getAllMentorApplications', () => { 127 | it('should get all mentor applications for the user', async () => { 128 | const user = { uuid: 'mock-uuid' } as unknown as Profile 129 | 130 | const mockMentorRepository = { 131 | find: jest.fn().mockResolvedValue([ 132 | { 133 | uuid: 'mentor-uuid-1', 134 | application: 'Application 1', 135 | profile: user, 136 | category: {} 137 | }, 138 | { 139 | uuid: 'mentor-uuid-2', 140 | application: 'Application 2', 141 | profile: user, 142 | category: {} 143 | } 144 | ] as const) 145 | } 146 | 147 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 148 | mockMentorRepository 149 | ) 150 | 151 | const result = await getAllApplications(user.uuid, 'mentor') 152 | 153 | expect(result.statusCode).toBe(200) 154 | expect(result.applications?.length).toBe(2) 155 | expect(result.applications?.[0].uuid).toBe('mentor-uuid-1') 156 | expect(result.applications?.[1].uuid).toBe('mentor-uuid-2') 157 | expect(result.message).toBe('mentor applications found') 158 | 159 | expect(mockMentorRepository.find).toHaveBeenCalled() 160 | }) 161 | }) 162 | }) 163 | -------------------------------------------------------------------------------- /src/services/auth.service.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | registerUser, 3 | loginUser, 4 | resetPassword, 5 | generateResetToken 6 | } from './auth.service' 7 | import { dataSource } from '../configs/dbConfig' 8 | 9 | jest.mock('bcrypt', () => ({ 10 | hash: jest.fn( 11 | async (password: string) => await Promise.resolve(`hashed_${password}`) 12 | ), 13 | compare: jest.fn( 14 | async (password, hashedPassword) => 15 | await Promise.resolve(password === hashedPassword) 16 | ) 17 | })) 18 | 19 | interface PayloadType { 20 | userId: string 21 | } 22 | 23 | jest.mock('jsonwebtoken', () => ({ 24 | sign: jest.fn((payload: PayloadType) => `mocked_token_${payload.userId}`) 25 | })) 26 | 27 | jest.mock('../configs/dbConfig', () => ({ 28 | dataSource: { 29 | getRepository: jest.fn() 30 | } 31 | })) 32 | 33 | describe('registerUser', () => { 34 | it('should register a new user successfully', async () => { 35 | const mockProfileRepository = { 36 | findOne: jest.fn().mockResolvedValue(null), 37 | create: jest.fn((data) => data), 38 | save: jest.fn(async (profile) => await Promise.resolve(profile)) 39 | } 40 | 41 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 42 | mockProfileRepository 43 | ) 44 | 45 | const result = await registerUser( 46 | 'newuser@example.com', 47 | 'password123', 48 | 'John', 49 | 'Doe' 50 | ) 51 | expect(result.message).toBe('Registration successful') 52 | }) 53 | 54 | it('should return conflict status code for an existing user', async () => { 55 | const mockProfileRepository = { 56 | findOne: jest.fn().mockResolvedValue({}) 57 | } 58 | 59 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 60 | mockProfileRepository 61 | ) 62 | 63 | const result = await registerUser( 64 | 'existinguser@example.com', 65 | 'password123', 66 | 'John', 67 | 'Doe' 68 | ) 69 | 70 | expect(result.statusCode).toBe(409) 71 | expect(result.message).toBe('Email already exists') 72 | expect(result.profile).toBeUndefined() 73 | }) 74 | 75 | it('should handle internal server error during registration', async () => { 76 | ;(dataSource.getRepository as jest.Mock).mockImplementation(() => { 77 | throw new Error('Test repository error') 78 | }) 79 | 80 | const result = await registerUser( 81 | 'testuser@example.com', 82 | 'password123', 83 | 'John', 84 | 'Doe' 85 | ) 86 | 87 | expect(result.statusCode).toBe(500) 88 | expect(result.message).toBe('Internal server error') 89 | expect(result.profile).toBeUndefined() 90 | }) 91 | }) 92 | 93 | describe('loginUser', () => { 94 | it('should return unauthorized status code for an invalid email or password', async () => { 95 | const mockProfileRepository = { 96 | createQueryBuilder: jest.fn().mockReturnThis(), 97 | addSelect: jest.fn().mockReturnThis(), 98 | where: jest.fn().mockReturnThis(), 99 | getOne: jest.fn().mockResolvedValue(null) 100 | } 101 | 102 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 103 | mockProfileRepository 104 | ) 105 | 106 | const result = await loginUser('nonexistentuser@example.com', 'password123') 107 | 108 | expect(result.statusCode).toBe(401) 109 | expect(result.message).toBe('Invalid email or password') 110 | expect(result.user).toBeUndefined() 111 | }) 112 | 113 | it('should return unauthorized status code for an incorrect password', async () => { 114 | const mockProfileRepository = { 115 | createQueryBuilder: jest.fn().mockReturnThis(), 116 | addSelect: jest.fn().mockReturnThis(), 117 | where: jest.fn().mockReturnThis(), 118 | getOne: jest.fn().mockResolvedValue({ 119 | uuid: 'user_uuid', 120 | password: 'hashed_incorrect_password' 121 | }) 122 | } 123 | 124 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 125 | mockProfileRepository 126 | ) 127 | 128 | const result = await loginUser('existinguser@example.com', 'password123') 129 | 130 | expect(result.statusCode).toBe(401) 131 | expect(result.message).toBe('Invalid email or password') 132 | expect(result.user).toBeUndefined() 133 | }) 134 | 135 | it('should handle an internal server error during login', async () => { 136 | ;(dataSource.getRepository as jest.Mock).mockImplementation(() => { 137 | throw new Error('Test repository error') 138 | }) 139 | 140 | const result = await loginUser('testuser@example.com', 'password123') 141 | 142 | expect(result.statusCode).toBe(500) 143 | expect(result.message).toBe('Internal server error') 144 | expect(result.user).toBeUndefined() 145 | }) 146 | 147 | it('should return unauthorized status code for an invalid email or password', async () => { 148 | const mockError = new Error('Test repository error') 149 | const mockProfileRepository = { 150 | createQueryBuilder: jest.fn().mockReturnThis(), 151 | addSelect: jest.fn().mockReturnThis(), 152 | where: jest.fn().mockReturnThis(), 153 | getOne: jest.fn().mockRejectedValue(mockError) 154 | } 155 | 156 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 157 | mockProfileRepository 158 | ) 159 | 160 | const result = await loginUser('nonexistentuser@example.com', 'password123') 161 | 162 | expect(result.statusCode).toBe(500) 163 | expect(result.message).toBe('Internal server error') 164 | expect(result.user).toBeUndefined() 165 | }) 166 | }) 167 | 168 | describe('Auth Service', () => { 169 | const validEmail = 'valid@gmail.com' 170 | const invalidEmail = 'invalid@ousl.lk' 171 | const newPassword = 'newpassword123' 172 | 173 | const token = generateResetToken(validEmail) 174 | 175 | it('should generate a password reset token', async () => { 176 | expect(token).toBeDefined() 177 | }) 178 | 179 | it('should not generate a password reset token for invalid email', async () => { 180 | const result = await generateResetToken(invalidEmail) 181 | 182 | expect(result.statusCode).toBe(500) 183 | }) 184 | 185 | it('should return error when parameters are missing', async () => { 186 | const result = await resetPassword('', newPassword) 187 | 188 | expect(result.statusCode).toBe(400) 189 | expect(result.message).toBe('Missing parameters') 190 | }) 191 | }) 192 | -------------------------------------------------------------------------------- /src/controllers/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response, NextFunction } from 'express' 2 | import { 3 | registerUser, 4 | loginUser, 5 | resetPassword, 6 | generateResetToken 7 | } from '../services/auth.service' 8 | import passport from 'passport' 9 | import type Profile from '../entities/profile.entity' 10 | import jwt from 'jsonwebtoken' 11 | import { JWT_SECRET } from '../configs/envConfig' 12 | import type { ApiResponse } from '../types' 13 | import { signAndSetCookie } from '../utils' 14 | 15 | export const googleRedirect = async ( 16 | req: Request, 17 | res: Response, 18 | next: NextFunction 19 | ): Promise => { 20 | passport.authenticate( 21 | 'google', 22 | { failureRedirect: '/login' }, 23 | (err: Error, user: Profile) => { 24 | if (err) { 25 | next(err) 26 | return 27 | } 28 | if (!user) { 29 | res.redirect('/login') 30 | return 31 | } 32 | signAndSetCookie(res, user.uuid) 33 | 34 | res.redirect(process.env.CLIENT_URL ?? '/') 35 | } 36 | )(req, res, next) 37 | } 38 | 39 | export const linkedinRedirect = async ( 40 | req: Request, 41 | res: Response, 42 | next: NextFunction 43 | ): Promise => { 44 | passport.authenticate( 45 | 'linkedin', 46 | { failureRedirect: '/login' }, 47 | (err: Error, user: Profile) => { 48 | if (err) { 49 | next(err) 50 | return 51 | } 52 | if (!user) { 53 | res.redirect('/login') 54 | return 55 | } 56 | signAndSetCookie(res, user.uuid) 57 | 58 | res.redirect(process.env.CLIENT_URL ?? '/') 59 | } 60 | )(req, res, next) 61 | } 62 | 63 | export const register = async ( 64 | req: Request, 65 | res: Response 66 | ): Promise> => { 67 | try { 68 | const { email, password, first_name, last_name } = req.body 69 | 70 | if (!email || !password) { 71 | return res 72 | .status(400) 73 | .json({ error: 'Email and password are required fields' }) 74 | } 75 | 76 | const { statusCode, message, profile } = await registerUser( 77 | email, 78 | password, 79 | first_name, 80 | last_name 81 | ) 82 | 83 | const { user } = await loginUser(email, password) 84 | 85 | if (user?.uuid) { 86 | signAndSetCookie(res, user.uuid) 87 | } 88 | 89 | return res.status(statusCode).json({ message, profile }) 90 | } catch (err) { 91 | if (err instanceof Error) { 92 | console.error('Error executing query', err) 93 | return res 94 | .status(500) 95 | .json({ error: 'Internal server error', message: err.message }) 96 | } 97 | throw err 98 | } 99 | } 100 | 101 | export const login = async ( 102 | req: Request, 103 | res: Response 104 | ): Promise> => { 105 | try { 106 | const { email, password } = req.body 107 | 108 | if (!email || !password) { 109 | return res 110 | .status(400) 111 | .json({ error: 'Email and password are required fields' }) 112 | } 113 | 114 | const { statusCode, message, user } = await loginUser(email, password) 115 | 116 | if (user?.uuid) { 117 | signAndSetCookie(res, user.uuid) 118 | } 119 | 120 | return res.status(statusCode).json({ user, message }) 121 | } catch (err) { 122 | if (err instanceof Error) { 123 | console.error('Error executing query', err) 124 | return res 125 | .status(500) 126 | .json({ error: 'Internal server error', message: err.message }) 127 | } 128 | 129 | throw err 130 | } 131 | } 132 | 133 | export const logout = async ( 134 | req: Request, 135 | res: Response 136 | ): Promise> => { 137 | try { 138 | res.clearCookie('jwt', { httpOnly: true }) 139 | return res.status(200).json({ message: 'Logged out successfully' }) 140 | } catch (err) { 141 | if (err instanceof Error) { 142 | console.error('Something went wrong', err) 143 | return res 144 | .status(500) 145 | .json({ error: 'Internal server error', message: err.message }) 146 | } 147 | 148 | throw err 149 | } 150 | } 151 | 152 | export const requireAuth = ( 153 | req: Request, 154 | res: Response, 155 | next: NextFunction 156 | ): void => { 157 | passport.authenticate( 158 | 'jwt', 159 | { session: false }, 160 | (err: Error, user: Profile) => { 161 | if (err) { 162 | next(err) 163 | return 164 | } 165 | 166 | const token = req.cookies.jwt 167 | 168 | if (!token) { 169 | return res.status(401).json({ error: 'User is not authenticated' }) 170 | } 171 | 172 | try { 173 | jwt.verify(token, JWT_SECRET) 174 | } catch (err) { 175 | return res 176 | .status(401) 177 | .json({ error: 'Invalid token, please log in again' }) 178 | } 179 | 180 | if (!user) { 181 | return res.status(401).json({ message: 'Unauthorized' }) 182 | } else { 183 | req.user = user 184 | next() 185 | } 186 | } 187 | )(req, res, next) 188 | } 189 | 190 | export const passwordResetRequest = async ( 191 | req: Request, 192 | res: Response 193 | ): Promise> => { 194 | const { email } = req.body 195 | 196 | if (!email) { 197 | return res.status(400).json({ error: 'Email is a required field' }) 198 | } 199 | 200 | try { 201 | const { statusCode, message, data: token } = await generateResetToken(email) 202 | return res.status(statusCode).json({ message, token }) 203 | } catch (err) { 204 | return res 205 | .status(500) 206 | .json({ error: 'Internal server error', message: (err as Error).message }) 207 | } 208 | } 209 | 210 | export const passwordReset = async ( 211 | req: Request, 212 | res: Response 213 | ): Promise => { 214 | try { 215 | const { token, newPassword } = req.body 216 | 217 | if (!token || !newPassword) { 218 | res 219 | .status(400) 220 | .json({ error: 'Token and new password are required fields' }) 221 | return 222 | } 223 | const { statusCode, message } = await resetPassword(token, newPassword) 224 | res.status(statusCode).json({ message }) 225 | } catch (err) { 226 | console.error('Error executing query', err) 227 | res.status(500).json({ 228 | error: 'Internal server error', 229 | message: (err as Error).message 230 | }) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt' 2 | import jwt from 'jsonwebtoken' 3 | import { dataSource } from '../configs/dbConfig' 4 | import { JWT_SECRET } from '../configs/envConfig' 5 | import Profile from '../entities/profile.entity' 6 | import { type CreateProfile, type ApiResponse } from '../types' 7 | import { 8 | capitalizeFirstLetter, 9 | getPasswordChangedEmailContent, 10 | getPasswordResetEmailContent 11 | } from '../utils' 12 | import { sendResetPasswordEmail } from './admin/email.service' 13 | 14 | export const registerUser = async ( 15 | email: string, 16 | password: string, 17 | first_name: string, 18 | last_name: string 19 | ): Promise<{ 20 | statusCode: number 21 | message: string 22 | profile?: Profile | null 23 | }> => { 24 | try { 25 | const profileRepository = dataSource.getRepository(Profile) 26 | 27 | const existingProfile = await profileRepository.findOne({ 28 | where: { primary_email: email } 29 | }) 30 | 31 | if (existingProfile != null) { 32 | return { statusCode: 409, message: 'Email already exists' } 33 | } 34 | 35 | const hashedPassword = await bcrypt.hash(password, 10) 36 | const newProfile = profileRepository.create({ 37 | primary_email: email, 38 | password: hashedPassword, 39 | first_name: capitalizeFirstLetter(first_name), 40 | last_name: capitalizeFirstLetter(last_name), 41 | image_url: '' 42 | }) 43 | 44 | await profileRepository.save(newProfile) 45 | 46 | const savedProfile = await profileRepository.findOne({ 47 | where: { primary_email: email } 48 | }) 49 | 50 | return { 51 | statusCode: 201, 52 | message: 'Registration successful', 53 | profile: savedProfile 54 | } 55 | } catch (error) { 56 | console.error('Error executing registration', error) 57 | return { statusCode: 500, message: 'Internal server error' } 58 | } 59 | } 60 | 61 | export const loginUser = async ( 62 | email: string, 63 | password: string 64 | ): Promise<{ statusCode: number; message: string; user?: Profile }> => { 65 | try { 66 | const profileRepository = dataSource.getRepository(Profile) 67 | const profile = await profileRepository 68 | .createQueryBuilder('profile') 69 | .addSelect('profile.password') 70 | .where({ primary_email: email }) 71 | .getOne() 72 | 73 | if (!profile) { 74 | return { statusCode: 401, message: 'Invalid email or password' } 75 | } 76 | 77 | const passwordMatch = await bcrypt.compare(password, profile.password) 78 | 79 | if (!passwordMatch) { 80 | return { statusCode: 401, message: 'Invalid email or password' } 81 | } 82 | 83 | return { statusCode: 200, message: 'Login successful', user: profile } 84 | } catch (error) { 85 | console.error('Error executing login', error) 86 | return { statusCode: 500, message: 'Internal server error' } 87 | } 88 | } 89 | 90 | export const findOrCreateUser = async ( 91 | createProfileDto: CreateProfile 92 | ): Promise => { 93 | const profileRepository = dataSource.getRepository(Profile) 94 | 95 | let user = await profileRepository.findOne({ 96 | where: { primary_email: createProfileDto.primary_email } 97 | }) 98 | 99 | if (!user) { 100 | const hashedPassword = await bcrypt.hash(createProfileDto.id, 10) 101 | user = profileRepository.create({ 102 | primary_email: createProfileDto.primary_email, 103 | password: hashedPassword, 104 | first_name: createProfileDto.first_name, 105 | last_name: createProfileDto.last_name, 106 | image_url: createProfileDto.image_url 107 | }) 108 | await profileRepository.save(user) 109 | } 110 | 111 | return user 112 | } 113 | 114 | export const generateResetToken = async ( 115 | email: string 116 | ): Promise> => { 117 | try { 118 | const profileRepository = dataSource.getRepository(Profile) 119 | const profile = await profileRepository.findOne({ 120 | where: { primary_email: email }, 121 | select: ['password', 'uuid'] 122 | }) 123 | 124 | if (!profile) { 125 | return { 126 | statusCode: 401, 127 | message: 'Invalid email or password' 128 | } 129 | } 130 | const token = jwt.sign({ userId: profile.uuid }, JWT_SECRET, { 131 | expiresIn: '1h' 132 | }) 133 | const content = getPasswordResetEmailContent(email, token) 134 | if (content) { 135 | await sendResetPasswordEmail(email, content.subject, content.message) 136 | } 137 | return { 138 | statusCode: 200, 139 | message: 'Password reset link successfully sent to email' 140 | } 141 | } catch (error) { 142 | console.error( 143 | 'Error executing Reset Password && Error sending password reset link', 144 | error 145 | ) 146 | return { statusCode: 500, message: 'Internal server error' } 147 | } 148 | } 149 | 150 | const hashPassword = async (password: string): Promise => { 151 | return await bcrypt.hash(password, 10) 152 | } 153 | 154 | const saveProfile = async ( 155 | profile: Profile, 156 | hashedPassword: string 157 | ): Promise => { 158 | profile.password = hashedPassword 159 | await dataSource.getRepository(Profile).save(profile) 160 | } 161 | 162 | export const resetPassword = async ( 163 | token: string, 164 | newPassword: string 165 | ): Promise> => { 166 | if (!token || !newPassword) { 167 | return { statusCode: 400, message: 'Missing parameters' } 168 | } 169 | 170 | let decoded 171 | try { 172 | decoded = jwt.verify(token, JWT_SECRET) as { userId: string } 173 | } catch (error) { 174 | throw new Error('Invalid token') 175 | } 176 | 177 | const profileRepository = dataSource.getRepository(Profile) 178 | const profile = await profileRepository.findOne({ 179 | where: { uuid: decoded.userId } 180 | }) 181 | 182 | if (!profile) { 183 | console.error('Error executing Reset Password: No profile found') 184 | return { statusCode: 409, message: 'No profile found' } 185 | } 186 | 187 | const hashedPassword = await hashPassword(newPassword) 188 | await saveProfile(profile, hashedPassword) 189 | const content = getPasswordChangedEmailContent(profile.primary_email) 190 | if (content) { 191 | await sendResetPasswordEmail( 192 | profile.primary_email, 193 | content.subject, 194 | content.message 195 | ) 196 | } 197 | return { statusCode: 200, message: 'Password reset successful' } 198 | } 199 | -------------------------------------------------------------------------------- /src/services/admin/mentee.service.test.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../../configs/dbConfig' 2 | import type Mentee from '../../entities/mentee.entity' 3 | import { MenteeApplicationStatus } from '../../enums' 4 | import { getAllMenteeEmailsService, getAllMentees } from './mentee.service' 5 | 6 | jest.mock('../../configs/dbConfig', () => ({ 7 | dataSource: { 8 | getRepository: jest.fn() 9 | } 10 | })) 11 | 12 | describe('Mentee Service - getAllMenteeEmailsService', () => { 13 | it('should get all mentee emails with a specific status successfully', async () => { 14 | const status: MenteeApplicationStatus = MenteeApplicationStatus.APPROVED 15 | 16 | const mockMentees = [ 17 | { 18 | profile: { 19 | primary_email: 'mentee1@example.com' 20 | } 21 | }, 22 | { 23 | profile: { 24 | primary_email: 'mentee2@example.com' 25 | } 26 | } 27 | ] as Mentee[] 28 | 29 | const mockMenteeRepository = { 30 | find: jest.fn().mockResolvedValue(mockMentees), 31 | findAndCount: jest.fn().mockResolvedValue([mockMentees, 2]) 32 | } 33 | 34 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 35 | mockMenteeRepository 36 | ) 37 | 38 | const result = await getAllMenteeEmailsService({ 39 | status, 40 | pageNumber: 1, 41 | pageSize: 2 42 | }) 43 | 44 | expect(result.statusCode).toBe(200) 45 | expect(result.items?.length).toBe(2) 46 | expect(result.items).toEqual(['mentee1@example.com', 'mentee2@example.com']) 47 | expect(result.message).toBe('All mentee emails with status ' + status) 48 | }) 49 | 50 | it('should get all mentee emails when status is undefined successfully', async () => { 51 | const mockMentees = [ 52 | { 53 | profile: { 54 | primary_email: 'mentee1@example.com' 55 | } 56 | }, 57 | { 58 | profile: { 59 | primary_email: 'mentee2@example.com' 60 | } 61 | } 62 | ] as Mentee[] 63 | 64 | const mockMenteeRepository = { 65 | find: jest.fn().mockResolvedValue(mockMentees), 66 | findAndCount: jest.fn().mockResolvedValue([mockMentees, 2]) 67 | } 68 | 69 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 70 | mockMenteeRepository 71 | ) 72 | 73 | const result = await getAllMenteeEmailsService({ 74 | status: undefined, 75 | pageNumber: 1, 76 | pageSize: 2 77 | }) 78 | 79 | expect(result.statusCode).toBe(200) 80 | expect(result.items?.length).toBe(2) 81 | expect(result.items).toEqual(['mentee1@example.com', 'mentee2@example.com']) 82 | expect(result.message).toBe('All mentee emails with status undefined') 83 | }) 84 | 85 | it('should handle mentees emails not found', async () => { 86 | const mockMenteeRepository = { 87 | find: jest.fn().mockResolvedValue([]), 88 | findAndCount: jest.fn().mockResolvedValue([[], 0]) 89 | } 90 | 91 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 92 | mockMenteeRepository 93 | ) 94 | 95 | const result = await getAllMenteeEmailsService({ 96 | status: MenteeApplicationStatus.PENDING, 97 | pageNumber: 1, 98 | pageSize: 2 99 | }) 100 | 101 | expect(result.items?.length).toBe(0) 102 | }) 103 | 104 | it('should handle error during mentee emails retrieval', async () => { 105 | const mockMenteeRepository = { 106 | find: jest.fn().mockRejectedValue(new Error('Test repository error')) 107 | } 108 | 109 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 110 | mockMenteeRepository 111 | ) 112 | 113 | await expect( 114 | getAllMenteeEmailsService({ 115 | status: MenteeApplicationStatus.APPROVED, 116 | pageNumber: 1, 117 | pageSize: 2 118 | }) 119 | ).rejects.toThrowError('Error getting mentee emails') 120 | }) 121 | }) 122 | 123 | describe('Mentee Service', () => { 124 | describe('getAllMentees', () => { 125 | it('should get all mentees successfully', async () => { 126 | const status: MenteeApplicationStatus = MenteeApplicationStatus.APPROVED 127 | 128 | const mockMentees = [ 129 | { 130 | uuid: 'mock-uuid-1', 131 | state: status, 132 | created_at: new Date(), 133 | updated_at: new Date(), 134 | updateTimestamps: jest.fn() 135 | }, 136 | { 137 | uuid: 'mock-uuid-2', 138 | state: status, 139 | created_at: new Date(), 140 | updated_at: new Date(), 141 | updateTimestamps: jest.fn() 142 | } 143 | ] as const 144 | 145 | const mockMenteeRepository = { 146 | find: jest.fn().mockResolvedValue(mockMentees), 147 | findAndCount: jest.fn().mockResolvedValue([mockMentees, 2]) 148 | } 149 | 150 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 151 | mockMenteeRepository 152 | ) 153 | 154 | const result = await getAllMentees({ status, pageNumber: 1, pageSize: 2 }) 155 | 156 | expect(result.statusCode).toBe(200) 157 | expect(result.items).toEqual(mockMentees) 158 | expect(result.message).toBe('All mentees found') 159 | }) 160 | 161 | it('should handle no mentees found', async () => { 162 | const status: MenteeApplicationStatus = MenteeApplicationStatus.APPROVED 163 | 164 | const mockMenteeRepository = { 165 | find: jest.fn().mockResolvedValue(null), 166 | findAndCount: jest.fn().mockResolvedValue([[], 0]) 167 | } 168 | 169 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 170 | mockMenteeRepository 171 | ) 172 | 173 | const result = await getAllMentees({ status, pageNumber: 1, pageSize: 2 }) 174 | 175 | expect(result.statusCode).toBe(404) 176 | expect(result.message).toBe('Mentees not found') 177 | }) 178 | 179 | it('should handle error during mentees retrieval', async () => { 180 | const status: MenteeApplicationStatus = MenteeApplicationStatus.APPROVED 181 | 182 | const mockMenteeRepository = { 183 | find: jest.fn().mockRejectedValue(new Error('Test repository error')) 184 | } 185 | 186 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 187 | mockMenteeRepository 188 | ) 189 | 190 | await expect( 191 | getAllMentees({ status, pageNumber: 1, pageSize: 2 }) 192 | ).rejects.toThrowError('Error getting mentees') 193 | }) 194 | }) 195 | }) 196 | -------------------------------------------------------------------------------- /src/services/admin/mentor_admin.service.test.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../../configs/dbConfig' 2 | import type Mentor from '../../entities/mentor.entity' 3 | import { MentorApplicationStatus } from '../../enums' 4 | import { 5 | findAllMentorEmails, 6 | getAllMentors, 7 | updateMentorStatus 8 | } from './mentor.service' 9 | 10 | jest.mock('../../configs/dbConfig', () => ({ 11 | dataSource: { 12 | getRepository: jest.fn() 13 | } 14 | })) 15 | 16 | describe('Mentor Service', () => { 17 | describe('updateMentorStatus', () => { 18 | it('should handle mentor not found during update', async () => { 19 | const mentorId = 'nonexistent-uuid' 20 | const status: MentorApplicationStatus = MentorApplicationStatus.APPROVED 21 | 22 | const mockMentorRepository = { 23 | findOne: jest.fn().mockResolvedValue(null) 24 | } 25 | 26 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 27 | mockMentorRepository 28 | ) 29 | 30 | const result = await updateMentorStatus(mentorId, status) 31 | 32 | expect(result.statusCode).toBe(404) 33 | expect(result.message).toBe('Mentor not found') 34 | }) 35 | 36 | it('should handle error during mentor status update', async () => { 37 | const mentorId = 'mock-uuid' 38 | const status: MentorApplicationStatus = MentorApplicationStatus.APPROVED 39 | 40 | const mockMentorRepository = { 41 | findOne: jest.fn().mockResolvedValue({}), 42 | update: jest.fn().mockRejectedValue(new Error('Test repository error')) 43 | } 44 | 45 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 46 | mockMentorRepository 47 | ) 48 | 49 | await expect(updateMentorStatus(mentorId, status)).rejects.toThrowError( 50 | 'Error updating the mentor status' 51 | ) 52 | }) 53 | }) 54 | 55 | describe('getAllMentors', () => { 56 | it('should get all mentors successfully', async () => { 57 | const status: MentorApplicationStatus = MentorApplicationStatus.APPROVED 58 | 59 | const mockMentors = [ 60 | { 61 | uuid: 'mock-uuid-1', 62 | state: status, 63 | created_at: new Date(), 64 | updated_at: new Date(), 65 | updateTimestamps: jest.fn(), 66 | generateUuid: jest.fn() 67 | }, 68 | { 69 | uuid: 'mock-uuid-2', 70 | state: status, 71 | created_at: new Date(), 72 | updated_at: new Date(), 73 | updateTimestamps: jest.fn(), 74 | generateUuid: jest.fn() 75 | } 76 | ] as const 77 | 78 | const mockMentorRepository = { 79 | find: jest.fn().mockResolvedValue(mockMentors), 80 | findAndCount: jest.fn().mockResolvedValue([mockMentors, 2]) 81 | } 82 | 83 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 84 | mockMentorRepository 85 | ) 86 | 87 | const result = await getAllMentors({ status, pageNumber: 1, pageSize: 2 }) 88 | 89 | expect(result.statusCode).toBe(200) 90 | expect(result.items).toEqual(mockMentors) 91 | expect(result.message).toBe('All Mentors found') 92 | }) 93 | 94 | it('should handle no mentors found', async () => { 95 | const status: MentorApplicationStatus = MentorApplicationStatus.APPROVED 96 | 97 | const mockMentorRepository = { 98 | find: jest.fn().mockResolvedValue(null), 99 | findAndCount: jest.fn().mockResolvedValue([[], 0]) 100 | } 101 | 102 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 103 | mockMentorRepository 104 | ) 105 | 106 | const result = await getAllMentors({ status, pageNumber: 1, pageSize: 2 }) 107 | 108 | console.log({ result }) 109 | 110 | expect(result.statusCode).toBe(404) 111 | expect(result.message).toBe('Mentors not found') 112 | }) 113 | 114 | it('should handle error during mentors retrieval', async () => { 115 | const status: MentorApplicationStatus = MentorApplicationStatus.APPROVED 116 | 117 | const mockMentorRepository = { 118 | find: jest.fn().mockRejectedValue(new Error('Test repository error')) 119 | } 120 | 121 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 122 | mockMentorRepository 123 | ) 124 | 125 | await expect( 126 | getAllMentors({ status, pageNumber: 1, pageSize: 3 }) 127 | ).rejects.toThrowError('Error getting mentors') 128 | }) 129 | }) 130 | 131 | describe('findAllMentorEmails', () => { 132 | it('should get all mentor emails successfully', async () => { 133 | const status: MentorApplicationStatus = MentorApplicationStatus.APPROVED 134 | 135 | const mockMentors = [ 136 | { 137 | uuid: 'mock-uuid-1', 138 | state: status, 139 | profile: { 140 | primary_email: 'mentor1@example.com' 141 | } 142 | }, 143 | { 144 | uuid: 'mock-uuid-2', 145 | state: status, 146 | profile: { 147 | primary_email: 'mentor2@example.com' 148 | } 149 | } 150 | ] as Mentor[] 151 | 152 | const mockMentorRepository = { 153 | find: jest.fn().mockResolvedValue(mockMentors) 154 | } 155 | 156 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 157 | mockMentorRepository 158 | ) 159 | 160 | const result = await findAllMentorEmails(status) 161 | 162 | expect(result.statusCode).toBe(200) 163 | expect(result.emails).toEqual([ 164 | 'mentor1@example.com', 165 | 'mentor2@example.com' 166 | ]) 167 | expect(result.message).toBe('All Mentors Emails found') 168 | }) 169 | 170 | it('should handle no mentor emails found', async () => { 171 | const status: MentorApplicationStatus = MentorApplicationStatus.APPROVED 172 | 173 | const mockMentorRepository = { 174 | find: jest.fn().mockResolvedValue(null) 175 | } 176 | 177 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 178 | mockMentorRepository 179 | ) 180 | 181 | try { 182 | await findAllMentorEmails(status) 183 | fail('Expected an error, but none was thrown') 184 | } catch (error: any) { 185 | expect(error.message).toBe('Error getting mentors emails') 186 | } 187 | }) 188 | 189 | it('should handle error during mentor emails retrieval', async () => { 190 | const status: MentorApplicationStatus = MentorApplicationStatus.APPROVED 191 | 192 | const mockMentorRepository = { 193 | find: jest.fn().mockRejectedValue(new Error('Test repository error')) 194 | } 195 | 196 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 197 | mockMentorRepository 198 | ) 199 | 200 | await expect(findAllMentorEmails(status)).rejects.toThrowError( 201 | 'Error getting mentors emails' 202 | ) 203 | }) 204 | }) 205 | }) 206 | -------------------------------------------------------------------------------- /src/scripts/seed-db.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker' 2 | import path from 'path' 3 | import fs from 'fs' 4 | import { dataSource } from '../configs/dbConfig' 5 | import Category from '../entities/category.entity' 6 | import Email from '../entities/email.entity' 7 | import Mentee from '../entities/mentee.entity' 8 | import Mentor from '../entities/mentor.entity' 9 | import Profile from '../entities/profile.entity' 10 | import Country from '../entities/country.entity' 11 | 12 | import { 13 | EmailStatusTypes, 14 | MenteeApplicationStatus, 15 | MentorApplicationStatus, 16 | ProfileTypes 17 | } from '../enums' 18 | 19 | export const seedDatabaseService = async (): Promise => { 20 | try { 21 | await dataSource.initialize() 22 | const profileRepository = dataSource.getRepository(Profile) 23 | const categoryRepository = dataSource.getRepository(Category) 24 | const emailRepository = dataSource.getRepository(Email) 25 | const menteeRepository = dataSource.getRepository(Mentee) 26 | const mentorRepository = dataSource.getRepository(Mentor) 27 | const countryRepository = dataSource.getRepository(Country) 28 | 29 | await menteeRepository.remove(await menteeRepository.find()) 30 | await mentorRepository.remove(await mentorRepository.find()) 31 | await profileRepository.remove(await profileRepository.find()) 32 | await categoryRepository.remove(await categoryRepository.find()) 33 | await emailRepository.remove(await emailRepository.find()) 34 | await countryRepository.remove(await countryRepository.find()) 35 | 36 | const genProfiles = faker.helpers.multiple(createRandomProfile, { 37 | count: 100 38 | }) 39 | 40 | const profiles = await profileRepository.save(genProfiles) 41 | 42 | const genEmails = faker.helpers.multiple( 43 | () => { 44 | return { 45 | recipient: faker.internet.email(), 46 | subject: faker.lorem.sentence(), 47 | content: faker.lorem.paragraph(), 48 | state: faker.helpers.enumValue(EmailStatusTypes) 49 | } 50 | }, 51 | { 52 | count: 30 53 | } 54 | ) 55 | 56 | await emailRepository.save(genEmails) 57 | 58 | const genCategories = faker.helpers.multiple( 59 | () => { 60 | return { 61 | category: faker.lorem.word() 62 | } 63 | }, 64 | { 65 | count: 8 66 | } 67 | ) 68 | const categories = await categoryRepository.save(genCategories) 69 | 70 | // Countries data file must be in the same directory as the seeding file in build directory. 71 | const countriesDataFilePath = path.join(__dirname, 'countries.json') 72 | const countriesData: Record = JSON.parse( 73 | fs.readFileSync(countriesDataFilePath, 'utf-8') 74 | ) 75 | 76 | for (const [code, name] of Object.entries(countriesData)) { 77 | const existingCountry = await countryRepository.findOne({ 78 | where: { code } 79 | }) 80 | 81 | if (!existingCountry) { 82 | await countryRepository.save(new Country(code, name)) 83 | } 84 | } 85 | 86 | const genMentors = ( 87 | categories: Category[], 88 | profiles: Profile[] 89 | ): Mentor[] => { 90 | return profiles.slice(0, 40).map((profile) => { 91 | const category = 92 | categories[faker.number.int({ min: 0, max: categories.length - 1 })] 93 | return createMentor(category, profile) 94 | }) 95 | } 96 | const mentorsEntities = genMentors(categories, profiles) 97 | const mentors = await mentorRepository.save(mentorsEntities) 98 | 99 | const genMentees = (mentors: Mentor[], profiles: Profile[]): Mentee[] => { 100 | return profiles.slice(40, profiles.length).map((profile) => { 101 | const mentor = 102 | mentors[faker.number.int({ min: 0, max: mentors.length - 1 })] 103 | return new Mentee( 104 | faker.helpers.enumValue(MenteeApplicationStatus), 105 | { 106 | firstName: faker.person.firstName(), 107 | lastName: faker.person.lastName(), 108 | email: faker.internet.email(), 109 | contactNo: faker.phone.number(), 110 | profilePic: faker.image.avatar(), 111 | cv: faker.internet.url(), 112 | isUndergrad: true, 113 | consentGiven: true, 114 | graduatedYear: faker.number.int({ 115 | min: 1980, 116 | max: new Date().getFullYear() 117 | }), 118 | university: faker.company.name() + ' University', 119 | yearOfStudy: faker.number.int({ min: 1, max: 4 }), 120 | course: faker.person.jobType(), 121 | submission: faker.internet.url() 122 | }, 123 | profile, 124 | mentor 125 | ) 126 | }) 127 | } 128 | 129 | const menteesEntities = genMentees(mentors, profiles) 130 | 131 | await menteeRepository.save(menteesEntities) 132 | 133 | await dataSource.destroy() 134 | return 'Database seeded successfully' 135 | } catch (err) { 136 | console.error(err) 137 | throw err 138 | } 139 | } 140 | 141 | const createRandomProfile = (): Partial => { 142 | const profile = { 143 | primary_email: faker.internet.email(), 144 | first_name: faker.person.firstName(), 145 | last_name: faker.person.lastName(), 146 | image_url: faker.image.avatar(), 147 | type: ProfileTypes.DEFAULT, 148 | password: '12345' 149 | } 150 | 151 | return profile 152 | } 153 | 154 | const createMentor = (category: Category, profile: Profile): Mentor => { 155 | return { 156 | state: faker.helpers.enumValue(MentorApplicationStatus), 157 | category, 158 | application: { 159 | firstName: faker.person.firstName(), 160 | lastName: faker.person.firstName(), 161 | contactNo: faker.phone.number(), 162 | country: faker.location.country(), 163 | position: faker.person.jobTitle(), 164 | expertise: faker.lorem.words({ min: 1, max: 3 }), 165 | bio: faker.person.bio(), 166 | isPastMentor: true, 167 | reasonToMentor: null, 168 | motivation: null, 169 | cv: null, 170 | menteeExpectations: faker.lorem.paragraphs({ min: 1, max: 2 }), 171 | mentoringPhilosophy: faker.lorem.paragraphs({ min: 1, max: 2 }), 172 | noOfMentees: faker.number.int({ min: 1, max: 10 }), 173 | canCommit: true, 174 | mentoredYear: faker.number.int({ 175 | min: 2019, 176 | max: new Date().getFullYear() 177 | }), 178 | category: category.uuid, 179 | institution: faker.company.name(), 180 | linkedin: faker.internet.url(), 181 | website: faker.internet.url(), 182 | email: faker.internet.email() 183 | }, 184 | availability: faker.datatype.boolean(), 185 | profile 186 | } as unknown as Mentor 187 | } 188 | 189 | seedDatabaseService() 190 | .then((res) => { 191 | console.log(res) 192 | }) 193 | .catch((err) => { 194 | console.error(err) 195 | }) 196 | -------------------------------------------------------------------------------- /src/scripts/countries.json: -------------------------------------------------------------------------------- 1 | { 2 | "ad": "Andorra", 3 | "ae": "United Arab Emirates", 4 | "af": "Afghanistan", 5 | "ag": "Antigua and Barbuda", 6 | "ai": "Anguilla", 7 | "al": "Albania", 8 | "am": "Armenia", 9 | "ao": "Angola", 10 | "aq": "Antarctica", 11 | "ar": "Argentina", 12 | "as": "American Samoa", 13 | "at": "Austria", 14 | "au": "Australia", 15 | "aw": "Aruba", 16 | "ax": "Åland Islands", 17 | "az": "Azerbaijan", 18 | "ba": "Bosnia and Herzegovina", 19 | "bb": "Barbados", 20 | "bd": "Bangladesh", 21 | "be": "Belgium", 22 | "bf": "Burkina Faso", 23 | "bg": "Bulgaria", 24 | "bh": "Bahrain", 25 | "bi": "Burundi", 26 | "bj": "Benin", 27 | "bl": "Saint Barthélemy", 28 | "bm": "Bermuda", 29 | "bn": "Brunei", 30 | "bo": "Bolivia", 31 | "bq": "Caribbean Netherlands", 32 | "br": "Brazil", 33 | "bs": "Bahamas", 34 | "bt": "Bhutan", 35 | "bv": "Bouvet Island", 36 | "bw": "Botswana", 37 | "by": "Belarus", 38 | "bz": "Belize", 39 | "ca": "Canada", 40 | "cc": "Cocos (Keeling) Islands", 41 | "cd": "DR Congo", 42 | "cf": "Central African Republic", 43 | "cg": "Republic of the Congo", 44 | "ch": "Switzerland", 45 | "ci": "Côte d'Ivoire (Ivory Coast)", 46 | "ck": "Cook Islands", 47 | "cl": "Chile", 48 | "cm": "Cameroon", 49 | "cn": "China", 50 | "co": "Colombia", 51 | "cr": "Costa Rica", 52 | "cu": "Cuba", 53 | "cv": "Cape Verde", 54 | "cw": "Curaçao", 55 | "cx": "Christmas Island", 56 | "cy": "Cyprus", 57 | "cz": "Czechia", 58 | "de": "Germany", 59 | "dj": "Djibouti", 60 | "dk": "Denmark", 61 | "dm": "Dominica", 62 | "do": "Dominican Republic", 63 | "dz": "Algeria", 64 | "ec": "Ecuador", 65 | "ee": "Estonia", 66 | "eg": "Egypt", 67 | "eh": "Western Sahara", 68 | "er": "Eritrea", 69 | "es": "Spain", 70 | "et": "Ethiopia", 71 | "eu": "European Union", 72 | "fi": "Finland", 73 | "fj": "Fiji", 74 | "fk": "Falkland Islands", 75 | "fm": "Micronesia", 76 | "fo": "Faroe Islands", 77 | "fr": "France", 78 | "ga": "Gabon", 79 | "gb": "United Kingdom", 80 | "gd": "Grenada", 81 | "ge": "Georgia", 82 | "gf": "French Guiana", 83 | "gg": "Guernsey", 84 | "gh": "Ghana", 85 | "gi": "Gibraltar", 86 | "gl": "Greenland", 87 | "gm": "Gambia", 88 | "gn": "Guinea", 89 | "gp": "Guadeloupe", 90 | "gq": "Equatorial Guinea", 91 | "gr": "Greece", 92 | "gs": "South Georgia", 93 | "gt": "Guatemala", 94 | "gu": "Guam", 95 | "gw": "Guinea-Bissau", 96 | "gy": "Guyana", 97 | "hk": "Hong Kong", 98 | "hm": "Heard Island and McDonald Islands", 99 | "hn": "Honduras", 100 | "hr": "Croatia", 101 | "ht": "Haiti", 102 | "hu": "Hungary", 103 | "id": "Indonesia", 104 | "ie": "Ireland", 105 | "il": "Israel", 106 | "im": "Isle of Man", 107 | "in": "India", 108 | "io": "British Indian Ocean Territory", 109 | "iq": "Iraq", 110 | "ir": "Iran", 111 | "is": "Iceland", 112 | "it": "Italy", 113 | "je": "Jersey", 114 | "jm": "Jamaica", 115 | "jo": "Jordan", 116 | "jp": "Japan", 117 | "ke": "Kenya", 118 | "kg": "Kyrgyzstan", 119 | "kh": "Cambodia", 120 | "ki": "Kiribati", 121 | "km": "Comoros", 122 | "kn": "Saint Kitts and Nevis", 123 | "kp": "North Korea", 124 | "kr": "South Korea", 125 | "kw": "Kuwait", 126 | "ky": "Cayman Islands", 127 | "kz": "Kazakhstan", 128 | "la": "Laos", 129 | "lb": "Lebanon", 130 | "lc": "Saint Lucia", 131 | "li": "Liechtenstein", 132 | "lk": "Sri Lanka", 133 | "lr": "Liberia", 134 | "ls": "Lesotho", 135 | "lt": "Lithuania", 136 | "lu": "Luxembourg", 137 | "lv": "Latvia", 138 | "ly": "Libya", 139 | "ma": "Morocco", 140 | "mc": "Monaco", 141 | "md": "Moldova", 142 | "me": "Montenegro", 143 | "mf": "Saint Martin", 144 | "mg": "Madagascar", 145 | "mh": "Marshall Islands", 146 | "mk": "North Macedonia", 147 | "ml": "Mali", 148 | "mm": "Myanmar", 149 | "mn": "Mongolia", 150 | "mo": "Macau", 151 | "mp": "Northern Mariana Islands", 152 | "mq": "Martinique", 153 | "mr": "Mauritania", 154 | "ms": "Montserrat", 155 | "mt": "Malta", 156 | "mu": "Mauritius", 157 | "mv": "Maldives", 158 | "mw": "Malawi", 159 | "mx": "Mexico", 160 | "my": "Malaysia", 161 | "mz": "Mozambique", 162 | "na": "Namibia", 163 | "nc": "New Caledonia", 164 | "ne": "Niger", 165 | "nf": "Norfolk Island", 166 | "ng": "Nigeria", 167 | "ni": "Nicaragua", 168 | "nl": "Netherlands", 169 | "no": "Norway", 170 | "np": "Nepal", 171 | "nr": "Nauru", 172 | "nu": "Niue", 173 | "nz": "New Zealand", 174 | "om": "Oman", 175 | "pa": "Panama", 176 | "pe": "Peru", 177 | "pf": "French Polynesia", 178 | "pg": "Papua New Guinea", 179 | "ph": "Philippines", 180 | "pk": "Pakistan", 181 | "pl": "Poland", 182 | "pm": "Saint Pierre and Miquelon", 183 | "pn": "Pitcairn Islands", 184 | "pr": "Puerto Rico", 185 | "ps": "Palestine", 186 | "pt": "Portugal", 187 | "pw": "Palau", 188 | "py": "Paraguay", 189 | "qa": "Qatar", 190 | "re": "Réunion", 191 | "ro": "Romania", 192 | "rs": "Serbia", 193 | "ru": "Russia", 194 | "rw": "Rwanda", 195 | "sa": "Saudi Arabia", 196 | "sb": "Solomon Islands", 197 | "sc": "Seychelles", 198 | "sd": "Sudan", 199 | "se": "Sweden", 200 | "sg": "Singapore", 201 | "sh": "Saint Helena, Ascension and Tristan da Cunha", 202 | "si": "Slovenia", 203 | "sj": "Svalbard and Jan Mayen", 204 | "sk": "Slovakia", 205 | "sl": "Sierra Leone", 206 | "sm": "San Marino", 207 | "sn": "Senegal", 208 | "so": "Somalia", 209 | "sr": "Suriname", 210 | "ss": "South Sudan", 211 | "st": "São Tomé and Príncipe", 212 | "sv": "El Salvador", 213 | "sx": "Sint Maarten", 214 | "sy": "Syria", 215 | "sz": "Eswatini (Swaziland)", 216 | "tc": "Turks and Caicos Islands", 217 | "td": "Chad", 218 | "tf": "French Southern and Antarctic Lands", 219 | "tg": "Togo", 220 | "th": "Thailand", 221 | "tj": "Tajikistan", 222 | "tk": "Tokelau", 223 | "tl": "Timor-Leste", 224 | "tm": "Turkmenistan", 225 | "tn": "Tunisia", 226 | "to": "Tonga", 227 | "tr": "Turkey", 228 | "tt": "Trinidad and Tobago", 229 | "tv": "Tuvalu", 230 | "tw": "Taiwan", 231 | "tz": "Tanzania", 232 | "ua": "Ukraine", 233 | "ug": "Uganda", 234 | "um": "United States Minor Outlying Islands", 235 | "un": "United Nations", 236 | "us": "United States", 237 | "uy": "Uruguay", 238 | "uz": "Uzbekistan", 239 | "va": "Vatican City (Holy See)", 240 | "vc": "Saint Vincent and the Grenadines", 241 | "ve": "Venezuela", 242 | "vg": "British Virgin Islands", 243 | "vi": "United States Virgin Islands", 244 | "vn": "Vietnam", 245 | "vu": "Vanuatu", 246 | "wf": "Wallis and Futuna", 247 | "ws": "Samoa", 248 | "xk": "Kosovo", 249 | "ye": "Yemen", 250 | "yt": "Mayotte", 251 | "za": "South Africa", 252 | "zm": "Zambia", 253 | "zw": "Zimbabwe" 254 | } -------------------------------------------------------------------------------- /src/services/mentor.service.test.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../configs/dbConfig' 2 | import { ProfileTypes } from '../enums' 3 | import { getAllMentors } from './mentor.service' 4 | 5 | interface Mentor { 6 | id: number 7 | name: string 8 | } 9 | 10 | const mentors = [ 11 | { 12 | uuid: '0d22aa50-48ba-4ec0-96bd-aca9b54c7e2f', 13 | created_at: '2023-07-01', 14 | updated_at: '2023-07-10', 15 | state: 'approved', 16 | category: { 17 | category: 'Computer Science', 18 | uuid: 'fef68adb-e710-4d9e-8772-dc4905885088', 19 | created_at: '2023-10-29T14:20:04.335Z', 20 | updated_at: '2023-10-29T14:20:04.335Z' 21 | }, 22 | application: { 23 | position: 'Software Engineer', 24 | country: 'United States', 25 | institution: 'Google', 26 | expertise: 'Web Development', 27 | menteeExpectations: 'Commitment and eagerness to learn', 28 | mentoringPhilosophy: 'Empowering mentees to reach their full potential', 29 | canCommit: true, 30 | isPastMentor: true, 31 | reasonToMentor: 'To give back to the community', 32 | cv: 'https://example.com/cv', 33 | firstName: '', 34 | lastName: '', 35 | email: '', 36 | contactNo: '', 37 | bio: '', 38 | noOfMentees: 0, 39 | category: '' 40 | }, 41 | availability: true, 42 | profile: { 43 | created_at: new Date('2021-07-05T00:00:00.000Z'), 44 | updated_at: new Date('2021-07-06T00:00:00.000Z'), 45 | primary_email: 'mentor1@example.com', 46 | contact_email: 'mentor1@example.com', 47 | first_name: 'John', 48 | last_name: 'Doe', 49 | image_url: 'https://xsgames.co/randomusers/avatar.php?g=male', 50 | linkedin_url: 'https://linkedin.com/in/mentor1', 51 | type: ProfileTypes.DEFAULT, 52 | uuid: 'abc123', 53 | mentor: [] 54 | } 55 | }, 56 | { 57 | uuid: '0d22aa50-48ba-4ec0-96bd-aca9b54c7e2e', 58 | created_at: '2023-07-02', 59 | updated_at: '2023-07-12', 60 | state: 'approved', 61 | category: { 62 | category: 'Business', 63 | uuid: 'fef68adb-e710-4d9e-8772-dc4905885088', 64 | created_at: '2023-10-29T14:20:04.335Z', 65 | updated_at: '2023-10-29T14:20:04.335Z' 66 | }, 67 | application: { 68 | position: 'Chief Marketing Officer', 69 | country: 'Canada', 70 | institution: 'Facebook', 71 | expertise: 'Marketing', 72 | menteeExpectations: 'Proactive attitude and willingness to learn', 73 | mentoringPhilosophy: 'Sharing practical insights for professional growth', 74 | canCommit: true, 75 | isPastMentor: false, 76 | reasonToMentor: 'Passion for supporting aspiring entrepreneurs', 77 | cv: 'https://example.com/cv', 78 | firstName: '', 79 | lastName: '', 80 | email: '', 81 | contactNo: '', 82 | bio: '', 83 | noOfMentees: 0, 84 | category: '' 85 | }, 86 | availability: true, 87 | profile: { 88 | created_at: new Date('2021-07-05T00:00:00.000Z'), 89 | updated_at: new Date('2021-07-06T00:00:00.000Z'), 90 | primary_email: 'mentor2@example.com', 91 | contact_email: 'mentor2@example.com', 92 | first_name: 'Jane', 93 | last_name: 'Smith', 94 | image_url: 'https://xsgames.co/randomusers/avatar.php?g=female', 95 | linkedin_url: 'https://linkedin.com/in/mentor2', 96 | type: ProfileTypes.DEFAULT, 97 | uuid: 'def456', 98 | mentor: [] 99 | } 100 | }, 101 | { 102 | uuid: '0d22aa50-48ba-4ec0-96bd-aca9b54c7e2f', 103 | created_at: '2023-07-03', 104 | updated_at: '2023-07-12', 105 | state: 'approved', 106 | category: { 107 | category: 'Design', 108 | uuid: 'fef68acb-e710-4d9e-8772-dc4905885088', 109 | created_at: '2023-10-29T14:20:04.335Z', 110 | updated_at: '2023-10-29T14:20:04.335Z' 111 | }, 112 | application: { 113 | position: 'UI/UX Designer', 114 | country: 'United Kingdom', 115 | expertise: 'UI/UX Design', 116 | institution: 'Facebook', 117 | menteeExpectations: 'Attention to detail and creativity', 118 | mentoringPhilosophy: 'Creating user-centric designs', 119 | canCommit: true, 120 | isPastMentor: true, 121 | reasonToMentor: 'To inspire and educate aspiring designers', 122 | cv: 'https://example.com/cv', 123 | firstName: '', 124 | lastName: '', 125 | email: '', 126 | contactNo: '', 127 | bio: '', 128 | noOfMentees: 0, 129 | category: '' 130 | }, 131 | availability: true, 132 | profile: { 133 | created_at: new Date('2021-07-05T00:00:00.000Z'), 134 | updated_at: new Date('2021-07-06T00:00:00.000Z'), 135 | primary_email: 'mentor3@example.com', 136 | contact_email: 'mentor3@example.com', 137 | first_name: 'Emily', 138 | last_name: 'Johnson', 139 | image_url: '', 140 | linkedin_url: 'https://linkedin.com/in/mentor3', 141 | type: ProfileTypes.DEFAULT, 142 | uuid: 'ghi789', 143 | mentor: [] 144 | } 145 | } 146 | ] 147 | 148 | interface MockMentorRepository { 149 | find: jest.Mock> 150 | findAndCount: jest.Mock> 151 | } 152 | 153 | jest.mock('../configs/dbConfig', () => ({ 154 | dataSource: { 155 | getRepository: jest.fn() 156 | } 157 | })) 158 | 159 | describe('getAllMentors', () => { 160 | it('should get all mentors without category', async () => { 161 | const mockMentorRepository = createMockMentorRepository(mentors) 162 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 163 | mockMentorRepository 164 | ) 165 | 166 | const result = await getAllMentors({ 167 | pageNumber: 1, 168 | pageSize: 3 169 | }) 170 | 171 | expect(result.statusCode).toBe(200) 172 | expect(result.message).toBe('Mentors found') 173 | expect(result.items).toEqual(mentors) 174 | }) 175 | 176 | it('should get mentors with category', async () => { 177 | // TODO: Fix the tests 178 | const mockMentorRepository = createMockMentorRepository(mentors) 179 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 180 | mockMentorRepository 181 | ) 182 | 183 | const result = await getAllMentors({ 184 | categoryId: 'fef68adb-e710-4d9e-8772-dc4905885088', 185 | pageNumber: 1, 186 | pageSize: 3 187 | }) 188 | 189 | expect(result.statusCode).toBe(200) 190 | expect(result.message).toBe('Mentors found') 191 | expect(result.items).toEqual(mentors) 192 | }) 193 | 194 | it('should return an empty array if no mentors found', async () => { 195 | const mockMentorRepository = createMockMentorRepository([]) 196 | ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( 197 | mockMentorRepository 198 | ) 199 | 200 | const result = await getAllMentors({ 201 | categoryId: 'SomeCategory', 202 | pageNumber: 1, 203 | pageSize: 2 204 | }) 205 | 206 | expect(result.statusCode).toBe(404) 207 | expect(result.message).toBe('No mentors found') 208 | expect(result.items).toStrictEqual([]) 209 | }) 210 | }) 211 | 212 | function createMockMentorRepository( 213 | data: typeof mentors, 214 | error?: Error 215 | ): MockMentorRepository { 216 | return { 217 | find: jest.fn().mockResolvedValue(data), 218 | findAndCount: jest.fn().mockResolvedValue([data, data.length]) 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/templates/emailTemplate.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ScholarX email template 5 | 6 | 7 | 12 | 13 | 43 | 44 | 45 | 53 |
64 | 68 | 69 | 209 | 210 |
70 | 84 | 85 | 107 | 108 | 109 | 141 | 142 | 143 | 206 | 207 |
93 | 106 |
118 |
132 |

<%- message %>

133 | 134 |

135 | Best regards,
136 | ScholarX Team,
137 | Sustainable Education Foundation. 138 |

139 |
140 |
152 |

153 | 157 | facebook-icon 164 | 165 | 169 | facebook-icon 176 | 177 | 181 | facebook-icon 188 | 189 | 193 | facebook-icon 200 | 201 |

202 |

203 | © Sustainable Education Foundation - SEF 2025 204 |

205 |
208 |
211 |
212 | 213 | 214 | -------------------------------------------------------------------------------- /src/templates/passwordresetEmailTemplate.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ScholarX email template 5 | 6 | 7 | 12 | 13 | 43 | 44 | 45 | 53 |
64 | 68 | 69 | 209 | 210 |
70 | 84 | 85 | 107 | 108 | 109 | 141 | 142 | 143 | 206 | 207 |
93 | 106 |
118 |
132 |

<%- message %>

133 | 134 |

135 | Best regards,
136 | ScholarX Team,
137 | Sustainable Education Foundation. 138 |

139 |
140 |
152 |

153 | 157 | facebook-icon 164 | 165 | 169 | facebook-icon 176 | 177 | 181 | facebook-icon 188 | 189 | 193 | facebook-icon 200 | 201 |

202 |

203 | © Sustainable Education Foundation - SEF 2024 204 |

205 |
208 |
211 |
212 | 213 | 214 | -------------------------------------------------------------------------------- /src/services/admin/mentee.service.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../../configs/dbConfig' 2 | import Mentee from '../../entities/mentee.entity' 3 | import Mentor from '../../entities/mentor.entity' 4 | import { 5 | MenteeApplicationStatus, 6 | MentorApplicationStatus, 7 | type StatusUpdatedBy 8 | } from '../../enums' 9 | import { type PaginatedApiResponse } from '../../types' 10 | import { getEmailContent } from '../../utils' 11 | import { sendEmail } from './email.service' 12 | 13 | export const getAllMentees = async ({ 14 | status, 15 | pageNumber, 16 | pageSize 17 | }: { 18 | status: MenteeApplicationStatus | undefined 19 | pageNumber: number 20 | pageSize: number 21 | }): Promise> => { 22 | try { 23 | const menteeRepository = dataSource.getRepository(Mentee) 24 | 25 | const [mentees, count] = await menteeRepository.findAndCount({ 26 | where: status ? { state: status } : {}, 27 | relations: ['profile', 'mentor'], 28 | skip: (pageNumber - 1) * pageSize, 29 | take: pageSize 30 | }) 31 | 32 | if (mentees.length === 0) { 33 | return { 34 | statusCode: 404, 35 | items: [], 36 | totalItemCount: 0, 37 | pageNumber, 38 | pageSize, 39 | message: 'Mentees not found' 40 | } 41 | } 42 | 43 | return { 44 | statusCode: 200, 45 | items: mentees, 46 | totalItemCount: count, 47 | pageNumber, 48 | pageSize, 49 | message: 'All mentees found' 50 | } 51 | } catch (err) { 52 | throw new Error('Error getting mentees') 53 | } 54 | } 55 | 56 | export const getAllMenteesByMentor = async ( 57 | status: MenteeApplicationStatus | undefined, 58 | userId: string 59 | ): Promise<{ 60 | statusCode: number 61 | mentees?: Mentee[] 62 | message: string 63 | }> => { 64 | try { 65 | const menteeRepository = dataSource.getRepository(Mentee) 66 | const mentorRepository = dataSource.getRepository(Mentor) 67 | 68 | const mentor: Mentor | null = await mentorRepository.findOne({ 69 | where: { 70 | profile: { uuid: userId }, 71 | state: MentorApplicationStatus.APPROVED 72 | }, 73 | relations: ['profile'] 74 | }) 75 | 76 | const mentees: Mentee[] = await menteeRepository.find({ 77 | where: status 78 | ? { state: status, mentor: { uuid: mentor?.uuid } } 79 | : { mentor: { uuid: mentor?.uuid } }, 80 | relations: ['profile', 'mentor'] 81 | }) 82 | 83 | if (!mentees) { 84 | return { 85 | statusCode: 404, 86 | message: 'Mentees not found' 87 | } 88 | } 89 | 90 | return { 91 | statusCode: 200, 92 | mentees, 93 | message: 'All mentees found' 94 | } 95 | } catch (err) { 96 | throw new Error('Error getting mentees') 97 | } 98 | } 99 | 100 | export const updateStatus = async ( 101 | menteeId: string, 102 | state: MenteeApplicationStatus, 103 | statusUpdatedBy: StatusUpdatedBy 104 | ): Promise<{ 105 | statusCode: number 106 | updatedMenteeApplication?: Mentee 107 | message: string 108 | }> => { 109 | try { 110 | const menteeRepository = dataSource.getRepository(Mentee) 111 | const mentee = await menteeRepository.findOne({ 112 | where: { 113 | uuid: menteeId 114 | }, 115 | relations: ['profile', 'mentor'] 116 | }) 117 | 118 | if (!mentee) { 119 | return { 120 | statusCode: 404, 121 | message: 'Mentee not found' 122 | } 123 | } 124 | 125 | const profileUuid = mentee.profile.uuid 126 | 127 | const approvedApplications = await menteeRepository.findOne({ 128 | where: { 129 | state: MenteeApplicationStatus.APPROVED, 130 | profile: { 131 | uuid: profileUuid 132 | } 133 | }, 134 | relations: ['mentor'] 135 | }) 136 | 137 | const menteeName = 138 | mentee.application.firstName + ' ' + mentee.application.lastName 139 | 140 | const content = await getEmailContent('mentee', state, menteeName) 141 | 142 | if (content) { 143 | await sendEmail( 144 | mentee.application.email as string, 145 | content.subject, 146 | content.message, 147 | content.attachment 148 | ) 149 | } 150 | // Handle Approve status 151 | if (approvedApplications && state === 'approved') { 152 | return { 153 | statusCode: 400, 154 | message: 'Mentee is already approved' 155 | } 156 | } else { 157 | await menteeRepository.update( 158 | { uuid: menteeId }, 159 | { 160 | state, 161 | status_updated_by: statusUpdatedBy, 162 | status_updated_date: new Date(), 163 | certificate_id: content?.uniqueId 164 | } 165 | ) 166 | return { 167 | statusCode: 200, 168 | message: 'Mentee application state successfully updated' 169 | } 170 | } 171 | } catch (err) { 172 | console.error('Error updating mentee status', err) 173 | throw new Error('Error updating mentee status') 174 | } 175 | } 176 | 177 | export const getAllMenteeEmailsService = async ({ 178 | status, 179 | pageNumber, 180 | pageSize 181 | }: { 182 | status: MenteeApplicationStatus | undefined 183 | pageNumber: number 184 | pageSize: number 185 | }): Promise> => { 186 | try { 187 | const menteeRepositroy = dataSource.getRepository(Mentee) 188 | const allMentees = await menteeRepositroy.findAndCount({ 189 | where: status ? { state: status } : {}, 190 | relations: ['profile'], 191 | skip: (pageNumber - 1) * pageSize, 192 | take: pageSize 193 | }) 194 | const emails = allMentees[0].map((mentee) => mentee?.profile?.primary_email) 195 | 196 | return { 197 | statusCode: 200, 198 | items: emails, 199 | pageNumber, 200 | pageSize, 201 | totalItemCount: allMentees[1], 202 | message: 'All mentee emails with status ' + (status ?? 'undefined') 203 | } 204 | } catch (err) { 205 | throw new Error('Error getting mentee emails') 206 | } 207 | } 208 | 209 | export const getMentee = async ( 210 | menteeId: string 211 | ): Promise<{ 212 | statusCode: number 213 | mentee: Mentee | null 214 | message: string 215 | }> => { 216 | try { 217 | const menteeRepository = dataSource.getRepository(Mentee) 218 | 219 | const mentee = await menteeRepository.findOne({ 220 | where: { uuid: menteeId }, 221 | relations: ['profile', 'mentor', 'mentor.profile'] 222 | }) 223 | 224 | if (!mentee) { 225 | return { 226 | statusCode: 404, 227 | mentee: null, 228 | message: 'Mentee not found' 229 | } 230 | } 231 | 232 | return { 233 | statusCode: 200, 234 | mentee, 235 | message: 'Mentees found' 236 | } 237 | } catch (err) { 238 | throw new Error('Error getting mentees') 239 | } 240 | } 241 | 242 | export const revoke = async ( 243 | userId: string 244 | ): Promise<{ 245 | statusCode: number 246 | updatedMenteeApplication?: Mentee 247 | message: string 248 | }> => { 249 | try { 250 | const menteeRepository = dataSource.getRepository(Mentee) 251 | const mentee = await menteeRepository.findOne({ 252 | where: { 253 | profile: { uuid: userId }, 254 | state: MenteeApplicationStatus.PENDING 255 | }, 256 | relations: ['profile', 'mentor'] 257 | }) 258 | 259 | if (!mentee) { 260 | return { 261 | statusCode: 404, 262 | message: 'Mentee not found' 263 | } 264 | } 265 | 266 | await menteeRepository.update( 267 | { uuid: mentee.uuid }, 268 | { 269 | state: MenteeApplicationStatus.REVOKED, 270 | status_updated_date: new Date() 271 | } 272 | ) 273 | 274 | return { 275 | statusCode: 200, 276 | message: 'Mentee application state successfully updated' 277 | } 278 | } catch (err) { 279 | console.error('Error updating mentee status', err) 280 | throw new Error('Error updating mentee status') 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/services/admin/mentor.service.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../../configs/dbConfig' 2 | import Category from '../../entities/category.entity' 3 | import { Country } from '../../entities/country.entity' 4 | import Mentor from '../../entities/mentor.entity' 5 | import Profile from '../../entities/profile.entity' 6 | import type { MentorApplicationStatus } from '../../enums' 7 | import { type CreateProfile, type PaginatedApiResponse } from '../../types' 8 | import { getEmailContent } from '../../utils' 9 | import { sendEmail } from './email.service' 10 | 11 | export const updateMentorStatus = async ( 12 | mentorId: string, 13 | status: MentorApplicationStatus 14 | ): Promise<{ 15 | statusCode: number 16 | mentor?: Mentor | null 17 | message: string 18 | }> => { 19 | try { 20 | const mentorRepository = dataSource.getRepository(Mentor) 21 | 22 | const mentor = await mentorRepository.findOne({ 23 | where: { uuid: mentorId } 24 | }) 25 | 26 | if (!mentor) { 27 | return { 28 | statusCode: 404, 29 | message: 'Mentor not found' 30 | } 31 | } 32 | 33 | await mentorRepository.update({ uuid: mentorId }, { state: status }) 34 | 35 | const content = await getEmailContent( 36 | 'mentor', 37 | status, 38 | mentor.application.firstName as string 39 | ) 40 | 41 | if (content) { 42 | await sendEmail( 43 | mentor.application.email as string, 44 | content.subject, 45 | content.message 46 | ) 47 | } 48 | 49 | return { 50 | statusCode: 200, 51 | mentor, 52 | message: 'Updated Mentor application status successfully' 53 | } 54 | } catch (err) { 55 | console.error('Error updating the mentor status', err) 56 | throw new Error('Error updating the mentor status') 57 | } 58 | } 59 | 60 | export const updateMentorDetails = async ( 61 | mentorId: string, 62 | mentorData: Partial, 63 | profileData?: Partial, 64 | categoryId?: string, 65 | countryId?: string 66 | ): Promise<{ 67 | statusCode: number 68 | mentor?: Mentor | null 69 | message: string 70 | }> => { 71 | try { 72 | const mentorRepository = dataSource.getRepository(Mentor) 73 | const profileRepository = dataSource.getRepository(Profile) 74 | const categoryRepository = dataSource.getRepository(Category) 75 | const countryRepository = dataSource.getRepository(Country) 76 | 77 | const mentor = await mentorRepository.findOne({ 78 | where: { uuid: mentorId }, 79 | relations: ['profile'] 80 | }) 81 | 82 | if (!mentor) { 83 | return { 84 | statusCode: 404, 85 | message: 'Mentor not found' 86 | } 87 | } 88 | 89 | if (mentorData.availability !== undefined) { 90 | mentor.availability = mentorData.availability 91 | } 92 | 93 | if (categoryId) { 94 | const category = await categoryRepository.findOne({ 95 | where: { uuid: categoryId } 96 | }) 97 | 98 | if (!category) { 99 | return { 100 | statusCode: 404, 101 | message: 'Category not found' 102 | } 103 | } 104 | mentor.category = category 105 | } 106 | 107 | if (countryId) { 108 | const country = await countryRepository.findOne({ 109 | where: { uuid: countryId } 110 | }) 111 | 112 | if (!country) { 113 | return { 114 | statusCode: 404, 115 | message: 'Country not found' 116 | } 117 | } 118 | mentor.country = country 119 | } 120 | 121 | // will override values of keys if exisitng keys provided. add new key-value pairs if not exists 122 | if (mentorData.application) { 123 | mentor.application = { 124 | ...mentor.application, 125 | ...mentorData.application 126 | } 127 | } 128 | 129 | await mentorRepository.save(mentor) 130 | 131 | if (profileData && mentor.profile) { 132 | const updatedProfileData: Partial = {} 133 | 134 | if (profileData.primary_email) { 135 | updatedProfileData.primary_email = profileData.primary_email 136 | } 137 | if (profileData.first_name) { 138 | updatedProfileData.first_name = profileData.first_name 139 | } 140 | if (profileData.last_name) { 141 | updatedProfileData.last_name = profileData.last_name 142 | } 143 | if (profileData.image_url) { 144 | updatedProfileData.image_url = profileData.image_url 145 | } 146 | 147 | if (Object.keys(updatedProfileData).length > 0) { 148 | await profileRepository.update( 149 | { uuid: mentor.profile.uuid }, 150 | updatedProfileData as CreateProfile 151 | ) 152 | } 153 | } 154 | 155 | const updatedMentor = await mentorRepository.findOne({ 156 | where: { uuid: mentorId }, 157 | relations: ['profile', 'category', 'country'] 158 | }) 159 | 160 | return { 161 | statusCode: 200, 162 | mentor: updatedMentor, 163 | message: 'Updated Mentor details successfully' 164 | } 165 | } catch (err) { 166 | console.error('Error updating the mentor details', err) 167 | throw new Error('Error updating the mentor details') 168 | } 169 | } 170 | 171 | export const getAllMentors = async ({ 172 | status, 173 | pageNumber, 174 | pageSize 175 | }: { 176 | status: MentorApplicationStatus | undefined 177 | pageNumber: number 178 | pageSize: number 179 | }): Promise> => { 180 | try { 181 | const mentorRepository = dataSource.getRepository(Mentor) 182 | 183 | const [mentors, count] = await mentorRepository.findAndCount({ 184 | where: status ? { state: status } : {}, 185 | relations: ['profile', 'category', 'country'], 186 | skip: (pageNumber - 1) * pageSize, 187 | take: pageSize 188 | }) 189 | 190 | if (mentors.length === 0) { 191 | return { 192 | statusCode: 404, 193 | pageNumber, 194 | pageSize, 195 | items: [], 196 | totalItemCount: 0, 197 | message: 'Mentors not found' 198 | } 199 | } 200 | 201 | return { 202 | statusCode: 200, 203 | pageNumber, 204 | pageSize, 205 | items: mentors, 206 | totalItemCount: count, 207 | message: 'All Mentors found' 208 | } 209 | } catch (err) { 210 | throw new Error('Error getting mentors') 211 | } 212 | } 213 | 214 | export const findAllMentorEmails = async ( 215 | status: MentorApplicationStatus | undefined 216 | ): Promise<{ 217 | statusCode: number 218 | emails?: string[] 219 | message: string 220 | }> => { 221 | try { 222 | const mentorRepository = dataSource.getRepository(Mentor) 223 | 224 | const allMentors: Mentor[] = await mentorRepository.find({ 225 | where: status ? { state: status } : {}, 226 | relations: ['profile'] 227 | }) 228 | 229 | const emails = allMentors.map((mentor) => mentor?.profile?.primary_email) 230 | 231 | if (!emails) { 232 | return { 233 | statusCode: 404, 234 | message: 'Mentors Emails not found' 235 | } 236 | } 237 | 238 | return { 239 | statusCode: 200, 240 | emails, 241 | message: 'All Mentors Emails found' 242 | } 243 | } catch (err) { 244 | throw new Error('Error getting mentors emails') 245 | } 246 | } 247 | 248 | export const getMentor = async ( 249 | mentorId: string 250 | ): Promise<{ 251 | statusCode: number 252 | mentor?: Mentor | null 253 | message: string 254 | }> => { 255 | try { 256 | const mentorRepository = dataSource.getRepository(Mentor) 257 | 258 | const mentor = await mentorRepository.findOne({ 259 | where: { uuid: mentorId }, 260 | relations: ['profile', 'category', 'country'] 261 | }) 262 | 263 | if (!mentor) { 264 | return { 265 | statusCode: 404, 266 | message: 'Mentor not found' 267 | } 268 | } 269 | 270 | return { 271 | statusCode: 200, 272 | mentor, 273 | message: 'Mentor application found' 274 | } 275 | } catch (err) { 276 | console.error('Error updating the mentor status', err) 277 | throw new Error('Error updating the mentor status') 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/services/mentor.service.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../configs/dbConfig' 2 | import Category from '../entities/category.entity' 3 | import { Country } from '../entities/country.entity' 4 | import Mentee from '../entities/mentee.entity' 5 | import Mentor from '../entities/mentor.entity' 6 | import type Profile from '../entities/profile.entity' 7 | import { MentorApplicationStatus } from '../enums' 8 | import { type PaginatedApiResponse } from '../types' 9 | import { 10 | capitalizeFirstLetter, 11 | getEmailContent, 12 | getMentorPublicData 13 | } from '../utils' 14 | import { sendEmail } from './admin/email.service' 15 | 16 | export const createMentor = async ( 17 | user: Profile, 18 | application: Record, 19 | categoryId: string, 20 | countryId: string 21 | ): Promise<{ 22 | statusCode: number 23 | mentor?: Mentor | null 24 | message: string 25 | }> => { 26 | try { 27 | const mentorRepository = dataSource.getRepository(Mentor) 28 | const categoryRepository = dataSource.getRepository(Category) 29 | const menteeRepository = dataSource.getRepository(Mentee) 30 | const countryRepository = dataSource.getRepository(Country) 31 | 32 | const mentee = await menteeRepository.findOne({ 33 | where: { 34 | profile: { 35 | uuid: user.uuid 36 | } 37 | } 38 | }) 39 | 40 | if (mentee) { 41 | return { 42 | statusCode: 409, 43 | message: 44 | 'A mentee cannot become a mentor, Please contact sustainableeducationfoundation@gmail.com' 45 | } 46 | } 47 | 48 | const existingMentorApplications = await mentorRepository.find({ 49 | where: { profile: { uuid: user.uuid } } 50 | }) 51 | 52 | const category = await categoryRepository.findOne({ 53 | where: { uuid: categoryId } 54 | }) 55 | 56 | if (!category) { 57 | return { 58 | statusCode: 404, 59 | message: 'Category not found' 60 | } 61 | } 62 | 63 | const country = await countryRepository.findOne({ 64 | where: { uuid: countryId } 65 | }) 66 | 67 | if (!country) { 68 | return { 69 | statusCode: 404, 70 | message: 'Country not found' 71 | } 72 | } 73 | 74 | for (const mentor of existingMentorApplications) { 75 | switch (mentor.state) { 76 | case MentorApplicationStatus.PENDING: 77 | return { 78 | mentor, 79 | statusCode: 409, 80 | message: 'You have already applied' 81 | } 82 | case MentorApplicationStatus.APPROVED: 83 | return { 84 | mentor, 85 | statusCode: 409, 86 | message: 'The user is already a mentor' 87 | } 88 | default: 89 | break 90 | } 91 | } 92 | 93 | application.firstName = capitalizeFirstLetter( 94 | application.firstName as string 95 | ) 96 | application.lastName = capitalizeFirstLetter(application.lastName as string) 97 | 98 | const newMentor = new Mentor( 99 | MentorApplicationStatus.PENDING, 100 | category, 101 | application, 102 | true, 103 | user, 104 | country 105 | ) 106 | 107 | const savedMentor = await mentorRepository.save(newMentor) 108 | 109 | const content = await getEmailContent( 110 | 'mentor', 111 | MentorApplicationStatus.PENDING, 112 | application.firstName as string 113 | ) 114 | 115 | if (content) { 116 | await sendEmail( 117 | application.email as string, 118 | content.subject, 119 | content.message 120 | ) 121 | } 122 | 123 | return { 124 | statusCode: 201, 125 | mentor: savedMentor, 126 | message: 'Mentor application is successful' 127 | } 128 | } catch (err) { 129 | console.error('Error creating mentor', err) 130 | throw new Error('Error creating mentor') 131 | } 132 | } 133 | 134 | export const updateAvailability = async ( 135 | mentorId: string, 136 | availability: boolean 137 | ): Promise<{ 138 | statusCode: number 139 | message: string 140 | }> => { 141 | try { 142 | const mentorRepository = dataSource.getRepository(Mentor) 143 | const mentor = await mentorRepository.findOne({ 144 | where: { uuid: mentorId } 145 | }) 146 | 147 | if (!mentor) { 148 | return { 149 | statusCode: 404, 150 | message: 'Mentor not found' 151 | } 152 | } 153 | 154 | await mentorRepository.update({ uuid: mentorId }, { availability }) 155 | 156 | return { 157 | statusCode: 200, 158 | message: 'Mentor availability updated successfully' 159 | } 160 | } catch (err) { 161 | console.error('Error updating mentor availability', err) 162 | throw new Error('Error updating mentor availability') 163 | } 164 | } 165 | 166 | export const getMentor = async ( 167 | mentorId: string 168 | ): Promise<{ 169 | statusCode: number 170 | mentor?: Mentor | null 171 | message: string 172 | }> => { 173 | try { 174 | const mentorRepository = dataSource.getRepository(Mentor) 175 | const mentor = await mentorRepository.findOne({ 176 | where: { uuid: mentorId }, 177 | relations: [ 178 | 'profile', 179 | 'category', 180 | 'mentees', 181 | 'mentees.profile', 182 | 'country' 183 | ], 184 | select: ['application', 'uuid', 'availability'] 185 | }) 186 | 187 | if (!mentor) { 188 | return { 189 | statusCode: 404, 190 | message: 'Mentor not found' 191 | } 192 | } 193 | 194 | const publicMentor = getMentorPublicData(mentor) 195 | 196 | return { 197 | statusCode: 200, 198 | mentor: publicMentor, 199 | message: 'Mentor found' 200 | } 201 | } catch (err) { 202 | console.error('Error getting mentor', err) 203 | throw new Error('Error getting mentor') 204 | } 205 | } 206 | 207 | export const searchMentorsByQuery = async ( 208 | q?: string 209 | ): Promise<{ 210 | statusCode: number 211 | mentors?: Mentor[] | null 212 | message: string 213 | }> => { 214 | try { 215 | const mentorRepository = dataSource.getRepository(Mentor) 216 | const query = q ? `${q}%` : '' 217 | 218 | const mentors: Mentor[] = await mentorRepository 219 | .createQueryBuilder('mentor') 220 | .innerJoinAndSelect('mentor.profile', 'profile') 221 | .where( 222 | 'profile.first_name ILIKE :name OR profile.last_name ILIKE :name', 223 | { 224 | name: query 225 | } 226 | ) 227 | .getMany() 228 | 229 | if (!mentors) { 230 | return { 231 | statusCode: 404, 232 | message: 'Mentors not found' 233 | } 234 | } 235 | 236 | return { 237 | statusCode: 200, 238 | mentors, 239 | message: 'All search Mentors found' 240 | } 241 | } catch (err) { 242 | console.error('Error getting mentor', err) 243 | throw new Error('Error getting mentor') 244 | } 245 | } 246 | 247 | export const getAllMentors = async ({ 248 | categoryId, 249 | pageNumber, 250 | pageSize 251 | }: { 252 | categoryId?: string | null 253 | pageNumber: number 254 | pageSize: number 255 | }): Promise> => { 256 | try { 257 | const mentorRepository = dataSource.getRepository(Mentor) 258 | const mentors = await mentorRepository.findAndCount({ 259 | where: categoryId 260 | ? { 261 | category: { uuid: categoryId }, 262 | state: MentorApplicationStatus.APPROVED 263 | } 264 | : { state: MentorApplicationStatus.APPROVED }, 265 | relations: [ 266 | 'profile', 267 | 'category', 268 | 'mentees', 269 | 'mentees.profile', 270 | 'country' 271 | ], 272 | select: ['application', 'uuid', 'availability'], 273 | order: { 274 | availability: 'DESC' 275 | }, 276 | skip: (pageNumber - 1) * pageSize, 277 | take: pageSize 278 | }) 279 | 280 | const publicMentors = mentors[0].map((mentor) => 281 | getMentorPublicData(mentor) 282 | ) 283 | 284 | if (publicMentors.length === 0) { 285 | return { 286 | statusCode: 404, 287 | items: [], 288 | totalItemCount: 0, 289 | pageNumber, 290 | pageSize, 291 | message: 'No mentors found' 292 | } 293 | } 294 | 295 | return { 296 | statusCode: 200, 297 | items: publicMentors, 298 | totalItemCount: mentors[1], 299 | pageNumber, 300 | pageSize, 301 | message: 'Mentors found' 302 | } 303 | } catch (err) { 304 | console.error('Error getting mentors', err) 305 | throw new Error('Error getting mentors') 306 | } 307 | } 308 | --------------------------------------------------------------------------------