├── .env.development ├── .env.production ├── .env.sample ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── build.ts ├── docker-compose.yml ├── docs └── EMAIL.md ├── ecosystem.config.js ├── eslint.config.mjs ├── logo.webp ├── modules.d.ts ├── package.json ├── pnpm-lock.yaml ├── public ├── index.html ├── logo.webp ├── script.js └── styles.css ├── src ├── common │ ├── common.schema.ts │ └── common.utils.ts ├── config │ └── config.service.ts ├── email │ ├── email.service.ts │ └── templates │ │ └── ResetPassword.tsx ├── enums.ts ├── healthcheck │ ├── healthcheck.controller.ts │ └── healthcheck.routes.ts ├── lib │ ├── aws.service.ts │ ├── common.schema.ts │ ├── database.ts │ ├── email.server.ts │ ├── logger.service.ts │ ├── mailgun.server.ts │ ├── queue.server.ts │ ├── realtime.server.ts │ ├── redis.server.ts │ └── session.store.ts ├── main.ts ├── middlewares │ ├── can-access.middleware.ts │ ├── extract-jwt-schema.middleware.ts │ ├── multer-s3.middleware.ts │ └── validate-zod-schema.middleware.ts ├── modules │ ├── auth │ │ ├── auth.constants.ts │ │ ├── auth.controller.ts │ │ ├── auth.router.ts │ │ ├── auth.schema.ts │ │ └── auth.service.ts │ └── user │ │ ├── user.controller.ts │ │ ├── user.dto.ts │ │ ├── user.model.ts │ │ ├── user.router.ts │ │ ├── user.schema.ts │ │ └── user.services.ts ├── openapi │ ├── magic-router.ts │ ├── openapi.utils.ts │ ├── swagger-doc-generator.ts │ ├── swagger-instance.ts │ └── zod-extend.ts ├── queues │ └── email.queue.ts ├── routes │ └── routes.ts ├── types.ts ├── upload │ ├── upload.controller.ts │ └── upload.router.ts └── utils │ ├── api.utils.ts │ ├── auth.utils.ts │ ├── common.utils.ts │ ├── getPaginator.ts │ ├── globalErrorHandler.ts │ ├── isUsername.ts │ └── responseInterceptor.ts └── tsconfig.json /.env.development: -------------------------------------------------------------------------------- 1 | # APP CONFIG 2 | PORT="3002" 3 | 4 | # FOR CORS AND EMAILS 5 | CLIENT_SIDE_URL="http://localhost:3001" 6 | 7 | # JWT 8 | JWT_SECRET="some-secret" 9 | JWT_EXPIRES_IN=3600 10 | 11 | # NODE_ENV 12 | NODE_ENV="development" 13 | 14 | # SESSION 15 | SESSION_EXPIRES_IN=86400 16 | 17 | # AUTH 18 | PASSWORD_RESET_TOKEN_EXPIRES_IN=86400 19 | SET_PASSWORD_TOKEN_EXPIRES_IN=86400 20 | SET_SESSION=1 21 | 22 | # DATABSES 23 | REDIS_URL="redis://localhost:6380" 24 | MONGO_DATABASE_URL="mongodb://root:example@localhost:27017/typescript-backend-toolkit?authSource=admin" 25 | 26 | # Mailgun Configuration (dummy values for development) 27 | MAILGUN_API_KEY="dummy-key" 28 | MAILGUN_DOMAIN="example.com" 29 | MAILGUN_FROM_EMAIL="no-reply@example.com" 30 | 31 | # ADMIN 32 | ADMIN_EMAIL="admin@example.com" 33 | ADMIN_PASSWORD="password" 34 | 35 | # USER 36 | OTP_VERIFICATION_ENABLED=0 37 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # APP CONFIG 2 | PORT="3000" 3 | 4 | # FOR CORS AND EMAILS 5 | CLIENT_SIDE_URL="http://localhost:3000" 6 | 7 | # JWT 8 | JWT_SECRET="some-secret" 9 | JWT_EXPIRES_IN="1h" 10 | 11 | # SESSION 12 | SESSION_EXPIRES_IN="1d" 13 | 14 | # AUTH 15 | PASSWORD_RESET_TOKEN_EXPIRES_IN="1d" 16 | SET_PASSWORD_TOKEN_EXPIRES_IN="1d" 17 | SET_SESSION=0 18 | 19 | # DATABSES 20 | REDIS_URL="redis://localhost:6380" 21 | MONGO_DATABASE_URL="mongodb://localhost:27018/typescript-backend-toolkit" -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # APP CONFIG 2 | PORT="" 3 | NODE_ENV="" 4 | 5 | # EMAIL (Choose either SMTP or Mailgun configuration) 6 | # SMTP Configuration (Legacy) 7 | SMTP_HOST="" 8 | SMTP_PORT="" 9 | SMTP_USERNAME="" 10 | EMAIL_FROM="" 11 | SMTP_FROM="" 12 | SMTP_PASSWORD="" 13 | 14 | # Mailgun Configuration (Recommended) 15 | MAILGUN_API_KEY="" 16 | MAILGUN_DOMAIN="" 17 | MAILGUN_FROM_EMAIL="" 18 | 19 | # FOR CORS AND EMAILS 20 | CLIENT_SIDE_URL="" 21 | 22 | # JWT 23 | JWT_SECRET="" 24 | JWT_EXPIRES_IN="" 25 | 26 | # SESSION 27 | SESSION_EXPIRES_IN="" 28 | 29 | # AUTH 30 | PASSWORD_RESET_TOKEN_EXPIRES_IN="" 31 | SET_PASSWORD_TOKEN_EXPIRES_IN="" 32 | SET_SESSION=0 33 | 34 | # GOOGLE AUTH 35 | GOOGLE_CLIENT_ID="" 36 | GOOGLE_CLIENT_SECRET='' 37 | GOOGLE_REDIRECT_URI = '' 38 | 39 | # DATABSES 40 | REDIS_URL="" 41 | MONGO_DATABASE_URL="" 42 | 43 | # ADMIN 44 | ADMIN_EMAIL="admin@example.com" 45 | ADMIN_PASSWORD="password" 46 | 47 | # USER 48 | OTP_VERIFICATION_ENABLED=0 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # database 133 | .database 134 | .aider* 135 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 80, 5 | "tabWidth": 2, 6 | "semi": true 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "mattpocock.ts-error-translator", 4 | "yoavbls.pretty-ts-errors", 5 | "prisma.prisma", 6 | "esbenp.prettier-vscode", 7 | "christian-kohler.path-intellisense", 8 | "dbaeumer.vscode-eslint", 9 | "ms-azuretools.vscode-docker", 10 | "digitalbrainstem.javascript-ejs-support", 11 | "j69.ejs-beautify", 12 | "dbaeumer.vscode-eslint" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.useFlatConfig": true, 3 | "typescript.preferences.importModuleSpecifier": "relative", 4 | "git.ignoreLimitWarning": true, 5 | "prettier.useEditorConfig": false, 6 | "totalTypeScript.hideAllTips": true, 7 | "typescript.tsdk": "node_modules/typescript/lib", 8 | "emmet.includeLanguages": { 9 | "ejs": "html" 10 | }, 11 | "[html]": { 12 | "editor.defaultFormatter": "j69.ejs-beautify" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Muneeb Hussain 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 |

2 | Zod logo 3 |

✨ TypeScript Backend Toolkit ✨

4 |

5 |
6 | Robust backend boilerplate designed for scalability, flexibility, and ease of development. It's packed with modern technologies and best practices to kickstart your next backend project. 7 |

8 |

9 |
10 | 11 | ## Prerequisites 12 | 13 | Before you get started, make sure you have the following installed on your machine: 14 | 15 | - **Docker + Docker Compose** 16 | - **PNPM** 17 | - **Node.js 20+ (LTS)** 18 | 19 | ## How to Run 20 | 21 | 1. **Set up Docker Services**: 22 | 23 | - Run the following command to start MongoDB and Redis instances locally: 24 | ```sh 25 | docker compose up -d 26 | ``` 27 | 2. **Install Dependencies**: 28 | 29 | - Use pnpm to install all the necessary dependencies: 30 | ```sh 31 | pnpm i 32 | ``` 33 | 3. **Configure Environment Variables**: 34 | 35 | - Create a `.env` file in the root directory. 36 | - Use the provided `.env.sample` as a template to enter all the required environment variables. 37 | 38 | ## What's Included 39 | 40 | - **OpenAPI Autogenerated Swagger Docs** : Automatically generated Swagger docs through MagicRouter API and Zod, accessible at `/api-docs`. 41 | - **Auth Module**: Includes Google Sign-In support for easy authentication. 42 | - **User Management**: Comprehensive user management functionality. 43 | - **File Upload**: Handles file uploads with Multer and Amazon S3. 44 | - **Data Validation & Serialization**: Zod is used for validation and serialization of data. 45 | - **Configuration Management**: Managed using dotenv-cli and validated with Zod for accuracy and safety. 46 | - **Middlewares**: 47 | - **Authorization**: Built-in authorization middleware. 48 | - **Zod Schema Validation**: Ensures your API inputs are correctly validated. 49 | - **JWT Extraction**: Easily extract and verify JWT tokens. 50 | - **Type-safe Email Handling**: Emails are managed using React Email and Mailgun for dynamic and flexible email handling. 51 | - **Queues**: Powered by BullMQ with Redis for handling background jobs. 52 | - **ESLint Setup**: Pre-configured ESLint setup for consistent code quality. 53 | ```sh 54 | pnpm run lint 55 | ``` 56 | - **Development Server**: Run the server in development mode using ts-node-dev: 57 | ```sh 58 | pnpm run dev 59 | ``` 60 | - **Build Process**: Efficiently bundle your project using tsup: 61 | ```sh 62 | pnpm run build 63 | ``` 64 | - **PM2 Support**: Out-of-the-box support for PM2 to manage your production processes. 65 | 66 | ## Folder Structure 67 | ```plaintext 68 | ├── build.ts 69 | ├── docker-compose.yml 70 | ├── docs 71 | │   └── EMAIL.md 72 | ├── ecosystem.config.js 73 | ├── eslint.config.mjs 74 | ├── LICENSE 75 | ├── logo.webp 76 | ├── modules.d.ts 77 | ├── package.json 78 | ├── pnpm-lock.yaml 79 | ├── public 80 | │   ├── index.html 81 | │   ├── logo.webp 82 | │   ├── script.js 83 | │   └── styles.css 84 | ├── README.md 85 | ├── src 86 | │   ├── common 87 | │   │   ├── common.schema.ts 88 | │   │   └── common.utils.ts 89 | │   ├── config 90 | │   │   └── config.service.ts 91 | │   ├── email 92 | │   │   ├── email.service.ts 93 | │   │   └── templates 94 | │   │   └── ResetPassword.tsx 95 | │   ├── enums.ts 96 | │   ├── healthcheck 97 | │   │   ├── healthcheck.controller.ts 98 | │   │   └── healthcheck.routes.ts 99 | │   ├── lib 100 | │   │   ├── aws.service.ts 101 | │   │   ├── common.schema.ts 102 | │   │   ├── database.ts 103 | │   │   ├── email.server.ts 104 | │   │   ├── logger.service.ts 105 | │   │   ├── mailgun.server.ts 106 | │   │   ├── queue.server.ts 107 | │   │   ├── realtime.server.ts 108 | │   │   ├── redis.server.ts 109 | │   │   └── session.store.ts 110 | │   ├── main.ts 111 | │   ├── middlewares 112 | │   │   ├── can-access.middleware.ts 113 | │   │   ├── extract-jwt-schema.middleware.ts 114 | │   │   ├── multer-s3.middleware.ts 115 | │   │   └── validate-zod-schema.middleware.ts 116 | │   ├── modules 117 | │   │   ├── auth 118 | │   │   │   ├── auth.constants.ts 119 | │   │   │   ├── auth.controller.ts 120 | │   │   │   ├── auth.router.ts 121 | │   │   │   ├── auth.schema.ts 122 | │   │   │   └── auth.service.ts 123 | │   │   └── user 124 | │   │   ├── user.controller.ts 125 | │   │   ├── user.dto.ts 126 | │   │   ├── user.model.ts 127 | │   │   ├── user.router.ts 128 | │   │   ├── user.schema.ts 129 | │   │   └── user.services.ts 130 | │   ├── openapi 131 | │   │   ├── magic-router.ts 132 | │   │   ├── openapi.utils.ts 133 | │   │   ├── swagger-doc-generator.ts 134 | │   │   ├── swagger-instance.ts 135 | │   │   └── zod-extend.ts 136 | │   ├── queues 137 | │   │   └── email.queue.ts 138 | │   ├── routes 139 | │   │   └── routes.ts 140 | │   ├── types.ts 141 | │   ├── upload 142 | │   │   ├── upload.controller.ts 143 | │   │   └── upload.router.ts 144 | │   └── utils 145 | │   ├── api.utils.ts 146 | │   ├── auth.utils.ts 147 | │   ├── common.utils.ts 148 | │   ├── email.utils.ts 149 | │   ├── getPaginator.ts 150 | │   ├── globalErrorHandler.ts 151 | │   ├── isUsername.ts 152 | │   └── responseInterceptor.ts 153 | └── tsconfig.json 154 | ``` 155 | 156 | ## Roadmap 157 | 158 | - **Socket.io Support:** Adding support for Redis adapter and a chat module. 159 | - **Notification Infrastructure**: Notifications via FCM and Novu. 160 | - **Ansible Playbook** : Create an Ansible playbook for server configuration to set up a basic environment quickly and consistently. 161 | - **AWS CDK Support** : Integrate AWS CDK for infrastructure management, making it easier to deploy and manage cloud resources. 162 | - **Monorepo Support** : Implement monorepo architecture using Turborepo and Pnpm for better project organization and scalability. 163 | - **AWS Lambda Support** : Add support for deploying serverless functions on AWS Lambda. 164 | - **Cloudflare Workers Support** : Enable Cloudflare Workers support for edge computing and faster request handling. 165 | - **Containerization with Docker** : Implement containerization to ensure the project can be easily deployed to any environment using Docker. 166 | - **Kubernetes Support** : Integrate Kubernetes for container orchestration, enabling scalable and automated deployment of the application. 167 | - **CI/CD with GitHub Actions** : Implement a CI/CD pipeline using GitHub Actions to automate testing, building, and deployment processes. 168 | - **Testing with Jest**: Add support for unit and integration testing using Jest to ensure code reliability and maintainability. 169 | 170 | ## Contributions 171 | 172 | Feel free to contribute to this project by submitting issues or pull requests. Let's build something amazing together! 173 | 174 | ## **License** 175 | 176 | This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details. 177 | -------------------------------------------------------------------------------- /build.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/main.ts'], 5 | splitting: false, 6 | sourcemap: true, 7 | clean: true, 8 | bundle: true, 9 | minify: true, 10 | platform: 'node', 11 | tsconfig: 'tsconfig.json', 12 | keepNames: true, 13 | }); 14 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mongo: 3 | image: mongo:5.0.2 4 | restart: 'unless-stopped' 5 | ports: 6 | - '27017:27017' 7 | environment: 8 | MONGO_INITDB_ROOT_USERNAME: root 9 | MONGO_INITDB_ROOT_PASSWORD: example 10 | volumes: 11 | - mongodb_ts_toolkit:/data/db 12 | 13 | redis: 14 | image: redis:latest 15 | ports: 16 | - 6380:6379 17 | volumes: 18 | - redis_ts_toolkit:/data 19 | 20 | volumes: 21 | mongodb_ts_toolkit: 22 | redis_ts_toolkit: 23 | -------------------------------------------------------------------------------- /docs/EMAIL.md: -------------------------------------------------------------------------------- 1 | # Email Service Documentation 2 | 3 | This document outlines the email service implementation using React Email for templating and Mailgun for delivery. 4 | 5 | ## Overview 6 | 7 | The email service provides a robust, type-safe way to send transactional emails using: 8 | - [React Email](https://react.email/) for building and maintaining email templates 9 | - [Mailgun](https://www.mailgun.com/) for reliable email delivery 10 | - TypeScript for type safety and better developer experience 11 | 12 | ## Configuration 13 | 14 | ### Environment Variables 15 | 16 | Add the following variables to your `.env` file: 17 | 18 | ```env 19 | MAILGUN_API_KEY="your-mailgun-api-key" 20 | MAILGUN_DOMAIN="your-mailgun-domain" 21 | MAILGUN_FROM_EMAIL="noreply@yourdomain.com" 22 | ``` 23 | 24 | ## Email Templates 25 | 26 | Email templates are built using React Email components and are located in `src/email/templates/`. Each template is a React component that accepts typed props for the dynamic content. 27 | 28 | ### Available Templates 29 | 30 | 1. **Reset Password Email** (`ResetPassword.tsx`) 31 | ```typescript 32 | interface ResetPasswordEmailProps { 33 | userName: string; 34 | resetLink: string; 35 | } 36 | ``` 37 | 38 | ## Usage 39 | 40 | ### Sending Reset Password Email 41 | 42 | ```typescript 43 | import { sendResetPasswordEmail } from '../email/email.service'; 44 | 45 | await sendResetPasswordEmail({ 46 | email: 'user@example.com', 47 | userName: 'John Doe', 48 | resetLink: 'https://yourdomain.com/reset-password?token=xyz' 49 | }); 50 | ``` 51 | 52 | ### Creating New Email Templates 53 | 54 | 1. Create a new template in `src/email/templates/` 55 | 2. Use React Email components for consistent styling 56 | 3. Export the template component with proper TypeScript interfaces 57 | 4. Add a new method in `EmailService` class to send the email 58 | 59 | Example: 60 | ```typescript 61 | // src/email/templates/WelcomeEmail.tsx 62 | import * as React from 'react'; 63 | import { Button, Container, Head, Html, Preview, Text } from '@react-email/components'; 64 | 65 | interface WelcomeEmailProps { 66 | userName: string; 67 | } 68 | 69 | export const WelcomeEmail = ({ userName }: WelcomeEmailProps) => ( 70 | 71 | 72 | Welcome to our platform 73 | 74 | Welcome {userName}! 75 | 78 | 79 | 80 | ); 81 | 82 | export default WelcomeEmail; 83 | ``` 84 | 85 | ## Error Handling 86 | 87 | The email service includes comprehensive error handling: 88 | 89 | - Custom `EmailError` class for email-specific errors 90 | - Detailed error logging using the application logger 91 | - Type-safe error propagation 92 | 93 | ## Benefits 94 | 95 | 1. **Type Safety**: Full TypeScript support for templates and service methods 96 | 2. **Maintainable Templates**: React components for building and maintaining email templates 97 | 3. **Reliable Delivery**: Mailgun integration for professional email delivery 98 | 4. **Error Handling**: Comprehensive error handling and logging 99 | 5. **Developer Experience**: Easy to create and modify email templates using React 100 | 101 | ## Migration from Nodemailer 102 | 103 | The service maintains backward compatibility with the previous Nodemailer implementation through exported functions. The internal implementation has been updated to use React Email and Mailgun while keeping the same interface. 104 | 105 | ## Testing Emails 106 | 107 | To test emails in development: 108 | 109 | 1. Set up a Mailgun sandbox domain (free) 110 | 2. Use the sandbox domain and API key in your `.env.development` 111 | 3. Add verified recipient emails in Mailgun sandbox settings 112 | 4. Use these verified emails for testing 113 | 114 | ## Best Practices 115 | 116 | 1. Always use TypeScript interfaces for template props 117 | 2. Include proper error handling in your email sending logic 118 | 3. Use React Email components for consistent styling 119 | 4. Test emails with different email clients 120 | 5. Keep templates simple and mobile-responsive 121 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'typescript-backend-toolkit', 5 | script: './dist/main.js', 6 | }, 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import pluginJs from '@eslint/js'; 2 | import globals from 'globals'; 3 | import tseslint from 'typescript-eslint'; 4 | 5 | export default [ 6 | { 7 | languageOptions: { globals: globals.node }, 8 | }, 9 | { 10 | ignores: [ 11 | 'node_modules/*', 12 | 'node_modules', 13 | 'node_modules/**/*', 14 | 'dist/*', 15 | 'dist/**/*', 16 | 'dist', 17 | '.database', 18 | '.database/*', 19 | ], 20 | }, 21 | pluginJs.configs.recommended, 22 | ...tseslint.configs.recommended, 23 | { 24 | rules: { 25 | "@typescript-eslint/no-explicit-any": "warn", 26 | '@typescript-eslint/no-unused-vars': [ 27 | 'error', 28 | { 29 | args: 'all', 30 | argsIgnorePattern: '^_', 31 | caughtErrors: 'all', 32 | caughtErrorsIgnorePattern: '^_', 33 | destructuredArrayIgnorePattern: '^_', 34 | varsIgnorePattern: '^_', 35 | ignoreRestSiblings: true, 36 | }, 37 | ], 38 | }, 39 | }, 40 | ]; 41 | -------------------------------------------------------------------------------- /logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muneebhashone/typescript-backend-toolkit/0fd76f622a79412c2596a24d70d6c8e18356f24f/logo.webp -------------------------------------------------------------------------------- /modules.d.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'socket.io'; 2 | import { JwtPayload } from '../utils/auth.utils'; 3 | import { Config } from './src/config/config.service'; 4 | 5 | declare global { 6 | namespace Express { 7 | export interface Request { 8 | user: JwtPayload; 9 | io: Server; 10 | } 11 | } 12 | 13 | namespace NodeJS { 14 | export interface ProcessEnv extends Config {} 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-backend-toolkit", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/main.js", 6 | "scripts": { 7 | "start:dev": "dotenv -e .env.development -- tsx --watch ./src/main.ts", 8 | "seeder": "tsx ./src/seeder.ts", 9 | "build": "tsup --config build.ts", 10 | "start:prod": "dotenv -e .env.production -- node ./dist/main.js", 11 | "start:local": "dotenv -e .env.local -- node ./dist/main.js", 12 | "lint": "eslint", 13 | "lint:fix": "eslint --fix", 14 | "email:dev": "email dev --dir ./src/email/templates", 15 | "dev": "concurrently \"pnpm start:dev\" \"pnpm email:dev\"" 16 | }, 17 | "devDependencies": { 18 | "@eslint/js": "^9.4.0", 19 | "@types/cookie-parser": "^1.4.3", 20 | "@types/cors": "^2.8.13", 21 | "@types/express": "^4.17.15", 22 | "@types/express-session": "^1.17.5", 23 | "@types/helmet": "^4.0.0", 24 | "@types/http-status-codes": "^1.2.0", 25 | "@types/jsonwebtoken": "^9.0.6", 26 | "@types/memory-cache": "^0.2.2", 27 | "@types/morgan": "^1.9.4", 28 | "@types/multer": "^1.4.7", 29 | "@types/multer-s3": "^3.0.3", 30 | "@types/node": "^18.11.18", 31 | "@types/nodemailer": "^6.4.8", 32 | "@types/passport": "^1.0.11", 33 | "@types/swagger-ui-express": "^4.1.6", 34 | "@types/validator": "^13.7.17", 35 | "@typescript-eslint/eslint-plugin": "^5.62.0", 36 | "@typescript-eslint/parser": "^7.11.0", 37 | "concurrently": "^9.1.0", 38 | "esbuild": "^0.19.8", 39 | "eslint": "~9.4.0", 40 | "eslint-config-prettier": "^9.1.0", 41 | "eslint-plugin-import": "^2.29.1", 42 | "eslint-plugin-prettier": "^5.1.3", 43 | "globals": "^15.3.0", 44 | "rimraf": "^5.0.1", 45 | "tsup": "^8.1.0", 46 | "tsx": "^4.19.2", 47 | "typescript": "*", 48 | "typescript-eslint": "^7.11.0" 49 | }, 50 | "dependencies": { 51 | "@asteasolutions/zod-to-openapi": "^7.1.1", 52 | "@aws-sdk/client-s3": "^3.606.0", 53 | "@bull-board/api": "^5.19.0", 54 | "@bull-board/express": "^5.16.0", 55 | "@react-email/components": "^0.0.28", 56 | "@react-email/render": "^1.0.2", 57 | "@types/compression": "^1.7.2", 58 | "@types/react": "^18.3.12", 59 | "argon2": "^0.30.3", 60 | "axios": "^1.4.0", 61 | "bullmq": "^5.7.6", 62 | "compression": "^1.7.4", 63 | "connect-redis": "^7.1.1", 64 | "cookie-parser": "^1.4.6", 65 | "cors": "^2.8.5", 66 | "cross-env": "^7.0.3", 67 | "dotenv": "^16.4.5", 68 | "dotenv-cli": "^7.4.2", 69 | "express": "^4.19.2", 70 | "express-async-handler": "^1.2.0", 71 | "express-session": "^1.18.0", 72 | "helmet": "^6.0.1", 73 | "http-status-codes": "^2.3.0", 74 | "ioredis": "^5.3.2", 75 | "jsonwebtoken": "^9.0.2", 76 | "mailgun.js": "^10.2.4", 77 | "mongoose": "^8.5.1", 78 | "morgan": "^1.10.0", 79 | "multer": "^1.4.5-lts.1", 80 | "multer-s3": "^3.0.1", 81 | "nanoid": "^3.3.7", 82 | "nodemailer": "^6.9.13", 83 | "openapi3-ts": "^4.3.3", 84 | "passport": "^0.7.0", 85 | "passport-jwt": "^4.0.1", 86 | "pino": "^9.1.0", 87 | "pino-http": "^10.1.0", 88 | "pino-pretty": "^11.1.0", 89 | "react": "^18.3.1", 90 | "react-email": "^3.0.2", 91 | "redis": "^4.6.11", 92 | "socket.io": "^4.7.5", 93 | "swagger-ui-express": "^5.0.1", 94 | "validator": "^13.12.0", 95 | "yaml": "^2.5.0", 96 | "zod": "^3.21.4" 97 | }, 98 | "author": "", 99 | "license": "ISC", 100 | "packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1" 101 | } 102 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TypeScript Backend Toolkit 7 | 8 | 9 | 10 | 11 | 12 |
13 | 26 |
27 | 28 |
29 |
30 |
31 |

