├── .gitignore ├── LICENSE ├── README.md ├── backend ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .husky │ ├── pre-commit │ └── pre-push ├── .huskyrc ├── .lintstagedrc.json ├── .prettierignore ├── .prettierrc ├── .swcrc ├── Dockerfile.dev ├── Dockerfile.prod ├── Makefile ├── docker-compose.yml ├── ecosystem.config.js ├── jest.config.js ├── nginx.conf ├── nodemon.json ├── package-lock.json ├── package.json ├── plop_templates │ ├── controller.hbs │ ├── dtos.hbs │ ├── interface.hbs │ ├── model.hbs │ ├── route.hbs │ ├── routeImport.hbs │ └── service.hbs ├── plopfile.js ├── src │ ├── app.ts │ ├── config │ │ └── index.ts │ ├── controllers │ │ ├── auth.controller.ts │ │ ├── member.controller.ts │ │ ├── secret.controller.ts │ │ └── user.controller.ts │ ├── database │ │ └── index.ts │ ├── dtos │ │ ├── member.dto.ts │ │ ├── otp.dto.ts │ │ ├── secret.dto.ts │ │ └── user.dto.ts │ ├── exceptions │ │ └── HttpException.ts │ ├── http │ │ ├── auth.http │ │ └── users.http │ ├── interfaces │ │ ├── activity.interface.ts │ │ ├── auth.interface.ts │ │ ├── member.interface.ts │ │ ├── otp.interface.ts │ │ ├── route.interface.ts │ │ ├── secret.interface.ts │ │ ├── template.interface.ts │ │ └── user.interface.ts │ ├── middlewares │ │ ├── auth.middleware.ts │ │ ├── error.middleware.ts │ │ └── validation.middleware.ts │ ├── models │ │ ├── activity.model.ts │ │ ├── member.model.ts │ │ ├── otp.model.ts │ │ ├── secret.model.ts │ │ └── user.model.ts │ ├── routes │ │ ├── auth.route.ts │ │ ├── member.route.ts │ │ ├── secret.route.ts │ │ └── users.route.ts │ ├── server.ts │ ├── services │ │ ├── auth.service.ts │ │ ├── member.service.ts │ │ ├── secret.service.ts │ │ └── users.service.ts │ ├── test │ │ ├── auth.test.ts │ │ ├── index.test.ts │ │ └── users.test.ts │ ├── typings │ │ └── express │ │ │ └── index.d.ts │ ├── utils │ │ ├── constants.ts │ │ ├── discord.ts │ │ ├── helper.ts │ │ ├── logger.ts │ │ ├── mail.ts │ │ ├── passport.ts │ │ └── validateEnv.ts │ └── views │ │ ├── otp.pug │ │ └── welcome.pug ├── swagger.yaml └── tsconfig.json ├── frontend ├── .eslintrc.cjs ├── .gitignore ├── index.html ├── package.json ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── index.css │ ├── main.tsx │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts └── gitCommit.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env-development 3 | .env-production 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Dependency directories 13 | node_modules/ 14 | jspm_packages/ 15 | 16 | # Optional npm cache directory 17 | .npm 18 | 19 | # Output of 'npm pack' 20 | *.tgz 21 | 22 | # Random crap 23 | trash 24 | .DS_Store 25 | .sass-cache 26 | .tmp 27 | .rvmrc 28 | .trashes 29 | *~ 30 | 31 | # Gatsby related 32 | .cache/ 33 | public 34 | 35 | # Editor 36 | .vscode 37 | .vscode-test 38 | *.sublime-project 39 | *.sublime-workspace 40 | sftp-config.json 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Pranav Mehta 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Hi Coders, 2 | ## This repo powers the ACodeDaily.com 3 | The repo contains 3 decoupled components i.e. Interaction has been limited via api calls (except for database) 4 | - Frontend: The website [later to be expanded to mobile apps] 5 | 6 | - Backend: The repo is responsible for 7 | - user authentication. 8 | - business logic. 9 | - It leverages the APIs provided by the infra to serve the front end requests. 10 | - queries the database for user queries. 11 | 12 | - Infra: The repo is responsible for 13 | - periodic refreshing of user handles to get updates on their latest achievements. 14 | - serving the backend with outgoing calls for accesssing various platforms for example: Codeforces, Codechef, Github, Leetcode, Atcoder 15 | - the process might need scraping at times hence the abstract nature makes the workflow smooth. 16 | - gathering user details from the discord server (ACodeDaily) 17 | - periodic analaysis and updation of the internal database. 18 | 19 | 20 | ## What this project aims to achieve? 21 | - To serve both the Competitive programmers as well as the open source specialists. 22 | - The platform would host people from the industry to refer people for roles in their respective orgs. 23 | - This would give a fair chance to both the CPers and developers. 24 | - To develop a platform where the CP and Dev questions can be asked without any hesitation and are resolved ASAP! 25 | - Would add the discord CP doubts section here for serving the rich content to non discord audience. 26 | 27 | 28 | 29 | ## specific features 30 | - signup/login for both job searchers and people willing to refer them. 31 | - add and authenticate handles from various platforms and discord server. 32 | - check your progress graph and compare your progress with others aka leaderboard. 33 | - reachout to people for referrals on the platforms. 34 | - People can check your profile and give referrals only to deserving candidates as they please. 35 | - show discord server (AcodeDaily) forum posts here (would serve as a **stackoverflow for DSA/Competitive programming**) 36 | - ask doubts on the websites these doubts would be converted to forum posts and would be posted on the discord server. 37 | - search through various doubts based on the tags to pick topics where you can help. 38 | 39 | ## tech stack to be followed 40 | - Front End 41 | - React+ Vite + TS 42 | - Back end 43 | - Node + TS 44 | - Infra 45 | - Node + TS + MongoDB 46 | 47 | 48 | ## future scope 49 | - test grpc 50 | - utilise microfrontends, microservices 51 | 52 | 53 | ## Progress 54 | - [x] updating the readme file. 55 | - [x] consensus on the tech stack to be followed 56 | - [x] set boiler plate code 57 | - [ ] figma design 58 | - [ ] HLD design 59 | - [ ] LLD design 60 | - [ ] Frontend tasks 61 | - [ ] user profile 62 | - [ ] login/signup page 63 | - [ ] heatmap (including dev + CP) 64 | - [ ] Discussion for the mobile application (Flutter based - to be lead by Prerak) 65 | - [ ] Backend tasks 66 | - [ ] to be populated 67 | - [ ] Infra tasks 68 | - [ ] user data fetching from all platforms 69 | - [ ] Leetcode 70 | - [ ] Codeforces 71 | - [ ] Github 72 | - [ ] Codechef 73 | - [ ] Atcoder 74 | - [ ] userhandle authentication (Everyone wants to be Tourist) 75 | - [ ] Leetcode 76 | - [ ] Codeforces 77 | - [ ] Github 78 | - [ ] Codechef 79 | - [ ] Atcoder 80 | - [ ] discord handle authentication and score fetching 81 | - [ ] the discord server score would be a function of individuals help to peers (in both CP and opensource). 82 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | .vscode 3 | /node_modules 4 | 5 | # code formatter 6 | .eslintrc 7 | .eslintignore 8 | .editorconfig 9 | .huskyrc 10 | .lintstagedrc.json 11 | .prettierrc 12 | 13 | # test 14 | jest.config.js 15 | 16 | # docker 17 | Dockerfile 18 | docker-compose.yml 19 | -------------------------------------------------------------------------------- /backend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = crlf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /backend/.eslintignore: -------------------------------------------------------------------------------- 1 | /dist -------------------------------------------------------------------------------- /backend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": ["prettier", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"], 4 | "parserOptions": { 5 | "ecmaVersion": 2018, 6 | "sourceType": "module" 7 | }, 8 | "rules": { 9 | "@typescript-eslint/explicit-member-accessibility": 0, 10 | "@typescript-eslint/explicit-function-return-type": 0, 11 | "@typescript-eslint/no-parameter-properties": 0, 12 | "@typescript-eslint/interface-name-prefix": 0, 13 | "@typescript-eslint/explicit-module-boundary-types": 0, 14 | "@typescript-eslint/no-explicit-any": "off", 15 | "@typescript-eslint/ban-types": "off", 16 | "@typescript-eslint/no-var-requires": "off", 17 | "no-console": "error" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .env.development.local 4 | .env.production.local 5 | .env.test.local -------------------------------------------------------------------------------- /backend/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | cd backend 5 | 6 | npm run prettier 7 | -------------------------------------------------------------------------------- /backend/.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | cd backend 5 | 6 | npm run lint:fix && npm run lint -------------------------------------------------------------------------------- /backend/.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged", 4 | "...": "..." 5 | } 6 | } 7 | 8 | -------------------------------------------------------------------------------- /backend/.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.ts": ["npm run lint"] 3 | } 4 | -------------------------------------------------------------------------------- /backend/.prettierignore: -------------------------------------------------------------------------------- 1 | # .prettierignore 2 | **/*.hbs 3 | plop_templates/** 4 | *.js 5 | ``` -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 150, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "semi": true, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /backend/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "parser": { 4 | "syntax": "typescript", 5 | "tsx": false, 6 | "dynamicImport": true, 7 | "decorators": true 8 | }, 9 | "transform": { 10 | "legacyDecorator": true, 11 | "decoratorMetadata": true 12 | }, 13 | "target": "es2017", 14 | "externalHelpers": false, 15 | "keepClassNames": true, 16 | "loose": false, 17 | "minify": { 18 | "compress": false, 19 | "mangle": false 20 | }, 21 | "baseUrl": "src", 22 | "paths": { 23 | "@/*": ["*"], 24 | "@config": ["config"], 25 | "@controllers/*": ["controllers/*"], 26 | "@database": ["database"], 27 | "@dtos/*": ["dtos/*"], 28 | "@exceptions/*": ["exceptions/*"], 29 | "@interfaces/*": ["interfaces/*"], 30 | "@middlewares/*": ["middlewares/*"], 31 | "@models/*": ["models/*"], 32 | "@routes/*": ["routes/*"], 33 | "@services/*": ["services/*"], 34 | "@utils/*": ["utils/*"] 35 | } 36 | }, 37 | "module": { 38 | "type": "commonjs" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | # NodeJS Version 16 2 | FROM node:16.18-buster-slim 3 | 4 | # Copy Dir 5 | COPY . ./app 6 | 7 | # Work to Dir 8 | WORKDIR /app 9 | 10 | # Install Node Package 11 | RUN npm install --legacy-peer-deps 12 | 13 | # Set Env 14 | ENV NODE_ENV development 15 | 16 | EXPOSE 3000 17 | 18 | # Cmd script 19 | CMD ["npm", "run", "dev"] 20 | -------------------------------------------------------------------------------- /backend/Dockerfile.prod: -------------------------------------------------------------------------------- 1 | # NodeJS Version 16 2 | FROM node:16.18-buster-slim 3 | 4 | # Copy Dir 5 | COPY . ./app 6 | 7 | # Work to Dir 8 | WORKDIR /app 9 | 10 | # Install Node Package 11 | RUN npm install --legacy-peer-deps 12 | 13 | # Set Env 14 | ENV NODE_ENV production 15 | 16 | EXPOSE 3000 17 | 18 | # Cmd script 19 | CMD ["npm", "run", "start"] 20 | -------------------------------------------------------------------------------- /backend/Makefile: -------------------------------------------------------------------------------- 1 | # app name should be overridden. 2 | # ex) production-stage: make build APP_NAME= 3 | # ex) development-stage: make build-dev APP_NAME= 4 | 5 | SHELL := /bin/bash 6 | 7 | APP_NAME = acd-backend 8 | APP_NAME := $(APP_NAME) 9 | 10 | .PHONY: help start clean db test 11 | 12 | help: 13 | @grep -E '^[1-9a-zA-Z_-]+:.*?## .*$$|(^#--)' $(MAKEFILE_LIST) \ 14 | | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m %-43s\033[0m %s\n", $$1, $$2}' \ 15 | | sed -e 's/\[32m #-- /[33m/' 16 | 17 | #-- Docker 18 | up: ## Up the container images 19 | docker-compose up -d 20 | 21 | down: ## Down the container images 22 | docker-compose down 23 | 24 | build: ## Build the container image - Production 25 | docker build -t ${APP_NAME}\ 26 | -f Dockerfile.prod . 27 | 28 | build-dev: ## Build the container image - Development 29 | docker build -t ${APP_NAME}\ 30 | -f Dockerfile.dev . 31 | 32 | run: ## Run the container image 33 | docker run -d -it -p 3000:3000 ${APP_NAME} 34 | 35 | pause: ## Pause the containers 36 | docker container rm -f ${APP_NAME} 37 | 38 | clean: ## Clean the images 39 | docker rmi -f ${APP_NAME} 40 | 41 | remove: ## Remove the volumes 42 | docker volume rm -f ${APP_NAME} 43 | 44 | #-- Database 45 | db: ## Start the local database MongoDB 46 | docker-compose up -d mongo 47 | -------------------------------------------------------------------------------- /backend/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | proxy: 5 | container_name: proxy 6 | image: nginx:alpine 7 | ports: 8 | - '80:80' 9 | volumes: 10 | - ./nginx.conf:/etc/nginx/nginx.conf 11 | restart: 'unless-stopped' 12 | networks: 13 | - backend 14 | 15 | server: 16 | container_name: server 17 | build: 18 | context: ./ 19 | dockerfile: Dockerfile.dev 20 | ports: 21 | - '3000:3000' 22 | environment: 23 | DB_HOST: localhost 24 | DB_PORT: 27017 25 | DB_DATABASE: dev 26 | volumes: 27 | - ./:/app 28 | - /app/node_modules 29 | restart: 'unless-stopped' 30 | networks: 31 | - backend 32 | links: 33 | - mongo 34 | depends_on: 35 | - mongo 36 | 37 | mongo: 38 | container_name: mongo 39 | image: mongo 40 | ports: 41 | - '27017:27017' 42 | environment: 43 | DB_HOST: localhost 44 | DB_PORT: 27017 45 | DB_DATABASE: dev 46 | networks: 47 | - backend 48 | 49 | networks: 50 | backend: 51 | driver: bridge 52 | 53 | volumes: 54 | data: 55 | driver: local 56 | -------------------------------------------------------------------------------- /backend/ecosystem.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description pm2 configuration file. 3 | * @example 4 | * production mode :: pm2 start ecosystem.config.js --only prod 5 | * development mode :: pm2 start ecosystem.config.js --only dev 6 | */ 7 | module.exports = { 8 | apps: [ 9 | { 10 | name: 'prod', // pm2 start App name 11 | script: 'dist/server.js', 12 | exec_mode: 'cluster', // 'cluster' or 'fork' 13 | instance_var: 'INSTANCE_ID', // instance variable 14 | instances: 2, // pm2 instance count 15 | autorestart: true, // auto restart if process crash 16 | watch: false, // files change automatic restart 17 | ignore_watch: ['node_modules', 'logs'], // ignore files change 18 | max_memory_restart: '1G', // restart if process use more than 1G memory 19 | merge_logs: true, // if true, stdout and stderr will be merged and sent to pm2 log 20 | output: './logs/access.log', // pm2 log file 21 | error: './logs/error.log', // pm2 error log file 22 | env: { 23 | // environment variable 24 | PORT: 3000, 25 | NODE_ENV: 'production', 26 | }, 27 | }, 28 | { 29 | name: 'dev', // pm2 start App name 30 | script: 'ts-node', // ts-node 31 | args: '-r tsconfig-paths/register --transpile-only src/server.ts', // ts-node args 32 | exec_mode: 'cluster', // 'cluster' or 'fork' 33 | instance_var: 'INSTANCE_ID', // instance variable 34 | instances: 2, // pm2 instance count 35 | autorestart: true, // auto restart if process crash 36 | watch: false, // files change automatic restart 37 | ignore_watch: ['node_modules', 'logs'], // ignore files change 38 | max_memory_restart: '1G', // restart if process use more than 1G memory 39 | merge_logs: true, // if true, stdout and stderr will be merged and sent to pm2 log 40 | output: './logs/access.log', // pm2 log file 41 | error: './logs/error.log', // pm2 error log file 42 | env: { 43 | // environment variable 44 | PORT: 3000, 45 | NODE_ENV: 'development', 46 | }, 47 | }, 48 | ], 49 | deploy: { 50 | production: { 51 | user: 'user', 52 | host: '0.0.0.0', 53 | ref: 'origin/master', 54 | repo: 'git@github.com:repo.git', 55 | path: 'dist/server.js', 56 | 'post-deploy': 'npm install && npm run build && pm2 reload ecosystem.config.js --only prod', 57 | }, 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /backend/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest'); 2 | const { compilerOptions } = require('./tsconfig.json'); 3 | 4 | module.exports = { 5 | preset: 'ts-jest', 6 | testEnvironment: 'node', 7 | roots: ['/src'], 8 | transform: { 9 | '^.+\\.tsx?$': 'ts-jest', 10 | }, 11 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/src' }), 12 | }; 13 | -------------------------------------------------------------------------------- /backend/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | http { 12 | include /etc/nginx/mime.types; 13 | default_type application/octet-stream; 14 | 15 | upstream api-server { 16 | server server:3000; 17 | keepalive 100; 18 | } 19 | 20 | server { 21 | listen 80; 22 | server_name localhost; 23 | 24 | location / { 25 | proxy_http_version 1.1; 26 | proxy_pass http://api-server; 27 | } 28 | 29 | } 30 | 31 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 32 | '$status $body_bytes_sent "$http_referer" ' 33 | '"$http_user_agent" "$http_x_forwarded_for"'; 34 | 35 | access_log /var/log/nginx/access.log main; 36 | 37 | sendfile on; 38 | keepalive_timeout 65; 39 | include /etc/nginx/conf.d/*.conf; 40 | } 41 | -------------------------------------------------------------------------------- /backend/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src", ".env"], 3 | "ext": "js,ts,json", 4 | "ignore": ["src/logs/*", "src/**/*.{spec,test}.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register --transpile-only src/server.ts" 6 | } 7 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "acd-backend", 3 | "version": "0.0.0", 4 | "description": "A code daily backend repository", 5 | "author": "shubhamjr", 6 | "license": "ISC", 7 | "scripts": { 8 | "start": "npm run build && cross-env NODE_ENV=production node dist/server.js", 9 | "dev": "cross-env NODE_ENV=development nodemon", 10 | "build": "swc src -d dist --source-maps --copy-files", 11 | "gen": "plop", 12 | "build:tsc": "tsc && tsc-alias", 13 | "test": "jest --forceExit --detectOpenHandles", 14 | "lint": "eslint --ignore-path .gitignore --ext .ts src/", 15 | "lint:fix": "npm run lint -- --fix", 16 | "deploy:prod": "npm run build && pm2 start ecosystem.config.js --only prod", 17 | "deploy:dev": "pm2 start ecosystem.config.js --only dev", 18 | "prepare": "cd .. && husky install backend/.husky", 19 | "prettier": "npx prettier --write .", 20 | "pre-commit": "lint-staged" 21 | }, 22 | "dependencies": { 23 | "@types/pug": "^2.0.10", 24 | "bcrypt": "^5.0.1", 25 | "class-transformer": "^0.5.1", 26 | "class-validator": "^0.13.2", 27 | "compression": "^1.7.4", 28 | "cookie-parser": "^1.4.6", 29 | "cors": "^2.8.5", 30 | "discord.js": "^14.14.1", 31 | "dotenv": "^16.0.1", 32 | "envalid": "^7.3.1", 33 | "express": "^4.18.1", 34 | "express-session": "^1.17.3", 35 | "helmet": "^5.1.1", 36 | "hpp": "^0.2.3", 37 | "jsonwebtoken": "^8.5.1", 38 | "mongoose": "^6.5.0", 39 | "morgan": "^1.10.0", 40 | "nodemailer": "^6.9.7", 41 | "passport": "^0.7.0", 42 | "passport-google-oauth20": "^2.0.0", 43 | "pug": "^3.0.2", 44 | "reflect-metadata": "^0.1.13", 45 | "swagger-jsdoc": "^6.2.1", 46 | "swagger-ui-express": "^4.5.0", 47 | "typedi": "^0.10.0", 48 | "winston": "^3.8.1", 49 | "winston-daily-rotate-file": "^4.7.1" 50 | }, 51 | "devDependencies": { 52 | "@swc/cli": "^0.1.57", 53 | "@swc/core": "^1.2.220", 54 | "@types/bcrypt": "^5.0.0", 55 | "@types/compression": "^1.7.2", 56 | "@types/cookie-parser": "^1.4.3", 57 | "@types/cors": "^2.8.12", 58 | "@types/express": "^4.17.13", 59 | "@types/hpp": "^0.2.2", 60 | "@types/jest": "^28.1.6", 61 | "@types/jsonwebtoken": "^8.5.8", 62 | "@types/mongoose": "^5.11.97", 63 | "@types/morgan": "^1.9.3", 64 | "@types/node": "^17.0.45", 65 | "@types/nodemailer": "^6.4.14", 66 | "@types/passport": "^1.0.16", 67 | "@types/passport-google-oauth20": "^2.0.14", 68 | "@types/supertest": "^2.0.12", 69 | "@types/swagger-jsdoc": "^6.0.1", 70 | "@types/swagger-ui-express": "^4.1.3", 71 | "@typescript-eslint/eslint-plugin": "^5.29.0", 72 | "@typescript-eslint/parser": "^5.29.0", 73 | "cross-env": "^7.0.3", 74 | "eslint": "^8.20.0", 75 | "eslint-config-prettier": "^8.8.0", 76 | "eslint-plugin-prettier": "^4.2.1", 77 | "husky": "^8.0.1", 78 | "jest": "^28.1.1", 79 | "lint-staged": "^13.0.3", 80 | "node-config": "^0.0.2", 81 | "node-gyp": "^9.1.0", 82 | "nodemon": "^2.0.19", 83 | "plop": "^4.0.0", 84 | "pm2": "^5.2.0", 85 | "prettier": "^2.7.1", 86 | "supertest": "^6.2.4", 87 | "ts-jest": "^28.0.7", 88 | "ts-node": "^10.9.1", 89 | "tsc-alias": "^1.7.0", 90 | "tsconfig-paths": "^4.0.0", 91 | "typescript": "^4.7.4" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /backend/plop_templates/controller.hbs: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { Container } from 'typedi'; 3 | import { {{camel name}} } from '@interfaces/{{name}}.interface'; 4 | import { {{camel name}}Service } from '@services/{{name}}.service'; 5 | 6 | export class {{camel name}}Controller { 7 | public {{name}} = Container.get({{camel name}}Service); 8 | 9 | public get{{camel name}}s = async (req: Request, res: Response, next: NextFunction) => { 10 | try { 11 | const findAll{{camel name}}sData: {{camel name}}[] = await this.{{name}}.findAll{{camel name}}(); 12 | 13 | res.status(200).json({ data: findAll{{camel name}}sData, message: 'findAll' }); 14 | } catch (error) { 15 | next(error); 16 | } 17 | }; 18 | 19 | public get{{camel name}}ById = async (req: Request, res: Response, next: NextFunction) => { 20 | try { 21 | const {{name}}Id: string = req.params.id; 22 | const findOne{{camel name}}Data: {{camel name}} = await this.{{name}}.find{{camel name}}ById({{name}}Id); 23 | 24 | res.status(200).json({ data: findOne{{camel name}}Data, message: 'findOne' }); 25 | } catch (error) { 26 | next(error); 27 | } 28 | }; 29 | 30 | public create{{camel name}} = async (req: Request, res: Response, next: NextFunction) => { 31 | try { 32 | const {{name}}Data: {{camel name}} = req.body; 33 | const create{{camel name}}Data: {{camel name}} = await this.{{name}}.create{{camel name}}({{name}}Data); 34 | 35 | res.status(201).json({ data: create{{camel name}}Data, message: 'created' }); 36 | } catch (error) { 37 | next(error); 38 | } 39 | }; 40 | 41 | public update{{camel name}} = async (req: Request, res: Response, next: NextFunction) => { 42 | try { 43 | const {{name}}Id: string = req.params.id; 44 | const {{name}}Data: {{camel name}} = req.body; 45 | const update{{camel name}}Data: {{camel name}} = await this.{{name}}.update{{camel name}}({{name}}Id, {{name}}Data); 46 | 47 | res.status(200).json({ data: update{{camel name}}Data, message: 'updated' }); 48 | } catch (error) { 49 | next(error); 50 | } 51 | }; 52 | 53 | public delete{{camel name}} = async (req: Request, res: Response, next: NextFunction) => { 54 | try { 55 | const {{camel name}}Id: string = req.params.id; 56 | const delete{{camel name}}Data: {{camel name}} = await this.{{name}}.delete{{camel name}}({{camel name}}Id); 57 | 58 | res.status(200).json({ data: delete{{camel name}}Data, message: 'deleted' }); 59 | } catch (error) { 60 | next(error); 61 | } 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /backend/plop_templates/dtos.hbs: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString, IsNotEmpty, MinLength, MaxLength } from 'class-validator'; 2 | 3 | export class Create{{camel name}}Dto { 4 | @IsEmail() 5 | public email: string; 6 | 7 | @IsString() 8 | @IsNotEmpty() 9 | @MinLength(9) 10 | @MaxLength(32) 11 | public password: string; 12 | } 13 | 14 | export class Update{{camel name}}Dto { 15 | @IsString() 16 | @IsNotEmpty() 17 | @MinLength(9) 18 | @MaxLength(32) 19 | public password: string; 20 | } 21 | -------------------------------------------------------------------------------- /backend/plop_templates/interface.hbs: -------------------------------------------------------------------------------- 1 | export interface {{camel name}} { 2 | _id?: string; 3 | } 4 | -------------------------------------------------------------------------------- /backend/plop_templates/model.hbs: -------------------------------------------------------------------------------- 1 | import { Schema, model, Document } from 'mongoose'; 2 | import { {{camel name}} } from '@interfaces/{{name}}.interface'; 3 | 4 | const {{name}}Schema: Schema = new Schema({ 5 | dummy: { 6 | type: String, 7 | required: true, 8 | }, 9 | }, 10 | { 11 | timestamps: true, 12 | },); 13 | 14 | export const {{camel name}}Model = model<{{camel name}} & Document>('{{camel name}}', {{name}}Schema); 15 | -------------------------------------------------------------------------------- /backend/plop_templates/route.hbs: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { {{camel name}}Controller } from '@controllers/{{name}}.controller'; 3 | import { Create{{camel name}}Dto } from '@dtos/{{name}}.dto'; 4 | import { Routes } from '@interfaces/routes.interface'; 5 | import { ValidationMiddleware } from '@middlewares/validation.middleware'; 6 | 7 | export class {{camel name}}Route implements Routes { 8 | public path = '/{{name}}'; 9 | public router = Router(); 10 | public {{name}} = new {{camel name}}Controller(); 11 | 12 | constructor() { 13 | this.initializeRoutes(); 14 | } 15 | 16 | private initializeRoutes() { 17 | this.router.get(`${this.path}`, this.{{name}}.get{{camel name}}s); 18 | this.router.get(`${this.path}/:id`, this.{{name}}.get{{camel name}}ById); 19 | this.router.post(`${this.path}`, ValidationMiddleware(Create{{camel name}}Dto), this.{{name}}.create{{camel name}}); 20 | this.router.put(`${this.path}/:id`, ValidationMiddleware(Create{{camel name}}Dto, true), this.{{name}}.update{{camel name}}); 21 | this.router.delete(`${this.path}/:id`, this.{{name}}.delete{{camel name}}); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/plop_templates/routeImport.hbs: -------------------------------------------------------------------------------- 1 | import { {{camel name}}Route } from '@routes/{{name}}.route'; -------------------------------------------------------------------------------- /backend/plop_templates/service.hbs: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { HttpException } from '@exceptions/HttpException'; 3 | import { {{camel name}}Model } from '@models/{{name}}.model'; 4 | import { {{camel name}} } from '@interfaces/{{name}}.interface'; 5 | 6 | @Service() 7 | export class {{camel name}}Service { 8 | public async findAll{{camel name}}(): Promise<{{camel name}}[]> { 9 | const {{name}}s: {{camel name}}[] = await {{camel name}}Model.find(); 10 | return {{name}}s; 11 | } 12 | 13 | public async find{{camel name}}ById({{name}}Id: string): Promise<{{camel name}}> { 14 | const find{{camel name}}: {{camel name}} = await {{camel name}}Model.findOne({ _id: {{name}}Id }); 15 | if (!find{{camel name}}) throw new HttpException(409, "{{camel name}} doesn't exist"); 16 | 17 | return find{{camel name}}; 18 | } 19 | 20 | public async create{{camel name}}({{name}}Data: {{camel name}}): Promise<{{camel name}}> { 21 | 22 | const create{{camel name}}Data: {{camel name}} = await {{camel name}}Model.create({ ...{{name}}Data }); 23 | 24 | return create{{camel name}}Data; 25 | } 26 | 27 | public async update{{camel name}}({{name}}Id: string, {{name}}Data: {{camel name}}): Promise<{{camel name}}> { 28 | 29 | const update{{camel name}}ById: {{camel name}} = await {{camel name}}Model.findByIdAndUpdate({{name}}Id, { {{name}}Data }); 30 | if (!update{{camel name}}ById) throw new HttpException(409, "{{camel name}} doesn't exist"); 31 | 32 | return update{{camel name}}ById; 33 | } 34 | 35 | public async delete{{camel name}}({{name}}Id: string): Promise<{{camel name}}> { 36 | const delete{{camel name}}ById: {{camel name}} = await {{camel name}}Model.findByIdAndDelete({{name}}Id); 37 | if (!delete{{camel name}}ById) throw new HttpException(409, "{{camel name}} doesn't exist"); 38 | 39 | return delete{{camel name}}ById; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/plopfile.js: -------------------------------------------------------------------------------- 1 | module.exports = plop => { 2 | plop.setGenerator('feature', { 3 | description: 'generating models + controller + routes + service + dto + interface', 4 | prompts: [ 5 | { 6 | type: 'input', 7 | name: 'name', 8 | message: 'type name of feature', 9 | }, 10 | ], 11 | actions: [ 12 | { 13 | type: 'add', 14 | path: './src/models/{{name}}.model.ts', 15 | templateFile: 'plop_templates/model.hbs', 16 | }, 17 | { 18 | type: 'add', 19 | path: './src/routes/{{name}}.route.ts', 20 | templateFile: 'plop_templates/route.hbs', 21 | }, 22 | { 23 | type: 'add', 24 | path: './src/controllers/{{name}}.controller.ts', 25 | templateFile: 'plop_templates/controller.hbs', 26 | }, 27 | { 28 | type: 'add', 29 | path: './src/services/{{name}}.service.ts', 30 | templateFile: 'plop_templates/service.hbs', 31 | }, 32 | { 33 | type: 'add', 34 | path: './src/interfaces/{{name}}.interface.ts', 35 | templateFile: 'plop_templates/interface.hbs', 36 | }, 37 | { 38 | type: 'add', 39 | path: './src/dtos/{{name}}.dto.ts', 40 | templateFile: 'plop_templates/dtos.hbs', 41 | }, 42 | { 43 | type: 'append', 44 | path: './src/server.ts', 45 | templateFile: 'plop_templates/routeImport.hbs', 46 | pattern: 'plop_append_import', 47 | }, 48 | ], 49 | }); 50 | plop.setHelper('camel', txt => txt.charAt(0).toUpperCase() + txt.slice(1)); 51 | }; 52 | -------------------------------------------------------------------------------- /backend/src/app.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import compression from 'compression'; 3 | import cookieParser from 'cookie-parser'; 4 | import cors from 'cors'; 5 | import express from 'express'; 6 | import helmet from 'helmet'; 7 | import hpp from 'hpp'; 8 | import Container from 'typedi'; 9 | import morgan from 'morgan'; 10 | import swaggerJSDoc from 'swagger-jsdoc'; 11 | import swaggerUi from 'swagger-ui-express'; 12 | import { NODE_ENV, PORT, LOG_FORMAT, ORIGIN, CREDENTIALS } from '@config'; 13 | import { dbConnection } from '@database'; 14 | import { Routes } from '@/interfaces/route.interface'; 15 | import { ErrorMiddleware } from '@middlewares/error.middleware'; 16 | import { logger, stream } from '@utils/logger'; 17 | import { DiscordBot } from '@utils/discord'; 18 | import '@utils/passport'; 19 | import passport from 'passport'; 20 | 21 | export class App { 22 | public app: express.Application; 23 | public env: string; 24 | public port: string | number; 25 | 26 | constructor(routes: Routes[]) { 27 | this.app = express(); 28 | this.env = NODE_ENV || 'development'; 29 | this.port = PORT || 3000; 30 | 31 | this.connectToDatabase(); 32 | this.initializeMiddlewares(); 33 | this.initializeRoutes(routes); 34 | this.initializeSwagger(); 35 | this.initializeErrorHandling(); 36 | this.initBot(); 37 | } 38 | 39 | public listen() { 40 | this.app.listen(this.port, () => { 41 | logger.info(`=================================`); 42 | logger.info(`======= ENV: ${this.env} =======`); 43 | logger.info(`🚀 App listening on the port ${this.port}`); 44 | logger.info(`=================================`); 45 | }); 46 | } 47 | 48 | public getServer() { 49 | return this.app; 50 | } 51 | 52 | private async connectToDatabase() { 53 | await dbConnection(); 54 | } 55 | 56 | private initializeMiddlewares() { 57 | this.app.use(morgan(LOG_FORMAT, { stream })); 58 | this.app.use(cors({ origin: ORIGIN, credentials: CREDENTIALS })); 59 | this.app.use(hpp()); 60 | this.app.use(helmet()); 61 | this.app.use(compression()); 62 | this.app.use(express.json()); 63 | this.app.use(express.urlencoded({ extended: true })); 64 | this.app.use(cookieParser()); 65 | this.app.use(passport.initialize()); 66 | } 67 | 68 | private initializeRoutes(routes: Routes[]) { 69 | routes.forEach(route => { 70 | this.app.use('/api/v3', route.router); 71 | }); 72 | } 73 | 74 | private initializeSwagger() { 75 | const options = { 76 | swaggerDefinition: { 77 | info: { 78 | title: 'REST API', 79 | version: '1.0.0', 80 | description: 'Example docs', 81 | }, 82 | }, 83 | apis: ['swagger.yaml'], 84 | }; 85 | 86 | const specs = swaggerJSDoc(options); 87 | this.app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs)); 88 | } 89 | 90 | private initializeErrorHandling() { 91 | this.app.use(ErrorMiddleware); 92 | } 93 | 94 | private async initBot() { 95 | const bot = Container.get(DiscordBot); 96 | await bot.sendDMToUser('844623517370810439i', 'test'); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /backend/src/config/index.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | config({ path: `.env.${process.env.NODE_ENV || 'development'}.local` }); 3 | 4 | export const CREDENTIALS = process.env.CREDENTIALS === 'true'; 5 | export const { NODE_ENV, PORT, SECRET_KEY, LOG_FORMAT, LOG_DIR, ORIGIN } = process.env; 6 | export const { DB_PASSWORD } = process.env; 7 | export const { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URL } = process.env; 8 | export const { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD } = process.env; 9 | export const { ACCESS_KEY_ID, SECRET_KEY_ID } = process.env; 10 | export const { BOT_TOKEN } = process.env; 11 | -------------------------------------------------------------------------------- /backend/src/controllers/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { Container } from 'typedi'; 3 | import { RequestWithUser } from '@interfaces/auth.interface'; 4 | import { User } from '@/interfaces/user.interface'; 5 | import { GenerateOtpRequestBody, VerifyOtpRequestBody, ForgotPasswordRequestBody, ResetPasswordRequestBody } from '@/interfaces/otp.interface'; 6 | import { AuthService } from '@services/auth.service'; 7 | 8 | export class AuthController { 9 | public auth = Container.get(AuthService); 10 | 11 | public signUp = async (req: Request, res: Response, next: NextFunction) => { 12 | try { 13 | const userData: User = req.body; 14 | const { createUserData: signUpUserData, cookie } = await this.auth.signup(userData); 15 | 16 | res.setHeader('Set-Cookie', [cookie]); 17 | res.status(201).json({ status: 'success', data: signUpUserData, message: 'signup' }); 18 | } catch (error) { 19 | next(error); 20 | } 21 | }; 22 | 23 | public logIn = async (req: Request, res: Response, next: NextFunction) => { 24 | try { 25 | const userData: User = req.body; 26 | const { cookie, findUser } = await this.auth.login(userData); 27 | 28 | res.setHeader('Set-Cookie', [cookie]); 29 | res.status(200).json({ status: 'success', message: 'login', data: findUser }); 30 | } catch (error) { 31 | next(error); 32 | } 33 | }; 34 | 35 | public logOut = async (req: RequestWithUser, res: Response, next: NextFunction) => { 36 | try { 37 | const userData: User = req.user; 38 | const logOutUserData: User = await this.auth.logout(userData); 39 | 40 | res.setHeader('Set-Cookie', ['Authorization=; Max-age=0']); 41 | res.status(200).json({ status: 'success', data: logOutUserData, message: 'logout' }); 42 | } catch (error) { 43 | next(error); 44 | } 45 | }; 46 | 47 | public googleCallback = async (req: Request, res: Response, next: NextFunction) => { 48 | try { 49 | const userData = req.user; 50 | const { token, findUser } = await this.auth.googleLogin(userData); 51 | 52 | res.status(200).send({ status: 'success', data: findUser, message: 'googleCallback', token }); 53 | } catch (error) { 54 | next(error); 55 | } 56 | }; 57 | 58 | public generateOTP = async (req: Request, res: Response, next: NextFunction) => { 59 | try { 60 | const OTPData: GenerateOtpRequestBody = req.body; 61 | await this.auth.generateOTP(OTPData); 62 | 63 | res.status(200).json({ status: 'success', message: 'OTP generated' }); 64 | } catch (error) { 65 | next(error); 66 | } 67 | }; 68 | 69 | public verifyOTP = async (req: Request, res: Response, next: NextFunction) => { 70 | try { 71 | const OTPData: VerifyOtpRequestBody = req.body; 72 | await this.auth.verifyOTP(OTPData); 73 | 74 | res.status(200).json({ status: 'success', message: 'OTP verified' }); 75 | } catch (error) { 76 | next(error); 77 | } 78 | }; 79 | 80 | public forgotPassword = async (req: Request, res: Response, next: NextFunction) => { 81 | try { 82 | const userData: ForgotPasswordRequestBody = req.body; 83 | await this.auth.forgotPassword(userData); 84 | 85 | res.status(200).json({ status: 'success', message: 'Password changed successfully' }); 86 | } catch (error) { 87 | next(error); 88 | } 89 | }; 90 | 91 | public resetPassword = async (req: Request, res: Response, next: NextFunction) => { 92 | try { 93 | const userData: ResetPasswordRequestBody = req.body; 94 | const { _id } = req.user; 95 | await this.auth.resetPassword(userData, _id); 96 | 97 | res.status(200).json({ status: 'success', message: 'Password changed' }); 98 | } catch (error) { 99 | next(error); 100 | } 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /backend/src/controllers/member.controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { Container } from 'typedi'; 3 | import { Member, UpdateMember } from '@interfaces/member.interface'; 4 | import { MemberService } from '@services/member.service'; 5 | 6 | export class MemberController { 7 | public member = Container.get(MemberService); 8 | 9 | public findAllMembers = async (req: Request, res: Response, next: NextFunction) => { 10 | try { 11 | const filter = req.query; 12 | const role = req.user.role; 13 | const org = req.user.org; 14 | const findAllMembersData: Member[] = await this.member.findAllMembers(filter, role, org); 15 | 16 | res.status(200).json({ data: findAllMembersData, message: 'findAll' }); 17 | } catch (error) { 18 | next(error); 19 | } 20 | }; 21 | 22 | public findMemberById = async (req: Request, res: Response, next: NextFunction) => { 23 | try { 24 | const memberId: string = req.params.id; 25 | const role = req.user.role; 26 | const org = req.user.org; 27 | const findOneMemberData: Member = await this.member.findMemeberById(memberId, role, org); 28 | 29 | res.status(200).json({ data: findOneMemberData, message: 'findOne' }); 30 | } catch (error) { 31 | next(error); 32 | } 33 | }; 34 | 35 | public createMember = async (req: Request, res: Response, next: NextFunction) => { 36 | try { 37 | const memberData: Member = { ...req.body }; 38 | const createMemberData: Member = await this.member.createMember(memberData, req.secretId, req.token); 39 | 40 | res.status(201).json({ data: createMemberData, message: 'created' }); 41 | } catch (error) { 42 | next(error); 43 | } 44 | }; 45 | 46 | public updateMemberStatus = async (req: Request, res: Response, next: NextFunction) => { 47 | try { 48 | const memberId: string = req.params.id; 49 | const memberData: UpdateMember = req.body; 50 | const referrerId: string = req.user._id; 51 | const updateMemberData: Member = await this.member.updateMemberStatus(memberId, referrerId, memberData); 52 | 53 | res.status(200).json({ data: updateMemberData, message: 'updated' }); 54 | } catch (error) { 55 | next(error); 56 | } 57 | }; 58 | 59 | public deleteMember = async (req: Request, res: Response, next: NextFunction) => { 60 | try { 61 | const MemberId: string = req.params.id; 62 | const deleteMemberData: Member = await this.member.deleteMember(MemberId); 63 | 64 | res.status(200).json({ data: deleteMemberData, message: 'deleted' }); 65 | } catch (error) { 66 | next(error); 67 | } 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /backend/src/controllers/secret.controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { Container } from 'typedi'; 3 | import { CreateSecret, Secret, VerifySecret } from '@interfaces/secret.interface'; 4 | import { SecretService } from '@services/secret.service'; 5 | 6 | export class SecretController { 7 | public secret = Container.get(SecretService); 8 | 9 | public getSecrets = async (req: Request, res: Response, next: NextFunction) => { 10 | try { 11 | const findAllSecretsData: Secret[] = await this.secret.findAllSecret(); 12 | 13 | res.status(200).json({ data: findAllSecretsData, message: 'findAll' }); 14 | } catch (error) { 15 | next(error); 16 | } 17 | }; 18 | 19 | public verifySecret = async (req: Request, res: Response, next: NextFunction) => { 20 | try { 21 | const { token, cfUserName }: VerifySecret = req.body; 22 | const findOneSecretData: Secret = await this.secret.findSecret({ token, cfUserName }); 23 | 24 | res.status(200).json({ data: findOneSecretData, message: 'findOne' }); 25 | } catch (error) { 26 | next(error); 27 | } 28 | }; 29 | 30 | public createSecret = async (req: Request, res: Response, next: NextFunction) => { 31 | try { 32 | const secretData: CreateSecret = req.body; 33 | const createSecretData: Secret = await this.secret.createSecret(secretData); 34 | 35 | res.status(201).json({ data: createSecretData, message: 'created' }); 36 | } catch (error) { 37 | next(error); 38 | } 39 | }; 40 | 41 | public updateSecret = async (req: Request, res: Response, next: NextFunction) => { 42 | try { 43 | const secretId: string = req.params.id; 44 | const secretData: Secret = req.body; 45 | const updateSecretData: Secret = await this.secret.updateSecret(secretId, secretData); 46 | 47 | res.status(200).json({ data: updateSecretData, message: 'updated' }); 48 | } catch (error) { 49 | next(error); 50 | } 51 | }; 52 | 53 | public deleteSecret = async (req: Request, res: Response, next: NextFunction) => { 54 | try { 55 | const SecretId: string = req.params.id; 56 | const deleteSecretData: Secret = await this.secret.deleteSecret(SecretId); 57 | 58 | res.status(200).json({ data: deleteSecretData, message: 'deleted' }); 59 | } catch (error) { 60 | next(error); 61 | } 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /backend/src/controllers/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { Container } from 'typedi'; 3 | import { UpdateUser, UpdateUserRoles, User } from '@/interfaces/user.interface'; 4 | import { UserService } from '@services/users.service'; 5 | 6 | export class UserController { 7 | public user = Container.get(UserService); 8 | 9 | public getUsers = async (req: Request, res: Response, next: NextFunction) => { 10 | try { 11 | const findAllUsersData: User[] = await this.user.findAllUser(); 12 | 13 | res.status(200).json({ data: findAllUsersData, message: 'findAll' }); 14 | } catch (error) { 15 | next(error); 16 | } 17 | }; 18 | 19 | public getUserById = async (req: Request, res: Response, next: NextFunction) => { 20 | try { 21 | const userId: string = req.params.id; 22 | const findOneUserData: User = await this.user.findUserById(userId); 23 | 24 | res.status(200).json({ data: findOneUserData, message: 'findOne' }); 25 | } catch (error) { 26 | next(error); 27 | } 28 | }; 29 | 30 | public createUser = async (req: Request, res: Response, next: NextFunction) => { 31 | try { 32 | const userData: User = req.body; 33 | const createUserData: User = await this.user.createUser(userData); 34 | 35 | res.status(201).json({ data: createUserData, message: 'created' }); 36 | } catch (error) { 37 | next(error); 38 | } 39 | }; 40 | 41 | public updateUser = async (req: Request, res: Response, next: NextFunction) => { 42 | try { 43 | const userId: string = req.user._id; 44 | const userData: UpdateUser = req.body; 45 | const updateUserData: User = await this.user.updateUser(userId, userData); 46 | 47 | res.status(200).json({ data: updateUserData, message: 'updated' }); 48 | } catch (error) { 49 | next(error); 50 | } 51 | }; 52 | 53 | public deleteUser = async (req: Request, res: Response, next: NextFunction) => { 54 | try { 55 | const userId: string = req.params.id; 56 | const deleteUserData: User = await this.user.deleteUser(userId); 57 | 58 | res.status(200).json({ data: deleteUserData, message: 'deleted' }); 59 | } catch (error) { 60 | next(error); 61 | } 62 | }; 63 | 64 | public verifyModerator = async (req: Request, res: Response, next: NextFunction) => { 65 | try { 66 | const userId: string = req.params.id; 67 | const adminId: string = req.user._id; 68 | const updateUserData: User = await this.user.verifyModerator(userId, adminId); 69 | 70 | res.status(200).json({ data: updateUserData, message: 'updated' }); 71 | } catch (error) { 72 | next(error); 73 | } 74 | }; 75 | 76 | public verifyReferrer = async (req: Request, res: Response, next: NextFunction) => { 77 | try { 78 | const userId: string = req.params.id; 79 | const adminId: string = req.user._id; 80 | const updateUserData: User = await this.user.verifyReferrer(userId, adminId); 81 | 82 | res.status(200).json({ data: updateUserData, message: 'updated' }); 83 | } catch (error) { 84 | next(error); 85 | } 86 | }; 87 | 88 | public getReferrers = async (req: Request, res: Response, next: NextFunction) => { 89 | try { 90 | const filter = req.query; 91 | const findAllReferrersData: User[] = await this.user.getReferrers(filter); 92 | 93 | res.status(200).json({ data: findAllReferrersData, message: 'findAll' }); 94 | } catch (error) { 95 | next(error); 96 | } 97 | }; 98 | 99 | public getModerators = async (req: Request, res: Response, next: NextFunction) => { 100 | try { 101 | const filter = req.query; 102 | const findAllModeratorsData: User[] = await this.user.getModerators(filter); 103 | 104 | res.status(200).json({ data: findAllModeratorsData, message: 'findAll' }); 105 | } catch (error) { 106 | next(error); 107 | } 108 | }; 109 | 110 | public getModeratorById = async (req: Request, res: Response, next: NextFunction) => { 111 | try { 112 | const moderatorId: string = req.params.id; 113 | const findOneModeratorData: User = await this.user.getModeratorById(moderatorId); 114 | 115 | res.status(200).json({ data: findOneModeratorData, message: 'findOne' }); 116 | } catch (error) { 117 | next(error); 118 | } 119 | }; 120 | 121 | public getReferrerById = async (req: Request, res: Response, next: NextFunction) => { 122 | try { 123 | const referrerId: string = req.params.id; 124 | const findOneReferrerData: User = await this.user.getReferrerById(referrerId); 125 | 126 | res.status(200).json({ data: findOneReferrerData, message: 'findOne' }); 127 | } catch (error) { 128 | next(error); 129 | } 130 | }; 131 | 132 | public updateRoles = async (req: Request, res: Response, next: NextFunction) => { 133 | try { 134 | const userId: string = req.params.id; 135 | const userData: UpdateUserRoles = req.body; 136 | const updateUserData: User = await this.user.updateRoles(userId, userData); 137 | 138 | res.status(200).json({ data: updateUserData, message: 'updated' }); 139 | } catch (error) { 140 | next(error); 141 | } 142 | }; 143 | } 144 | -------------------------------------------------------------------------------- /backend/src/database/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, set } from 'mongoose'; 2 | import { NODE_ENV, DB_PASSWORD } from '@config'; 3 | 4 | export const dbConnection = async () => { 5 | const dbConfig = { 6 | url: `mongodb+srv://shubhamjr:${DB_PASSWORD}@cluster0.72jmhwi.mongodb.net/?retryWrites=true&w=majority`, 7 | }; 8 | // const dbConfig = { 9 | // url: `mongodb://localhost:27017/acd`, 10 | // }; 11 | 12 | if (NODE_ENV !== 'production') { 13 | set('debug', true); 14 | } 15 | 16 | await connect(dbConfig.url); 17 | }; 18 | -------------------------------------------------------------------------------- /backend/src/dtos/member.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString, IsNotEmpty, IsOptional, IsNumber, IsPositive, Matches, IsEnum, Min, Max, IsLowercase } from 'class-validator'; 2 | 3 | export class CreateMemberDto { 4 | @IsString() 5 | @IsNotEmpty() 6 | public cfUserName: string; 7 | 8 | @IsEmail() 9 | public email: string; 10 | 11 | @IsString() 12 | @IsNotEmpty() 13 | public jobId: number; 14 | 15 | @IsString() 16 | @IsNotEmpty() 17 | public name: string; 18 | 19 | @IsString() 20 | @IsNotEmpty() 21 | public message: string; 22 | 23 | @IsString() 24 | @IsNotEmpty() 25 | @Matches(/^https:\/\/drive\.google\.com\//) 26 | public resume: string; 27 | 28 | @IsString() 29 | @IsNotEmpty() 30 | @IsLowercase() 31 | public org: string; 32 | 33 | @IsOptional() 34 | @IsString() 35 | public phoneNumber?: string; 36 | 37 | @IsNumber() 38 | @IsPositive() 39 | @Min(0, { message: 'CGPA must be at least 1' }) 40 | @Max(20, { message: 'CGPA cannot exceed 10' }) 41 | public cgpa: number; 42 | 43 | @IsNumber() 44 | @IsPositive() 45 | @Min(0, { message: 'Years of experience must be at least 0' }) 46 | @Max(20, { message: 'Years of experience cannot exceed 20' }) 47 | public yoe: number; 48 | 49 | @IsString() 50 | @IsNotEmpty() 51 | public token: string; 52 | } 53 | 54 | export class UpdateMemberStatusDto { 55 | @IsString() 56 | @IsNotEmpty() 57 | @IsEnum(['accepted', 'rejected']) 58 | public status: string; 59 | 60 | @IsString() 61 | @IsNotEmpty() 62 | public referrerResponse: string; 63 | } 64 | -------------------------------------------------------------------------------- /backend/src/dtos/otp.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString, IsNotEmpty, IsEnum, IsNumber } from 'class-validator'; 2 | 3 | export class GenerateOTPDto { 4 | @IsEmail() 5 | @IsNotEmpty() 6 | public email: string; 7 | 8 | @IsString() 9 | @IsNotEmpty() 10 | @IsEnum(['signup', 'reset-password']) 11 | public reason: string; 12 | } 13 | 14 | export class VerifyOTPDto { 15 | @IsEmail() 16 | @IsNotEmpty() 17 | public email: string; 18 | 19 | @IsNumber() 20 | @IsNotEmpty() 21 | public otp: number; 22 | } 23 | 24 | export class ForgotPasswordDto { 25 | @IsEmail() 26 | @IsNotEmpty() 27 | public email: string; 28 | 29 | @IsNumber() 30 | @IsNotEmpty() 31 | public otp: number; 32 | 33 | @IsString() 34 | @IsNotEmpty() 35 | public password: string; 36 | } 37 | 38 | export class ResetPassword { 39 | @IsString() 40 | @IsNotEmpty() 41 | public oldPassword: string; 42 | 43 | @IsString() 44 | @IsNotEmpty() 45 | public newPassword: string; 46 | } 47 | -------------------------------------------------------------------------------- /backend/src/dtos/secret.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNotEmpty, MinLength, MaxLength } from 'class-validator'; 2 | 3 | export class CreateSecretDto { 4 | @IsString() 5 | @IsNotEmpty() 6 | @MinLength(1) 7 | @MaxLength(20) 8 | public cfUserName: string; 9 | 10 | @IsString() 11 | @IsNotEmpty() 12 | @MinLength(1) 13 | @MaxLength(256) 14 | public token: string; 15 | 16 | @IsString() 17 | @IsNotEmpty() 18 | public discordId: string; 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/dtos/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString, IsNotEmpty, MinLength, MaxLength, IsNumber, IsEnum, IsArray, IsOptional } from 'class-validator'; 2 | 3 | enum UpdateUserRoles { 4 | Moderator = 'moderator', 5 | Referrer = 'referrer', 6 | } 7 | export class SignupUserDto { 8 | @IsEmail() 9 | public email: string; 10 | 11 | @IsString() 12 | @IsNotEmpty() 13 | @MinLength(9) 14 | @MaxLength(32) 15 | public password: string; 16 | 17 | @IsString() 18 | @IsNotEmpty() 19 | public firstName: string; 20 | 21 | @IsString() 22 | @IsNotEmpty() 23 | public lastName: string; 24 | 25 | @IsString() 26 | @IsNotEmpty() 27 | public username: string; 28 | 29 | @IsNumber() 30 | @IsNotEmpty() 31 | public otp: number; 32 | 33 | @IsString() 34 | @IsNotEmpty() 35 | public org: string; 36 | } 37 | 38 | export class LoginUserDto { 39 | @IsEmail() 40 | public email: string; 41 | 42 | @IsString() 43 | @IsNotEmpty() 44 | public password: string; 45 | } 46 | 47 | export class UpdateUserDto { 48 | @IsOptional() 49 | @IsString() 50 | @IsNotEmpty() 51 | public firstName: string; 52 | 53 | @IsOptional() 54 | @IsString() 55 | @IsNotEmpty() 56 | public lastName: string; 57 | 58 | @IsOptional() 59 | @IsString() 60 | @IsNotEmpty() 61 | public org: string; 62 | } 63 | 64 | export class UpdateUserRolesDto { 65 | @IsArray() 66 | @IsNotEmpty() 67 | @IsEnum(UpdateUserRoles, { each: true }) 68 | public role: string; 69 | } 70 | -------------------------------------------------------------------------------- /backend/src/exceptions/HttpException.ts: -------------------------------------------------------------------------------- 1 | export class HttpException extends Error { 2 | public status: number; 3 | public message: string; 4 | public stack?: string; 5 | 6 | constructor(status: number, message: string, stack?: string) { 7 | super(message); 8 | this.status = status; 9 | this.message = message; 10 | if (stack) { 11 | this.stack = stack; 12 | } else { 13 | Error.captureStackTrace(this, this.constructor); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/http/auth.http: -------------------------------------------------------------------------------- 1 | # baseURL 2 | @baseURL = http://localhost:3000 3 | 4 | ### 5 | # User Signup 6 | POST {{ baseURL }}/signup 7 | Content-Type: application/json 8 | 9 | { 10 | "email": "example@email.com", 11 | "password": "password" 12 | } 13 | 14 | ### 15 | # User Login 16 | POST {{ baseURL }}/login 17 | Content-Type: application/json 18 | 19 | { 20 | "email": "example@email.com", 21 | "password": "password" 22 | } 23 | 24 | ### 25 | # User Logout 26 | POST {{ baseURL }}/logout 27 | Content-Type: application/json 28 | -------------------------------------------------------------------------------- /backend/src/http/users.http: -------------------------------------------------------------------------------- 1 | # baseURL 2 | @baseURL = http://localhost:3000 3 | 4 | ### 5 | # Find All Users 6 | GET {{ baseURL }}/users 7 | 8 | ### 9 | # Find User By Id 10 | GET {{ baseURL }}/users/1 11 | 12 | ### 13 | # Create User 14 | POST {{ baseURL }}/users 15 | Content-Type: application/json 16 | 17 | { 18 | "email": "example@email.com", 19 | "password": "password" 20 | } 21 | 22 | ### 23 | # Modify User By Id 24 | PUT {{ baseURL }}/users/1 25 | Content-Type: application/json 26 | 27 | { 28 | "email": "example@email.com", 29 | "password": "password" 30 | } 31 | 32 | ### 33 | # Delete User By Id 34 | DELETE {{ baseURL }}/users/1 35 | -------------------------------------------------------------------------------- /backend/src/interfaces/activity.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Activity { 2 | _id?: string; 3 | email: string; 4 | cfUsername: string; 5 | status: string; 6 | referrerResponse: string; 7 | jobId: string; 8 | responseDate?: Date; 9 | applicationDate: Date; 10 | createdAt?: Date; 11 | updatedAt?: Date; 12 | activities?: string[]; 13 | } 14 | 15 | export interface CreateActivity { 16 | email: string; 17 | cfUserName: string; 18 | status: string; 19 | referrerResponse: string; 20 | jobId: string; 21 | referrerId?: string; 22 | applicationDate: Date; 23 | message: string; 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/interfaces/auth.interface.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { User } from '@/interfaces/user.interface'; 3 | 4 | export interface DataStoredInToken { 5 | _id: string; 6 | } 7 | 8 | export interface TokenData { 9 | token: string; 10 | expiresIn: number; 11 | } 12 | 13 | export interface RequestWithUser extends Request { 14 | user: User; 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/interfaces/member.interface.ts: -------------------------------------------------------------------------------- 1 | import { Secret } from './secret.interface'; 2 | 3 | export interface Member { 4 | _id?: string; 5 | name: string; 6 | email: string; 7 | phoneNumber?: string; 8 | org: string; 9 | message: string; 10 | resume: string; 11 | jobId: string; 12 | cfUserName: string; 13 | cgpa: number; 14 | yoe: number; 15 | createdAt?: Date; 16 | updatedAt?: Date; 17 | status?: string; 18 | referrerResponse?: string; 19 | secretId?: Secret | string; 20 | } 21 | 22 | export interface UpdateMember { 23 | status: string; 24 | referrerResponse: string; 25 | referrerId: string; 26 | } 27 | 28 | export interface MemberFilter { 29 | status?: string; 30 | org?: string; 31 | sort?: -1 | 1; 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/interfaces/otp.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Otp { 2 | _id?: string; 3 | email: string; 4 | otp: number; 5 | reason: string; 6 | expiresIn: number; 7 | attempts: number; 8 | } 9 | 10 | export interface GenerateOtpRequestBody { 11 | email: string; 12 | reason: string; 13 | } 14 | 15 | export interface VerifyOtpRequestBody { 16 | email: string; 17 | otp: number; 18 | } 19 | 20 | export interface ForgotPasswordRequestBody { 21 | email: string; 22 | otp: number; 23 | password: string; 24 | } 25 | 26 | export interface ResetPasswordRequestBody { 27 | oldPassword: string; 28 | newPassword: string; 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/interfaces/route.interface.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | export interface Routes { 4 | path?: string; 5 | router: Router; 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/interfaces/secret.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Secret { 2 | _id?: string; 3 | token: string; 4 | cfUserName: string; 5 | discordId: string; 6 | date: Map; 7 | updatedAt?: Date; 8 | tokenIssuedAt?: Date; 9 | } 10 | 11 | export interface CreateSecret { 12 | cfUserName: string; 13 | token: string; 14 | discordId: string; 15 | } 16 | 17 | export interface VerifySecret { 18 | token: string; 19 | cfUserName: string; 20 | } 21 | 22 | export interface GetSecret { 23 | token: string; 24 | } 25 | 26 | export interface VerifyAccessKeyAndSecretKey { 27 | access_key: string | undefined; 28 | secret_key: string | undefined; 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/interfaces/template.interface.ts: -------------------------------------------------------------------------------- 1 | export interface renderTemplateData { 2 | otp?: number; 3 | expiresIn?: number; 4 | to: string; 5 | template: string; 6 | username?: string; 7 | subject: string; 8 | reason?: string; 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/interfaces/user.interface.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | _id?: string; 3 | email: string; 4 | password?: string; 5 | role: [string]; 6 | googleId?: string; 7 | otp?: number; 8 | username: string; 9 | isVerified: boolean; 10 | verifiedBy?: string; 11 | totalReferred?: number; 12 | activities?: [string]; 13 | org?: string; 14 | } 15 | 16 | export interface GoogleUser { 17 | _id?: string; 18 | googleId: string; 19 | email: string; 20 | name: string; 21 | picture: string; 22 | accessToken: string; 23 | refreshToken: string; 24 | } 25 | 26 | export interface UserFilter { 27 | isVerified?: boolean; 28 | totalReferred?: -1 | 1; 29 | org?: string; 30 | sort?: -1 | 1; 31 | } 32 | 33 | export interface UpdateUserRoles { 34 | role: 'referrer' | 'moderator'; 35 | } 36 | 37 | export interface UpdateUser { 38 | firstName: string; 39 | lastName: string; 40 | org: string; 41 | } 42 | -------------------------------------------------------------------------------- /backend/src/middlewares/auth.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Response, Request } from 'express'; 2 | import { verify } from 'jsonwebtoken'; 3 | import { SECRET_KEY, ACCESS_KEY_ID, SECRET_KEY_ID } from '@config'; 4 | import { HttpException } from '@exceptions/HttpException'; 5 | import { DataStoredInToken, RequestWithUser } from '@interfaces/auth.interface'; 6 | import { CreateSecret } from '@/interfaces/secret.interface'; 7 | import { UserModel } from '@/models/user.model'; 8 | import { SecretModel } from '@/models/secret.model'; 9 | import { Constants } from '@utils/constants'; 10 | import { Helper } from '@utils/helper'; 11 | import { Container } from 'typedi'; 12 | 13 | const helper = Container.get(Helper); 14 | 15 | export const AuthMiddleware = async (req: RequestWithUser, res: Response, next: NextFunction) => { 16 | try { 17 | const Authorization = helper.getAuthorization(req); 18 | 19 | if (Authorization) { 20 | const { _id } = (await verify(Authorization, SECRET_KEY)) as DataStoredInToken; 21 | const findUser = await UserModel.findById(_id); 22 | 23 | if (findUser) { 24 | req.user = findUser; 25 | next(); 26 | } else { 27 | next(new HttpException(401, 'Wrong authentication token')); 28 | } 29 | } else { 30 | next(new HttpException(404, 'Authentication token missing')); 31 | } 32 | } catch (error) { 33 | next(new HttpException(401, 'Wrong authentication token')); 34 | } 35 | }; 36 | 37 | export const VerifySecretKey = async (req: Request, res: Response, next: NextFunction) => { 38 | const { access_key, secret_key } = helper.getAccessAndSecretKey(req); 39 | if (access_key && secret_key && access_key === ACCESS_KEY_ID && secret_key === SECRET_KEY_ID) { 40 | next(); 41 | } else { 42 | next(new HttpException(404, 'Wrong access key or secret key')); 43 | } 44 | }; 45 | 46 | export const VerifyToken = async (req: Request, res: Response, next: NextFunction): Promise => { 47 | try { 48 | const { token, cfUserName }: CreateSecret = req.body; 49 | const findSecret = await SecretModel.findOne({ token, cfUserName }); 50 | if (!findSecret) throw new HttpException(409, 'Wrong token or cf-username'); 51 | const tokenAge = helper.calculateTimeDifference(findSecret.tokenIssuedAt); 52 | 53 | if (!findSecret || tokenAge > Constants.oneDay) throw new HttpException(409, 'Token is incorrect or expired'); 54 | req.token = token; 55 | req.secretId = findSecret._id; 56 | next(); 57 | } catch (error) { 58 | next(error); 59 | } 60 | }; 61 | 62 | export const RestrictTo = (...roles: string[]) => { 63 | return (req: RequestWithUser, res: Response, next: NextFunction) => { 64 | if (!req.user.isVerified) throw new HttpException(409, 'Only verified users can access this route'); 65 | if (!roles.some(role => req.user.role.includes(role))) { 66 | return next(new HttpException(403, 'You do not have permission to perform this action')); 67 | } 68 | next(); 69 | }; 70 | }; 71 | 72 | export const RestrictedOnlyTo = (...roles: string[]) => { 73 | return (req: RequestWithUser, res: Response, next: NextFunction) => { 74 | if (req.user.role.includes('admin')) return next(); 75 | if (!helper.areArraysEqual(roles, req.user.role)) throw new HttpException(409, 'You do not have permission to perform this action'); 76 | next(); 77 | }; 78 | }; 79 | -------------------------------------------------------------------------------- /backend/src/middlewares/error.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { HttpException } from '@exceptions/HttpException'; 3 | import { NODE_ENV } from '@config'; 4 | import { logger } from '@utils/logger'; 5 | 6 | export const ErrorMiddleware = (error: HttpException, req: Request, res: Response, next: NextFunction) => { 7 | try { 8 | const status: number = error.status || 500; 9 | const message: string = error.message || 'Something went wrong'; 10 | const stack: string | undefined = error.stack; 11 | 12 | const logMessage = `[${req.method}] ${req.path} >> StatusCode:: ${status}, Message:: ${message}${stack ? `, Stack:: ${stack}` : ''}`; 13 | logger.error(logMessage); 14 | 15 | // NODE_ENV = development, then show console.error 16 | /* eslint-disable no-console */ 17 | console.error(logMessage); 18 | 19 | const response = { 20 | status: 'fail', 21 | message, 22 | ...(stack && NODE_ENV === 'development' && { stack }), // Add stack to the response only if it's present and environment is development 23 | }; 24 | 25 | res.status(status).json(response); 26 | } catch (error) { 27 | next(error); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /backend/src/middlewares/validation.middleware.ts: -------------------------------------------------------------------------------- 1 | import { plainToInstance } from 'class-transformer'; 2 | import { validateOrReject, ValidationError } from 'class-validator'; 3 | import { NextFunction, Request, Response } from 'express'; 4 | import { HttpException } from '@exceptions/HttpException'; 5 | 6 | /** 7 | * @name ValidationMiddleware 8 | * @description Allows use of decorator and non-decorator based validation 9 | * @param type dto 10 | * @param skipMissingProperties When skipping missing properties 11 | * @param whitelist Even if your object is an instance of a validation class it can contain additional properties that are not defined 12 | * @param forbidNonWhitelisted If you would rather to have an error thrown when any non-whitelisted properties are present 13 | */ 14 | export const ValidationMiddleware = (type: any, skipMissingProperties = false, whitelist = true, forbidNonWhitelisted = true) => { 15 | return (req: Request, res: Response, next: NextFunction) => { 16 | const dto = plainToInstance(type, req.body); 17 | validateOrReject(dto, { skipMissingProperties, whitelist, forbidNonWhitelisted }) 18 | .then(() => { 19 | req.body = dto; 20 | next(); 21 | }) 22 | .catch((errors: ValidationError[]) => { 23 | const message = errors.map((error: ValidationError) => Object.values(error.constraints)).join(', '); 24 | next(new HttpException(400, message)); 25 | }); 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /backend/src/models/activity.model.ts: -------------------------------------------------------------------------------- 1 | import { model, Schema, Document } from 'mongoose'; 2 | import { Activity } from '@/interfaces/activity.interface'; 3 | 4 | const ActivitySchema: Schema = new Schema( 5 | { 6 | email: { 7 | type: String, 8 | required: true, 9 | }, 10 | cfUserName: { 11 | type: String, 12 | required: true, 13 | }, 14 | status: { 15 | type: String, 16 | enum: ['pending', 'accepted', 'rejected'], 17 | default: 'pending', 18 | }, 19 | referrerResponse: { 20 | type: String, 21 | }, 22 | jobId: { 23 | type: String, 24 | required: true, 25 | }, 26 | responseDate: { 27 | type: Date, 28 | default: Date.now, 29 | }, 30 | applicationDate: { 31 | type: Date, 32 | required: true, 33 | }, 34 | referrerId: { 35 | type: Schema.Types.ObjectId, 36 | ref: 'User', 37 | }, 38 | message: { 39 | type: String, 40 | required: true, 41 | }, 42 | }, 43 | { timestamps: true }, 44 | ); 45 | 46 | export const ActivityModel = model('Activity', ActivitySchema); 47 | -------------------------------------------------------------------------------- /backend/src/models/member.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, Document } from 'mongoose'; 2 | import { Member } from '@interfaces/member.interface'; 3 | const { ObjectId } = Schema.Types; 4 | 5 | const memberSchema: Schema = new Schema( 6 | { 7 | email: { 8 | type: String, 9 | required: true, 10 | lowercase: true, 11 | }, 12 | cfUserName: { 13 | type: String, 14 | required: true, 15 | unique: true, 16 | }, 17 | jobId: { 18 | type: String, 19 | required: true, 20 | }, 21 | name: { 22 | type: String, 23 | required: true, 24 | }, 25 | message: { 26 | type: String, 27 | required: true, 28 | }, 29 | resume: { 30 | type: String, 31 | required: true, 32 | match: /^https:\/\/drive\.google\.com\//, // Match Google Drive link prefix 33 | }, 34 | org: { 35 | type: String, 36 | required: true, 37 | lowercase: true, 38 | }, 39 | phoneNumber: { 40 | type: String, 41 | }, 42 | cgpa: { 43 | type: Number, 44 | required: true, 45 | }, 46 | yoe: { 47 | type: Number, 48 | required: true, 49 | }, 50 | status: { 51 | type: String, 52 | enum: ['pending', 'accepted', 'rejected'], 53 | default: 'pending', 54 | lowercase: true, 55 | }, 56 | referrerResponse: { 57 | type: String, 58 | }, 59 | activities: { 60 | type: [ObjectId], 61 | ref: 'Activity', 62 | default: [], 63 | }, 64 | secretId: { 65 | type: ObjectId, 66 | ref: 'Secret', 67 | }, 68 | }, 69 | { 70 | timestamps: true, 71 | }, 72 | ); 73 | 74 | export const MemberModel = model('Member', memberSchema); 75 | -------------------------------------------------------------------------------- /backend/src/models/otp.model.ts: -------------------------------------------------------------------------------- 1 | import { model, Schema, Document } from 'mongoose'; 2 | import { Otp } from '@interfaces/otp.interface'; 3 | 4 | const OtpSchema: Schema = new Schema( 5 | { 6 | email: { 7 | type: String, 8 | required: true, 9 | }, 10 | otp: { 11 | type: Number, 12 | required: true, 13 | }, 14 | reason: { 15 | type: String, 16 | required: true, 17 | }, 18 | expiresIn: { 19 | type: String, 20 | required: true, 21 | }, 22 | attempts: { 23 | type: Number, 24 | required: true, 25 | default: 0, 26 | }, 27 | }, 28 | { timestamps: true }, 29 | ); 30 | 31 | export const OtpModel = model('Otp', OtpSchema); 32 | -------------------------------------------------------------------------------- /backend/src/models/secret.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, Document } from 'mongoose'; 2 | import { Secret } from '@interfaces/secret.interface'; 3 | 4 | const secretSchema: Schema = new Schema( 5 | { 6 | token: { 7 | type: String, 8 | required: true, 9 | }, 10 | cfUserName: { 11 | type: String, 12 | required: true, 13 | unique: true, 14 | }, 15 | date: { 16 | type: Map, 17 | of: Number, 18 | required: true, 19 | }, 20 | tokenIssuedAt: { 21 | type: Date, 22 | default: Date.now, 23 | }, 24 | discordId: { 25 | type: String, 26 | required: true, 27 | }, 28 | }, 29 | { 30 | timestamps: true, 31 | }, 32 | ); 33 | 34 | secretSchema.index({ cfUserName: 1, token: 1 }, { unique: true }); 35 | 36 | export const SecretModel = model('Secret', secretSchema); 37 | -------------------------------------------------------------------------------- /backend/src/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { model, Schema, Document } from 'mongoose'; 2 | import { User } from '@/interfaces/user.interface'; 3 | 4 | const { ObjectId } = mongoose.Schema.Types; 5 | 6 | const UserSchema: Schema = new Schema( 7 | { 8 | firstName: { 9 | type: String, 10 | required: true, 11 | }, 12 | lastName: { 13 | type: String, 14 | required: true, 15 | }, 16 | username: { 17 | type: String, 18 | required: true, 19 | unique: true, 20 | }, 21 | email: { 22 | type: String, 23 | required: true, 24 | unique: true, 25 | }, 26 | password: { 27 | type: String, 28 | private: true, 29 | select: false, 30 | }, 31 | googleId: { 32 | type: String, 33 | }, 34 | role: { 35 | type: [String], 36 | enum: ['referrer', 'moderator', 'admin'], 37 | default: 'referrer', 38 | }, 39 | org: { 40 | type: String, 41 | default: 'none', 42 | }, 43 | isVerified: { 44 | type: Boolean, 45 | default: false, 46 | }, 47 | verifiedBy: { 48 | type: ObjectId, 49 | ref: 'User', 50 | }, 51 | totalReferred: { 52 | type: Number, 53 | default: 0, 54 | }, 55 | activities: { 56 | type: [ObjectId], 57 | ref: 'Activity', 58 | default: [], 59 | }, 60 | }, 61 | { timestamps: true }, 62 | ); 63 | 64 | export const UserModel = model('User', UserSchema); 65 | -------------------------------------------------------------------------------- /backend/src/routes/auth.route.ts: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import { Router } from 'express'; 3 | import { AuthController } from '@controllers/auth.controller'; 4 | import { LoginUserDto, SignupUserDto } from '@/dtos/user.dto'; 5 | import { GenerateOTPDto, VerifyOTPDto, ForgotPasswordDto, ResetPassword } from '@dtos/otp.dto'; 6 | import { Routes } from '@/interfaces/route.interface'; 7 | import { AuthMiddleware } from '@middlewares/auth.middleware'; 8 | import { ValidationMiddleware } from '@middlewares/validation.middleware'; 9 | 10 | export class AuthRoute implements Routes { 11 | public path = '/auth/'; 12 | public router = Router(); 13 | public auth = new AuthController(); 14 | 15 | constructor() { 16 | this.initializeRoutes(); 17 | } 18 | 19 | private initializeRoutes() { 20 | this.router.post(`${this.path}signup`, ValidationMiddleware(SignupUserDto), this.auth.signUp); 21 | this.router.post(`${this.path}login`, ValidationMiddleware(LoginUserDto), this.auth.logIn); 22 | this.router.post(`${this.path}logout`, AuthMiddleware, this.auth.logOut); 23 | this.router.post(`${this.path}otp`, ValidationMiddleware(GenerateOTPDto), this.auth.generateOTP); 24 | this.router.post(`${this.path}otp/verify`, ValidationMiddleware(VerifyOTPDto), this.auth.verifyOTP); 25 | this.router.post(`${this.path}forgot/password`, ValidationMiddleware(ForgotPasswordDto), this.auth.forgotPassword); 26 | this.router.post(`${this.path}reset/password`, AuthMiddleware, ValidationMiddleware(ResetPassword), this.auth.resetPassword); 27 | this.router.get(`${this.path}google`, passport.authenticate('google', { scope: ['profile', 'email'] })); 28 | this.router.get( 29 | `${this.path}google/callback`, 30 | passport.authenticate('google', { failureRedirect: '/login', session: false }), 31 | this.auth.googleCallback, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/src/routes/member.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { MemberController } from '@controllers/member.controller'; 3 | import { CreateMemberDto, UpdateMemberStatusDto } from '@dtos/member.dto'; 4 | import { Routes } from '@interfaces/route.interface'; 5 | import { ValidationMiddleware } from '@middlewares/validation.middleware'; 6 | import { VerifyToken } from '@middlewares/auth.middleware'; 7 | import { AuthMiddleware, RestrictTo, RestrictedOnlyTo } from '@middlewares/auth.middleware'; 8 | 9 | export class MemberRoute implements Routes { 10 | public path = '/member'; 11 | public router = Router(); 12 | public member = new MemberController(); 13 | 14 | constructor() { 15 | this.initializeRoutes(); 16 | } 17 | 18 | private initializeRoutes() { 19 | this.router.get(`${this.path}/`, AuthMiddleware, RestrictTo('admin', 'moderator', 'referrer'), this.member.findAllMembers); 20 | this.router.get(`${this.path}/:id`, AuthMiddleware, RestrictTo('admin', 'moderator', 'referrer'), this.member.findMemberById); 21 | this.router.post(`${this.path}`, ValidationMiddleware(CreateMemberDto), VerifyToken, this.member.createMember); 22 | this.router.patch( 23 | `${this.path}/status/:id`, 24 | AuthMiddleware, 25 | ValidationMiddleware(UpdateMemberStatusDto), 26 | RestrictedOnlyTo('referrer'), 27 | this.member.updateMemberStatus, 28 | ); 29 | this.router.delete(`${this.path}/:id`, this.member.deleteMember); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/src/routes/secret.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { SecretController } from '@controllers/secret.controller'; 3 | import { CreateSecretDto } from '@dtos/secret.dto'; 4 | import { Routes } from '@interfaces/route.interface'; 5 | import { ValidationMiddleware } from '@middlewares/validation.middleware'; 6 | import { VerifySecretKey } from '@middlewares/auth.middleware'; 7 | 8 | export class SecretRoute implements Routes { 9 | public path = '/secret'; 10 | public router = Router(); 11 | public secret = new SecretController(); 12 | 13 | constructor() { 14 | this.initializeRoutes(); 15 | } 16 | 17 | private initializeRoutes() { 18 | this.router.post(`${this.path}`, ValidationMiddleware(CreateSecretDto), VerifySecretKey, this.secret.createSecret); 19 | this.router.post(`${this.path}/verify`, ValidationMiddleware(CreateSecretDto), this.secret.verifySecret); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/routes/users.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { UserController } from '@/controllers/user.controller'; 3 | import { UpdateUserDto, UpdateUserRolesDto } from '@/dtos/user.dto'; 4 | import { Routes } from '@/interfaces/route.interface'; 5 | import { ValidationMiddleware } from '@middlewares/validation.middleware'; 6 | import { AuthMiddleware, RestrictTo } from '@middlewares/auth.middleware'; 7 | 8 | export class UserRoute implements Routes { 9 | public path = '/users'; 10 | public router = Router(); 11 | public user = new UserController(); 12 | 13 | constructor() { 14 | this.initializeRoutes(); 15 | } 16 | 17 | private initializeRoutes() { 18 | this.router.get(`${this.path}/referrers`, AuthMiddleware, RestrictTo('admin', 'moderator'), this.user.getReferrers); 19 | this.router.get(`${this.path}/moderators`, AuthMiddleware, RestrictTo('admin'), this.user.getModerators); 20 | this.router.get(`${this.path}/referrers/:id`, AuthMiddleware, RestrictTo('admin', 'moderator'), this.user.getReferrerById); 21 | this.router.get(`${this.path}/moderators/:id`, AuthMiddleware, RestrictTo('admin'), this.user.getModeratorById); 22 | this.router.patch(`${this.path}/verify/moderator/:id`, AuthMiddleware, RestrictTo('admin'), this.user.verifyModerator); 23 | this.router.patch(`${this.path}/verify/referrer/:id`, AuthMiddleware, RestrictTo('admin', 'moderator'), this.user.verifyReferrer); 24 | this.router.patch(`${this.path}/roles/:id`, ValidationMiddleware(UpdateUserRolesDto), AuthMiddleware, RestrictTo('admin'), this.user.updateRoles); 25 | this.router.patch( 26 | `${this.path}`, 27 | ValidationMiddleware(UpdateUserDto), 28 | AuthMiddleware, 29 | RestrictTo('admin', 'moderator', 'referrer'), 30 | this.user.updateUser, 31 | ); 32 | this.router.delete(`${this.path}/:id`, this.user.deleteUser); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/src/server.ts: -------------------------------------------------------------------------------- 1 | import { App } from '@/app'; 2 | 3 | // plop_append_import 4 | import { MemberRoute } from '@routes/member.route'; 5 | import { SecretRoute } from '@routes/secret.route'; 6 | import { AuthRoute } from '@routes/auth.route'; 7 | import { UserRoute } from '@routes/users.route'; 8 | 9 | import { ValidateEnv } from '@utils/validateEnv'; 10 | 11 | ValidateEnv(); 12 | 13 | const app = new App([new UserRoute(), new AuthRoute(), new SecretRoute(), new MemberRoute()]); 14 | 15 | app.listen(); 16 | -------------------------------------------------------------------------------- /backend/src/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { hash, compare } from 'bcrypt'; 2 | import { Container } from 'typedi'; 3 | import { sign } from 'jsonwebtoken'; 4 | import { Service } from 'typedi'; 5 | import { SECRET_KEY } from '@config'; 6 | import { HttpException } from '@exceptions/HttpException'; 7 | import { DataStoredInToken, TokenData } from '@interfaces/auth.interface'; 8 | import { User } from '@/interfaces/user.interface'; 9 | import { UserModel } from '@/models/user.model'; 10 | import { OtpModel } from '@/models/otp.model'; 11 | import { EmailSender } from '@utils/mail'; 12 | import { Mail } from '@utils/constants'; 13 | import { GenerateOtpRequestBody, Otp, VerifyOtpRequestBody, ForgotPasswordRequestBody, ResetPasswordRequestBody } from '@/interfaces/otp.interface'; 14 | 15 | const createToken = (user: User): TokenData => { 16 | const dataStoredInToken: DataStoredInToken = { _id: user._id }; 17 | const expiresIn: number = 60 * 60 * 24; 18 | 19 | return { expiresIn, token: sign(dataStoredInToken, SECRET_KEY, { expiresIn }) }; 20 | }; 21 | 22 | const createCookie = (tokenData: TokenData): string => { 23 | return `Authorization=${tokenData.token}; HttpOnly; Max-Age=${tokenData.expiresIn};`; 24 | }; 25 | 26 | @Service() 27 | export class AuthService { 28 | private emailSender = Container.get(EmailSender); 29 | 30 | public async signup(userData: User): Promise<{ createUserData: User; cookie: string }> { 31 | const findUser: User = await UserModel.findOne({ email: userData.email }); 32 | 33 | if (findUser) throw new HttpException(409, `This email ${userData.email} already exists`); 34 | await this.verifyOTP({ email: userData.email, otp: userData.otp }); 35 | 36 | const hashedPassword = await hash(userData.password, 10); 37 | const createUserData: User = await UserModel.create({ ...userData, password: hashedPassword }); 38 | 39 | createUserData.password = undefined; 40 | 41 | const tokenData = createToken(createUserData); 42 | const cookie = createCookie(tokenData); 43 | 44 | await this.emailSender.sendMailWrapper({ 45 | to: createUserData.email, 46 | template: 'welcome', 47 | username: createUserData.username, 48 | subject: Mail.WelcomeSubject, 49 | }); 50 | 51 | return { createUserData, cookie }; 52 | } 53 | 54 | public async login(userData: User): Promise<{ cookie: string; findUser: User }> { 55 | const findUser: User = await UserModel.findOne({ email: userData.email }).select('+password'); 56 | 57 | if (!findUser || !findUser.password) throw new HttpException(409, `Incorrect email or password`); 58 | 59 | const isPasswordMatching: boolean = await compare(userData.password, findUser.password); 60 | if (!isPasswordMatching) throw new HttpException(409, 'Incorrect email or password'); 61 | 62 | const tokenData = createToken(findUser); 63 | const cookie = createCookie(tokenData); 64 | 65 | findUser.password = undefined; 66 | 67 | return { cookie, findUser }; 68 | } 69 | 70 | public async logout(userData: User): Promise { 71 | const findUser: User = await UserModel.findOne({ email: userData.email, password: userData.password }); 72 | if (!findUser) throw new HttpException(409, `This email ${userData.email} was not found`); 73 | 74 | return findUser; 75 | } 76 | 77 | public async googleLogin(userData: any): Promise<{ token: string; findUser: User }> { 78 | const findUser: User = await UserModel.findOne({ email: userData.email }); 79 | if (!findUser) throw new HttpException(409, `This googleId ${userData.googleId} was not found`); 80 | 81 | const tokenData = createToken(findUser); 82 | 83 | return { token: tokenData.token, findUser }; 84 | } 85 | 86 | public async generateOTP(otpData: GenerateOtpRequestBody): Promise { 87 | await OtpModel.deleteMany({ email: otpData.email }); 88 | const otp = Math.floor(100000 + Math.random() * 900000); 89 | const expiresIn = Date.now(); 90 | await OtpModel.create({ ...otpData, otp, expiresIn }); 91 | await this.emailSender.sendMailWrapper({ 92 | to: otpData.email, 93 | otp, 94 | template: 'otp', 95 | subject: Mail.OTPSubject, 96 | reason: otpData.reason, 97 | expiresIn: Mail.OtpExpiresIn, 98 | }); 99 | } 100 | 101 | public async verifyOTP(otpData: VerifyOtpRequestBody): Promise { 102 | const findOtp: Otp = await OtpModel.findOne({ email: otpData.email }); 103 | 104 | if (findOtp) await OtpModel.updateOne({ email: otpData.email }, { $inc: { attempts: 1 } }); 105 | if (!findOtp) throw new HttpException(409, `OTP not found`); 106 | if (findOtp.attempts >= 3) throw new HttpException(409, `Attempts exceeded, please generate new OTP`); 107 | if (findOtp.otp !== otpData.otp) throw new HttpException(409, `Wrong OTP, ${2 - findOtp.attempts} attempts left`); 108 | 109 | const now = Date.now(); 110 | const diff = now - findOtp.expiresIn; 111 | if (diff > 60000 * 5) throw new HttpException(409, `OTP expired`); 112 | 113 | await OtpModel.deleteMany({ email: otpData.email }); 114 | } 115 | 116 | public async forgotPassword(forgotPasswordData: ForgotPasswordRequestBody): Promise { 117 | const findUser: User = await UserModel.findOne({ email: forgotPasswordData.email }); 118 | if (!findUser) throw new HttpException(409, `This email ${forgotPasswordData.email} was not found`); 119 | await this.verifyOTP({ email: forgotPasswordData.email, otp: forgotPasswordData.otp }); 120 | const hashedPassword = await hash(forgotPasswordData.password, 10); 121 | await UserModel.updateOne({ email: forgotPasswordData.email }, { password: hashedPassword }); 122 | // can send a mail to user that password has been changed 123 | } 124 | 125 | public async resetPassword(resetPasswordData: ResetPasswordRequestBody, id: string): Promise { 126 | const findUser: User = await UserModel.findById(id).select('+password'); 127 | if (!findUser) throw new HttpException(409, `This id ${id} was not found`); 128 | const isPasswordMatching: boolean = await compare(resetPasswordData.oldPassword, findUser.password); 129 | if (!isPasswordMatching) throw new HttpException(409, 'Incorrect password'); 130 | const hashedPassword = await hash(resetPasswordData.newPassword, 10); 131 | await UserModel.findByIdAndUpdate(id, { password: hashedPassword }); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /backend/src/services/member.service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { HttpException } from '@exceptions/HttpException'; 3 | import { MemberModel } from '@models/member.model'; 4 | import { ActivityModel } from '@/models/activity.model'; 5 | import { UserModel } from '@/models/user.model'; 6 | import { SecretModel } from '@/models/secret.model'; 7 | import { Member, UpdateMember, MemberFilter } from '@interfaces/member.interface'; 8 | import { CreateActivity } from '@/interfaces/activity.interface'; 9 | import { Helper } from '@utils/helper'; 10 | import { DiscordBot } from '@utils/discord'; 11 | import { Container } from 'typedi'; 12 | 13 | @Service() 14 | export class MemberService { 15 | public helper = Container.get(Helper); 16 | public bot = Container.get(DiscordBot); 17 | 18 | public async findAllMembers(filter: MemberFilter, role: string[], org: string): Promise { 19 | let members: Member[] = []; 20 | let memberQuery = MemberModel.find(); 21 | memberQuery = memberQuery.select('-activities'); 22 | 23 | const containsOnlyReferrer = role.every(role => role === 'referrer'); 24 | 25 | if (containsOnlyReferrer) { 26 | memberQuery = memberQuery.find({ status: 'pending' }); 27 | memberQuery = memberQuery.sort({ createdAt: filter.sort ?? -1 }); 28 | memberQuery = memberQuery.where('org').equals(org); 29 | } else { 30 | memberQuery = memberQuery.find({ status: filter.status ?? 'pending' }); 31 | memberQuery = memberQuery.find({ org: filter.org ?? org }); 32 | memberQuery = memberQuery.sort({ createdAt: filter.sort ?? -1 }); 33 | } 34 | members = await memberQuery; 35 | return members; 36 | } 37 | 38 | public async findMemeberById(memberId: string, role: string[], org: string): Promise { 39 | const containsOnlyReferrer = role.every(role => role === 'referrer'); 40 | const findMember: Member = await MemberModel.findOne({ _id: memberId }).populate('activities').populate({ 41 | path: 'secretId', 42 | select: 'date tokenIssuedAt', 43 | }); 44 | if (!findMember) throw new HttpException(409, "Member doesn't exist"); 45 | if (containsOnlyReferrer && findMember.status !== 'pending') throw new HttpException(409, 'Member application already resolved'); 46 | if (org && findMember.org !== org) throw new HttpException(409, 'Member does not belong to your organization'); 47 | return findMember; 48 | } 49 | 50 | public async createMember(memberData: Member, secretId: string, token: string): Promise { 51 | const isMemberExist = await MemberModel.findOne({ cfUserName: memberData.cfUserName }); 52 | if (isMemberExist) { 53 | const updatedMember = await MemberModel.findByIdAndUpdate( 54 | isMemberExist._id, 55 | { cfUserName: memberData.cfUserName, status: 'pending' }, 56 | { new: true }, 57 | ); 58 | if (!updatedMember) throw new HttpException(409, 'Member not found'); 59 | await SecretModel.findOneAndUpdate({ cfUserName: memberData.cfUserName, token }, { tokenIssuedAt: 0 }); 60 | return updatedMember; 61 | } 62 | const createMemberData: Member = await MemberModel.create({ ...memberData, secretId }); 63 | await SecretModel.findOneAndUpdate({ cfUserName: memberData.cfUserName, token }, { tokenIssuedAt: 0 }); 64 | 65 | return createMemberData; 66 | } 67 | 68 | public async updateMemberStatus(memberId: string, referrerId: string, memberData: UpdateMember): Promise { 69 | const isMemberExist = await MemberModel.findOne({ _id: memberId }); 70 | if (!isMemberExist) throw new HttpException(409, "Member doesn't exist"); 71 | if (isMemberExist.status === 'accepted' || isMemberExist.status === 'rejected') 72 | throw new HttpException(409, 'Member application already resolved'); 73 | let updateMemberById = await MemberModel.findByIdAndUpdate(memberId, memberData); 74 | const secret = await SecretModel.findOne({ cfUserName: updateMemberById.cfUserName }); 75 | if (!updateMemberById) throw new HttpException(409, "Member doesn't exist"); 76 | const count = memberData.status === 'accepted' ? 1 : 0; 77 | const activityData: CreateActivity = this.getActivityData(updateMemberById, memberData); 78 | const activity = await ActivityModel.create(activityData); 79 | const updatedUser = await UserModel.findByIdAndUpdate( 80 | referrerId, 81 | { $push: { activities: activity }, $inc: { totalReferred: count } }, 82 | { new: true }, 83 | ); 84 | 85 | updateMemberById = await MemberModel.findByIdAndUpdate(memberId, { $push: { activities: activity } }, { new: true }); 86 | await this.bot.sendDMToUser(secret.discordId, updateMemberById.status); 87 | if (!updatedUser) throw new HttpException(409, "Referrer doesn't exist"); 88 | return updateMemberById; 89 | } 90 | 91 | public async deleteMember(memberId: string): Promise { 92 | const deleteMemberById: Member = await MemberModel.findByIdAndDelete(memberId); 93 | if (!deleteMemberById) throw new HttpException(409, "Member doesn't exist"); 94 | 95 | return deleteMemberById; 96 | } 97 | 98 | private getActivityData(member: Member, memberData: UpdateMember): CreateActivity { 99 | return { 100 | email: member.email, 101 | cfUserName: member.cfUserName, 102 | status: memberData.status, 103 | referrerResponse: memberData.referrerResponse, 104 | jobId: member.jobId, 105 | applicationDate: member.updatedAt, 106 | message: member.message, 107 | }; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /backend/src/services/secret.service.ts: -------------------------------------------------------------------------------- 1 | import Container, { Service } from 'typedi'; 2 | import { HttpException } from '@exceptions/HttpException'; 3 | import { SecretModel } from '@models/secret.model'; 4 | import { CreateSecret, Secret, VerifySecret } from '@interfaces/secret.interface'; 5 | import { Helper } from '@/utils/helper'; 6 | import { Constants } from '@/utils/constants'; 7 | 8 | @Service() 9 | export class SecretService { 10 | private helper = Container.get(Helper); 11 | 12 | public async findAllSecret(): Promise { 13 | const secrets: Secret[] = await SecretModel.find(); 14 | return secrets; 15 | } 16 | 17 | public async findSecret(data: VerifySecret): Promise { 18 | const findSecret: Secret = await SecretModel.findOne(data); 19 | 20 | if (!findSecret) throw new HttpException(409, 'Wrong token or username '); 21 | 22 | const tokenAge = this.helper.calculateTimeDifference(findSecret.tokenIssuedAt); 23 | 24 | if (tokenAge > Constants.oneDay) throw new HttpException(409, 'Token is incorrect or expired'); 25 | 26 | return findSecret; 27 | } 28 | 29 | public async createSecret(secretData: CreateSecret): Promise { 30 | const presentDate = new Date(); 31 | const thisMonth = presentDate.getMonth() + 1; 32 | const thisYear = presentDate.getFullYear(); 33 | 34 | const findSecretData = await SecretModel.findOne({ cfUserName: secretData.cfUserName }); 35 | const key = `${thisMonth}_${thisYear}`; 36 | 37 | if (findSecretData) { 38 | const request: number = findSecretData.date.get(key) || 0; 39 | 40 | if (request > 7) { 41 | throw new HttpException(409, 'You have reached the limit of requests for this month'); 42 | } 43 | 44 | const updateSecretData: Secret = await SecretModel.findByIdAndUpdate( 45 | findSecretData._id, 46 | { 47 | $set: { 48 | [`date.${key}`]: request + 1, 49 | }, 50 | token: secretData.token, 51 | tokenIssuedAt: Date.now(), 52 | }, 53 | { new: true }, 54 | ); 55 | return updateSecretData; 56 | } 57 | 58 | const createSecretData: Secret = await SecretModel.create({ ...secretData, date: { [key]: 1 } }); 59 | 60 | return createSecretData; 61 | } 62 | 63 | public async updateSecret(secretId: string, secretData: Secret): Promise { 64 | const updateSecretById: Secret = await SecretModel.findByIdAndUpdate(secretId, { secretData }); 65 | if (!updateSecretById) throw new HttpException(409, "Secret doesn't exist"); 66 | 67 | return updateSecretById; 68 | } 69 | 70 | public async deleteSecret(secretId: string): Promise { 71 | const deleteSecretById: Secret = await SecretModel.findByIdAndDelete(secretId); 72 | if (!deleteSecretById) throw new HttpException(409, "Secret doesn't exist"); 73 | 74 | return deleteSecretById; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /backend/src/services/users.service.ts: -------------------------------------------------------------------------------- 1 | import { hash } from 'bcrypt'; 2 | import { Service } from 'typedi'; 3 | import { HttpException } from '@exceptions/HttpException'; 4 | import { UpdateUser, UpdateUserRoles, User, UserFilter } from '@/interfaces/user.interface'; 5 | import { UserModel } from '@/models/user.model'; 6 | 7 | @Service() 8 | export class UserService { 9 | public async findAllUser(): Promise { 10 | const users: User[] = await UserModel.find(); 11 | return users; 12 | } 13 | 14 | public async findUserById(userId: string): Promise { 15 | const findUser: User = await UserModel.findOne({ _id: userId }); 16 | if (!findUser) throw new HttpException(409, "User doesn't exist"); 17 | 18 | return findUser; 19 | } 20 | 21 | public async createUser(userData: User): Promise { 22 | const findUser: User = await UserModel.findOne({ email: userData.email }); 23 | if (findUser) throw new HttpException(409, `This email ${userData.email} already exists`); 24 | 25 | const hashedPassword = await hash(userData.password, 10); 26 | const createUserData: User = await UserModel.create({ ...userData, password: hashedPassword }); 27 | 28 | return createUserData; 29 | } 30 | 31 | public async updateUser(userId: string, userData: UpdateUser): Promise { 32 | let updateQuery = UserModel.findByIdAndUpdate(userId, userData, { new: true }); 33 | if (userData.org) updateQuery = UserModel.findByIdAndUpdate(userId, { isVerified: false }, { new: true }); 34 | const updateUserById: User = await updateQuery; 35 | 36 | return updateUserById; 37 | } 38 | 39 | public async deleteUser(userId: string): Promise { 40 | const deleteUserById: User = await UserModel.findByIdAndDelete(userId); 41 | if (!deleteUserById) throw new HttpException(409, "User doesn't exist"); 42 | 43 | return deleteUserById; 44 | } 45 | 46 | public async verifyModerator(userId: string, adminId: string): Promise { 47 | const findUser: User = await UserModel.findById(userId); 48 | if (!findUser) throw new HttpException(409, "User doesn't exist"); 49 | 50 | const updateUserById = await UserModel.findByIdAndUpdate( 51 | userId, 52 | { 53 | role: ['moderator'], 54 | verifiedBy: adminId, 55 | isVerified: true, 56 | }, 57 | { 58 | new: true, 59 | }, 60 | ); 61 | 62 | if (!updateUserById) throw new HttpException(409, "User doesn't exist"); 63 | 64 | return updateUserById; 65 | } 66 | 67 | public async verifyReferrer(userId: string, adminOrModId: string): Promise { 68 | const findUser: User = await UserModel.findById(userId); 69 | if (!findUser) throw new HttpException(409, "User doesn't exist"); 70 | 71 | const updateUserById: User = await UserModel.findByIdAndUpdate( 72 | userId, 73 | { verifiedBy: adminOrModId, isVerified: true }, 74 | { 75 | new: true, 76 | }, 77 | ); 78 | if (!updateUserById) throw new HttpException(409, "User doesn't exist"); 79 | 80 | return updateUserById; 81 | } 82 | 83 | public async getReferrers(filter: UserFilter): Promise { 84 | let referrers: User[] = []; 85 | let referrerQuery = UserModel.find({ role: { $in: ['referrer'] } }); 86 | 87 | if (filter.org) referrerQuery = referrerQuery.find({ org: filter.org }); 88 | if (filter.isVerified) referrerQuery = referrerQuery.find({ isVerified: filter.isVerified }); 89 | if (filter.sort) referrerQuery = referrerQuery.sort({ createdAt: filter.sort }); 90 | if (filter.totalReferred) referrerQuery = referrerQuery.sort({ totalReferred: filter.totalReferred }); 91 | 92 | referrers = await referrerQuery; 93 | return referrers; 94 | } 95 | 96 | public async getModerators(filter: UserFilter): Promise { 97 | let referrers: User[] = []; 98 | let referrerQuery = UserModel.find({ role: { $in: ['moderator'] } }); 99 | 100 | if (filter.org) referrerQuery = referrerQuery.find({ org: filter.org }); 101 | if (filter.isVerified) referrerQuery = referrerQuery.find({ isVerified: filter.isVerified }); 102 | if (filter.sort) referrerQuery = referrerQuery.sort({ createdAt: filter.sort }); 103 | if (filter.totalReferred) referrerQuery = referrerQuery.sort({ totalReferred: filter.totalReferred }); 104 | 105 | referrers = await referrerQuery; 106 | return referrers; 107 | } 108 | 109 | public async getModeratorById(moderatorId: string): Promise { 110 | const findModerator: User = await UserModel.findOne({ _id: moderatorId }).populate('activities').populate({ 111 | path: 'verifiedBy', 112 | select: 'username role', 113 | }); 114 | if (!findModerator) throw new HttpException(409, "Moderator doesn't exist"); 115 | return findModerator; 116 | } 117 | 118 | public async getReferrerById(referrerId: string): Promise { 119 | const findReferrer: User = await UserModel.findOne({ _id: referrerId }).populate('activities').populate({ 120 | path: 'verifiedBy', 121 | select: 'username role', 122 | }); 123 | if (!findReferrer) throw new HttpException(409, "Referrer doesn't exist"); 124 | return findReferrer; 125 | } 126 | 127 | public async updateRoles(userId: string, userData: UpdateUserRoles): Promise { 128 | const updatedUser = await UserModel.findByIdAndUpdate(userId, userData, { new: true }); 129 | if (!updatedUser) throw new HttpException(409, "User doesn't exist"); 130 | return updatedUser; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /backend/src/test/auth.test.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | import mongoose from 'mongoose'; 3 | import request from 'supertest'; 4 | import { App } from '@/app'; 5 | import { CreateUserDto } from '@/dtos/user.dto'; 6 | import { AuthRoute } from '@routes/auth.route'; 7 | 8 | afterAll(async () => { 9 | await new Promise(resolve => setTimeout(() => resolve(), 500)); 10 | }); 11 | 12 | describe('Testing Auth', () => { 13 | describe('[POST] /signup', () => { 14 | it('response should have the Create userData', async () => { 15 | const userData: CreateUserDto = { 16 | email: 'test@email.com', 17 | password: 'q1w2e3r4!', 18 | }; 19 | 20 | const authRoute = new AuthRoute(); 21 | const users = authRoute.authController.authService.users; 22 | 23 | users.findOne = jest.fn().mockReturnValue(null); 24 | users.create = jest.fn().mockReturnValue({ 25 | _id: '60706478aad6c9ad19a31c84', 26 | email: userData.email, 27 | password: await bcrypt.hash(userData.password, 10), 28 | }); 29 | 30 | (mongoose as any).connect = jest.fn(); 31 | const app = new App([authRoute]); 32 | return request(app.getServer()).post(`${authRoute.path}signup`).send(userData); 33 | }); 34 | }); 35 | 36 | describe('[POST] /login', () => { 37 | it('response should have the Set-Cookie header with the Authorization token', async () => { 38 | const userData: CreateUserDto = { 39 | email: 'test@email.com', 40 | password: 'q1w2e3r4!', 41 | }; 42 | 43 | const authRoute = new AuthRoute(); 44 | const users = authRoute.authController.authService.users; 45 | 46 | users.findOne = jest.fn().mockReturnValue({ 47 | _id: '60706478aad6c9ad19a31c84', 48 | email: userData.email, 49 | password: await bcrypt.hash(userData.password, 10), 50 | }); 51 | 52 | (mongoose as any).connect = jest.fn(); 53 | const app = new App([authRoute]); 54 | return request(app.getServer()) 55 | .post(`${authRoute.path}login`) 56 | .send(userData) 57 | .expect('Set-Cookie', /^Authorization=.+/); 58 | }); 59 | }); 60 | 61 | // describe('[POST] /logout', () => { 62 | // it('logout Set-Cookie Authorization=; Max-age=0', async () => { 63 | // const userData: User = { 64 | // _id: '60706478aad6c9ad19a31c84', 65 | // email: 'test@email.com', 66 | // password: await bcrypt.hash('q1w2e3r4!', 10), 67 | // }; 68 | 69 | // const authRoute = new AuthRoute(); 70 | // const users = authRoute.authController.authService.users; 71 | 72 | // users.findOne = jest.fn().mockReturnValue(userData); 73 | 74 | // (mongoose as any).connect = jest.fn(); 75 | // const app = new App([authRoute]); 76 | // return request(app.getServer()) 77 | // .post(`${authRoute.path}logout`) 78 | // .send(userData) 79 | // .set('Set-Cookie', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ') 80 | // .expect('Set-Cookie', /^Authorization=\; Max-age=0/); 81 | // }); 82 | // }); 83 | }); 84 | -------------------------------------------------------------------------------- /backend/src/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import App from '@/app'; 3 | import IndexRoute from '@routes/index.route'; 4 | 5 | afterAll(async () => { 6 | await new Promise(resolve => setTimeout(() => resolve(), 500)); 7 | }); 8 | 9 | describe('Testing Index', () => { 10 | describe('[GET] /', () => { 11 | it('response statusCode 200', () => { 12 | const indexRoute = new IndexRoute(); 13 | const app = new App([indexRoute]); 14 | 15 | return request(app.getServer()).get(`${indexRoute.path}`).expect(200); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/test/users.test.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | import mongoose from 'mongoose'; 3 | import request from 'supertest'; 4 | import { App } from '@/app'; 5 | import { CreateUserDto } from '@/dtos/user.dto'; 6 | import { UserRoute } from '@routes/users.route'; 7 | 8 | afterAll(async () => { 9 | await new Promise(resolve => setTimeout(() => resolve(), 500)); 10 | }); 11 | 12 | describe('Testing Users', () => { 13 | describe('[GET] /users', () => { 14 | it('response fineAll Users', async () => { 15 | const usersRoute = new UserRoute(); 16 | const users = usersRoute.usersController.userService.users; 17 | 18 | users.find = jest.fn().mockReturnValue([ 19 | { 20 | _id: 'qpwoeiruty', 21 | email: 'a@email.com', 22 | password: await bcrypt.hash('q1w2e3r4!', 10), 23 | }, 24 | { 25 | _id: 'alskdjfhg', 26 | email: 'b@email.com', 27 | password: await bcrypt.hash('a1s2d3f4!', 10), 28 | }, 29 | { 30 | _id: 'zmxncbv', 31 | email: 'c@email.com', 32 | password: await bcrypt.hash('z1x2c3v4!', 10), 33 | }, 34 | ]); 35 | 36 | (mongoose as any).connect = jest.fn(); 37 | const app = new App([usersRoute]); 38 | return request(app.getServer()).get(`${usersRoute.path}`).expect(200); 39 | }); 40 | }); 41 | 42 | describe('[GET] /users/:id', () => { 43 | it('response findOne User', async () => { 44 | const userId = 'qpwoeiruty'; 45 | 46 | const usersRoute = new UserRoute(); 47 | const users = usersRoute.usersController.userService.users; 48 | 49 | users.findOne = jest.fn().mockReturnValue({ 50 | _id: 'qpwoeiruty', 51 | email: 'a@email.com', 52 | password: await bcrypt.hash('q1w2e3r4!', 10), 53 | }); 54 | 55 | (mongoose as any).connect = jest.fn(); 56 | const app = new App([usersRoute]); 57 | return request(app.getServer()).get(`${usersRoute.path}/${userId}`).expect(200); 58 | }); 59 | }); 60 | 61 | describe('[POST] /users', () => { 62 | it('response Create User', async () => { 63 | const userData: CreateUserDto = { 64 | email: 'test@email.com', 65 | password: 'q1w2e3r4', 66 | }; 67 | 68 | const usersRoute = new UserRoute(); 69 | const users = usersRoute.usersController.userService.users; 70 | 71 | users.findOne = jest.fn().mockReturnValue(null); 72 | users.create = jest.fn().mockReturnValue({ 73 | _id: '60706478aad6c9ad19a31c84', 74 | email: userData.email, 75 | password: await bcrypt.hash(userData.password, 10), 76 | }); 77 | 78 | (mongoose as any).connect = jest.fn(); 79 | const app = new App([usersRoute]); 80 | return request(app.getServer()).post(`${usersRoute.path}`).send(userData).expect(201); 81 | }); 82 | }); 83 | 84 | describe('[PUT] /users/:id', () => { 85 | it('response Update User', async () => { 86 | const userId = '60706478aad6c9ad19a31c84'; 87 | const userData: CreateUserDto = { 88 | email: 'test@email.com', 89 | password: 'q1w2e3r4', 90 | }; 91 | 92 | const usersRoute = new UserRoute(); 93 | const users = usersRoute.usersController.userService.users; 94 | 95 | if (userData.email) { 96 | users.findOne = jest.fn().mockReturnValue({ 97 | _id: userId, 98 | email: userData.email, 99 | password: await bcrypt.hash(userData.password, 10), 100 | }); 101 | } 102 | 103 | users.findByIdAndUpdate = jest.fn().mockReturnValue({ 104 | _id: userId, 105 | email: userData.email, 106 | password: await bcrypt.hash(userData.password, 10), 107 | }); 108 | 109 | (mongoose as any).connect = jest.fn(); 110 | const app = new App([usersRoute]); 111 | return request(app.getServer()).put(`${usersRoute.path}/${userId}`).send(userData); 112 | }); 113 | }); 114 | 115 | describe('[DELETE] /users/:id', () => { 116 | it('response Delete User', async () => { 117 | const userId = '60706478aad6c9ad19a31c84'; 118 | 119 | const usersRoute = new UserRoute(); 120 | const users = usersRoute.usersController.userService.users; 121 | 122 | users.findByIdAndDelete = jest.fn().mockReturnValue({ 123 | _id: '60706478aad6c9ad19a31c84', 124 | email: 'test@email.com', 125 | password: await bcrypt.hash('q1w2e3r4!', 10), 126 | }); 127 | 128 | (mongoose as any).connect = jest.fn(); 129 | const app = new App([usersRoute]); 130 | return request(app.getServer()).delete(`${usersRoute.path}/${userId}`).expect(200); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /backend/src/typings/express/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Express { 2 | export interface Request { 3 | user?: { 4 | _id?: string; 5 | role?: string[]; 6 | org?: string; 7 | isVerified?: boolean; 8 | }; 9 | token?: string; 10 | secretId?: string; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const Mail = { 2 | Brand: 'A Code Daily', 3 | Url: 'https://acodedaily.com', 4 | OtpExpiresIn: 5, 5 | EmailFrom: 'A Code Daily ', 6 | OTPSubject: 'OTP for A Code Daily', 7 | WelcomeSubject: 'Welcome to A Code Daily', 8 | }; 9 | 10 | export const Constants = { 11 | oneDay: 1, 12 | }; 13 | -------------------------------------------------------------------------------- /backend/src/utils/discord.ts: -------------------------------------------------------------------------------- 1 | import { Client, GatewayIntentBits } from 'discord.js'; 2 | import { Service } from 'typedi'; 3 | import { BOT_TOKEN } from '@config'; 4 | import { logger } from '@utils/logger'; 5 | 6 | @Service() 7 | export class DiscordBot { 8 | private client: Client; 9 | private token: string; 10 | 11 | constructor() { 12 | this.client = new Client({ 13 | intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], 14 | }); 15 | 16 | this.token = BOT_TOKEN; 17 | 18 | this.client.once('ready', () => { 19 | logger.info(`Logged in as ${this.client.user?.tag}`); 20 | }); 21 | 22 | this.client.login(this.token); 23 | } 24 | 25 | public async sendDMToUser(userId: string, content: string) { 26 | try { 27 | const user = await this.client.users.fetch(userId); 28 | 29 | if (user) { 30 | user 31 | .send(content) 32 | .then(sentMessage => logger.info(`DM sent to ${user.tag}: ${sentMessage.content}`)) 33 | .catch(error => logger.error(`Error sending DM to ${user.tag}: ${error.message}`)); 34 | } else { 35 | logger.error('User not found.'); 36 | } 37 | } catch (error) { 38 | logger.error(`Error fetching user with ID ${userId}: ${error.message}`); 39 | } 40 | } 41 | } 42 | 43 | export default DiscordBot; 44 | -------------------------------------------------------------------------------- /backend/src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import pug from 'pug'; 2 | import { Service } from 'typedi'; 3 | import { Mail } from '@utils/constants'; 4 | import { renderTemplateData } from '@/interfaces/template.interface'; 5 | import { VerifyAccessKeyAndSecretKey } from '@/interfaces/secret.interface'; 6 | import { Request } from 'express'; 7 | import { Member } from '@/interfaces/member.interface'; 8 | 9 | @Service() 10 | export class Helper { 11 | public renderTemplate(data: renderTemplateData): string { 12 | return pug.compileFile(`${__dirname}/../views/${data.template}.pug`)({ ...data, brand: Mail.Brand, url: Mail.Url }); 13 | } 14 | 15 | public calculateTimeDifference(updatedAt: Date): number { 16 | const currentTime = new Date(); 17 | const updatedTime = new Date(updatedAt); 18 | const differenceInMilliseconds = currentTime.getTime() - updatedTime.getTime(); 19 | return differenceInMilliseconds / (1000 * 60 * 60 * 24); // Convert milliseconds to days 20 | } 21 | 22 | public getAuthorization(req: Request): string | null { 23 | const coockie = req.cookies['Authorization']; 24 | if (coockie) return coockie; 25 | 26 | const header = req.header('Authorization'); 27 | if (header) return header.split('Bearer ')[1]; 28 | 29 | return null; 30 | } 31 | 32 | public getAccessAndSecretKey(req: Request): VerifyAccessKeyAndSecretKey { 33 | const access_key = req.header('access_key'); 34 | const secret_key = req.header('secret_key'); 35 | return { access_key, secret_key }; 36 | } 37 | 38 | public sendStatusUpdateToDiscord(member: Member): void { 39 | // const { cfUserName, status, referrerResponse } = member; 40 | return; 41 | } 42 | 43 | public areArraysEqual(array1: any[], array2: any[]): boolean { 44 | if (array1.length !== array2.length) { 45 | return false; 46 | } 47 | 48 | for (let i = 0; i < array1.length; i++) { 49 | if (array1[i] !== array2[i]) { 50 | return false; 51 | } 52 | } 53 | 54 | return true; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /backend/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync } from 'fs'; 2 | import { join } from 'path'; 3 | import winston from 'winston'; 4 | import winstonDaily from 'winston-daily-rotate-file'; 5 | import { LOG_DIR } from '@config'; 6 | 7 | // logs dir 8 | const logDir: string = join(__dirname, LOG_DIR); 9 | 10 | if (!existsSync(logDir)) { 11 | mkdirSync(logDir); 12 | } 13 | 14 | // Define log format 15 | const logFormat = winston.format.printf(({ timestamp, level, message }) => `${timestamp} ${level}: ${message}`); 16 | 17 | /* 18 | * Log Level 19 | * error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6 20 | */ 21 | const logger = winston.createLogger({ 22 | format: winston.format.combine( 23 | winston.format.timestamp({ 24 | format: 'YYYY-MM-DD HH:mm:ss', 25 | }), 26 | logFormat, 27 | ), 28 | transports: [ 29 | // debug log setting 30 | new winstonDaily({ 31 | level: 'debug', 32 | datePattern: 'YYYY-MM-DD', 33 | dirname: logDir + '/debug', // log file /logs/debug/*.log in save 34 | filename: `%DATE%.log`, 35 | maxFiles: 30, // 30 Days saved 36 | json: false, 37 | zippedArchive: true, 38 | }), 39 | // error log setting 40 | new winstonDaily({ 41 | level: 'error', 42 | datePattern: 'YYYY-MM-DD', 43 | dirname: logDir + '/error', // log file /logs/error/*.log in save 44 | filename: `%DATE%.log`, 45 | maxFiles: 30, // 30 Days saved 46 | handleExceptions: true, 47 | json: false, 48 | zippedArchive: true, 49 | }), 50 | ], 51 | }); 52 | 53 | logger.add( 54 | new winston.transports.Console({ 55 | format: winston.format.combine(winston.format.splat(), winston.format.colorize()), 56 | }), 57 | ); 58 | 59 | const stream = { 60 | write: (message: string) => { 61 | logger.info(message.substring(0, message.lastIndexOf('\n'))); 62 | }, 63 | }; 64 | 65 | export { logger, stream }; 66 | -------------------------------------------------------------------------------- /backend/src/utils/mail.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@utils/logger'; 2 | import { Service, Container } from 'typedi'; 3 | import nodemailer, { Transporter, SendMailOptions, SentMessageInfo } from 'nodemailer'; 4 | import { SMTP_HOST, SMTP_PORT, SMTP_PASSWORD, SMTP_USERNAME } from '@config'; 5 | import { Helper } from '@/utils/helper'; 6 | import { Mail } from '@utils/constants'; 7 | import { renderTemplateData } from '@/interfaces/template.interface'; 8 | 9 | @Service() 10 | export class EmailSender { 11 | private transporter: Transporter; 12 | private helper = Container.get(Helper); 13 | 14 | constructor() { 15 | this.transporter = nodemailer.createTransport({ 16 | host: SMTP_HOST, 17 | port: SMTP_PORT, 18 | secure: false, 19 | requireTLS: true, 20 | auth: { 21 | user: SMTP_USERNAME, 22 | pass: SMTP_PASSWORD, 23 | }, 24 | logger: false, 25 | } as any); // Explicitly cast as any to resolve TypeScript error 26 | 27 | this.transporter 28 | .verify() 29 | .then(() => logger.info('Connected to EMAIL SERVER')) 30 | .catch(err => { 31 | // eslint-disable-next-line 32 | console.log(err); 33 | logger.warn('Unable to connect to email server. Make sure you have configured the SMTP options in .env'); 34 | }); 35 | } 36 | 37 | async sendEmail(mailOptions: SendMailOptions): Promise { 38 | this.transporter.sendMail(mailOptions, (error: Error | null, info: SentMessageInfo) => { 39 | if (error) { 40 | logger.error('Error:', error.message); 41 | } else { 42 | logger.notice('Email sent:', info.response); 43 | } 44 | 45 | // Close the transporter after sending the email 46 | this.transporter.close(); 47 | }); 48 | } 49 | 50 | async sendMailWrapper(data: renderTemplateData): Promise { 51 | const text = this.helper.renderTemplate(data); 52 | 53 | const mailOptions: SendMailOptions = { 54 | from: Mail.EmailFrom, 55 | to: data.to, 56 | subject: data.subject, 57 | text, 58 | }; 59 | 60 | await this.sendEmail(mailOptions); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /backend/src/utils/passport.ts: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import { Container } from 'typedi'; 3 | import { EmailSender } from '@utils/mail'; 4 | import passportGoogle from 'passport-google-oauth20'; 5 | import { UserModel } from '@/models/user.model'; 6 | import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET } from '@config'; 7 | import { Mail } from './constants'; 8 | 9 | const GoogleStrategy = passportGoogle.Strategy; 10 | const emailSender = Container.get(EmailSender); 11 | 12 | passport.use( 13 | new GoogleStrategy( 14 | { 15 | clientID: GOOGLE_CLIENT_ID, 16 | clientSecret: GOOGLE_CLIENT_SECRET, 17 | callbackURL: '/api/v3/auth/google/callback', 18 | }, 19 | async (accessToken, refreshToken, profile, done) => { 20 | const user = await UserModel.findOne({ email: profile.emails?.[0].value }); 21 | 22 | // If user doesn't exist creates a new user. (similar to sign up) 23 | if (!user) { 24 | const newUser = await UserModel.create({ 25 | googleId: profile.id, 26 | username: profile.displayName, 27 | email: profile.emails?.[0].value, 28 | firstName: profile.name?.givenName, 29 | lastName: profile.name?.familyName, 30 | }); 31 | await emailSender.sendMailWrapper({ to: newUser.email, template: 'welcome', username: newUser.username, subject: Mail.WelcomeSubject }); 32 | if (newUser) { 33 | // set the user object in the done callback 34 | done(null, newUser); 35 | } 36 | } else { 37 | done(null, user); 38 | } 39 | }, 40 | ), 41 | ); 42 | -------------------------------------------------------------------------------- /backend/src/utils/validateEnv.ts: -------------------------------------------------------------------------------- 1 | import { cleanEnv, port, str } from 'envalid'; 2 | 3 | export const ValidateEnv = () => { 4 | cleanEnv(process.env, { 5 | NODE_ENV: str(), 6 | PORT: port(), 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /backend/src/views/otp.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | meta(charset="UTF-8") 5 | meta(name="viewport", content="width=device-width, initial-scale=1.0") 6 | title= title 7 | style. 8 | /* Email styles */ 9 | body { 10 | font-family: Arial, sans-serif; 11 | font-size: 16px; 12 | line-height: 1.5; 13 | color: #333; 14 | margin: 0; 15 | padding: 0; 16 | } 17 | h1 { 18 | font-size: 36px; 19 | font-weight: bold; 20 | letter-spacing: 3px; 21 | margin: 30px 0; 22 | } 23 | .container { 24 | max-width: 600px; 25 | margin: 0 auto; 26 | padding: 20px; 27 | box-sizing: border-box; 28 | background-color: #f5f5f5; 29 | } 30 | .header { 31 | background-color: #003366; 32 | color: #fff; 33 | padding: 20px; 34 | text-align: center; 35 | } 36 | .header h2 { 37 | margin: 0; 38 | } 39 | .body { 40 | padding: 30px 20px; 41 | } 42 | .footer { 43 | background-color: #003366; 44 | color: #fff; 45 | padding: 20px; 46 | text-align: center; 47 | } 48 | .footer a { 49 | color: #fff; 50 | text-decoration: none; 51 | } 52 | h2 { 53 | color: #fff; 54 | } 55 | body 56 | .container 57 | .header 58 | h2 59 | a(href="#{url}", target="_blank") #{brand} Team 60 | .body 61 | p Dear #{username}, 62 | p Your OTP for #{reason} is: 63 | center 64 | h1 #{otp} 65 | p This OTP is valid for #{expiresIn} minutes. Please do not share it with anyone. 66 | p If you did not request this OTP, please ignore this email. 67 | .footer 68 | p Regards, 69 | p #{brand} Team 70 | //- a(href="#{url}", target="_blank") #{brand} Team 71 | -------------------------------------------------------------------------------- /backend/src/views/welcome.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | meta(charset="UTF-8") 5 | meta(name="viewport", content="width=device-width, initial-scale=1.0") 6 | title= title 7 | style. 8 | /* Email styles */ 9 | body { 10 | font-family: Arial, sans-serif; 11 | font-size: 16px; 12 | line-height: 1.5; 13 | color: #333; 14 | margin: 0; 15 | padding: 0; 16 | } 17 | h1 { 18 | font-size: 36px; 19 | font-weight: bold; 20 | letter-spacing: 3px; 21 | margin: 30px 0; 22 | } 23 | .container { 24 | max-width: 600px; 25 | margin: 0 auto; 26 | padding: 20px; 27 | box-sizing: border-box; 28 | background-color: #f5f5f5; 29 | } 30 | .header { 31 | background-color: #003366; 32 | color: #fff; 33 | padding: 20px; 34 | text-align: center; 35 | } 36 | .header h2 { 37 | margin: 0; 38 | } 39 | .body { 40 | padding: 30px 20px; 41 | } 42 | .footer { 43 | background-color: #003366; 44 | color: #fff; 45 | padding: 20px; 46 | text-align: center; 47 | } 48 | .footer a { 49 | color: #fff; 50 | text-decoration: none; 51 | } 52 | body 53 | .container 54 | .header 55 | h2 56 | a(href="#{url}", target="_blank") #{brand} Team 57 | .body 58 | p Dear #{username}, 59 | p Thank you for signing up for #{brand}! We're excited to have you on board. 60 | p Your account has been created successfully. Please click on the link below and proceed further: 61 | p 62 | a(href="https://www.acodedaily.com", target="_blank") A code Daily 63 | p If you did not sign up for #{brand}, please ignore this email. 64 | .footer 65 | p Regards, 66 | p 67 | a(href="#{url}", target="_blank") #{brand} Team 68 | -------------------------------------------------------------------------------- /backend/swagger.yaml: -------------------------------------------------------------------------------- 1 | tags: 2 | - name: users 3 | description: users API 4 | 5 | paths: 6 | # [GET] users 7 | /users: 8 | get: 9 | tags: 10 | - users 11 | summary: Find All Users 12 | responses: 13 | 200: 14 | description: 'OK' 15 | 500: 16 | description: 'Server Error' 17 | 18 | # [POST] users 19 | post: 20 | tags: 21 | - users 22 | summary: Add User 23 | parameters: 24 | - name: body 25 | in: body 26 | description: user Data 27 | required: true 28 | schema: 29 | $ref: '#/definitions/users' 30 | responses: 31 | 201: 32 | description: 'Created' 33 | 400: 34 | description: 'Bad Request' 35 | 409: 36 | description: 'Conflict' 37 | 500: 38 | description: 'Server Error' 39 | 40 | # [GET] users/id 41 | /users/{id}: 42 | get: 43 | tags: 44 | - users 45 | summary: Find User By Id 46 | parameters: 47 | - name: id 48 | in: path 49 | description: User Id 50 | required: true 51 | responses: 52 | 200: 53 | description: 'OK' 54 | 409: 55 | description: 'Conflict' 56 | 500: 57 | description: 'Server Error' 58 | 59 | # [PUT] users/id 60 | put: 61 | tags: 62 | - users 63 | summary: Update User By Id 64 | parameters: 65 | - name: id 66 | in: path 67 | description: user Id 68 | required: true 69 | - name: body 70 | in: body 71 | description: user Data 72 | required: true 73 | schema: 74 | $ref: '#/definitions/users' 75 | responses: 76 | 200: 77 | description: 'OK' 78 | 400: 79 | description: 'Bad Request' 80 | 409: 81 | description: 'Conflict' 82 | 500: 83 | description: 'Server Error' 84 | 85 | # [DELETE] users/id 86 | delete: 87 | tags: 88 | - users 89 | summary: Delete User By Id 90 | parameters: 91 | - name: id 92 | in: path 93 | description: user Id 94 | required: true 95 | responses: 96 | 200: 97 | description: 'OK' 98 | 409: 99 | description: 'Conflict' 100 | 500: 101 | description: 'Server Error' 102 | 103 | # definitions 104 | definitions: 105 | users: 106 | type: object 107 | required: 108 | - email 109 | - password 110 | properties: 111 | email: 112 | type: string 113 | description: user Email 114 | password: 115 | type: string 116 | description: user Password 117 | 118 | schemes: 119 | - https 120 | - http 121 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "es2017", 5 | "lib": ["es2017", "esnext.asynciterable"], 6 | "typeRoots": ["node_modules/@types", "./src/typings"], 7 | "allowSyntheticDefaultImports": true, 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "moduleResolution": "node", 12 | "module": "commonjs", 13 | "pretty": true, 14 | "sourceMap": true, 15 | "declaration": true, 16 | "outDir": "dist", 17 | "allowJs": true, 18 | "noEmit": false, 19 | "esModuleInterop": true, 20 | "resolveJsonModule": true, 21 | "importHelpers": true, 22 | "baseUrl": "src", 23 | "paths": { 24 | "@/*": ["*"], 25 | "@config": ["config"], 26 | "@controllers/*": ["controllers/*"], 27 | "@database": ["database"], 28 | "@dtos/*": ["dtos/*"], 29 | "@exceptions/*": ["exceptions/*"], 30 | "@interfaces/*": ["interfaces/*"], 31 | "@middlewares/*": ["middlewares/*"], 32 | "@models/*": ["models/*"], 33 | "@routes/*": ["routes/*"], 34 | "@services/*": ["services/*"], 35 | "@utils/*": ["utils/*"], 36 | "@typings/*": ["typings/*"], 37 | "@enums": ["enums/index.ts"], 38 | "@views/*": ["views/*"] 39 | } 40 | }, 41 | "include": ["src/**/*.ts", "src/**/*.json", ".env"], 42 | "exclude": ["node_modules", "src/http", "src/logs"] 43 | } 44 | -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react-hooks/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | plugins: ['react-refresh'], 11 | rules: { 12 | 'react-refresh/only-export-components': 'warn', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "acodedailyfrontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.0.37", 18 | "@types/react-dom": "^18.0.11", 19 | "@typescript-eslint/eslint-plugin": "^5.59.0", 20 | "@typescript-eslint/parser": "^5.59.0", 21 | "@vitejs/plugin-react-swc": "^3.0.0", 22 | "eslint": "^8.38.0", 23 | "eslint-plugin-react-hooks": "^4.6.0", 24 | "eslint-plugin-react-refresh": "^0.3.4", 25 | "typescript": "^5.0.2", 26 | "vite": "^4.3.9" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import reactLogo from './assets/react.svg' 3 | import viteLogo from '/vite.svg' 4 | import './App.css' 5 | 6 | function App() { 7 | const [count, setCount] = useState(0) 8 | 9 | return ( 10 | <> 11 | 19 |