TypeScript Backend Toolkit

32 |

33 | A robust backend boilerplate designed for scalability, flexibility, and ease of development. 34 | Packed with modern technologies and best practices to kickstart your next backend project. 35 |

36 | 46 |
47 |
48 |
49 |
50 | 51 | 52 | 53 |
54 |
# Start MongoDB and Redis
 55 | docker compose up -d
 56 | 
 57 | # Install dependencies
 58 | pnpm install
 59 | 
 60 | # Start development server
 61 | pnpm run dev
 62 | 
 63 | 🚀 Server running at http://localhost:3000
 64 | 📚 API Docs: http://localhost:3000/api-docs
 65 | 📊 Queue Dashboard: http://localhost:3000/admin/queues
66 |
67 |
68 |
69 | 70 |
71 |

What's Included

72 |
73 |
74 | 75 |

OpenAPI Docs

76 |

Auto-generated Swagger documentation through MagicRouter API and Zod for perfect type safety

77 |
78 |
79 | 80 |

Auth Module

81 |

Complete authentication system with Google Sign-In support and JWT handling

82 |
83 |
84 | 85 |

User Management

86 |

Comprehensive user management with role-based access control and profile handling

87 |
88 |
89 | 90 |

File Upload

91 |

Seamless file uploads with Multer and Amazon S3 integration for scalable storage

92 |
93 |
94 | 95 |

Data Validation

96 |

Type-safe data validation and serialization powered by Zod

97 |
98 |
99 | 100 |

Config Management

101 |

Environment configuration with dotenv-cli and Zod validation for type safety

102 |
103 |
104 |
105 | 106 |
107 |

Before You Start

108 |

109 | To ensure a smooth development experience, make sure you have these essential tools installed. 110 | They form the foundation of your development environment and are crucial for running the project efficiently. 111 |

112 |
113 |
114 | 115 |

Docker + Docker Compose

116 |

Required for running MongoDB and Redis services in isolated containers

117 |
118 |
119 | 120 |

PNPM

121 |

Fast, disk space efficient package manager for managing dependencies

122 |
123 |
124 | 125 |

Node.js 20+ (LTS)

126 |

Latest LTS version for optimal performance and security

127 |
128 |
129 |
130 | 131 |
132 |

What's Next

133 |

134 | Our development roadmap outlines exciting features and improvements planned for future releases. 135 | These additions will enhance scalability, developer experience, and deployment options. 136 |

137 |
138 |
139 | 140 |

Real-time Support

141 |

Socket.io integration with Redis adapter for scalable real-time communication

142 |
143 |
144 | 145 |

Advanced Notifications

146 |

FCM & Novu integration for powerful, multi-channel notifications

147 |
148 |
149 | 150 |

Server Automation

151 |

Ansible playbooks for automated server provisioning and configuration

152 |
153 |
154 | 155 |

Cloud Infrastructure

156 |

AWS CDK support for infrastructure as code and easy cloud deployment

157 |
158 |
159 | 160 |

Monorepo Structure

161 |

Turborepo integration for efficient monorepo management

162 |
163 |
164 | 165 |

Serverless Ready

166 |

Support for AWS Lambda and Cloudflare Workers deployment