Vite + React

20 |
21 | 24 |

25 | Edit src/App.tsx and save to test HMR 26 |

27 |
28 |

29 | Click on the Vite and React logos to learn more 30 |

31 | 32 | ) 33 | } 34 | 35 | export default App 36 | -------------------------------------------------------------------------------- /frontend/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | @media (prefers-color-scheme: light) { 59 | :root { 60 | color: #213547; 61 | background-color: #ffffff; 62 | } 63 | a:hover { 64 | color: #747bff; 65 | } 66 | button { 67 | background-color: #f9f9f9; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /gitCommit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if there are any changes to commit 4 | if [[ -z $(git status -s) ]]; then 5 | echo "No changes to commit." 6 | exit 0 7 | fi 8 | 9 | # Prompt for Gitmoji 10 | echo "Select a Gitmoji:" 11 | gitmoji_list=("✨ New feature" "🐛 Bug fix" "🔧 Maintenance" "📚 Documentation" "✅ Tests" "♻️ Refactor" "🎨 Style" "🔥 Remove" "🚀 Performance" "🏗️ Initial Construction" "🚧 WIP") 12 | for i in "${!gitmoji_list[@]}"; do 13 | echo "$i. ${gitmoji_list[$i]}" 14 | done 15 | 16 | read -p "Enter the number corresponding to the Gitmoji: " gitmoji_number 17 | 18 | if [[ ! "$gitmoji_number" =~ ^[0-9]+$ ]] || [ "$gitmoji_number" -lt 0 ] || [ "$gitmoji_number" -ge "${#gitmoji_list[@]}" ]; then 19 | echo "Invalid selection." 20 | exit 1 21 | fi 22 | 23 | selected_gitmoji="${gitmoji_list[$gitmoji_number]}" 24 | 25 | # Prompt for commit message 26 | read -p "Enter commit message: " commit_message 27 | 28 | # Commit changes 29 | git add . 30 | git commit -m "$selected_gitmoji $commit_message" 31 | git push origin main 32 | echo "Changes committed and pushed successfully." --------------------------------------------------------------------------------