167 |
168 |
169 |
170 |
171 | 172 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /public/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muneebhashone/typescript-backend-toolkit/0fd76f622a79412c2596a24d70d6c8e18356f24f/public/logo.webp -------------------------------------------------------------------------------- /public/script.js: -------------------------------------------------------------------------------- 1 | // Add smooth scrolling for navigation links 2 | document.querySelectorAll('a[href^="#"]').forEach(anchor => { 3 | anchor.addEventListener('click', function (e) { 4 | e.preventDefault(); 5 | const target = document.querySelector(this.getAttribute('href')); 6 | if (target) { 7 | target.scrollIntoView({ 8 | behavior: 'smooth', 9 | block: 'start' 10 | }); 11 | } 12 | }); 13 | }); 14 | 15 | // Header transparency on scroll 16 | const header = document.querySelector('header'); 17 | let lastScroll = 0; 18 | 19 | window.addEventListener('scroll', () => { 20 | const currentScroll = window.pageYOffset; 21 | 22 | if (currentScroll <= 0) { 23 | header.classList.remove('scrolled'); 24 | } else { 25 | header.classList.add('scrolled'); 26 | } 27 | 28 | lastScroll = currentScroll; 29 | }); 30 | 31 | // Intersection Observer for fade-in animations 32 | const observerOptions = { 33 | root: null, 34 | rootMargin: '0px', 35 | threshold: 0.1 36 | }; 37 | 38 | const observer = new IntersectionObserver((entries) => { 39 | entries.forEach(entry => { 40 | if (entry.isIntersecting) { 41 | entry.target.classList.add('visible'); 42 | observer.unobserve(entry.target); 43 | } 44 | }); 45 | }, observerOptions); 46 | 47 | // Observe all feature cards, roadmap items, and prerequisite items 48 | const animatedElements = document.querySelectorAll('.feature-card, .roadmap-item, .prerequisite-item'); 49 | animatedElements.forEach(element => { 50 | element.classList.add('fade-in'); 51 | observer.observe(element); 52 | }); 53 | 54 | // Add CSS classes for animations 55 | const style = document.createElement('style'); 56 | style.textContent = ` 57 | .fade-in { 58 | opacity: 0; 59 | transform: translateY(20px); 60 | transition: opacity 0.6s ease-out, transform 0.6s ease-out; 61 | } 62 | 63 | .fade-in.visible { 64 | opacity: 1; 65 | transform: translateY(0); 66 | } 67 | 68 | header.scrolled { 69 | background-color: rgba(255, 255, 255, 0.9); 70 | backdrop-filter: blur(10px); 71 | } 72 | `; 73 | document.head.appendChild(style); -------------------------------------------------------------------------------- /public/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: #6d28d9; 3 | --primary-dark: #5b21b6; 4 | --secondary: #f97316; 5 | --slate-800: #1e293b; 6 | --slate-700: #334155; 7 | --slate-600: #475569; 8 | --slate-100: #f1f5f9; 9 | --white: #ffffff; 10 | --max-width: 1200px; 11 | --border-radius: 12px; 12 | } 13 | 14 | * { 15 | margin: 0; 16 | padding: 0; 17 | box-sizing: border-box; 18 | } 19 | 20 | html { 21 | scroll-behavior: smooth; 22 | scroll-padding-top: 80px; 23 | } 24 | 25 | body { 26 | font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 27 | line-height: 1.6; 28 | color: var(--slate-800); 29 | background: linear-gradient(135deg, var(--slate-100), #ffffff); 30 | min-height: 100vh; 31 | } 32 | 33 | /* Section Description */ 34 | .section-description { 35 | text-align: center; 36 | max-width: 800px; 37 | margin: 0 auto 3rem; 38 | color: var(--slate-600); 39 | font-size: 1.125rem; 40 | line-height: 1.7; 41 | padding: 0 1rem; 42 | } 43 | 44 | /* Header & Navigation */ 45 | header { 46 | background-color: rgba(255, 255, 255, 0.95); 47 | backdrop-filter: blur(10px); 48 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); 49 | position: fixed; 50 | width: 100%; 51 | top: 0; 52 | z-index: 1000; 53 | transition: all 0.3s ease; 54 | } 55 | 56 | nav { 57 | max-width: var(--max-width); 58 | margin: 0 auto; 59 | padding: 1rem 2rem; 60 | display: flex; 61 | justify-content: space-between; 62 | align-items: center; 63 | } 64 | 65 | .logo { 66 | display: flex; 67 | align-items: center; 68 | } 69 | 70 | .logo img { 71 | height: 40px; 72 | width: auto; 73 | object-fit: contain; 74 | } 75 | 76 | .nav-links { 77 | display: flex; 78 | gap: 2rem; 79 | align-items: center; 80 | } 81 | 82 | .nav-links a { 83 | color: var(--slate-700); 84 | text-decoration: none; 85 | font-weight: 500; 86 | transition: all 0.3s ease; 87 | padding: 0.5rem 1rem; 88 | border-radius: var(--border-radius); 89 | } 90 | 91 | .nav-links a:hover { 92 | color: var(--primary); 93 | background-color: rgba(109, 40, 217, 0.1); 94 | } 95 | 96 | .github-link { 97 | background-color: var(--slate-800); 98 | color: var(--white) !important; 99 | padding: 0.75rem 1.25rem !important; 100 | border-radius: var(--border-radius); 101 | display: flex; 102 | align-items: center; 103 | gap: 0.5rem; 104 | transition: all 0.3s ease !important; 105 | } 106 | 107 | .github-link:hover { 108 | background-color: var(--primary) !important; 109 | transform: translateY(-2px); 110 | } 111 | 112 | /* Hero Section */ 113 | .hero { 114 | padding: 8rem 2rem 6rem; 115 | max-width: var(--max-width); 116 | margin: 0 auto; 117 | display: grid; 118 | grid-template-columns: 1.2fr 0.8fr; 119 | gap: 4rem; 120 | align-items: center; 121 | min-height: 90vh; 122 | } 123 | 124 | .hero-content { 125 | position: relative; 126 | } 127 | 128 | .hero-content h1 { 129 | font-size: 4rem; 130 | line-height: 1.1; 131 | margin-bottom: 1.5rem; 132 | background: linear-gradient(135deg, var(--primary), var(--secondary)); 133 | -webkit-background-clip: text; 134 | -webkit-text-fill-color: transparent; 135 | position: relative; 136 | } 137 | 138 | .hero-content h1::after { 139 | content: ''; 140 | position: absolute; 141 | bottom: -10px; 142 | left: 0; 143 | width: 100px; 144 | height: 4px; 145 | background: linear-gradient(135deg, var(--primary), var(--secondary)); 146 | border-radius: 2px; 147 | } 148 | 149 | .hero-description { 150 | font-size: 1.25rem; 151 | color: var(--slate-700); 152 | margin-bottom: 2.5rem; 153 | max-width: 600px; 154 | } 155 | 156 | .cta-buttons { 157 | display: flex; 158 | gap: 1.5rem; 159 | } 160 | 161 | .cta-primary, .cta-secondary { 162 | padding: 1rem 2rem; 163 | border-radius: var(--border-radius); 164 | text-decoration: none; 165 | font-weight: 600; 166 | transition: all 0.3s ease; 167 | display: inline-flex; 168 | align-items: center; 169 | gap: 0.5rem; 170 | } 171 | 172 | .cta-primary { 173 | background-color: var(--primary); 174 | color: var(--white); 175 | box-shadow: 0 4px 6px rgba(109, 40, 217, 0.2); 176 | } 177 | 178 | .cta-secondary { 179 | background-color: var(--white); 180 | color: var(--primary); 181 | border: 2px solid var(--primary); 182 | } 183 | 184 | .cta-primary:hover, .cta-secondary:hover { 185 | transform: translateY(-2px); 186 | box-shadow: 0 6px 12px rgba(109, 40, 217, 0.2); 187 | } 188 | 189 | /* Code Preview */ 190 | .code-preview { 191 | background-color: var(--slate-800); 192 | padding: 2rem; 193 | border-radius: var(--border-radius); 194 | color: var(--white); 195 | font-family: 'Fira Code', monospace; 196 | box-shadow: 0 8px 24px rgba(0,0,0,0.2); 197 | position: relative; 198 | overflow: hidden; 199 | } 200 | 201 | .code-dots { 202 | position: absolute; 203 | top: 1rem; 204 | left: 1rem; 205 | display: flex; 206 | gap: 0.5rem; 207 | } 208 | 209 | .code-dots span { 210 | width: 12px; 211 | height: 12px; 212 | border-radius: 50%; 213 | display: block; 214 | } 215 | 216 | .code-dots span:nth-child(1) { 217 | background-color: #ff5f56; 218 | } 219 | 220 | .code-dots span:nth-child(2) { 221 | background-color: #ffbd2e; 222 | } 223 | 224 | .code-dots span:nth-child(3) { 225 | background-color: #27c93f; 226 | } 227 | 228 | .code-preview pre { 229 | margin-top: 1.5rem; 230 | font-size: 0.9rem; 231 | line-height: 1.5; 232 | } 233 | 234 | .code-preview code { 235 | color: #a5b4fc; 236 | } 237 | 238 | /* Features Section */ 239 | .features { 240 | padding: 6rem 2rem; 241 | max-width: var(--max-width); 242 | margin: 0 auto; 243 | } 244 | 245 | .features h2 { 246 | text-align: center; 247 | font-size: 2.5rem; 248 | margin-bottom: 4rem; 249 | color: var(--slate-800); 250 | position: relative; 251 | } 252 | 253 | .features h2::after { 254 | content: ''; 255 | position: absolute; 256 | bottom: -10px; 257 | left: 50%; 258 | transform: translateX(-50%); 259 | width: 60px; 260 | height: 4px; 261 | background: linear-gradient(135deg, var(--primary), var(--secondary)); 262 | border-radius: 2px; 263 | } 264 | 265 | .features-grid { 266 | display: grid; 267 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 268 | gap: 2rem; 269 | } 270 | 271 | .feature-card { 272 | background-color: var(--white); 273 | padding: 2.5rem 2rem; 274 | border-radius: var(--border-radius); 275 | text-align: center; 276 | transition: all 0.3s ease; 277 | box-shadow: 0 4px 6px rgba(0,0,0,0.05); 278 | position: relative; 279 | overflow: hidden; 280 | } 281 | 282 | .feature-card::before { 283 | content: ''; 284 | position: absolute; 285 | top: 0; 286 | left: 0; 287 | width: 100%; 288 | height: 4px; 289 | background: linear-gradient(135deg, var(--primary), var(--secondary)); 290 | transform: scaleX(0); 291 | transition: transform 0.3s ease; 292 | } 293 | 294 | .feature-card:hover::before { 295 | transform: scaleX(1); 296 | } 297 | 298 | .feature-card:hover { 299 | transform: translateY(-5px); 300 | box-shadow: 0 8px 16px rgba(0,0,0,0.1); 301 | } 302 | 303 | .feature-card i { 304 | font-size: 2.5rem; 305 | color: var(--primary); 306 | margin-bottom: 1.5rem; 307 | } 308 | 309 | .feature-card h3 { 310 | margin-bottom: 1rem; 311 | color: var(--slate-800); 312 | font-size: 1.5rem; 313 | } 314 | 315 | .feature-card p { 316 | color: var(--slate-700); 317 | line-height: 1.6; 318 | } 319 | 320 | /* Prerequisites Section */ 321 | .prerequisites { 322 | padding: 6rem 2rem; 323 | background: linear-gradient(to bottom, var(--white), var(--slate-100)); 324 | position: relative; 325 | overflow: hidden; 326 | } 327 | 328 | .prerequisites::before { 329 | content: ''; 330 | position: absolute; 331 | top: 0; 332 | left: 0; 333 | right: 0; 334 | height: 4px; 335 | background: linear-gradient(135deg, var(--primary), var(--secondary)); 336 | } 337 | 338 | .prerequisites h2 { 339 | text-align: center; 340 | font-size: 2.5rem; 341 | margin-bottom: 1.5rem; 342 | position: relative; 343 | } 344 | 345 | .prerequisites-list { 346 | max-width: var(--max-width); 347 | margin: 0 auto; 348 | display: grid; 349 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 350 | gap: 2rem; 351 | } 352 | 353 | .prerequisite-item { 354 | text-align: center; 355 | padding: 2.5rem 2rem; 356 | transition: all 0.3s ease; 357 | background-color: var(--white); 358 | border-radius: var(--border-radius); 359 | box-shadow: 0 4px 6px rgba(0,0,0,0.05); 360 | display: flex; 361 | flex-direction: column; 362 | align-items: center; 363 | height: 100%; 364 | } 365 | 366 | .prerequisite-item:hover { 367 | transform: translateY(-5px); 368 | box-shadow: 0 8px 16px rgba(0,0,0,0.1); 369 | } 370 | 371 | .prerequisite-item i { 372 | font-size: 3rem; 373 | color: var(--primary); 374 | margin-bottom: 1.5rem; 375 | } 376 | 377 | .prerequisite-item h3 { 378 | color: var(--slate-800); 379 | font-size: 1.25rem; 380 | margin-bottom: 1rem; 381 | } 382 | 383 | .prerequisite-item p { 384 | color: var(--slate-600); 385 | font-size: 0.95rem; 386 | line-height: 1.6; 387 | } 388 | 389 | /* Roadmap Section */ 390 | .roadmap { 391 | padding: 6rem 2rem; 392 | max-width: var(--max-width); 393 | margin: 0 auto; 394 | background-color: var(--white); 395 | } 396 | 397 | .roadmap h2 { 398 | text-align: center; 399 | font-size: 2.5rem; 400 | margin-bottom: 1.5rem; 401 | position: relative; 402 | } 403 | 404 | .roadmap-grid { 405 | display: grid; 406 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 407 | gap: 2rem; 408 | } 409 | 410 | .roadmap-item { 411 | background-color: var(--white); 412 | padding: 2.5rem 2rem; 413 | border-radius: var(--border-radius); 414 | text-align: center; 415 | transition: all 0.3s ease; 416 | box-shadow: 0 4px 6px rgba(0,0,0,0.05); 417 | height: 100%; 418 | display: flex; 419 | flex-direction: column; 420 | align-items: center; 421 | } 422 | 423 | .roadmap-item:hover { 424 | transform: translateY(-5px); 425 | box-shadow: 0 8px 16px rgba(0,0,0,0.1); 426 | } 427 | 428 | .roadmap-item i { 429 | font-size: 2.5rem; 430 | color: var(--primary); 431 | margin-bottom: 1.5rem; 432 | } 433 | 434 | .roadmap-item h3 { 435 | color: var(--slate-800); 436 | font-size: 1.25rem; 437 | margin-bottom: 1rem; 438 | } 439 | 440 | .roadmap-item p { 441 | color: var(--slate-600); 442 | font-size: 0.95rem; 443 | line-height: 1.6; 444 | } 445 | 446 | /* Footer */ 447 | footer { 448 | background-color: var(--slate-800); 449 | color: var(--white); 450 | padding: 3rem 2rem; 451 | margin-top: 4rem; 452 | } 453 | 454 | .footer-content { 455 | max-width: var(--max-width); 456 | margin: 0 auto; 457 | display: flex; 458 | justify-content: space-between; 459 | align-items: center; 460 | } 461 | 462 | .social-links a { 463 | color: var(--white); 464 | font-size: 1.5rem; 465 | transition: all 0.3s ease; 466 | margin-left: 1rem; 467 | } 468 | 469 | .social-links a:hover { 470 | color: var(--secondary); 471 | transform: translateY(-2px); 472 | } 473 | 474 | /* Responsive Design */ 475 | @media (max-width: 1024px) { 476 | .hero { 477 | grid-template-columns: 1fr; 478 | text-align: center; 479 | gap: 3rem; 480 | padding-top: 6rem; 481 | min-height: auto; 482 | } 483 | 484 | .hero-content h1 { 485 | font-size: 3rem; 486 | } 487 | 488 | .hero-content h1::after { 489 | left: 50%; 490 | transform: translateX(-50%); 491 | } 492 | 493 | .hero-description { 494 | margin: 0 auto 2rem; 495 | } 496 | 497 | .cta-buttons { 498 | justify-content: center; 499 | } 500 | 501 | .code-preview { 502 | max-width: 600px; 503 | margin: 0 auto; 504 | } 505 | 506 | .section-description { 507 | padding: 0 2rem; 508 | } 509 | } 510 | 511 | @media (max-width: 768px) { 512 | .nav-links { 513 | display: none; 514 | } 515 | 516 | .hero-content h1 { 517 | font-size: 2.5rem; 518 | } 519 | 520 | .features-grid, .prerequisites-list, .roadmap-grid { 521 | grid-template-columns: 1fr; 522 | gap: 1.5rem; 523 | } 524 | 525 | .footer-content { 526 | flex-direction: column; 527 | gap: 1.5rem; 528 | text-align: center; 529 | } 530 | 531 | .social-links { 532 | margin-top: 1rem; 533 | } 534 | 535 | .social-links a { 536 | margin: 0 0.5rem; 537 | } 538 | 539 | .cta-buttons { 540 | flex-direction: column; 541 | gap: 1rem; 542 | } 543 | 544 | .cta-primary, .cta-secondary { 545 | width: 100%; 546 | justify-content: center; 547 | } 548 | 549 | .section-description { 550 | font-size: 1rem; 551 | padding: 0 1rem; 552 | } 553 | 554 | h2 { 555 | font-size: 2rem !important; 556 | } 557 | } -------------------------------------------------------------------------------- /src/common/common.schema.ts: -------------------------------------------------------------------------------- 1 | import validator from "validator"; 2 | import { z } from "zod"; 3 | 4 | export const successResponseSchema = z.object({ 5 | success: z.boolean().default(true), 6 | message: z.string().optional(), 7 | data: z.record(z.string(), z.any()).optional(), 8 | }); 9 | 10 | export const errorResponseSchema = z.object({ 11 | message: z.string(), 12 | success: z.boolean().default(false), 13 | data: z.record(z.string(), z.any()), 14 | stack: z.string().optional(), 15 | }); 16 | 17 | export const paginatorSchema = z.object({ 18 | skip: z.number().min(0), 19 | limit: z.number().min(1), 20 | currentPage: z.number().min(1), 21 | pages: z.number().min(0), 22 | hasNextPage: z.boolean(), 23 | totalRecords: z.number().min(0), 24 | pageSize: z.number().min(1), 25 | }); 26 | 27 | export const paginatedResponseSchema = z.object({ 28 | success: z.boolean().default(true), 29 | message: z.string().optional(), 30 | data: z 31 | .object({ 32 | items: z.array(z.unknown()), 33 | paginator: paginatorSchema, 34 | }) 35 | .optional(), 36 | }); 37 | 38 | export const mongoIdSchema = z.object({ 39 | id: z.string().refine((value) => validator.isMongoId(value)), 40 | }); 41 | 42 | export const idSchema = z.object({ 43 | id: z 44 | .string() 45 | .refine((value) => Number.isNaN(Number(value))) 46 | .transform(Number), 47 | }); 48 | 49 | export const passwordValidationSchema = (fieldName: string) => 50 | z 51 | .string({ required_error: `${fieldName} is required` }) 52 | .min(8) 53 | .max(64) 54 | .refine( 55 | (value) => 56 | validator.isStrongPassword(value, { 57 | minLength: 8, 58 | minLowercase: 1, 59 | minNumbers: 1, 60 | minUppercase: 1, 61 | minSymbols: 1, 62 | }), 63 | "Password is too weak", 64 | ); 65 | 66 | export type MongoIdSchemaType = z.infer; 67 | export type IdSchemaType = z.infer; 68 | -------------------------------------------------------------------------------- /src/common/common.utils.ts: -------------------------------------------------------------------------------- 1 | import { type ZodRawShape, type ZodSchema, z } from "zod"; 2 | import { paginatorSchema, successResponseSchema } from "./common.schema"; 3 | 4 | export const defineSuccessResponse = (schema: ZodRawShape) => { 5 | return successResponseSchema.extend(schema); 6 | }; 7 | 8 | export const definePaginatedResponse = (schema: ZodSchema) => { 9 | return defineSuccessResponse({ 10 | data: z.object({ 11 | results: z.array(schema), 12 | paginatorInfo: paginatorSchema, 13 | }), 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /src/config/config.service.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import { z } from "zod"; 3 | 4 | dotenv.config(); 5 | 6 | // Remove .optional() from requried schema properties 7 | 8 | const configSchema = z.object({ 9 | REDIS_URL: z.string().url(), 10 | PORT: z.string().regex(/^\d+$/).transform(Number), 11 | MONGO_DATABASE_URL: z.string().url(), 12 | SMTP_HOST: z.string().min(1).optional(), 13 | SMTP_PORT: z.string().regex(/^\d+$/).transform(Number).optional(), 14 | SMTP_USERNAME: z.string().email().optional(), 15 | EMAIL_FROM: z.string().email().optional(), 16 | SMTP_FROM: z.string().min(1).optional(), 17 | SMTP_PASSWORD: z.string().min(1).optional(), 18 | CLIENT_SIDE_URL: z.string().url(), 19 | JWT_SECRET: z.string().min(1), 20 | JWT_EXPIRES_IN: z.string().default("86400").transform(Number), 21 | SESSION_EXPIRES_IN: z.string().default("86400").transform(Number), 22 | PASSWORD_RESET_TOKEN_EXPIRES_IN: z.string().default("86400").transform(Number), 23 | SET_PASSWORD_TOKEN_EXPIRES_IN: z.string().default("86400").transform(Number), 24 | STATIC_OTP: z.enum(["1", "0"]).transform(Number).optional(), 25 | NODE_ENV: z 26 | .union([z.literal("production"), z.literal("development")]) 27 | .default("development") 28 | .optional(), 29 | SET_SESSION: z 30 | .string() 31 | .transform((value) => !!Number(value)) 32 | .optional(), 33 | GOOGLE_CLIENT_ID: z.string().optional(), 34 | GOOGLE_CLIENT_SECRET: z.string().optional(), 35 | GOOGLE_REDIRECT_URI: z.string().optional(), 36 | APP_NAME: z.string().default("API V1"), 37 | APP_VERSION: z.string().default("1.0.0"), 38 | // Mailgun configuration 39 | MAILGUN_API_KEY: z.string().min(1), 40 | MAILGUN_DOMAIN: z.string().min(1), 41 | MAILGUN_FROM_EMAIL: z.string().email(), 42 | ADMIN_EMAIL: z.string().email(), 43 | ADMIN_PASSWORD: z.string().min(1), 44 | OTP_VERIFICATION_ENABLED: z.string().transform((value) => !!Number(value)), 45 | }); 46 | 47 | export type Config = z.infer; 48 | 49 | const config = configSchema.parse(process.env); 50 | 51 | export default config; 52 | -------------------------------------------------------------------------------- /src/email/email.service.ts: -------------------------------------------------------------------------------- 1 | import { render } from "@react-email/render"; 2 | import config from "../config/config.service"; 3 | import logger from "../lib/logger.service"; 4 | import mailgunClient from "../lib/mailgun.server"; 5 | import ResetPasswordEmail from "./templates/ResetPassword"; 6 | 7 | export type SendResetPasswordTypePayload = { 8 | email: string; 9 | resetLink: string; 10 | userName: string; 11 | }; 12 | 13 | class EmailError extends Error { 14 | constructor( 15 | message: string, 16 | public readonly cause?: unknown, 17 | ) { 18 | super(message); 19 | this.name = "EmailError"; 20 | } 21 | } 22 | 23 | // Utility functions for sending emails 24 | export const sendEmail = async ({ 25 | to, 26 | subject, 27 | html, 28 | }: { 29 | to: string; 30 | subject: string; 31 | html: string; 32 | }) => { 33 | try { 34 | const messageData = { 35 | from: config.MAILGUN_FROM_EMAIL, 36 | to, 37 | subject, 38 | html, 39 | }; 40 | 41 | const result = await mailgunClient.messages.create( 42 | config.MAILGUN_DOMAIN, 43 | messageData, 44 | ); 45 | 46 | logger.info({ 47 | msg: "Email sent successfully", 48 | id: result.id, 49 | to, 50 | subject, 51 | }); 52 | 53 | return result; 54 | } catch (error) { 55 | logger.error({ 56 | msg: "Failed to send email", 57 | error, 58 | to, 59 | subject, 60 | }); 61 | 62 | throw new EmailError("Failed to send email", error); 63 | } 64 | }; 65 | 66 | export const sendResetPasswordEmail = async ( 67 | payload: SendResetPasswordTypePayload, 68 | ) => { 69 | const { email, resetLink, userName } = payload; 70 | 71 | try { 72 | // Render the React email template to HTML 73 | const emailHtml = await render( 74 | ResetPasswordEmail({ 75 | resetLink, 76 | userName, 77 | }), 78 | ); 79 | 80 | // Send the email with the rendered HTML 81 | await sendEmail({ 82 | to: email, 83 | subject: "Reset Your Password", 84 | html: emailHtml, 85 | }); 86 | } catch (error) { 87 | logger.error({ 88 | msg: "Failed to send reset password email", 89 | error, 90 | email, 91 | }); 92 | 93 | throw new EmailError("Failed to send reset password email", error); 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /src/email/templates/ResetPassword.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Button, 4 | Container, 5 | Head, 6 | Heading, 7 | Html, 8 | Preview, 9 | Section, 10 | Text, 11 | } from "@react-email/components"; 12 | import * as React from "react"; 13 | 14 | interface ResetPasswordEmailProps { 15 | userName: string; 16 | resetLink: string; 17 | } 18 | 19 | export const ResetPasswordEmail = ({ 20 | userName, 21 | resetLink, 22 | }: ResetPasswordEmailProps) => { 23 | return ( 24 | 25 | 26 | Reset your password 27 | 28 | 29 | Password Reset Request 30 | Hi {userName}, 31 | 32 | We received a request to reset your password. Click the button below 33 | to create a new password: 34 | 35 |
36 | 39 |
40 | 41 | If you didn't request this password reset, you can safely ignore 42 | this email. 43 | 44 | 45 | This link will expire in 1 hour for security reasons. 46 | 47 | 48 | If you're having trouble clicking the button, copy and paste this 49 | URL into your web browser: {resetLink} 50 | 51 |
52 | 53 | 54 | ); 55 | }; 56 | 57 | const main = { 58 | backgroundColor: "#f6f9fc", 59 | fontFamily: 60 | '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif', 61 | }; 62 | 63 | const container = { 64 | backgroundColor: "#ffffff", 65 | margin: "0 auto", 66 | padding: "20px 0 48px", 67 | marginBottom: "64px", 68 | }; 69 | 70 | const heading = { 71 | fontSize: "24px", 72 | letterSpacing: "-0.5px", 73 | lineHeight: "1.3", 74 | fontWeight: "400", 75 | color: "#484848", 76 | padding: "17px 0 0", 77 | }; 78 | 79 | const text = { 80 | margin: "0 0 12px", 81 | fontSize: "16px", 82 | lineHeight: "24px", 83 | color: "#484848", 84 | }; 85 | 86 | const buttonContainer = { 87 | padding: "27px 0 27px", 88 | }; 89 | 90 | const button = { 91 | backgroundColor: "#5469d4", 92 | borderRadius: "4px", 93 | color: "#ffffff", 94 | fontSize: "16px", 95 | textDecoration: "none", 96 | textAlign: "center" as const, 97 | display: "block", 98 | padding: "12px 20px", 99 | }; 100 | 101 | const footer = { 102 | fontSize: "13px", 103 | lineHeight: "24px", 104 | color: "#777", 105 | padding: "0 20px", 106 | }; 107 | 108 | export default ResetPasswordEmail; 109 | -------------------------------------------------------------------------------- /src/enums.ts: -------------------------------------------------------------------------------- 1 | export const ROLE_ENUM = { 2 | DEFAULT_USER: "DEFAULT_USER", 3 | SUPER_ADMIN: "SUPER_ADMIN", 4 | } as const; 5 | 6 | export const SOCIAL_ACCOUNT_ENUM = { 7 | GOOGLE: "GOOGLE", 8 | FACEBOOK: "FACEBOOK", 9 | APPLE: "APPLE", 10 | } as const; 11 | 12 | export type SocialAccountType = keyof typeof SOCIAL_ACCOUNT_ENUM; 13 | export type RoleType = keyof typeof ROLE_ENUM; 14 | -------------------------------------------------------------------------------- /src/healthcheck/healthcheck.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "express"; 2 | import { StatusCodes } from "http-status-codes"; 3 | 4 | export const handleHealthCheck = async (_: Request, res: Response) => { 5 | const healthCheck = { 6 | uptime: process.uptime(), 7 | responseTime: process.hrtime(), 8 | message: "OK", 9 | timestamp: Date.now(), 10 | }; 11 | 12 | try { 13 | res.send(healthCheck); 14 | } catch (error) { 15 | healthCheck.message = (error as Error).message; 16 | 17 | res.status(StatusCodes.SERVICE_UNAVAILABLE).send(healthCheck); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/healthcheck/healthcheck.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { handleHealthCheck } from "./healthcheck.controller"; 3 | 4 | export const HEALTH_ROUTER_ROOT = "/healthcheck"; 5 | 6 | const healthCheckRouter = Router(); 7 | 8 | healthCheckRouter.get("/", handleHealthCheck); 9 | 10 | export default healthCheckRouter; 11 | -------------------------------------------------------------------------------- /src/lib/aws.service.ts: -------------------------------------------------------------------------------- 1 | import { S3Client } from "@aws-sdk/client-s3"; 2 | 3 | export const BUCKET_NAME = "your-bucket-name"; 4 | 5 | const s3 = new S3Client(); 6 | 7 | export default s3; 8 | -------------------------------------------------------------------------------- /src/lib/common.schema.ts: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from "http-status-codes"; 2 | import z from "zod"; 3 | 4 | export const searchAndPaginationSchema = z.object({ 5 | search: z.string().optional(), 6 | page: z.string().default("1").transform(Number).optional(), 7 | limit: z.string().default("10").transform(Number).optional(), 8 | }); 9 | 10 | export const returnMessageSchema = z.object({ 11 | status: z 12 | .number() 13 | .refine((value) => Object.values(StatusCodes).includes(value)), 14 | message: z.string(), 15 | }); 16 | 17 | export type ReturnMessageSchemaType = z.infer; 18 | -------------------------------------------------------------------------------- /src/lib/database.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import config from "../config/config.service"; 3 | import logger from "./logger.service"; 4 | 5 | export const connectDatabase = async () => { 6 | try { 7 | logger.info("Connecting database..."); 8 | await mongoose.connect(config.MONGO_DATABASE_URL); 9 | logger.info("Database connected"); 10 | } catch (err) { 11 | logger.error((err as Error).message); 12 | process.exit(1); 13 | } 14 | }; 15 | 16 | export const disconnectDatabase = async () => { 17 | await mongoose.disconnect(); 18 | logger.info("Database disconnected"); 19 | }; 20 | -------------------------------------------------------------------------------- /src/lib/email.server.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from "nodemailer"; 2 | import type SMTPTransport from "nodemailer/lib/smtp-transport"; 3 | import config from "../config/config.service"; 4 | 5 | const mailer = nodemailer.createTransport({ 6 | host: config.SMTP_HOST, 7 | port: config.SMTP_PORT, 8 | auth: { 9 | user: config.SMTP_USERNAME, 10 | pass: config.SMTP_PASSWORD, 11 | }, 12 | } as SMTPTransport.Options); 13 | 14 | export default mailer; 15 | -------------------------------------------------------------------------------- /src/lib/logger.service.ts: -------------------------------------------------------------------------------- 1 | import pino from "pino"; 2 | import pinohttpLogger from "pino-http"; 3 | 4 | const logger = pino({ 5 | transport: { 6 | target: "pino-pretty", 7 | options: { 8 | colorize: true, 9 | }, 10 | }, 11 | }); 12 | 13 | export const httpLogger = pinohttpLogger({ logger: logger }); 14 | 15 | export default logger; 16 | -------------------------------------------------------------------------------- /src/lib/mailgun.server.ts: -------------------------------------------------------------------------------- 1 | import formData from "form-data"; 2 | import Mailgun from "mailgun.js"; 3 | import config from "../config/config.service"; 4 | 5 | const mailgun = new Mailgun(formData); 6 | 7 | const mailgunClient = mailgun.client({ 8 | username: "api", 9 | key: config.MAILGUN_API_KEY, 10 | }); 11 | 12 | export default mailgunClient; 13 | -------------------------------------------------------------------------------- /src/lib/queue.server.ts: -------------------------------------------------------------------------------- 1 | import type { Processor } from "bullmq"; 2 | import { Queue as BullQueue, Worker } from "bullmq"; 3 | 4 | import logger from "./logger.service"; 5 | import redisClient from "./redis.server"; 6 | 7 | type RegisteredQueue = { 8 | queue: BullQueue; 9 | worker: Worker; 10 | }; 11 | 12 | declare global { 13 | // eslint-disable-next-line no-var 14 | var __registeredQueues: Record | undefined; 15 | } 16 | 17 | if (!global.__registeredQueues) { 18 | global.__registeredQueues = {}; 19 | } 20 | const registeredQueues = global.__registeredQueues; 21 | 22 | export function Queue( 23 | name: string, 24 | handler: Processor, 25 | ): BullQueue { 26 | if (registeredQueues[name]) { 27 | return registeredQueues[name].queue as BullQueue; 28 | } 29 | 30 | const queue = new BullQueue(name, { connection: redisClient }); 31 | 32 | const worker = new Worker(name, handler, { 33 | connection: redisClient, 34 | }); 35 | 36 | registeredQueues[name] = { queue, worker }; 37 | 38 | logger.info({ name: "Queue" }, `${name}: Initialize`); 39 | 40 | return queue; 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/realtime.server.ts: -------------------------------------------------------------------------------- 1 | import type { Server as IServer } from "node:http"; 2 | import { Server as RealtimeServer } from "socket.io"; 3 | 4 | export const useSocketIo = (server: IServer): RealtimeServer => { 5 | const io = new RealtimeServer(server, { 6 | transports: ["polling", "websocket"], 7 | cors: { 8 | origin: "*", 9 | methods: ["GET", "POST"], 10 | }, 11 | }); 12 | 13 | return io; 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/redis.server.ts: -------------------------------------------------------------------------------- 1 | import type { RedisOptions } from "ioredis"; 2 | import Redis from "ioredis"; 3 | import config from "../config/config.service"; 4 | 5 | const redisOptions: RedisOptions = { 6 | maxRetriesPerRequest: null, 7 | enableReadyCheck: false, 8 | host: "redis", 9 | }; 10 | 11 | const redisClient = new Redis(config.REDIS_URL || "", redisOptions); 12 | 13 | export default redisClient; 14 | -------------------------------------------------------------------------------- /src/lib/session.store.ts: -------------------------------------------------------------------------------- 1 | import RedisStore from "connect-redis"; 2 | import redisClient from "./redis.server"; 3 | 4 | const redisStore = new RedisStore({ 5 | client: redisClient, 6 | }); 7 | 8 | export default redisStore; 9 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import "./openapi/zod-extend"; 2 | 3 | import { createServer } from "node:http"; 4 | import path from "node:path"; 5 | import process from "node:process"; 6 | import { createBullBoard } from "@bull-board/api"; 7 | import { BullMQAdapter } from "@bull-board/api/bullMQAdapter"; 8 | import { ExpressAdapter } from "@bull-board/express"; 9 | import compression from "compression"; 10 | import cookieParser from "cookie-parser"; 11 | import cors from "cors"; 12 | import express from "express"; 13 | import session from "express-session"; 14 | import helmet from "helmet"; 15 | import morgan from "morgan"; 16 | import config from "./config/config.service"; 17 | import { connectDatabase, disconnectDatabase } from "./lib/database"; 18 | import logger, { httpLogger } from "./lib/logger.service"; 19 | import { useSocketIo } from "./lib/realtime.server"; 20 | import redisStore from "./lib/session.store"; 21 | import { extractJwt } from "./middlewares/extract-jwt-schema.middleware"; 22 | import apiRoutes from "./routes/routes"; 23 | 24 | import swaggerUi from "swagger-ui-express"; 25 | 26 | import YAML from "yaml"; 27 | import { convertDocumentationToYaml } from "./openapi/swagger-doc-generator"; 28 | import globalErrorHandler from "./utils/globalErrorHandler"; 29 | 30 | const app = express(); 31 | 32 | app.set("trust proxy", true); 33 | 34 | const server = createServer(app); 35 | 36 | const io = useSocketIo(server); 37 | 38 | const boostrapServer = async () => { 39 | await connectDatabase(); 40 | 41 | app.use((req, _, next) => { 42 | req.io = io; 43 | next(); 44 | }); 45 | 46 | app.use( 47 | cors({ 48 | origin: [config.CLIENT_SIDE_URL], 49 | optionsSuccessStatus: 200, 50 | credentials: true, 51 | }), 52 | ); 53 | 54 | if (config.NODE_ENV === "development") { 55 | app.use(morgan("dev")); 56 | } else { 57 | app.use(httpLogger); 58 | } 59 | 60 | app.use(express.json()); 61 | app.use(express.urlencoded({ extended: false })); 62 | 63 | app.use( 64 | session({ 65 | secret: config.JWT_SECRET, 66 | resave: false, 67 | saveUninitialized: true, 68 | cookie: { secure: true }, 69 | store: redisStore, 70 | }), 71 | ); 72 | 73 | // Middleware to serve static files 74 | app.use(express.static(path.join(__dirname, "..", "public"))); 75 | 76 | app.use(cookieParser()); 77 | 78 | app.use(compression()); 79 | 80 | app.use(extractJwt); 81 | 82 | if (config.NODE_ENV === "production") { 83 | app.use(helmet()); 84 | } 85 | 86 | app.use("/api", apiRoutes); 87 | 88 | const swaggerDocument = YAML.parse(convertDocumentationToYaml()); 89 | app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); 90 | 91 | const serverAdapter = new ExpressAdapter(); 92 | serverAdapter.setBasePath("/admin/queues"); 93 | 94 | createBullBoard({ 95 | queues: Object.entries(global.__registeredQueues || {}).map( 96 | ([, values]) => new BullMQAdapter(values.queue), 97 | ), 98 | serverAdapter, 99 | }); 100 | 101 | // Dashbaord for BullMQ 102 | app.use("/admin/queues", serverAdapter.getRouter()); 103 | 104 | // Global Error Handler 105 | app.use(globalErrorHandler); 106 | 107 | server.listen(config.PORT, () => { 108 | logger.info(`Server is running on http://localhost:${config.PORT}`); 109 | logger.info(`RESTful API: http://localhost:${config.PORT}/api`); 110 | logger.info(`Swagger API Docs: http://localhost:${config.PORT}/api-docs`); 111 | logger.info(`BullBoard: http://localhost:${config.PORT}/admin/queues`); 112 | logger.info(`Client-side url set to: ${config.CLIENT_SIDE_URL}`); 113 | }); 114 | }; 115 | 116 | boostrapServer().catch((err) => { 117 | logger.error(err.message); 118 | process.exit(1); 119 | }); 120 | 121 | for (const signal of ["SIGINT", "SIGTERM"]) { 122 | process.on(signal, async () => { 123 | await disconnectDatabase(); 124 | logger.info("Server is shutting down..."); 125 | io.disconnectSockets(true); 126 | logger.info("Server disconnected from sockets"); 127 | server.close(); 128 | logger.info("Server closed"); 129 | process.exit(0); 130 | }); 131 | } 132 | 133 | process.on("uncaughtException", (err) => { 134 | logger.error(err.message); 135 | process.exit(1); 136 | }); 137 | -------------------------------------------------------------------------------- /src/middlewares/can-access.middleware.ts: -------------------------------------------------------------------------------- 1 | import type { NextFunction } from "express"; 2 | import { StatusCodes } from "http-status-codes"; 3 | import type { RoleType } from "../enums"; 4 | import { getUserById } from "../modules/user/user.services"; 5 | import { errorResponse } from "../utils/api.utils"; 6 | import type { JwtPayload } from "../utils/auth.utils"; 7 | import type { RequestAny, ResponseAny } from "../openapi/magic-router"; 8 | 9 | export type CanAccessByType = "roles"; 10 | 11 | export type CanAccessOptions = { 12 | roles: RoleType | "*"; 13 | }; 14 | 15 | export const canAccess = 16 | (by?: T, access?: CanAccessOptions[T][]) => 17 | async (req: RequestAny, res: ResponseAny, next?: NextFunction) => { 18 | try { 19 | const requestUser = req?.user as JwtPayload; 20 | 21 | if (!requestUser) { 22 | return errorResponse( 23 | res, 24 | "token isn't attached or expired", 25 | StatusCodes.UNAUTHORIZED, 26 | ); 27 | } 28 | const currentUser = await getUserById(requestUser.sub); 29 | 30 | if (!currentUser) { 31 | return errorResponse(res, "Login again", StatusCodes.UNAUTHORIZED); 32 | } 33 | 34 | if (currentUser.otp !== null) { 35 | return errorResponse( 36 | res, 37 | "Your account is not verified", 38 | StatusCodes.UNAUTHORIZED, 39 | ); 40 | } 41 | 42 | let can = false; 43 | 44 | const accessorsToScanFor = access; 45 | 46 | if (by === "roles" && accessorsToScanFor) { 47 | can = (accessorsToScanFor as RoleType[]).includes( 48 | currentUser.role as RoleType, 49 | ); 50 | } 51 | 52 | if (!accessorsToScanFor) { 53 | can = Boolean(currentUser.email); 54 | } 55 | 56 | if (!can && by === "roles") { 57 | return errorResponse( 58 | res, 59 | "User is not authorized to perform this action", 60 | StatusCodes.UNAUTHORIZED, 61 | { [`${by}_required`]: access }, 62 | ); 63 | } 64 | 65 | if (currentUser && !by && !access) { 66 | can = true; 67 | } 68 | 69 | if (!can) { 70 | return errorResponse( 71 | res, 72 | "User is not authenticated", 73 | StatusCodes.UNAUTHORIZED, 74 | access, 75 | ); 76 | } 77 | 78 | if (currentUser) { 79 | req.user = { ...currentUser, sub: currentUser._id }; 80 | } 81 | } catch (err) { 82 | return errorResponse( 83 | res, 84 | (err as Error).message, 85 | StatusCodes.UNAUTHORIZED, 86 | access, 87 | ); 88 | } 89 | 90 | next?.(); 91 | }; 92 | -------------------------------------------------------------------------------- /src/middlewares/extract-jwt-schema.middleware.ts: -------------------------------------------------------------------------------- 1 | import type { NextFunction } from "express"; 2 | import { type JwtPayload, verifyToken } from "../utils/auth.utils"; 3 | import type { RequestAny, ResponseAny } from "../openapi/magic-router"; 4 | 5 | export const extractJwt = async ( 6 | req: RequestAny, 7 | _: ResponseAny, 8 | next: NextFunction, 9 | ) => { 10 | try { 11 | const token = 12 | req.cookies?.accessToken ?? req.headers.authorization?.split(" ")[1]; 13 | 14 | if (!token) { 15 | return next(); 16 | } 17 | 18 | const decode = await verifyToken(token); 19 | 20 | req.user = decode; 21 | return next(); 22 | } catch { 23 | return next(); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/middlewares/multer-s3.middleware.ts: -------------------------------------------------------------------------------- 1 | import type { NextFunction } from "express"; 2 | import { StatusCodes } from "http-status-codes"; 3 | import multer from "multer"; 4 | import multerS3 from "multer-s3"; 5 | import s3, { BUCKET_NAME } from "../lib/aws.service"; 6 | import { errorResponse } from "../utils/api.utils"; 7 | import { checkFiletype } from "../utils/common.utils"; 8 | import type { RequestAny, ResponseAny } from "../openapi/magic-router"; 9 | 10 | const storageEngineProfile: multer.StorageEngine = multerS3({ 11 | s3: s3, 12 | bucket: BUCKET_NAME, 13 | metadata: (_, file, cb) => { 14 | cb(null, { fieldName: file.fieldname }); 15 | }, 16 | key: (req: RequestAny, file, cb) => { 17 | const key = `user-${req.user.id}/profile/${file.originalname}`; 18 | 19 | if (checkFiletype(file)) { 20 | cb(null, key); 21 | } else { 22 | cb("File format is not valid", key); 23 | } 24 | }, 25 | }); 26 | 27 | export const uploadProfile = ( 28 | req: RequestAny, 29 | res: ResponseAny, 30 | next: NextFunction, 31 | ) => { 32 | const upload = multer({ 33 | storage: storageEngineProfile, 34 | limits: { fileSize: 1000000 * 10 }, 35 | }).single("avatar"); 36 | 37 | upload(req, res, (err) => { 38 | if (err) { 39 | return errorResponse( 40 | res, 41 | (err as Error).message, 42 | StatusCodes.BAD_REQUEST, 43 | err, 44 | ); 45 | } 46 | 47 | next(); 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /src/middlewares/validate-zod-schema.middleware.ts: -------------------------------------------------------------------------------- 1 | import type { NextFunction } from "express"; 2 | import { StatusCodes } from "http-status-codes"; 3 | import { ZodError, type ZodSchema } from "zod"; 4 | import type { RequestZodSchemaType } from "../types"; 5 | import { errorResponse } from "../utils/api.utils"; 6 | import { sanitizeRecord } from "../utils/common.utils"; 7 | import type { RequestAny, ResponseAny } from "../openapi/magic-router"; 8 | 9 | export const validateZodSchema = 10 | (payload: RequestZodSchemaType) => 11 | (req: RequestAny, res: ResponseAny, next?: NextFunction) => { 12 | let error: ZodError | null = null; 13 | 14 | for (const [key, value] of Object.entries(payload)) { 15 | const typedProp = [key, value] as [keyof RequestZodSchemaType, ZodSchema]; 16 | const [typedKey, typedValue] = typedProp; 17 | 18 | const parsed = typedValue.safeParse(req[typedKey]); 19 | 20 | if (!parsed.success) { 21 | if (error instanceof ZodError) { 22 | error.addIssues(parsed.error.issues); 23 | } else { 24 | error = parsed.error; 25 | } 26 | } 27 | 28 | req[typedKey] = sanitizeRecord(parsed.data); 29 | } 30 | 31 | if (error) { 32 | return errorResponse( 33 | res, 34 | "Invalid input", 35 | StatusCodes.BAD_REQUEST, 36 | error, 37 | ); 38 | } 39 | 40 | next?.(); 41 | }; 42 | -------------------------------------------------------------------------------- /src/modules/auth/auth.constants.ts: -------------------------------------------------------------------------------- 1 | import type { CookieOptions } from "express"; 2 | import config from "../../config/config.service"; 3 | 4 | const clientSideUrl = new URL(config.CLIENT_SIDE_URL); 5 | 6 | export const AUTH_COOKIE_KEY = "accessToken"; 7 | 8 | export const COOKIE_CONFIG: CookieOptions = { 9 | httpOnly: true, 10 | sameSite: "lax", 11 | secure: config.NODE_ENV === "production", 12 | maxAge: config.SESSION_EXPIRES_IN * 1000, 13 | domain: clientSideUrl.hostname, 14 | }; 15 | -------------------------------------------------------------------------------- /src/modules/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "express"; 2 | import config from "../../config/config.service"; 3 | import type { GoogleCallbackQuery } from "../../types"; 4 | import { successResponse } from "../../utils/api.utils"; 5 | import type { JwtPayload } from "../../utils/auth.utils"; 6 | import { AUTH_COOKIE_KEY, COOKIE_CONFIG } from "./auth.constants"; 7 | import type { 8 | ChangePasswordSchemaType, 9 | ForgetPasswordSchemaType, 10 | LoginUserByEmailSchemaType, 11 | RegisterUserByEmailSchemaType, 12 | ResetPasswordSchemaType, 13 | } from "./auth.schema"; 14 | import { 15 | changePassword, 16 | forgetPassword, 17 | googleLogin, 18 | loginUserByEmail, 19 | registerUserByEmail, 20 | resetPassword, 21 | } from "./auth.service"; 22 | 23 | export const handleResetPassword = async ( 24 | req: Request, 25 | res: Response, 26 | ) => { 27 | await resetPassword(req.body); 28 | 29 | return successResponse(res, "Password successfully reset"); 30 | }; 31 | 32 | export const handleForgetPassword = async ( 33 | req: Request, 34 | res: Response, 35 | ) => { 36 | const user = await forgetPassword(req.body); 37 | 38 | return successResponse(res, "Code has been sent", { userId: user._id }); 39 | }; 40 | 41 | export const handleChangePassword = async ( 42 | req: Request, 43 | res: Response, 44 | ) => { 45 | await changePassword((req.user as JwtPayload).sub, req.body); 46 | 47 | return successResponse(res, "Password successfully changed"); 48 | }; 49 | 50 | export const handleRegisterUser = async ( 51 | req: Request, 52 | res: Response, 53 | ) => { 54 | const user = await registerUserByEmail(req.body); 55 | 56 | if (config.OTP_VERIFICATION_ENABLED) { 57 | return successResponse(res, "Please check your email for OTP", user); 58 | } 59 | 60 | return successResponse(res, "User has been reigstered", user); 61 | }; 62 | 63 | export const handleLogout = async (_: Request, res: Response) => { 64 | res.cookie(AUTH_COOKIE_KEY, undefined, COOKIE_CONFIG); 65 | 66 | return successResponse(res, "Logout successful"); 67 | }; 68 | 69 | export const handleLoginByEmail = async ( 70 | req: Request, 71 | res: Response, 72 | ) => { 73 | const token = await loginUserByEmail(req.body); 74 | if (config.SET_SESSION) { 75 | res.cookie(AUTH_COOKIE_KEY, token, COOKIE_CONFIG); 76 | } 77 | return successResponse(res, "Login successful", { token: token }); 78 | }; 79 | 80 | export const handleGetCurrentUser = async (req: Request, res: Response) => { 81 | const user = req.user; 82 | 83 | return successResponse(res, undefined, user); 84 | }; 85 | export const handleGoogleLogin = async (_: Request, res: Response) => { 86 | if (!config.GOOGLE_CLIENT_ID || !config.GOOGLE_REDIRECT_URI) { 87 | throw new Error("Google credentials are not set"); 88 | } 89 | 90 | const googleAuthURL = `https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=${config.GOOGLE_CLIENT_ID}&redirect_uri=${config.GOOGLE_REDIRECT_URI}&scope=email profile`; 91 | 92 | res.redirect(googleAuthURL); 93 | }; 94 | export const handleGoogleCallback = async ( 95 | req: Request, 96 | res: Response, 97 | ) => { 98 | const user = await googleLogin(req.query); 99 | if (!user) throw new Error("Failed to login"); 100 | res.cookie( 101 | AUTH_COOKIE_KEY, 102 | user.socialAccount?.[0]?.accessToken, 103 | COOKIE_CONFIG, 104 | ); 105 | 106 | return successResponse(res, "Logged in successfully", { 107 | token: user.socialAccount?.[0]?.accessToken, 108 | }); 109 | }; 110 | -------------------------------------------------------------------------------- /src/modules/auth/auth.router.ts: -------------------------------------------------------------------------------- 1 | import { canAccess } from "../../middlewares/can-access.middleware"; 2 | import MagicRouter from "../../openapi/magic-router"; 3 | import { 4 | handleChangePassword, 5 | handleForgetPassword, 6 | handleGetCurrentUser, 7 | handleGoogleCallback, 8 | handleGoogleLogin, 9 | handleLoginByEmail, 10 | handleLogout, 11 | handleRegisterUser, 12 | handleResetPassword, 13 | } from "./auth.controller"; 14 | import { 15 | changePasswordSchema, 16 | forgetPasswordSchema, 17 | loginUserByEmailSchema, 18 | registerUserByEmailSchema, 19 | resetPasswordSchema, 20 | } from "./auth.schema"; 21 | 22 | export const AUTH_ROUTER_ROOT = "/auth"; 23 | 24 | const authRouter = new MagicRouter(AUTH_ROUTER_ROOT); 25 | 26 | authRouter.post( 27 | "/login/email", 28 | { requestType: { body: loginUserByEmailSchema } }, 29 | handleLoginByEmail, 30 | ); 31 | 32 | authRouter.post( 33 | "/register/email", 34 | { requestType: { body: registerUserByEmailSchema } }, 35 | handleRegisterUser, 36 | ); 37 | 38 | authRouter.post("/logout", {}, handleLogout); 39 | 40 | authRouter.get("/me", {}, canAccess(), handleGetCurrentUser); 41 | 42 | authRouter.post( 43 | "/forget-password", 44 | { requestType: { body: forgetPasswordSchema } }, 45 | handleForgetPassword, 46 | ); 47 | 48 | authRouter.post( 49 | "/change-password", 50 | { requestType: { body: changePasswordSchema } }, 51 | canAccess(), 52 | handleChangePassword, 53 | ); 54 | 55 | authRouter.post( 56 | "/reset-password", 57 | { requestType: { body: resetPasswordSchema } }, 58 | handleResetPassword, 59 | ); 60 | 61 | authRouter.get("/google", {}, handleGoogleLogin); 62 | authRouter.get("/google/callback", {}, handleGoogleCallback); 63 | 64 | export default authRouter.getRouter(); 65 | -------------------------------------------------------------------------------- /src/modules/auth/auth.schema.ts: -------------------------------------------------------------------------------- 1 | import validator from "validator"; 2 | import z from "zod"; 3 | import { passwordValidationSchema } from "../../common/common.schema"; 4 | import { baseCreateUser } from "../user/user.schema"; 5 | 6 | export const resetPasswordSchema = z.object({ 7 | userId: z 8 | .string({ required_error: "userId is required" }) 9 | .min(1) 10 | .refine((value) => validator.isMongoId(value), "userId must be valid"), 11 | code: z 12 | .string({ required_error: "code is required" }) 13 | .min(4) 14 | .max(4) 15 | .refine((value) => validator.isAlphanumeric(value), "code must be valid"), 16 | password: passwordValidationSchema("Password"), 17 | confirmPassword: passwordValidationSchema("Confirm password"), 18 | }); 19 | 20 | export const changePasswordSchema = z.object({ 21 | currentPassword: passwordValidationSchema("Current password"), 22 | newPassword: passwordValidationSchema("New password"), 23 | }); 24 | 25 | export const forgetPasswordSchema = z.object({ 26 | email: z 27 | .string({ required_error: "Email is required" }) 28 | .email("Email must be valid"), 29 | }); 30 | 31 | export const registerUserByEmailSchema = z 32 | .object({ 33 | name: z.string({ required_error: "Name is required" }).min(1), 34 | confirmPassword: passwordValidationSchema("Confirm Password"), 35 | }) 36 | .merge(baseCreateUser) 37 | .strict() 38 | .refine(({ password, confirmPassword }) => { 39 | if (password !== confirmPassword) { 40 | return false; 41 | } 42 | 43 | return true; 44 | }, "Password and confirm password must be same"); 45 | 46 | export const loginUserByEmailSchema = z.object({ 47 | email: z 48 | .string({ required_error: "Email is required" }) 49 | .email({ message: "Email is not valid" }), 50 | password: z.string().min(1, "Password is required"), 51 | }); 52 | 53 | export type RegisterUserByEmailSchemaType = z.infer< 54 | typeof registerUserByEmailSchema 55 | >; 56 | 57 | export type LoginUserByEmailSchemaType = z.infer; 58 | export type ChangePasswordSchemaType = z.infer; 59 | export type ForgetPasswordSchemaType = z.infer; 60 | export type ResetPasswordSchemaType = z.infer; 61 | -------------------------------------------------------------------------------- /src/modules/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import config from "../../config/config.service"; 2 | import { ROLE_ENUM, type RoleType, SOCIAL_ACCOUNT_ENUM } from "../../enums"; 3 | import type { GoogleCallbackQuery } from "../../types"; 4 | import { 5 | type JwtPayload, 6 | compareHash, 7 | fetchGoogleTokens, 8 | generateOTP, 9 | getUserInfo, 10 | hashPassword, 11 | signToken, 12 | } from "../../utils/auth.utils"; 13 | import { generateRandomNumbers } from "../../utils/common.utils"; 14 | import type { UserType } from "../user/user.dto"; 15 | import { 16 | createUser, 17 | getUserByEmail, 18 | getUserById, 19 | updateUser, 20 | } from "../user/user.services"; 21 | import type { 22 | ChangePasswordSchemaType, 23 | ForgetPasswordSchemaType, 24 | LoginUserByEmailSchemaType, 25 | RegisterUserByEmailSchemaType, 26 | ResetPasswordSchemaType, 27 | } from "./auth.schema"; 28 | 29 | export const resetPassword = async (payload: ResetPasswordSchemaType) => { 30 | const user = await getUserById(payload.userId); 31 | 32 | if (!user || user.passwordResetCode !== payload.code) { 33 | throw new Error("token is not valid or expired, please try again"); 34 | } 35 | 36 | if (payload.confirmPassword !== payload.password) { 37 | throw new Error("Password and confirm password must be same"); 38 | } 39 | 40 | const hashedPassword = await hashPassword(payload.password); 41 | 42 | await updateUser(payload.userId, { 43 | password: hashedPassword, 44 | passwordResetCode: null, 45 | }); 46 | }; 47 | 48 | export const forgetPassword = async ( 49 | payload: ForgetPasswordSchemaType, 50 | ): Promise => { 51 | const user = await getUserByEmail(payload.email); 52 | 53 | if (!user) { 54 | throw new Error("user doesn't exists"); 55 | } 56 | 57 | const code = generateRandomNumbers(4); 58 | 59 | await updateUser(user._id, { passwordResetCode: code }); 60 | 61 | return user; 62 | }; 63 | 64 | export const changePassword = async ( 65 | userId: string, 66 | payload: ChangePasswordSchemaType, 67 | ): Promise => { 68 | const user = await getUserById(userId, "+password"); 69 | 70 | if (!user || !user.password) { 71 | throw new Error("User is not found"); 72 | } 73 | 74 | const isCurrentPassowordCorrect = await compareHash( 75 | user.password, 76 | payload.currentPassword, 77 | ); 78 | 79 | if (!isCurrentPassowordCorrect) { 80 | throw new Error("current password is not valid"); 81 | } 82 | 83 | const hashedPassword = await hashPassword(payload.newPassword); 84 | 85 | await updateUser(userId, { password: hashedPassword }); 86 | }; 87 | 88 | export const registerUserByEmail = async ( 89 | payload: RegisterUserByEmailSchemaType, 90 | ): Promise => { 91 | const userExistByEmail = await getUserByEmail(payload.email); 92 | 93 | if (userExistByEmail) { 94 | throw new Error("Account already exist with same email address"); 95 | } 96 | 97 | const { confirmPassword, ...rest } = payload; 98 | 99 | const otp = config.OTP_VERIFICATION_ENABLED ? generateOTP() : null; 100 | 101 | const user = await createUser( 102 | { ...rest, role: "DEFAULT_USER", otp }, 103 | false, 104 | ); 105 | 106 | return user; 107 | }; 108 | 109 | export const loginUserByEmail = async ( 110 | payload: LoginUserByEmailSchemaType, 111 | ): Promise => { 112 | const user = await getUserByEmail(payload.email, "+password"); 113 | 114 | if (!user || !(await compareHash(String(user.password), payload.password))) { 115 | throw new Error("Invalid email or password"); 116 | } 117 | 118 | const jwtPayload: JwtPayload = { 119 | sub: String(user._id), 120 | email: user?.email, 121 | phoneNo: user?.phoneNo, 122 | role: String(user.role) as RoleType, 123 | username: user.username, 124 | }; 125 | 126 | const token = await signToken(jwtPayload); 127 | 128 | return token; 129 | }; 130 | 131 | export const googleLogin = async ( 132 | payload: GoogleCallbackQuery, 133 | ): Promise => { 134 | const { code, error } = payload; 135 | 136 | if (error) { 137 | throw new Error(error); 138 | } 139 | 140 | if (!code) { 141 | throw new Error("Code Not Provided"); 142 | } 143 | const tokenResponse = await fetchGoogleTokens({ code }); 144 | 145 | const { access_token, refresh_token, expires_in } = tokenResponse; 146 | 147 | const userInfoResponse = await getUserInfo(access_token); 148 | 149 | const { id, email, name, picture } = userInfoResponse; 150 | 151 | const user = await getUserByEmail(email); 152 | 153 | if (!user) { 154 | const newUser = await createUser({ 155 | email, 156 | username: name, 157 | avatar: picture, 158 | role: ROLE_ENUM.DEFAULT_USER, 159 | password: generateRandomNumbers(4), 160 | socialAccount: [ 161 | { 162 | refreshToken: refresh_token, 163 | tokenExpiry: new Date(Date.now() + expires_in * 1000), 164 | accountType: SOCIAL_ACCOUNT_ENUM.GOOGLE, 165 | accessToken: access_token, 166 | accountID: id, 167 | }, 168 | ], 169 | }); 170 | 171 | return newUser; 172 | } 173 | 174 | const updatedUser = await updateUser(user._id, { 175 | socialAccount: [ 176 | { 177 | refreshToken: refresh_token, 178 | tokenExpiry: new Date(Date.now() + expires_in * 1000), 179 | accountType: SOCIAL_ACCOUNT_ENUM.GOOGLE, 180 | accessToken: access_token, 181 | accountID: id, 182 | }, 183 | ], 184 | }); 185 | 186 | return updatedUser; 187 | }; 188 | -------------------------------------------------------------------------------- /src/modules/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "express"; 2 | import { StatusCodes } from "http-status-codes"; 3 | import type { MongoIdSchemaType } from "../../common/common.schema"; 4 | import config from "../../config/config.service"; 5 | import { successResponse } from "../../utils/api.utils"; 6 | import { generateRandomPassword } from "../../utils/auth.utils"; 7 | import type { CreateUserSchemaType, GetUsersSchemaType } from "./user.schema"; 8 | import { createUser, deleteUser, getUsers } from "./user.services"; 9 | 10 | export const handleDeleteUser = async ( 11 | req: Request, 12 | res: Response, 13 | ) => { 14 | await deleteUser({ id: req.params.id }); 15 | 16 | return successResponse(res, "User has been deleted"); 17 | }; 18 | 19 | export const handleCreateUser = async ( 20 | req: Request, 21 | res: Response, 22 | ) => { 23 | const data = req.body; 24 | 25 | const user = await createUser({ 26 | ...data, 27 | password: generateRandomPassword(), 28 | role: "DEFAULT_USER", 29 | }); 30 | 31 | return successResponse( 32 | res, 33 | "Email has been sent to the user", 34 | user, 35 | StatusCodes.CREATED, 36 | ); 37 | }; 38 | 39 | export const handleCreateSuperAdmin = async ( 40 | _: Request, 41 | res: Response, 42 | ) => { 43 | 44 | const user = await createUser({ 45 | email: config.ADMIN_EMAIL, 46 | name: "Super Admin", 47 | username: "super_admin", 48 | password: config.ADMIN_PASSWORD, 49 | role: "SUPER_ADMIN", 50 | phoneNo: "123456789", 51 | otp: null, 52 | }); 53 | 54 | return successResponse( 55 | res, 56 | "Super Admin has been created", 57 | { email: user.email, password: config.ADMIN_PASSWORD }, 58 | StatusCodes.CREATED, 59 | ); 60 | }; 61 | 62 | export const handleGetUsers = async ( 63 | req: Request, 64 | res: Response, 65 | ) => { 66 | const { results, paginatorInfo } = await getUsers( 67 | { 68 | id: req.user.sub, 69 | }, 70 | req.query, 71 | ); 72 | 73 | return successResponse(res, undefined, { results, paginatorInfo }); 74 | }; 75 | -------------------------------------------------------------------------------- /src/modules/user/user.dto.ts: -------------------------------------------------------------------------------- 1 | import z from "zod"; 2 | import { definePaginatedResponse } from "../../common/common.utils"; 3 | import { 4 | ROLE_ENUM, 5 | type RoleType, 6 | SOCIAL_ACCOUNT_ENUM, 7 | type SocialAccountType, 8 | } from "../../enums"; 9 | 10 | export const SocialAccountTypeZ = z.enum( 11 | Object.keys(SOCIAL_ACCOUNT_ENUM) as [SocialAccountType], 12 | ); 13 | 14 | export const RoleTypeZ = z.enum(Object.keys(ROLE_ENUM) as [RoleType]); 15 | 16 | export const socialAccountInfoSchema = z.object({ 17 | accountType: SocialAccountTypeZ, 18 | accessToken: z.string(), 19 | tokenExpiry: z.date(), 20 | refreshToken: z.string().optional(), 21 | accountID: z.string(), 22 | }); 23 | 24 | export const userOutSchema = z.object({ 25 | email: z.string().email(), 26 | avatar: z.string().url().optional(), 27 | name: z.string().optional(), 28 | username: z.string(), 29 | role: RoleTypeZ, 30 | phoneNo: z.string().optional(), 31 | socialAccount: z.array(socialAccountInfoSchema).optional(), 32 | updatedAt: z.date().optional(), 33 | createdAt: z.date().optional(), 34 | }); 35 | 36 | export const userSchema = userOutSchema.extend({ 37 | otp: z.string().nullable().optional(), 38 | password: z.string(), 39 | passwordResetCode: z.string().optional().nullable(), 40 | }); 41 | 42 | export const usersPaginatedSchema = definePaginatedResponse(userOutSchema); 43 | 44 | export type UserModelType = z.infer; 45 | export type UserType = z.infer & { id: string; _id: string }; 46 | export type SocialAccountInfoType = z.infer; 47 | export type UserPaginatedType = z.infer; 48 | -------------------------------------------------------------------------------- /src/modules/user/user.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { type Document, Schema } from "mongoose"; 2 | import { ROLE_ENUM, SOCIAL_ACCOUNT_ENUM } from "../../enums"; // Import rolesEnums 3 | import type { 4 | SocialAccountInfoType, 5 | UserModelType, 6 | UserType, 7 | } from "./user.dto"; 8 | 9 | const SocialAccountSchema = new Schema({ 10 | accountType: { 11 | type: String, 12 | required: true, 13 | enum: Object.keys(SOCIAL_ACCOUNT_ENUM), 14 | }, 15 | accessToken: { type: String, required: true }, 16 | tokenExpiry: { type: Date }, 17 | refreshToken: { type: String }, 18 | accountID: { type: String, required: true }, 19 | }); 20 | 21 | const UserSchema: Schema = new Schema( 22 | { 23 | email: { type: String, unique: true, required: true }, 24 | avatar: { type: String }, 25 | username: { type: String, required: true, unique: true }, 26 | name: { type: String }, 27 | otp: { type: String }, 28 | role: { 29 | type: String, 30 | required: true, 31 | enum: Object.keys(ROLE_ENUM), 32 | default: ROLE_ENUM.DEFAULT_USER, 33 | }, 34 | password: { type: String, required: true, select: false }, 35 | passwordResetCode: { type: String }, 36 | socialAccount: [{ type: SocialAccountSchema, required: false }], 37 | }, 38 | { timestamps: true }, 39 | ); 40 | 41 | export interface ISocialAccountDocument 42 | extends SocialAccountInfoType, 43 | Document {} 44 | export interface IUserDocument extends Document, UserModelType {} 45 | const User = mongoose.model("User", UserSchema); 46 | export default User; 47 | -------------------------------------------------------------------------------- /src/modules/user/user.router.ts: -------------------------------------------------------------------------------- 1 | import { canAccess } from "../../middlewares/can-access.middleware"; 2 | import MagicRouter from "../../openapi/magic-router"; 3 | import { 4 | handleCreateSuperAdmin, 5 | handleCreateUser, 6 | handleGetUsers, 7 | } from "./user.controller"; 8 | import { createUserSchema, getUsersSchema } from "./user.schema"; 9 | 10 | export const USER_ROUTER_ROOT = "/users"; 11 | 12 | const userRouter = new MagicRouter(USER_ROUTER_ROOT); 13 | 14 | userRouter.get( 15 | "/", 16 | { 17 | requestType: { query: getUsersSchema }, 18 | }, 19 | canAccess(), 20 | handleGetUsers, 21 | ); 22 | 23 | userRouter.post( 24 | "/user", 25 | { requestType: { body: createUserSchema } }, 26 | canAccess("roles", ["SUPER_ADMIN"]), 27 | handleCreateUser, 28 | ); 29 | 30 | userRouter.post("/_super-admin", {}, handleCreateSuperAdmin); 31 | 32 | export default userRouter.getRouter(); 33 | -------------------------------------------------------------------------------- /src/modules/user/user.schema.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | import { passwordValidationSchema } from "../../common/common.schema"; 3 | import { ROLE_ENUM, type RoleType } from "../../enums"; 4 | import { isValidUsername } from "../../utils/isUsername"; 5 | 6 | export const baseCreateUser = z.object({ 7 | email: z 8 | .string({ required_error: "Email is required" }) 9 | .email({ message: "Email is not valid" }), 10 | password: passwordValidationSchema("Password"), 11 | username: z 12 | .string({ required_error: "Username is required" }) 13 | .min(1) 14 | .refine((value) => isValidUsername(value), "Username must be valid"), 15 | }); 16 | 17 | export const createUserSchema = z 18 | .object({ 19 | name: z.string({ required_error: "First name is required" }).min(1), 20 | }) 21 | .merge(baseCreateUser); 22 | 23 | export const getUsersSchema = z.object({ 24 | searchString: z.string().optional(), 25 | limitParam: z 26 | .string() 27 | .default("10") 28 | .refine( 29 | (value) => !Number.isNaN(Number(value)) && Number(value) >= 0, 30 | "Input must be positive integer", 31 | ) 32 | .transform(Number), 33 | pageParam: z 34 | .string() 35 | .default("1") 36 | .refine( 37 | (value) => !Number.isNaN(Number(value)) && Number(value) >= 0, 38 | "Input must be positive integer", 39 | ) 40 | .transform(Number), 41 | filterByRole: z.enum(Object.keys(ROLE_ENUM) as [RoleType]).optional(), 42 | }); 43 | 44 | export type CreateUserSchemaType = z.infer; 45 | export type GetUsersSchemaType = z.infer; 46 | -------------------------------------------------------------------------------- /src/modules/user/user.services.ts: -------------------------------------------------------------------------------- 1 | import type { FilterQuery } from "mongoose"; 2 | import type { MongoIdSchemaType } from "../../common/common.schema"; 3 | import { hashPassword } from "../../utils/auth.utils"; 4 | import { getPaginator } from "../../utils/getPaginator"; 5 | import type { UserModelType, UserType } from "./user.dto"; 6 | import User, { type IUserDocument } from "./user.model"; 7 | import type { GetUsersSchemaType } from "./user.schema"; 8 | 9 | export const updateUser = async ( 10 | userId: string, 11 | payload: Partial, 12 | ): Promise => { 13 | const user = await User.findOneAndUpdate( 14 | { 15 | _id: userId, 16 | }, 17 | { $set: { ...payload } }, 18 | { 19 | new: true, 20 | }, 21 | ); 22 | 23 | if (!user) throw new Error("User not found"); 24 | 25 | return user.toObject(); 26 | }; 27 | 28 | export const getUserById = async (userId: string, select?: string) => { 29 | const user = await User.findOne({ 30 | _id: userId, 31 | }).select(select ?? ""); 32 | 33 | if (!user) { 34 | throw new Error("User not found"); 35 | } 36 | 37 | return user.toObject(); 38 | }; 39 | 40 | export const getUserByEmail = async ( 41 | email: string, 42 | select?: string, 43 | ): Promise => { 44 | const user = await User.findOne({ email }).select(select ?? ""); 45 | 46 | return user?.toObject() ?? null; 47 | }; 48 | 49 | export const deleteUser = async (userId: MongoIdSchemaType) => { 50 | const user = await User.findByIdAndDelete({ _id: userId.id }); 51 | 52 | if (!user) { 53 | throw new Error("User not found"); 54 | } 55 | }; 56 | 57 | export const getUsers = async ( 58 | userId: MongoIdSchemaType, 59 | payload: GetUsersSchemaType, 60 | ) => { 61 | const { id } = userId; 62 | const currentUser = await User.findById({ _id: id }); 63 | if (!currentUser) { 64 | throw new Error("User must be logged in"); 65 | } 66 | 67 | const conditions: FilterQuery = {}; 68 | 69 | if (payload.searchString) { 70 | conditions.$or = [ 71 | { firstName: { $regex: payload.searchString, $options: "i" } }, 72 | { lastName: { $regex: payload.searchString, $options: "i" } }, 73 | { email: { $regex: payload.searchString, $options: "i" } }, 74 | ]; 75 | } 76 | 77 | if (payload.filterByRole) { 78 | conditions.role = payload.filterByRole; 79 | } else { 80 | conditions.role = { $in: ["DEFAULT_USER"] }; 81 | } 82 | 83 | const totalRecords = await User.countDocuments(conditions); 84 | const paginatorInfo = getPaginator( 85 | payload.limitParam, 86 | payload.pageParam, 87 | totalRecords, 88 | ); 89 | 90 | const results = await User.find(conditions) 91 | .limit(paginatorInfo.limit) 92 | .skip(paginatorInfo.skip) 93 | .exec(); 94 | 95 | return { 96 | results, 97 | paginatorInfo, 98 | }; 99 | }; 100 | 101 | export const createUser = async ( 102 | payload: UserModelType & { password: string }, 103 | checkExist = true, 104 | ): Promise => { 105 | if (checkExist) { 106 | const isUserExist = await User.findOne({ email: payload.email }); 107 | 108 | if (isUserExist) { 109 | throw new Error("User already exists"); 110 | } 111 | } 112 | 113 | if (!payload.password) { 114 | throw new Error("Password is required"); 115 | } 116 | 117 | const hashedPassword = await hashPassword(payload.password); 118 | 119 | const createdUser = await User.create({ 120 | ...payload, 121 | password: hashedPassword, 122 | }); 123 | 124 | return { ...createdUser.toObject(), password: "", otp: null }; 125 | }; 126 | -------------------------------------------------------------------------------- /src/openapi/magic-router.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type NextFunction, 3 | type Request, 4 | type Response, 5 | Router, 6 | } from "express"; 7 | import asyncHandler from "express-async-handler"; 8 | import type { ZodTypeAny } from "zod"; 9 | import { 10 | errorResponseSchema, 11 | successResponseSchema, 12 | } from "../common/common.schema"; 13 | import { canAccess } from "../middlewares/can-access.middleware"; 14 | import { validateZodSchema } from "../middlewares/validate-zod-schema.middleware"; 15 | import type { 16 | RequestExtended, 17 | RequestZodSchemaType, 18 | ResponseExtended, 19 | } from "../types"; 20 | import responseInterceptor from "../utils/responseInterceptor"; 21 | import { 22 | camelCaseToTitleCase, 23 | parseRouteString, 24 | routeToClassName, 25 | } from "./openapi.utils"; 26 | import { bearerAuth, registry } from "./swagger-instance"; 27 | 28 | type Method = 29 | | "get" 30 | | "post" 31 | | "put" 32 | | "delete" 33 | | "patch" 34 | | "head" 35 | | "options" 36 | | "trace"; 37 | 38 | // biome-ignore lint/suspicious/noExplicitAny: 39 | export type IDontKnow = unknown | never | any; 40 | export type MaybePromise = void | Promise; 41 | export type RequestAny = Request; 42 | export type ResponseAny = Response>; 43 | export type MagicPathType = `/${string}`; 44 | export type MagicRoutePType = PathSet extends true 45 | ? [reqAndRes: RequestAndResponseType, ...handlers: MagicMiddleware[]] 46 | : [ 47 | path: MagicPathType, 48 | reqAndRes: RequestAndResponseType, 49 | ...handlers: MagicMiddleware[], 50 | ]; 51 | export type MagicRouteRType = Omit< 52 | MagicRouter, 53 | "route" | "getRouter" | "use" 54 | >; 55 | export type MagicMiddleware = ( 56 | req: RequestAny, 57 | res: ResponseAny, 58 | next?: NextFunction, 59 | ) => MaybePromise; 60 | 61 | export type RequestAndResponseType = { 62 | requestType?: RequestZodSchemaType; 63 | responseModel?: ZodTypeAny; 64 | }; 65 | 66 | export class MagicRouter { 67 | private router: Router; 68 | private rootRoute: string; 69 | private currentPath?: MagicPathType; 70 | 71 | constructor(rootRoute: string, currentPath?: MagicPathType) { 72 | this.router = Router(); 73 | this.rootRoute = rootRoute; 74 | this.currentPath = currentPath; 75 | } 76 | 77 | private getPath(path: string) { 78 | return this.rootRoute + parseRouteString(path); 79 | } 80 | 81 | private wrapper( 82 | method: Method, 83 | path: MagicPathType, 84 | requestAndResponseType: RequestAndResponseType, 85 | ...middlewares: Array 86 | ): void { 87 | const bodyType = requestAndResponseType.requestType?.body; 88 | const paramsType = requestAndResponseType.requestType?.params; 89 | const queryType = requestAndResponseType.requestType?.query; 90 | const responseType = 91 | requestAndResponseType.responseModel ?? successResponseSchema; 92 | 93 | const className = routeToClassName(this.rootRoute); 94 | const title = camelCaseToTitleCase( 95 | middlewares[middlewares.length - 1]?.name, 96 | ); 97 | 98 | const bodySchema = bodyType 99 | ? registry.register(`${title} Input`, bodyType) 100 | : null; 101 | 102 | const hasSecurity = middlewares.some((m) => m.name === canAccess().name); 103 | 104 | const attachResponseModelMiddleware = ( 105 | _: RequestAny, 106 | res: ResponseAny, 107 | next: NextFunction, 108 | ) => { 109 | res.locals.validateSchema = requestAndResponseType.responseModel; 110 | next(); 111 | }; 112 | 113 | registry.registerPath({ 114 | method: method, 115 | tags: [className], 116 | path: this.getPath(path), 117 | security: hasSecurity ? [{ [bearerAuth.name]: ["bearer"] }] : [], 118 | description: title, 119 | summary: title, 120 | request: { 121 | params: paramsType, 122 | query: queryType, 123 | ...(bodySchema 124 | ? { 125 | body: { 126 | content: { 127 | "application/json": { 128 | schema: bodySchema, 129 | }, 130 | }, 131 | }, 132 | } 133 | : {}), 134 | }, 135 | responses: { 136 | 200: { 137 | description: "", 138 | content: { 139 | "application/json": { 140 | schema: responseType, 141 | }, 142 | }, 143 | }, 144 | 400: { 145 | description: "API Error Response", 146 | content: { 147 | "application/json": { 148 | schema: errorResponseSchema, 149 | }, 150 | }, 151 | }, 152 | 404: { 153 | description: "API Error Response", 154 | content: { 155 | "application/json": { 156 | schema: errorResponseSchema, 157 | }, 158 | }, 159 | }, 160 | 500: { 161 | description: "API Error Response", 162 | content: { 163 | "application/json": { 164 | schema: errorResponseSchema, 165 | }, 166 | }, 167 | }, 168 | }, 169 | }); 170 | 171 | const requestType = requestAndResponseType.requestType ?? {}; 172 | 173 | const controller = asyncHandler(middlewares[middlewares.length - 1]); 174 | 175 | const responseInterceptorWrapper = ( 176 | req: RequestAny | RequestExtended, 177 | res: ResponseAny | ResponseExtended, 178 | next: NextFunction, 179 | ) => { 180 | return responseInterceptor( 181 | req as RequestExtended, 182 | res as ResponseExtended, 183 | next, 184 | ); 185 | }; 186 | 187 | middlewares.pop(); 188 | 189 | if (Object.keys(requestType).length) { 190 | this.router[method]( 191 | path, 192 | attachResponseModelMiddleware, 193 | responseInterceptorWrapper, 194 | validateZodSchema(requestType), 195 | ...middlewares, 196 | controller, 197 | ); 198 | } else { 199 | this.router[method]( 200 | path, 201 | attachResponseModelMiddleware, 202 | ...middlewares, 203 | responseInterceptorWrapper, 204 | controller, 205 | ); 206 | } 207 | } 208 | 209 | public get(...args: MagicRoutePType): MagicRouteRType { 210 | return this.routeHandler("get", ...args); 211 | } 212 | 213 | public post(...args: MagicRoutePType): MagicRouteRType { 214 | return this.routeHandler("post", ...args); 215 | } 216 | 217 | public delete(...args: MagicRoutePType): MagicRouteRType { 218 | return this.routeHandler("delete", ...args); 219 | } 220 | 221 | public patch(...args: MagicRoutePType): MagicRouteRType { 222 | return this.routeHandler("patch", ...args); 223 | } 224 | 225 | public put(...args: MagicRoutePType): MagicRouteRType { 226 | return this.routeHandler("put", ...args); 227 | } 228 | 229 | public use(...args: Parameters): void { 230 | this.router.use(...args); 231 | } 232 | 233 | public route(path: MagicPathType): MagicRouteRType { 234 | // Create a proxy object that will use the same router instance 235 | const proxy = { 236 | get: (...args: [RequestAndResponseType, ...MagicMiddleware[]]) => { 237 | this.wrapper("get", path, ...args); 238 | return proxy; 239 | }, 240 | post: (...args: [RequestAndResponseType, ...MagicMiddleware[]]) => { 241 | this.wrapper("post", path, ...args); 242 | return proxy; 243 | }, 244 | put: (...args: [RequestAndResponseType, ...MagicMiddleware[]]) => { 245 | this.wrapper("put", path, ...args); 246 | return proxy; 247 | }, 248 | delete: (...args: [RequestAndResponseType, ...MagicMiddleware[]]) => { 249 | this.wrapper("delete", path, ...args); 250 | return proxy; 251 | }, 252 | patch: (...args: [RequestAndResponseType, ...MagicMiddleware[]]) => { 253 | this.wrapper("patch", path, ...args); 254 | return proxy; 255 | }, 256 | }; 257 | return proxy; 258 | } 259 | 260 | private routeHandler(method: Method, ...args: MagicRoutePType) { 261 | if (this.currentPath) { 262 | const [reqAndRes, ...handlers] = args as [ 263 | RequestAndResponseType, 264 | ...MagicMiddleware[], 265 | ]; 266 | this.wrapper(method, this.currentPath, reqAndRes, ...handlers); 267 | } else { 268 | const [path, reqAndRes, ...handlers] = args as [ 269 | MagicPathType, 270 | RequestAndResponseType, 271 | ...MagicMiddleware[], 272 | ]; 273 | this.wrapper(method, path, reqAndRes, ...handlers); 274 | } 275 | return this; 276 | } 277 | 278 | // Method to get the router instance 279 | public getRouter(): Router { 280 | return this.router; 281 | } 282 | } 283 | 284 | export default MagicRouter; 285 | -------------------------------------------------------------------------------- /src/openapi/openapi.utils.ts: -------------------------------------------------------------------------------- 1 | export const parseRouteString = (route?: string): string => { 2 | return (route ?? "").replace(/\/:(\w+)/g, "/{$1}"); 3 | }; 4 | 5 | export const routeToClassName = (route?: string): string => { 6 | const cleanedRoute = (route ?? "").replace(/^\/|\/$/g, ""); 7 | 8 | const className = 9 | cleanedRoute.charAt(0).toUpperCase() + cleanedRoute.slice(1).toLowerCase(); 10 | 11 | return className.endsWith("s") ? className.slice(0, -1) : className; 12 | }; 13 | 14 | export const camelCaseToTitleCase = (input?: string): string => { 15 | let titleCase = (input ?? "") 16 | .replace(/([A-Z])/g, " $1") 17 | .replace(/^./, (str) => str.toUpperCase()) 18 | .trim(); 19 | 20 | if (titleCase.split(" ")[0] === "Handle") { 21 | titleCase = titleCase.slice(6); 22 | } 23 | 24 | return titleCase; 25 | }; 26 | -------------------------------------------------------------------------------- /src/openapi/swagger-doc-generator.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import { OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi"; 3 | import * as yaml from "yaml"; 4 | 5 | import type { OpenAPIObject } from "openapi3-ts/oas30"; 6 | import config from "../config/config.service"; 7 | import { registry } from "./swagger-instance"; 8 | 9 | export const getOpenApiDocumentation = (): OpenAPIObject => { 10 | const generator = new OpenApiGeneratorV3(registry.definitions); 11 | 12 | return generator.generateDocument({ 13 | openapi: "3.0.0", 14 | info: { 15 | version: config.APP_VERSION, 16 | title: config.APP_NAME, 17 | description: 18 | "Robust backend boilerplate designed for scalability, flexibility, and ease of development. It's packed with modern technologies and best practices to kickstart your next backend project", 19 | }, 20 | servers: [{ url: "/api" }], 21 | }); 22 | }; 23 | 24 | export const convertDocumentationToYaml = (): string => { 25 | const docs = getOpenApiDocumentation(); 26 | 27 | const fileContent = yaml.stringify(docs); 28 | 29 | return fileContent; 30 | }; 31 | 32 | export const writeDocumentationToDisk = async (): Promise => { 33 | const fileContent = convertDocumentationToYaml(); 34 | 35 | await fs.writeFile(`${__dirname}/openapi-docs.yml`, fileContent, { 36 | encoding: "utf-8", 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /src/openapi/swagger-instance.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi"; 2 | 3 | export const registry = new OpenAPIRegistry(); 4 | 5 | export const bearerAuth = registry.registerComponent( 6 | "securitySchemes", 7 | "bearerAuth", 8 | { 9 | type: "http", 10 | scheme: "bearer", 11 | bearerFormat: "JWT", 12 | }, 13 | ); 14 | -------------------------------------------------------------------------------- /src/openapi/zod-extend.ts: -------------------------------------------------------------------------------- 1 | import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; 2 | import { z } from "zod"; 3 | extendZodWithOpenApi(z); 4 | -------------------------------------------------------------------------------- /src/queues/email.queue.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type SendResetPasswordTypePayload, 3 | sendResetPasswordEmail, 4 | } from "../email/email.service"; 5 | import logger from "../lib/logger.service"; 6 | import { Queue } from "../lib/queue.server"; 7 | 8 | export const ResetPasswordQueue = Queue( 9 | "ResetPasswordQueue", 10 | async (job) => { 11 | try { 12 | const { data } = job; 13 | 14 | await sendResetPasswordEmail({ 15 | ...data, 16 | }); 17 | 18 | return true; 19 | } catch (err) { 20 | if (err instanceof Error) logger.error(err.message); 21 | 22 | throw err; 23 | } 24 | }, 25 | ); 26 | -------------------------------------------------------------------------------- /src/routes/routes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import authRouter, { AUTH_ROUTER_ROOT } from "../modules/auth/auth.router"; 3 | 4 | import healthCheckRouter, { 5 | HEALTH_ROUTER_ROOT, 6 | } from "../healthcheck/healthcheck.routes"; 7 | import userRouter, { USER_ROUTER_ROOT } from "../modules/user/user.router"; 8 | import uploadRouter, { UPLOAD_ROUTER_ROOT } from "../upload/upload.router"; 9 | 10 | const router = express.Router(); 11 | 12 | router.use(HEALTH_ROUTER_ROOT, healthCheckRouter); 13 | router.use(USER_ROUTER_ROOT, userRouter); 14 | router.use(AUTH_ROUTER_ROOT, authRouter); 15 | router.use(UPLOAD_ROUTER_ROOT, uploadRouter); 16 | 17 | export default router; 18 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "express"; 2 | import type { Server } from "socket.io"; 3 | import type { AnyZodObject, ZodEffects, ZodSchema } from "zod"; 4 | import type { JwtPayload } from "./utils/auth.utils"; 5 | 6 | export type ZodObjectWithEffect = 7 | | AnyZodObject 8 | | ZodEffects; 9 | 10 | export interface GoogleCallbackQuery { 11 | code: string; 12 | error?: string; 13 | } 14 | 15 | export type RequestZodSchemaType = { 16 | params?: ZodObjectWithEffect; 17 | query?: ZodObjectWithEffect; 18 | body?: ZodSchema; 19 | }; 20 | 21 | export interface RequestExtended extends Request { 22 | user: JwtPayload; 23 | io: Server; 24 | } 25 | 26 | export interface ResponseExtended extends Response { 27 | locals: { 28 | validateSchema?: ZodSchema; 29 | }; 30 | jsonValidate: Response["json"]; 31 | sendValidate: Response["send"]; 32 | } 33 | -------------------------------------------------------------------------------- /src/upload/upload.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "express"; 2 | import type { UserType } from "../modules/user/user.dto"; 3 | import { updateUser } from "../modules/user/user.services"; 4 | import { errorResponse, successResponse } from "../utils/api.utils"; 5 | 6 | export const handleProfileUpload = async (req: Request, res: Response) => { 7 | try { 8 | const file = req.file; 9 | const currentUser = req.user as UserType; 10 | 11 | if ((file && !("location" in file)) || !file) { 12 | return errorResponse(res, "File not uploaded, Please try again"); 13 | } 14 | 15 | const user = await updateUser( 16 | { avatar: String(file.location) }, 17 | { id: String(currentUser._id) }, 18 | ); 19 | 20 | return successResponse(res, "Profile picture has been uploaded", user); 21 | } catch (err) { 22 | return errorResponse(res, (err as Error).message); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/upload/upload.router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { canAccess } from "../middlewares/can-access.middleware"; 3 | import { uploadProfile } from "../middlewares/multer-s3.middleware"; 4 | import { handleProfileUpload } from "./upload.controller"; 5 | 6 | export const UPLOAD_ROUTER_ROOT = "/upload"; 7 | 8 | const uploadRouter = Router(); 9 | 10 | uploadRouter.post("/profile", canAccess(), uploadProfile, handleProfileUpload); 11 | 12 | export default uploadRouter; 13 | -------------------------------------------------------------------------------- /src/utils/api.utils.ts: -------------------------------------------------------------------------------- 1 | import type { Response } from "express"; 2 | import { StatusCodes } from "http-status-codes"; 3 | import config from "../config/config.service"; 4 | import logger from "../lib/logger.service"; 5 | import type { ResponseExtended } from "../types"; 6 | 7 | export const errorResponse = ( 8 | res: ResponseExtended | Response, 9 | message?: string, 10 | statusCode?: StatusCodes, 11 | payload?: unknown, 12 | stack?: string, 13 | ): void => { 14 | try { 15 | if ("jsonValidate" in res) { 16 | (res as ResponseExtended) 17 | .status(statusCode ?? StatusCodes.BAD_REQUEST) 18 | .jsonValidate({ 19 | success: false, 20 | message: message, 21 | data: payload, 22 | stack: stack, 23 | }); 24 | } else { 25 | (res as ResponseExtended) 26 | .status(statusCode ?? StatusCodes.BAD_REQUEST) 27 | .json({ 28 | success: false, 29 | message: message, 30 | data: payload, 31 | stack: stack, 32 | }); 33 | } 34 | 35 | return; 36 | } catch (err) { 37 | logger.error(err); 38 | } 39 | }; 40 | 41 | export const successResponse = ( 42 | res: ResponseExtended | Response, 43 | message?: string, 44 | payload?: Record, 45 | statusCode: StatusCodes = StatusCodes.OK, 46 | ): void => { 47 | try { 48 | if ("jsonValidate" in res) { 49 | (res as ResponseExtended) 50 | .status(statusCode) 51 | .jsonValidate({ success: true, message: message, data: payload }); 52 | } else { 53 | (res as ResponseExtended) 54 | .status(statusCode) 55 | .json({ success: true, message: message, data: payload }); 56 | } 57 | 58 | return; 59 | } catch (err) { 60 | logger.error(err); 61 | } 62 | }; 63 | 64 | export const generateResetPasswordLink = (token: string) => { 65 | return `${config.CLIENT_SIDE_URL}/reset-password?token=${token}`; 66 | }; 67 | 68 | export const generateSetPasswordLink = (token: string) => { 69 | return `${config.CLIENT_SIDE_URL}/set-password?token=${token}`; 70 | }; 71 | -------------------------------------------------------------------------------- /src/utils/auth.utils.ts: -------------------------------------------------------------------------------- 1 | import crypto from "node:crypto"; 2 | import argon2 from "argon2"; 3 | import { JsonWebTokenError, sign, verify } from "jsonwebtoken"; 4 | import config from "../config/config.service"; 5 | import type { RoleType } from "../enums"; 6 | import logger from "../lib/logger.service"; 7 | 8 | export interface GoogleTokenResponse { 9 | access_token: string; 10 | expires_in: number; 11 | id_token: string; 12 | refresh_token?: string; 13 | scope: string; 14 | token_type: string; 15 | } 16 | 17 | export interface GoogleTokensRequestParams { 18 | code: string; 19 | } 20 | 21 | export type JwtPayload = { 22 | sub: string; 23 | email?: string | null; 24 | phoneNo?: string | null; 25 | username: string; 26 | role: RoleType; 27 | }; 28 | 29 | export type PasswordResetTokenPayload = { 30 | email: string; 31 | userId: string; 32 | }; 33 | 34 | export type SetPasswordTokenPayload = { 35 | email: string; 36 | userId: string; 37 | }; 38 | 39 | export const hashPassword = async (password: string): Promise => { 40 | return argon2.hash(password); 41 | }; 42 | 43 | export const compareHash = async ( 44 | hashed: string, 45 | plainPassword: string, 46 | ): Promise => { 47 | return argon2.verify(hashed, plainPassword); 48 | }; 49 | export const signToken = async (payload: JwtPayload): Promise => { 50 | return sign(payload, String(config.JWT_SECRET), { 51 | expiresIn: Number(config.JWT_EXPIRES_IN) * 1000, 52 | }); 53 | }; 54 | 55 | export const signPasswordResetToken = async ( 56 | payload: PasswordResetTokenPayload, 57 | ) => { 58 | return sign(payload, String(config.JWT_SECRET), { 59 | expiresIn: config.PASSWORD_RESET_TOKEN_EXPIRES_IN * 1000, 60 | }); 61 | }; 62 | 63 | export const signSetPasswordToken = async ( 64 | payload: SetPasswordTokenPayload, 65 | ) => { 66 | return sign(payload, String(config.JWT_SECRET), { 67 | expiresIn: config.SET_PASSWORD_TOKEN_EXPIRES_IN, 68 | }); 69 | }; 70 | 71 | export const verifyToken = async < 72 | T extends JwtPayload | PasswordResetTokenPayload | SetPasswordTokenPayload, 73 | >( 74 | token: string, 75 | ): Promise => { 76 | try { 77 | return verify(token, String(config.JWT_SECRET)) as T; 78 | } catch (err) { 79 | if (err instanceof Error) { 80 | throw new Error(err.message); 81 | } 82 | 83 | if (err instanceof JsonWebTokenError) { 84 | throw new Error(err.message); 85 | } 86 | 87 | logger.error("verifyToken", { err }); 88 | throw err; 89 | } 90 | }; 91 | 92 | export const generateRandomPassword = (length = 16): string => { 93 | return crypto.randomBytes(length).toString("hex"); 94 | }; 95 | export const fetchGoogleTokens = async ( 96 | params: GoogleTokensRequestParams, 97 | ): Promise => { 98 | if ( 99 | !config.GOOGLE_CLIENT_ID || 100 | !config.GOOGLE_CLIENT_SECRET || 101 | !config.GOOGLE_REDIRECT_URI 102 | ) { 103 | throw new Error("Google credentials are not set"); 104 | } 105 | 106 | const url = "https://oauth2.googleapis.com/token"; 107 | const response = await fetch(url, { 108 | method: "POST", 109 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 110 | body: new URLSearchParams({ 111 | code: params.code, 112 | client_id: config.GOOGLE_CLIENT_ID, 113 | client_secret: config.GOOGLE_CLIENT_SECRET, 114 | redirect_uri: config.GOOGLE_REDIRECT_URI, 115 | grant_type: "authorization_code", 116 | }), 117 | }); 118 | 119 | if (!response.ok) { 120 | throw new Error("Failed to exchange code for tokens"); 121 | } 122 | 123 | const data: GoogleTokenResponse = await response.json(); 124 | return data; 125 | }; 126 | export interface GoogleUserInfo { 127 | id: string; 128 | email: string; 129 | verified_email: boolean; 130 | name: string; 131 | given_name: string; 132 | family_name: string; 133 | picture: string; 134 | locale: string; 135 | } 136 | 137 | export const getUserInfo = async (accessToken: string) => { 138 | const userInfoResponse = await fetch( 139 | "https://www.googleapis.com/oauth2/v2/userinfo", 140 | { 141 | headers: { Authorization: `Bearer ${accessToken}` }, 142 | }, 143 | ); 144 | if (!userInfoResponse.ok) { 145 | throw new Error("Error fetching user info"); 146 | } 147 | return userInfoResponse.json(); 148 | }; 149 | 150 | export const generateOTP = (length = 6): string => { 151 | return crypto.randomBytes(length).toString("hex").slice(0, length); 152 | }; 153 | -------------------------------------------------------------------------------- /src/utils/common.utils.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { customAlphabet } from "nanoid"; 3 | import config from "../config/config.service"; 4 | 5 | export const customNanoId = customAlphabet("0123456789", 4); 6 | 7 | const transformableToBooleanTruthy = ["true", "TRUE", "t", "T", "1"]; 8 | const transformableToBooleanFalsy = ["false", "FALSE", "f", "F", "0"]; 9 | 10 | export const transformableToBooleanError = `Value must be one of ${transformableToBooleanTruthy.join(", ")} or ${transformableToBooleanFalsy.join(", ")}`; 11 | 12 | export const stringToBoolean = (value: string): boolean => { 13 | if (transformableToBooleanTruthy.includes(value)) { 14 | return true; 15 | } 16 | 17 | if (transformableToBooleanFalsy.includes(value)) { 18 | return false; 19 | } 20 | 21 | throw new Error("Value is not transformable to boolean"); 22 | }; 23 | 24 | export const isTransformableToBoolean = (value: string) => { 25 | if ( 26 | !transformableToBooleanTruthy.includes(value) && 27 | !transformableToBooleanFalsy.includes(value) 28 | ) { 29 | return false; 30 | } 31 | 32 | return true; 33 | }; 34 | 35 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 36 | export const sanitizeRecord = >( 37 | record: T, 38 | ): T => { 39 | try { 40 | return Object.fromEntries( 41 | Object.entries(record).filter( 42 | ([_, value]) => value !== null && value !== undefined, 43 | ), 44 | ) as T; 45 | } catch { 46 | return record; 47 | } 48 | }; 49 | 50 | export const checkRecordForEmptyArrays = >( 51 | record: T, 52 | ): T => { 53 | try { 54 | return Object.fromEntries( 55 | Object.entries(record).filter( 56 | ([_, value]) => Array.isArray(value) && !!value.length, 57 | ), 58 | ) as T; 59 | } catch { 60 | return record; 61 | } 62 | }; 63 | 64 | export const generateRandomNumbers = (length: number): string => { 65 | let id = ""; 66 | 67 | if (config.STATIC_OTP) { 68 | id = "1234"; 69 | } else { 70 | id = customNanoId(length); 71 | } 72 | 73 | return id; 74 | }; 75 | 76 | export const checkFiletype = (file: Express.Multer.File): boolean => { 77 | const filetypes = /jpeg|jpg|png/; 78 | 79 | const checkExtname = filetypes.test( 80 | path.extname(file.originalname).toLowerCase(), 81 | ); 82 | const checkMimetype = filetypes.test(file.mimetype); 83 | 84 | return checkExtname && checkMimetype; 85 | }; 86 | -------------------------------------------------------------------------------- /src/utils/getPaginator.ts: -------------------------------------------------------------------------------- 1 | export type GetPaginatorReturnType = { 2 | skip: number; 3 | limit: number; 4 | currentPage: number; 5 | pages: number; 6 | hasNextPage: boolean; 7 | totalRecords: number; 8 | pageSize: number; 9 | }; 10 | 11 | export const getPaginator = ( 12 | limitParam: number, 13 | pageParam: number, 14 | totalRecords: number, 15 | ): GetPaginatorReturnType => { 16 | let skip = pageParam; 17 | const limit = limitParam; 18 | 19 | if (pageParam <= 1) { 20 | skip = 0; 21 | } else { 22 | skip = limit * (pageParam - 1); 23 | } 24 | 25 | const currentPage = Math.max(1, pageParam as number); 26 | 27 | const pages = Math.ceil(totalRecords / Number(limit)); 28 | 29 | const hasNextPage = pages > currentPage; 30 | 31 | return { 32 | skip, 33 | limit, 34 | currentPage, 35 | pages, 36 | hasNextPage, 37 | totalRecords, 38 | pageSize: limit, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/utils/globalErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import type { NextFunction, Request, Response } from "express"; 2 | import config from "../config/config.service"; 3 | import logger from "../lib/logger.service"; 4 | import type { RequestExtended, ResponseExtended } from "../types"; 5 | import { errorResponse } from "./api.utils"; 6 | 7 | interface CustomError extends Error { 8 | status?: number; 9 | message: string; 10 | } 11 | 12 | export const globalErrorHandler = ( 13 | err: CustomError, 14 | _: RequestExtended | Request, 15 | res: ResponseExtended | Response, 16 | __: NextFunction, 17 | ): void => { 18 | const statusCode = err.status || 500; 19 | const errorMessage = err.message || "Internal Server Error"; 20 | 21 | logger.error(`${statusCode}: ${errorMessage}`); 22 | 23 | errorResponse( 24 | res as ResponseExtended, 25 | errorMessage, 26 | statusCode, 27 | err, 28 | config.NODE_ENV === "development" ? err.stack : undefined, 29 | ); 30 | 31 | return; 32 | }; 33 | 34 | export default globalErrorHandler; 35 | -------------------------------------------------------------------------------- /src/utils/isUsername.ts: -------------------------------------------------------------------------------- 1 | const usernameRegex = /^[a-zA-Z0-9_]{3,16}$/; 2 | 3 | // Usage 4 | export const isValidUsername = (username: string) => 5 | usernameRegex.test(username); 6 | -------------------------------------------------------------------------------- /src/utils/responseInterceptor.ts: -------------------------------------------------------------------------------- 1 | import type { NextFunction } from "express"; 2 | import { ZodError } from "zod"; 3 | import type { RequestExtended, ResponseExtended } from "../types"; 4 | 5 | const responseInterceptor = ( 6 | _: RequestExtended, 7 | res: ResponseExtended, 8 | next: NextFunction, 9 | ) => { 10 | const originalJson = res.json; 11 | const originalSend = res.send; 12 | const validateSchema = res.locals.validateSchema ?? null; 13 | 14 | res.jsonValidate = function (body) { 15 | if (validateSchema) { 16 | try { 17 | validateSchema.parse(body); 18 | } catch (err) { 19 | if (err instanceof ZodError) { 20 | return originalJson.call(this, { 21 | success: false, 22 | message: "Response Validation Error - Server Error", 23 | data: err.errors, 24 | stack: err.stack, 25 | }); 26 | } 27 | } 28 | } 29 | 30 | return originalJson.call( 31 | this, 32 | validateSchema ? validateSchema.parse(body) : body, 33 | ); 34 | }; 35 | 36 | res.sendValidate = function (body) { 37 | if (validateSchema) { 38 | try { 39 | validateSchema.parse(body); 40 | } catch (err) { 41 | if (err instanceof ZodError) { 42 | return originalSend.call(this, { 43 | success: false, 44 | message: "Response Validation Error - Server Error", 45 | data: err.errors, 46 | stack: err.stack, 47 | }); 48 | } 49 | } 50 | } 51 | 52 | return originalSend.call( 53 | this, 54 | validateSchema ? validateSchema.parse(body) : body, 55 | ); 56 | }; 57 | 58 | next(); 59 | }; 60 | 61 | export default responseInterceptor; 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["modules.d.ts", "**/*.ts", "**/*.tsx"], 3 | "exclude": ["node_modules"], 4 | "compilerOptions": { 5 | /* Visit https://aka.ms/tsconfig to read more about this file */ 6 | 7 | /* Projects */ 8 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 9 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 10 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 11 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 12 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 13 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 14 | 15 | /* Language and Environment */ 16 | "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 17 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 18 | "jsx": "react" /* Specify what JSX code is generated. */, 19 | "experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */, 20 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 21 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 22 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 23 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 24 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 25 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 26 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 27 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 28 | 29 | /* Modules */ 30 | "module": "NodeNext" /* Specify what module code is generated. */, 31 | "rootDir": "." /* Specify the root folder within your source files. */, 32 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 33 | "baseUrl": "." /* Specify the base directory to resolve non-relative module names. */, 34 | "paths": {} /* Specify a set of entries that re-map imports to additional lookup locations. */, 35 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 36 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 37 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 38 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 39 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 40 | "resolveJsonModule": true /* Enable importing .json files. */, 41 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 42 | 43 | /* JavaScript Support */ 44 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 45 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 46 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 47 | 48 | /* Emit */ 49 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 50 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 51 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 52 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 53 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 54 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 55 | // "removeComments": true, /* Disable emitting comments. */ 56 | // "noEmit": true, /* Disable emitting files from a compilation. */ 57 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 58 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 59 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 60 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 61 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 62 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 63 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 64 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 65 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 66 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 67 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 68 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 69 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 70 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 71 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 72 | 73 | /* Interop Constraints */ 74 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 75 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 76 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 77 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 78 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 79 | 80 | /* Type Checking */ 81 | "strict": true /* Enable all strict type-checking options. */, 82 | "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, 83 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 84 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 85 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 86 | "strictPropertyInitialization": false /* Check for class properties that are declared but not set in the constructor. */, 87 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 88 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 89 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 90 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 91 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 92 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 93 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 94 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 95 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 96 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 97 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 98 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 99 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 100 | 101 | /* Completeness */ 102 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 103 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 104 | } 105 | } 106 | --------------------------------------------------------------------------------