├── .env.example ├── .eslintrc.js ├── .github └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ ├── deploy-dev.yml │ └── deploy-prod.yml ├── .gitignore ├── .prettierrc ├── README.md ├── nest-cli.json ├── package-lock.json ├── package.json ├── prisma ├── migrations │ ├── 20210320180834_init │ │ └── migration.sql │ └── migration_lock.toml ├── schema.prisma └── seeds │ ├── index.ts │ └── users.seed.ts ├── src ├── configs │ ├── config.interface.ts │ └── global.config.ts ├── exceptions │ └── invalid.form.exception.ts ├── filters │ ├── all.exceptions.filter.ts │ └── invalid.form.exception.filter.ts ├── main.ts ├── middlewares │ └── logger.middleware.ts ├── modules │ ├── app │ │ ├── app.controller.spec.ts │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ └── app.service.ts │ ├── auth │ │ ├── auth.constants.ts │ │ ├── auth.controller.ts │ │ ├── auth.dto.ts │ │ ├── auth.jwt.guard.ts │ │ ├── auth.jwt.strategy.ts │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ └── auth.user.decorator.ts │ ├── kafka │ │ ├── kafka.consumer.service.ts │ │ ├── kafka.module.ts │ │ └── kafka.producer.service.ts │ ├── logger │ │ ├── logger.module.ts │ │ └── logger.service.ts │ ├── post │ │ ├── post.controller.ts │ │ ├── post.module.ts │ │ └── post.service.ts │ ├── prisma │ │ ├── prisma.config.ts │ │ ├── prisma.module.ts │ │ └── prisma.service.ts │ ├── sqs │ │ ├── sqs.controller.ts │ │ ├── sqs.module.ts │ │ └── sqs.service.ts │ └── user │ │ ├── user.controller.ts │ │ ├── user.listener.ts │ │ ├── user.module.ts │ │ └── user.service.ts └── shared │ ├── constants │ ├── global.constants.ts │ ├── postgres.constants.ts │ ├── prisma.constants.ts │ └── strings.ts │ └── helpers │ └── auth.helpers.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL and SQLite. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | DATABASE_URL=postgresql://postgres:root@localhost:5432/db_name?schema=public 8 | JWT_SIGNATURE=secret123456789 9 | FRONTEND_URL=http://localhost:3000 10 | PORT=3300 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin', 'unused-imports', 'import'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | 25 | "@typescript-eslint/no-unused-vars": "off", 26 | "unused-imports/no-unused-imports": "error", 27 | "unused-imports/no-unused-vars": [ 28 | "warn", 29 | { 30 | "vars": "all", 31 | "varsIgnorePattern": "^_", 32 | "args": "after-used", 33 | "argsIgnorePattern": "^_", 34 | } 35 | ], 36 | 37 | "import/order": [ 38 | "warn", 39 | { 40 | "newlines-between": "always" 41 | } 42 | ], 43 | 44 | "padding-line-between-statements": [ 45 | "error", 46 | { blankLine: "always", prev: ['var', 'const', 'function', 'class'], next: ['if', 'while', 'function'] } 47 | ] 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' # matches every branch 7 | - '!dev' # excludes dev 8 | - '!master' # excludes master 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Install Node.js 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: '16.x' 20 | 21 | - name: Install npm dependencies 22 | run: npm install --legacy-peer-deps 23 | 24 | - name: Prisma Client Generate 25 | run: npx prisma generate 26 | 27 | - name: Run build task 28 | run: npm run build 29 | 30 | - name: Run lint 31 | run: npm run lint 32 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '43 18 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/deploy-dev.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - test-branch # change to "dev" or smth else 7 | 8 | jobs: 9 | deploy: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Install Node.js 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: '16.x' 19 | 20 | - name: Install npm dependencies 21 | run: npm install --legacy-peer-deps 22 | 23 | - name: Run build task 24 | run: npm run build 25 | 26 | - name: store-env 27 | uses: Garryone/store-env@master 28 | env: 29 | PORT: 5000 30 | NODE_ENV: dev 31 | 32 | DATABASE_NAME: db_name 33 | DATABASE_HOST: localhost 34 | DATABASE_USERNAME: ${{ secrets.DB_USER }} 35 | DATABASE_PASSWORD: ${{ secrets.DB_PASS }} 36 | DATABASE_PORT: 5432 37 | 38 | APP_FRONTEND_URL: http://localhost:4200 39 | JWT_SIGNATURE: secret12356789 40 | 41 | - name: Copy build to Server 42 | uses: GarryOne/ssh-deploy@master 43 | env: 44 | SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} 45 | ARGS: "-avzr --delete" 46 | SOURCE: "/" 47 | REMOTE_HOST: ${{ secrets.REMOTE_HOST }} 48 | REMOTE_USER: ${{ secrets.REMOTE_USER }} 49 | TARGET: /var/www/nest-prisma-starter/dev/prisma-api 50 | 51 | - name: DB Sync & Start App 52 | uses: appleboy/ssh-action@master 53 | with: 54 | script_stop: true 55 | host: ${{ secrets.REMOTE_HOST }} 56 | username: ${{ secrets.REMOTE_USER }} 57 | key: ${{ secrets.SSH_PRIVATE_KEY }} 58 | script: | 59 | cd /var/www/nest-prisma-starter/dev/prisma-api 60 | npx prisma migrate deploy 61 | pm2 startOrReload dev.ecosystem.config.js --update-env --time 62 | -------------------------------------------------------------------------------- /.github/workflows/deploy-prod.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - test-branch # change to "master" or smth else 7 | 8 | jobs: 9 | deploy: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Install Node.js 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: '16.x' 19 | 20 | - name: Install npm dependencies 21 | run: npm install --legacy-peer-deps 22 | 23 | - name: Run build task 24 | run: npm run build 25 | 26 | - name: store-env 27 | uses: Garryone/store-env@master 28 | env: 29 | PORT: 5001 30 | NODE_ENV: production 31 | 32 | DATABASE_NAME: db_name 33 | DATABASE_HOST: localhost 34 | DATABASE_USERNAME: ${{ secrets.DB_USER }} 35 | DATABASE_PASSWORD: ${{ secrets.DB_PASS }} 36 | DATABASE_PORT: 5432 37 | 38 | APP_FRONTEND_URL: http://localhost:4200 39 | JWT_SIGNATURE: secret12356789 40 | 41 | - name: Copy build to Server 42 | uses: GarryOne/ssh-deploy@master 43 | env: 44 | SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} 45 | ARGS: "-avzr --delete" 46 | SOURCE: "/" 47 | REMOTE_HOST: ${{ secrets.REMOTE_HOST }} 48 | REMOTE_USER: ${{ secrets.REMOTE_USER }} 49 | TARGET: /var/www/nest-prisma-starter/prod/prisma-api 50 | 51 | - name: DB Sync & Start App 52 | uses: appleboy/ssh-action@master 53 | with: 54 | script_stop: true 55 | host: ${{ secrets.REMOTE_HOST }} 56 | username: ${{ secrets.REMOTE_USER }} 57 | key: ${{ secrets.SSH_PRIVATE_KEY }} 58 | script: | 59 | cd /var/www/nest-prisma-starter/prod/prisma-api 60 | npx prisma migrate deploy 61 | pm2 startOrReload prod.ecosystem.config.js --update-env --time 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/dictionaries 10 | 11 | # Sensitive or high-churn files: 12 | .idea/**/dataSources/ 13 | .idea/**/dataSources.ids 14 | .idea/**/dataSources.xml 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | 20 | # Gradle: 21 | .idea/**/gradle.xml 22 | .idea/**/libraries 23 | 24 | # CMake 25 | cmake-build-debug/ 26 | 27 | # Mongo Explorer plugin: 28 | .idea/**/mongoSettings.xml 29 | 30 | ## File-based project format: 31 | *.iws 32 | 33 | ## Plugin-specific files: 34 | 35 | # IntelliJ 36 | out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Cursive Clojure plugin 45 | .idea/replstate.xml 46 | 47 | # Crashlytics plugin (for Android Studio and IntelliJ) 48 | com_crashlytics_export_strings.xml 49 | crashlytics.properties 50 | crashlytics-build.properties 51 | fabric.properties 52 | ### VisualStudio template 53 | ## Ignore Visual Studio temporary files, build results, and 54 | ## files generated by popular Visual Studio add-ons. 55 | ## 56 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 57 | 58 | # User-specific files 59 | *.suo 60 | *.user 61 | *.userosscache 62 | *.sln.docstates 63 | 64 | # User-specific files (MonoDevelop/Xamarin Studio) 65 | *.userprefs 66 | 67 | # Build results 68 | [Dd]ebug/ 69 | [Dd]ebugPublic/ 70 | [Rr]elease/ 71 | [Rr]eleases/ 72 | x64/ 73 | x86/ 74 | bld/ 75 | [Bb]in/ 76 | [Oo]bj/ 77 | [Ll]og/ 78 | 79 | # Visual Studio 2015 cache/options directory 80 | .vs/ 81 | # Uncomment if you have tasks that create the project's static files in wwwroot 82 | #wwwroot/ 83 | 84 | # MSTest test Results 85 | [Tt]est[Rr]esult*/ 86 | [Bb]uild[Ll]og.* 87 | 88 | # NUNIT 89 | *.VisualState.xml 90 | TestResult.xml 91 | 92 | # Build Results of an ATL Project 93 | [Dd]ebugPS/ 94 | [Rr]eleasePS/ 95 | dlldata.c 96 | 97 | # Benchmark Results 98 | BenchmarkDotNet.Artifacts/ 99 | 100 | # .NET Core 101 | project.lock.json 102 | project.fragment.lock.json 103 | artifacts/ 104 | **/Properties/launchSettings.json 105 | 106 | *_i.c 107 | *_p.c 108 | *_i.h 109 | *.ilk 110 | *.meta 111 | *.obj 112 | *.pch 113 | *.pdb 114 | *.pgc 115 | *.pgd 116 | *.rsp 117 | *.sbr 118 | *.tlb 119 | *.tli 120 | *.tlh 121 | *.tmp 122 | *.tmp_proj 123 | *.log 124 | *.vspscc 125 | *.vssscc 126 | .builds 127 | *.pidb 128 | *.svclog 129 | *.scc 130 | 131 | # Chutzpah Test files 132 | _Chutzpah* 133 | 134 | # Visual C++ cache files 135 | ipch/ 136 | *.aps 137 | *.ncb 138 | *.opendb 139 | *.opensdf 140 | *.sdf 141 | *.cachefile 142 | *.VC.db 143 | *.VC.VC.opendb 144 | 145 | # Visual Studio profiler 146 | *.psess 147 | *.vsp 148 | *.vspx 149 | *.sap 150 | 151 | # Visual Studio Trace Files 152 | *.e2e 153 | 154 | # TFS 2012 Local Workspace 155 | $tf/ 156 | 157 | # Guidance Automation Toolkit 158 | *.gpState 159 | 160 | # ReSharper is a .NET coding add-in 161 | _ReSharper*/ 162 | *.[Rr]e[Ss]harper 163 | *.DotSettings.user 164 | 165 | # JustCode is a .NET coding add-in 166 | .JustCode 167 | 168 | # TeamCity is a build add-in 169 | _TeamCity* 170 | 171 | # DotCover is a Code Coverage Tool 172 | *.dotCover 173 | 174 | # AxoCover is a Code Coverage Tool 175 | .axoCover/* 176 | !.axoCover/settings.json 177 | 178 | # Visual Studio code coverage results 179 | *.coverage 180 | *.coveragexml 181 | 182 | # NCrunch 183 | _NCrunch_* 184 | .*crunch*.local.xml 185 | nCrunchTemp_* 186 | 187 | # MightyMoose 188 | *.mm.* 189 | AutoTest.Net/ 190 | 191 | # Web workbench (sass) 192 | .sass-cache/ 193 | 194 | # Installshield output folder 195 | [Ee]xpress/ 196 | 197 | # DocProject is a documentation generator add-in 198 | DocProject/buildhelp/ 199 | DocProject/Help/*.HxT 200 | DocProject/Help/*.HxC 201 | DocProject/Help/*.hhc 202 | DocProject/Help/*.hhk 203 | DocProject/Help/*.hhp 204 | DocProject/Help/Html2 205 | DocProject/Help/html 206 | 207 | # Click-Once directory 208 | publish/ 209 | 210 | # Publish Web Output 211 | *.[Pp]ublish.xml 212 | *.azurePubxml 213 | # Note: Comment the next line if you want to checkin your web deploy settings, 214 | # but database connection strings (with potential passwords) will be unencrypted 215 | *.pubxml 216 | *.publishproj 217 | 218 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 219 | # checkin your Azure Web App publish settings, but sensitive information contained 220 | # in these scripts will be unencrypted 221 | PublishScripts/ 222 | 223 | # NuGet Packages 224 | *.nupkg 225 | # The packages folder can be ignored because of Package Restore 226 | **/[Pp]ackages/* 227 | # except build/, which is used as an MSBuild target. 228 | !**/[Pp]ackages/build/ 229 | # Uncomment if necessary however generally it will be regenerated when needed 230 | #!**/[Pp]ackages/repositories.config 231 | # NuGet v3's project.json files produces more ignorable files 232 | *.nuget.props 233 | *.nuget.targets 234 | 235 | # Microsoft Azure Build Output 236 | csx/ 237 | *.build.csdef 238 | 239 | # Microsoft Azure Emulator 240 | ecf/ 241 | rcf/ 242 | 243 | # Windows Store app package directories and files 244 | AppPackages/ 245 | BundleArtifacts/ 246 | Package.StoreAssociation.xml 247 | _pkginfo.txt 248 | *.appx 249 | 250 | # Visual Studio cache files 251 | # files ending in .cache can be ignored 252 | *.[Cc]ache 253 | # but keep track of directories ending in .cache 254 | !*.[Cc]ache/ 255 | 256 | # Others 257 | ClientBin/ 258 | ~$* 259 | *~ 260 | *.dbmdl 261 | *.dbproj.schemaview 262 | *.jfm 263 | *.pfx 264 | *.publishsettings 265 | orleans.codegen.cs 266 | 267 | # Since there are multiple workflows, uncomment next line to ignore bower_components 268 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 269 | #bower_components/ 270 | 271 | # RIA/Silverlight projects 272 | Generated_Code/ 273 | 274 | # Backup & report files from converting an old project file 275 | # to a newer Visual Studio version. Backup files are not needed, 276 | # because we have git ;-) 277 | _UpgradeReport_Files/ 278 | Backup*/ 279 | UpgradeLog*.XML 280 | UpgradeLog*.htm 281 | 282 | # SQL Server files 283 | *.mdf 284 | *.ldf 285 | *.ndf 286 | 287 | # Business Intelligence projects 288 | *.rdl.data 289 | *.bim.layout 290 | *.bim_*.settings 291 | 292 | # Microsoft Fakes 293 | FakesAssemblies/ 294 | 295 | # GhostDoc plugin setting file 296 | *.GhostDoc.xml 297 | 298 | # Node.js Tools for Visual Studio 299 | .ntvs_analysis.dat 300 | node_modules/ 301 | 302 | # Typescript v1 declaration files 303 | typings/ 304 | 305 | # Visual Studio 6 build log 306 | *.plg 307 | 308 | # Visual Studio 6 workspace options file 309 | *.opt 310 | 311 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 312 | *.vbw 313 | 314 | # Visual Studio LightSwitch build output 315 | **/*.HTMLClient/GeneratedArtifacts 316 | **/*.DesktopClient/GeneratedArtifacts 317 | **/*.DesktopClient/ModelManifest.xml 318 | **/*.Server/GeneratedArtifacts 319 | **/*.Server/ModelManifest.xml 320 | _Pvt_Extensions 321 | 322 | # Paket dependency manager 323 | .paket/paket.exe 324 | paket-files/ 325 | 326 | # FAKE - F# Make 327 | .fake/ 328 | 329 | # JetBrains Rider 330 | .idea/ 331 | *.sln.iml 332 | 333 | # IDE - VSCode 334 | .vscode/* 335 | !.vscode/settings.json 336 | !.vscode/tasks.json 337 | !.vscode/launch.json 338 | !.vscode/extensions.json 339 | 340 | # CodeRush 341 | .cr/ 342 | 343 | # Python Tools for Visual Studio (PTVS) 344 | __pycache__/ 345 | *.pyc 346 | 347 | # Cake - Uncomment if you are using it 348 | # tools/** 349 | # !tools/packages.config 350 | 351 | # Tabs Studio 352 | *.tss 353 | 354 | # Telerik's JustMock configuration file 355 | *.jmconfig 356 | 357 | # BizTalk build output 358 | *.btp.cs 359 | *.btm.cs 360 | *.odx.cs 361 | *.xsd.cs 362 | 363 | # OpenCover UI analysis results 364 | OpenCover/ 365 | coverage/ 366 | 367 | ### macOS template 368 | # General 369 | .DS_Store 370 | .AppleDouble 371 | .LSOverride 372 | 373 | # Icon must end with two \r 374 | Icon 375 | 376 | # Thumbnails 377 | ._* 378 | 379 | # Files that might appear in the root of a volume 380 | .DocumentRevisions-V100 381 | .fseventsd 382 | .Spotlight-V100 383 | .TemporaryItems 384 | .Trashes 385 | .VolumeIcon.icns 386 | .com.apple.timemachine.donotpresent 387 | 388 | # Directories potentially created on remote AFP share 389 | .AppleDB 390 | .AppleDesktop 391 | Network Trash Folder 392 | Temporary Items 393 | .apdisk 394 | 395 | ======= 396 | # Local 397 | .env 398 | dist 399 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 7Code Logo 3 |

4 | 5 | ## Description 6 | 7 | [Nest](https://github.com/nestjs/nest) + [Prisma](https://github.com/prisma/prisma) + [TypeScript](https://github.com/microsoft/TypeScript) starter repository. 8 | 9 | ### Production-ready REST API: 10 | * Error Handling (Exception Filters) 11 | * Logging System 12 | * DB Seeds/Migrations 13 | * Built-in AuthModule, using JWT. Route Guards 14 | * Model Events Listener (onCreated, …) 15 | * Deployable. CI/CD pipeline using Github Actions. 16 | * Advanced ESLint/TSLint config. (e.g: auto-fix will remove unused imports) 17 | * Shared services/constants/helpers 18 | * Middlewares/Interceptors implementation example. 19 | 20 | ## TO-DO 21 | * Add Mail Service 22 | * Add [Recap.DEV](https://recap.dev/) integration - Tracing/Monitoring service 23 | * Add Unit tests. 24 | * Add Social Media Auth 25 | * Add documentation for setting the GitHub Secrets for the CI/CD pipeline 26 | * Add API Throttling - https://docs.nestjs.com/security/rate-limiting 27 | * ... 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | 47 | ``` 48 | 49 | ## Test 50 | 51 | ```bash 52 | # unit tests 53 | $ npm run test 54 | 55 | # e2e tests 56 | $ npm run test:e2e 57 | 58 | # test coverage 59 | $ npm run test:cov 60 | ``` 61 | 62 | ## Prisma (ORM) 63 | ```bash 64 | # IDE for your database 65 | $ npx prisma studio 66 | 67 | # run migrations (apply schema changes) 68 | $ npx prisma migrate dev 69 | 70 | # run migrations on CI/CD 71 | $ npx prisma migrate deploy 72 | 73 | # apply db schema changes to the prisma client 74 | $ npx prisma generate 75 | ``` 76 | 77 | 78 | ## Auth 79 | This implementation uses `httpOnly` (server-side) cookie-based authentication. [Read more](https://dev.to/guillerbr/authentication-cookies-http-http-only-jwt-reactjs-context-api-and-node-on-backend-industry-structure-3f8e) 80 | 81 | That means that the `JWT Token` is never stored on the client. 82 | Usually it was stored in `localStorage` / `sesionStorage` / `cookies` (browser), but this is not secure. 83 | 84 | Storing the token on a server side cookie is more secure, but it requires a small adjustment on frontend HTTP requests in order to work. 85 | 86 | Frontend adjustments 87 | * If you're using `axios` then you need to set: `withCredentials: true`. [Read more](https://flaviocopes.com/axios-credentials/) 88 | * If you're using `fetch` then you need to set: `credentials: 'include'`. [Read more](https://github.com/github/fetch#sending-cookies) 89 | 90 | 91 | ## Code Style 92 | Sync your IDE with project eslintrc.js. 93 | 94 | Check `Run ESLint --fix on save` 95 | 96 | ## Stay in touch 97 | 98 | - Author - [Igor Mardari](https://www.linkedin.com/in/igor-mardari-7code/) | [GarryOne](https://github.com/GarryOne) 99 | - Website - [7code.ro](https://7code.ro/) 100 | - Github - [@7codeRO](https://github.com/7codeRO/) 101 | 102 | ## License 103 | 104 | [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). 105 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-typescript-starter", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Nest TypeScript starter repository", 6 | "license": "MIT", 7 | "scripts": { 8 | "prebuild": "rimraf dist", 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json", 21 | "prisma:seed": "ts-node prisma/seed.ts" 22 | }, 23 | "dependencies": { 24 | "@aws-sdk/client-sqs": "^3.454.0", 25 | "@nestjs/common": "^9.3.9", 26 | "@nestjs/config": "^2.3.1", 27 | "@nestjs/core": "^9.3.9", 28 | "@nestjs/jwt": "^9.0.0", 29 | "@nestjs/passport": "^9.0.3", 30 | "@nestjs/platform-express": "^9.3.9", 31 | "@nestjs/swagger": "^6.2.1", 32 | "@prisma/client": "^4.4.0", 33 | "class-transformer": "^0.5.1", 34 | "class-validator": "^0.13.2", 35 | "cookie-parser": "^1.4.6", 36 | "cors": "^2.8.5", 37 | "crypto": "^1.0.1", 38 | "dotenv": "^8.2.0", 39 | "kafkajs": "^2.2.4", 40 | "passport": "^0.4.1", 41 | "passport-jwt": "^4.0.0", 42 | "reflect-metadata": "^0.1.13", 43 | "rimraf": "^3.0.2", 44 | "rxjs": "^6.6.6", 45 | "swagger-ui-express": "^4.1.6", 46 | "winston": "^3.3.3" 47 | }, 48 | "devDependencies": { 49 | "@nestjs/cli": "^9.2.0", 50 | "@nestjs/schematics": "^9.0.4", 51 | "@nestjs/testing": "^9.3.9", 52 | "@types/bcrypt": "^3.0.0", 53 | "@types/express": "^4.17.11", 54 | "@types/jest": "^26.0.20", 55 | "@types/node": "^18.14.6", 56 | "@types/passport": "^1.0.6", 57 | "@types/passport-jwt": "^3.0.5", 58 | "@types/passport-local": "^1.0.33", 59 | "@types/supertest": "^2.0.10", 60 | "@typescript-eslint/eslint-plugin": "^5.52.0", 61 | "@typescript-eslint/parser": "^5.52.0", 62 | "eslint": "^7.20.0", 63 | "eslint-config-prettier": "^8.1.0", 64 | "eslint-plugin-import": "^2.22.1", 65 | "eslint-plugin-prettier": "^3.3.1", 66 | "eslint-plugin-sort-imports-es6-autofix": "^0.6.0", 67 | "eslint-plugin-unused-imports": "^1.1.0", 68 | "jest": "^26.6.3", 69 | "prettier": "^2.2.1", 70 | "prisma": "^4.4.0", 71 | "supertest": "^6.1.3", 72 | "ts-jest": "^26.5.2", 73 | "ts-loader": "^8.0.17", 74 | "ts-node": "^9.1.1", 75 | "tsconfig-paths": "^3.9.0", 76 | "typescript": "^4.1.5" 77 | }, 78 | "jest": { 79 | "moduleFileExtensions": [ 80 | "js", 81 | "json", 82 | "ts" 83 | ], 84 | "rootDir": "src", 85 | "testRegex": ".*\\.spec\\.ts$", 86 | "transform": { 87 | "^.+\\.(t|j)s$": "ts-jest" 88 | }, 89 | "collectCoverageFrom": [ 90 | "**/*.(t|j)s" 91 | ], 92 | "coverageDirectory": "../coverage", 93 | "testEnvironment": "node" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /prisma/migrations/20210320180834_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Post" ( 3 | "id" SERIAL NOT NULL, 4 | "title" TEXT NOT NULL, 5 | "content" TEXT, 6 | "published" BOOLEAN DEFAULT false, 7 | "authorId" INTEGER, 8 | 9 | PRIMARY KEY ("id") 10 | ); 11 | 12 | -- CreateTable 13 | CREATE TABLE "User" ( 14 | "id" SERIAL NOT NULL, 15 | "email" TEXT NOT NULL, 16 | "name" TEXT, 17 | "password" TEXT NOT NULL, 18 | 19 | PRIMARY KEY ("id") 20 | ); 21 | 22 | -- CreateIndex 23 | CREATE UNIQUE INDEX "User.email_unique" ON "User"("email"); 24 | 25 | -- AddForeignKey 26 | ALTER TABLE "Post" ADD FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 27 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model Post { 11 | id Int @id @default(autoincrement()) 12 | title String 13 | content String? 14 | published Boolean? @default(false) 15 | authorId Int? 16 | User User? @relation(fields: [authorId], references: [id]) 17 | } 18 | 19 | model User { 20 | id Int @id @default(autoincrement()) 21 | email String @unique 22 | password String 23 | name String? 24 | Post Post[] 25 | } 26 | -------------------------------------------------------------------------------- /prisma/seeds/index.ts: -------------------------------------------------------------------------------- 1 | // export all seeds from here 2 | -------------------------------------------------------------------------------- /prisma/seeds/users.seed.ts: -------------------------------------------------------------------------------- 1 | // 2 | -------------------------------------------------------------------------------- /src/configs/config.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | nest: NestConfig; 3 | cors: CorsConfig; 4 | swagger: SwaggerConfig; 5 | security: SecurityConfig; 6 | } 7 | 8 | export interface NestConfig { 9 | port: number; 10 | } 11 | 12 | export interface CorsConfig { 13 | enabled: boolean; 14 | } 15 | 16 | export interface SwaggerConfig { 17 | enabled: boolean; 18 | title: string; 19 | description: string; 20 | version: string; 21 | path: string; 22 | } 23 | 24 | export interface GraphqlConfig { 25 | playgroundEnabled: boolean; 26 | debug: boolean; 27 | schemaDestination: string; 28 | sortSchema: boolean; 29 | } 30 | 31 | export interface SecurityConfig { 32 | expiresIn: number; 33 | bcryptSaltOrRound: string | number; 34 | } 35 | -------------------------------------------------------------------------------- /src/configs/global.config.ts: -------------------------------------------------------------------------------- 1 | import { API_PREFIX } from '../shared/constants/global.constants'; 2 | 3 | import { Config } from './config.interface'; 4 | 5 | export const GLOBAL_CONFIG: Config = { 6 | nest: { 7 | port: 3000, 8 | }, 9 | cors: { 10 | enabled: true, 11 | }, 12 | swagger: { 13 | enabled: true, 14 | title: 'Nestjs Prisma Starter', 15 | description: 'The nestjs API description', 16 | version: '1.5', 17 | path: API_PREFIX, 18 | }, 19 | security: { 20 | expiresIn: 3600 * 24, // 24h 21 | bcryptSaltOrRound: 10, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/exceptions/invalid.form.exception.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException } from '@nestjs/common'; 2 | 3 | export class InvalidFormException extends BadRequestException { 4 | constructor(private errors: { [key: string]: string }, message: string) { 5 | super(message); 6 | } 7 | 8 | getErrorMessage(): string { 9 | return this.message; 10 | } 11 | 12 | getFieldErrors(): { [key: string]: string } { 13 | return this.errors; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/filters/all.exceptions.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExceptionFilter, 3 | Catch, 4 | ArgumentsHost, 5 | HttpException, 6 | HttpStatus, 7 | } from '@nestjs/common'; 8 | import { HttpAdapterHost } from '@nestjs/core'; 9 | 10 | import { MyLogger } from '../modules/logger/logger.service'; 11 | 12 | @Catch() 13 | export class AllExceptionsFilter implements ExceptionFilter { 14 | constructor(private readonly httpAdapterHost: HttpAdapterHost) {} 15 | 16 | catch(exception: unknown, host: ArgumentsHost): void { 17 | const { httpAdapter } = this.httpAdapterHost; 18 | const ctx = host.switchToHttp(); 19 | 20 | const logger = new MyLogger(); 21 | logger.setContext(exception['name']); 22 | logger.error(exception['message']); 23 | 24 | const httpStatus = 25 | exception instanceof HttpException 26 | ? exception.getStatus() 27 | : HttpStatus.INTERNAL_SERVER_ERROR; 28 | 29 | httpAdapter.reply(ctx.getResponse(), exception['response'], httpStatus); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/filters/invalid.form.exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; 2 | import { Response } from 'express'; 3 | 4 | import { InvalidFormException } from '../exceptions/invalid.form.exception'; 5 | 6 | @Catch(InvalidFormException) 7 | export class InvalidFormExceptionFilter implements ExceptionFilter { 8 | catch(exception: InvalidFormException, host: ArgumentsHost) { 9 | const ctx = host.switchToHttp(); 10 | const response = ctx.getResponse(); 11 | const status = exception.getStatus(); 12 | 13 | response.status(status).json({ 14 | statusCode: status, 15 | errors: exception.getFieldErrors(), 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { HttpAdapterHost, NestFactory } from '@nestjs/core'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 4 | import { ValidationPipe } from '@nestjs/common'; 5 | import cors from 'cors'; 6 | import cookieParser from 'cookie-parser'; 7 | 8 | import { AppModule } from './modules/app/app.module'; 9 | import { API_PREFIX } from './shared/constants/global.constants'; 10 | import { SwaggerConfig } from './configs/config.interface'; 11 | import { GLOBAL_CONFIG } from './configs/global.config'; 12 | import { MyLogger } from './modules/logger/logger.service'; 13 | import { InvalidFormExceptionFilter } from './filters/invalid.form.exception.filter'; 14 | import { AllExceptionsFilter } from './filters/all.exceptions.filter'; 15 | 16 | async function bootstrap() { 17 | const app = await NestFactory.create(AppModule, { 18 | logger: ['error', 'error', 'warn'], 19 | }); 20 | 21 | app.setGlobalPrefix(API_PREFIX); 22 | 23 | app.useGlobalFilters( 24 | new AllExceptionsFilter(app.get(HttpAdapterHost)), 25 | new InvalidFormExceptionFilter(), 26 | ); 27 | 28 | app.use( 29 | cors({ 30 | origin: process.env.FRONTEND_URL, 31 | methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], 32 | credentials: true, 33 | }), 34 | ); 35 | 36 | app.use(cookieParser()); 37 | 38 | app.useGlobalPipes(new ValidationPipe()); 39 | 40 | const configService = app.get(ConfigService); 41 | const swaggerConfig = configService.get('swagger'); 42 | 43 | // Swagger Api 44 | if (swaggerConfig.enabled) { 45 | const options = new DocumentBuilder() 46 | .setTitle(swaggerConfig.title || 'Nestjs') 47 | .setDescription(swaggerConfig.description || 'The nestjs API description') 48 | .setVersion(swaggerConfig.version || '1.0') 49 | .addBearerAuth() 50 | .build(); 51 | const document = SwaggerModule.createDocument(app, options); 52 | 53 | SwaggerModule.setup(swaggerConfig.path || 'api', app, document); 54 | } 55 | 56 | const PORT = process.env.PORT || GLOBAL_CONFIG.nest.port; 57 | await app.listen(PORT, async () => { 58 | const myLogger = await app.resolve(MyLogger); 59 | myLogger.log(`Server started listening: ${PORT}`); 60 | }); 61 | } 62 | bootstrap(); 63 | -------------------------------------------------------------------------------- /src/middlewares/logger.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestMiddleware } from '@nestjs/common'; 2 | import { Request, Response } from 'express'; 3 | 4 | @Injectable() 5 | export class LoggerMiddleware implements NestMiddleware { 6 | use(req: Request, res: Response, next: CallableFunction): void { 7 | // TODO: use this.logger.log 8 | console.log( 9 | `[API Request] ${req.method} ${req.originalUrl}: ${JSON.stringify( 10 | req.body, 11 | )}`, 12 | ); 13 | next(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/app/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | 6 | describe('AppController', () => { 7 | let _app: TestingModule; 8 | 9 | beforeAll(async () => { 10 | _app = await Test.createTestingModule({ 11 | controllers: [AppController], 12 | providers: [AppService], 13 | }).compile(); 14 | }); 15 | 16 | describe('root', () => { 17 | it('should return "Hello World!"', () => { 18 | const appController = _app.get(AppController); 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/modules/app/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | 3 | @Controller() 4 | export class AppController { 5 | @Get() 6 | getHello(): string { 7 | return 'Hello World!'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MiddlewareConsumer } from '@nestjs/common'; 3 | import { ConfigModule } from '@nestjs/config'; 4 | 5 | import { UserModule } from '../user/user.module'; 6 | import { PostModule } from '../post/post.module'; 7 | import { AuthModule } from '../auth/auth.module'; 8 | import { PrismaModule } from '../prisma/prisma.module'; 9 | import { GLOBAL_CONFIG } from '../../configs/global.config'; 10 | import { LoggerModule } from '../logger/logger.module'; 11 | import { AppService } from './app.service'; 12 | import { AppController } from './app.controller'; 13 | import { LoggerMiddleware } from '../../middlewares/logger.middleware'; 14 | 15 | @Module({ 16 | imports: [ 17 | LoggerModule, 18 | PrismaModule, 19 | AuthModule, 20 | UserModule, 21 | PostModule, 22 | ConfigModule.forRoot({ isGlobal: true, load: [() => GLOBAL_CONFIG] }), 23 | ], 24 | controllers: [AppController], 25 | providers: [AppService], 26 | exports: [], 27 | }) 28 | export class AppModule { 29 | configure(consumer: MiddlewareConsumer) { 30 | consumer.apply(LoggerMiddleware).forRoutes('*'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/app/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/auth/auth.constants.ts: -------------------------------------------------------------------------------- 1 | export const ADMIN_ROLE = 'admin'; 2 | export const USER_ROLE = 'user'; 3 | -------------------------------------------------------------------------------- /src/modules/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post, Response } from '@nestjs/common'; 2 | import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; 3 | import { User } from '@prisma/client'; 4 | 5 | import { JWT_EXPIRY_SECONDS } from '../../shared/constants/global.constants'; 6 | 7 | import { AuthService } from './auth.service'; 8 | import { AuthResponseDTO, LoginUserDTO, RegisterUserDTO } from './auth.dto'; 9 | 10 | @ApiTags('auth') 11 | @Controller('auth') 12 | export class AuthController { 13 | constructor(private readonly authService: AuthService) {} 14 | 15 | @Post('login') 16 | @ApiOperation({ description: 'Login user' }) 17 | @ApiBody({ type: LoginUserDTO }) 18 | @ApiResponse({ type: AuthResponseDTO }) 19 | async login( 20 | @Body() user: LoginUserDTO, 21 | @Response() res, 22 | ): Promise { 23 | const loginData = await this.authService.login(user); 24 | 25 | res.cookie('accessToken', loginData.accessToken, { 26 | expires: new Date(new Date().getTime() + JWT_EXPIRY_SECONDS * 1000), 27 | sameSite: 'strict', 28 | secure: true, 29 | httpOnly: true, 30 | }); 31 | 32 | return res.status(200).send(loginData); 33 | } 34 | 35 | @Post('register') 36 | async register(@Body() user: RegisterUserDTO): Promise { 37 | return this.authService.register(user); 38 | } 39 | 40 | @Post('logout') 41 | logout(@Response() res): void { 42 | res.clearCookie('accessToken'); 43 | res.status(200).send({ success: true }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/modules/auth/auth.dto.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@prisma/client'; 2 | import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; 3 | import { ApiProperty } from '@nestjs/swagger'; 4 | 5 | import { INVALID_EMAIL } from '../../shared/constants/strings'; 6 | 7 | export class AuthResponseDTO { 8 | user: User; 9 | accessToken: string; 10 | } 11 | 12 | export class RegisterUserDTO { 13 | @IsString() 14 | @ApiProperty() 15 | email: string; 16 | 17 | @IsString() 18 | @ApiProperty() 19 | name: string; 20 | 21 | @IsString() 22 | @ApiProperty() 23 | password: string; 24 | } 25 | 26 | export class LoginUserDTO { 27 | @IsString() 28 | @IsNotEmpty() 29 | @ApiProperty() 30 | @IsEmail({}, { message: INVALID_EMAIL }) 31 | email: string; 32 | 33 | @IsString() 34 | @IsNotEmpty() 35 | @ApiProperty() 36 | password: string; 37 | } 38 | -------------------------------------------------------------------------------- /src/modules/auth/auth.jwt.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExecutionContext, 3 | Injectable, 4 | UnauthorizedException, 5 | } from '@nestjs/common'; 6 | import { AuthGuard } from '@nestjs/passport'; 7 | import { Reflector } from '@nestjs/core'; 8 | import { Observable } from 'rxjs'; 9 | import { User } from '@prisma/client'; 10 | 11 | @Injectable() 12 | export class JwtAuthGuard extends AuthGuard('jwt') { 13 | roles: string[]; 14 | 15 | constructor(private reflector: Reflector) { 16 | super(reflector); 17 | } 18 | 19 | canActivate( 20 | context: ExecutionContext, 21 | ): boolean | Promise | Observable { 22 | this.roles = this.reflector.get('roles', context.getHandler()); 23 | return super.canActivate(context); 24 | } 25 | 26 | handleRequest(err: Error, user: User): any { 27 | if (err || !user) { 28 | throw err || new UnauthorizedException(); 29 | } 30 | 31 | if (!this.roles) { 32 | return user; 33 | } 34 | 35 | // const hasPermission = this.roles.includes(user.role); 36 | // if (!hasPermission) { 37 | // throw new ForbiddenException(); 38 | // } 39 | 40 | return user; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/modules/auth/auth.jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 4 | import { User } from '@prisma/client'; 5 | 6 | import { JWT_SECRET } from '../../shared/constants/global.constants'; 7 | import { PrismaService } from '../prisma/prisma.service'; 8 | 9 | const cookieExtractor = (req) => req?.cookies.accessToken; 10 | 11 | @Injectable() 12 | export class JwtStrategy extends PassportStrategy(Strategy) { 13 | constructor(private prisma: PrismaService) { 14 | super({ 15 | jwtFromRequest: ExtractJwt.fromExtractors([ 16 | ExtractJwt.fromAuthHeaderAsBearerToken(), 17 | ExtractJwt.fromUrlQueryParameter('token'), 18 | cookieExtractor, 19 | ]), 20 | ignoreExpiration: process.env.NODE_ENV === 'dev', 21 | secretOrKey: JWT_SECRET, 22 | }); 23 | } 24 | 25 | async validate(payload: User): Promise { 26 | const email = payload.email; 27 | const user = await this.prisma.user.findUnique({ where: { email } }); 28 | 29 | if (!user) { 30 | throw new UnauthorizedException(); 31 | } 32 | 33 | return user; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { JwtModule } from '@nestjs/jwt'; 3 | 4 | import { UserService } from '../user/user.service'; 5 | import { JWT_SECRET } from '../../shared/constants/global.constants'; 6 | import { PrismaModule } from '../prisma/prisma.module'; 7 | import { PrismaService } from '../prisma/prisma.service'; 8 | 9 | import { JwtStrategy } from './auth.jwt.strategy'; 10 | import { AuthController } from './auth.controller'; 11 | import { AuthService } from './auth.service'; 12 | 13 | @Module({ 14 | imports: [ 15 | JwtModule.register({ 16 | secret: JWT_SECRET, 17 | }), 18 | PrismaModule, 19 | ], 20 | providers: [UserService, AuthService, JwtStrategy, PrismaService], 21 | controllers: [AuthController], 22 | }) 23 | export class AuthModule {} 24 | -------------------------------------------------------------------------------- /src/modules/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { User } from '@prisma/client'; 4 | 5 | import { UserService } from '../user/user.service'; 6 | import { PrismaService } from '../prisma/prisma.service'; 7 | import { AuthHelpers } from '../../shared/helpers/auth.helpers'; 8 | import { GLOBAL_CONFIG } from '../../configs/global.config'; 9 | 10 | import { AuthResponseDTO, LoginUserDTO, RegisterUserDTO } from './auth.dto'; 11 | 12 | @Injectable() 13 | export class AuthService { 14 | constructor( 15 | private userService: UserService, 16 | private prisma: PrismaService, 17 | private jwtService: JwtService, 18 | ) {} 19 | 20 | public async login(loginUserDTO: LoginUserDTO): Promise { 21 | const userData = await this.userService.findUser({ 22 | email: loginUserDTO.email, 23 | }); 24 | 25 | if (!userData) { 26 | throw new UnauthorizedException(); 27 | } 28 | 29 | const isMatch = await AuthHelpers.verify( 30 | loginUserDTO.password, 31 | userData.password, 32 | ); 33 | 34 | if (!isMatch) { 35 | throw new UnauthorizedException(); 36 | } 37 | 38 | const payload = { 39 | id: userData.id, 40 | name: userData.name, 41 | email: userData.email, 42 | password: null, 43 | // role: userData.role, 44 | }; 45 | 46 | const accessToken = this.jwtService.sign(payload, { 47 | expiresIn: GLOBAL_CONFIG.security.expiresIn, 48 | }); 49 | 50 | return { 51 | user: payload, 52 | accessToken: accessToken, 53 | }; 54 | } 55 | public async register(user: RegisterUserDTO): Promise { 56 | return this.userService.createUser(user); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/modules/auth/auth.user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, createParamDecorator } from '@nestjs/common'; 2 | 3 | export const AuthUser = createParamDecorator( 4 | (data: unknown, ctx: ExecutionContext) => { 5 | const request = ctx.switchToHttp().getRequest(); 6 | 7 | return request.user; 8 | }, 9 | ); 10 | -------------------------------------------------------------------------------- /src/modules/kafka/kafka.consumer.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnApplicationShutdown } from '@nestjs/common'; 2 | import { 3 | Consumer, 4 | ConsumerRunConfig, 5 | ConsumerSubscribeTopics, 6 | Kafka, 7 | } from 'kafkajs'; 8 | 9 | @Injectable() 10 | export class ConsumerService implements OnApplicationShutdown { 11 | private readonly kafka = new Kafka({ 12 | brokers: ['localhost:9092'], 13 | }); 14 | 15 | private readonly consumers: Consumer[] = []; 16 | 17 | async consume(topic: ConsumerSubscribeTopics, config: ConsumerRunConfig) { 18 | const consumer = this.kafka.consumer({ groupId: 'nestjs-kafka' }); 19 | await consumer.connect(); 20 | await consumer.subscribe(topic); 21 | await consumer.run(config); 22 | this.consumers.push(consumer); 23 | } 24 | 25 | async onApplicationShutdown(signal?: string) { 26 | for (const consumer of this.consumers) { 27 | await consumer.disconnect(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/kafka/kafka.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ProducerService } from './kafka.producer.service'; 3 | import { ConsumerService } from './kafka.consumer.service'; 4 | 5 | @Module({ 6 | providers: [ProducerService, ConsumerService], 7 | exports: [ProducerService, ConsumerService], 8 | }) 9 | export class KafkaModule {} 10 | -------------------------------------------------------------------------------- /src/modules/kafka/kafka.producer.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | OnApplicationShutdown, 4 | OnModuleInit, 5 | } from '@nestjs/common'; 6 | import { Kafka } from 'kafkajs'; 7 | 8 | @Injectable() 9 | export class ProducerService implements OnModuleInit, OnApplicationShutdown { 10 | private readonly kafka = new Kafka({ 11 | brokers: ['localhost:9092'], 12 | }); 13 | 14 | private readonly producer = this.kafka.producer(); 15 | 16 | async onModuleInit() { 17 | await this.producer.connect(); 18 | } 19 | 20 | async produce(record: any) { 21 | await this.producer.send(record); 22 | } 23 | 24 | async onApplicationShutdown(signal?: string) { 25 | await this.producer.disconnect(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/logger/logger.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { MyLogger } from './logger.service'; 4 | 5 | @Module({ 6 | providers: [MyLogger], 7 | exports: [MyLogger], 8 | }) 9 | export class LoggerModule {} 10 | -------------------------------------------------------------------------------- /src/modules/logger/logger.service.ts: -------------------------------------------------------------------------------- 1 | import * as winston from 'winston'; 2 | import * as chalk from 'chalk'; 3 | import { createLogger, LoggerOptions } from 'winston'; 4 | import { Injectable, LoggerService } from '@nestjs/common'; 5 | 6 | @Injectable() 7 | export class MyLogger implements LoggerService { 8 | private readonly logger; 9 | 10 | private level = 'info'; 11 | 12 | private context: string; 13 | 14 | private static LOGS_PATH = 'storage/logs'; 15 | 16 | constructor() { 17 | this.logger = createLogger(this.getLoggerOptions(this.level)); 18 | this.setContext('Main'); 19 | } 20 | 21 | public getLoggerOptions(level: string): LoggerOptions { 22 | return { 23 | level: level, 24 | transports: [ 25 | new winston.transports.File({ 26 | filename: `${MyLogger.LOGS_PATH}/${level}.log`, 27 | }), 28 | ], 29 | }; 30 | } 31 | public setContext(context: string): this { 32 | this.context = context; 33 | 34 | return this; 35 | } 36 | 37 | public setLevel(level: string): this { 38 | this.level = level; 39 | 40 | const loggerOptions = this.getLoggerOptions(level); 41 | this.overrideOptions(loggerOptions); 42 | 43 | return this; 44 | } 45 | 46 | log(message: string): void { 47 | this.setLevel('info'); 48 | const currentDate = new Date(); 49 | this.logger.info(message, { 50 | timestamp: currentDate.toISOString(), 51 | context: this.context, 52 | }); 53 | 54 | this.logToConsole('info', message); 55 | } 56 | 57 | error(message: string, trace?: string): void { 58 | this.setLevel('error'); 59 | const currentDate = new Date(); 60 | // i think the trace should be JSON Stringified 61 | this.logger.error(`${message} -> (${trace || 'trace not provided !'})`, { 62 | timestamp: currentDate.toISOString(), 63 | context: this.context, 64 | }); 65 | this.logToConsole('error', message); 66 | } 67 | 68 | warn(message: string): void { 69 | this.setLevel('warn'); 70 | const currentDate = new Date(); 71 | this.logger.warn(message, { 72 | timestamp: currentDate.toISOString(), 73 | context: this.context, 74 | }); 75 | this.logToConsole('warn', message); 76 | } 77 | 78 | info(message: string): void { 79 | this.setLevel('info'); 80 | const currentDate = new Date(); 81 | this.logger.info(message, { 82 | timestamp: currentDate.toISOString(), 83 | context: this.context, 84 | }); 85 | this.logToConsole('info', message); 86 | } 87 | 88 | debug(message: string): void { 89 | this.setLevel('debug'); 90 | const currentDate = new Date(); 91 | this.logger.info(message, { 92 | timestamp: currentDate.toISOString(), 93 | context: this.context, 94 | }); 95 | this.logToConsole('debug', message); 96 | } 97 | 98 | overrideOptions(options: LoggerOptions): void { 99 | this.logger.configure(options); 100 | } 101 | 102 | // this method just for printing a cool log in your terminal , using chalk 103 | private logToConsole(level: string, message: string): void { 104 | let result; 105 | const color = chalk.default; 106 | const currentDate = new Date(); 107 | const time = `${currentDate.getHours()}:${currentDate.getMinutes()}:${currentDate.getSeconds()}`; 108 | 109 | switch (level) { 110 | default: 111 | case 'info': 112 | result = `[${color.blue('INFO')}] ${color.dim.yellow.bold.underline( 113 | time, 114 | )} [${color.green(this.context)}] ${message}`; 115 | break; 116 | case 'error': 117 | result = `[${color.red('ERR')}] ${color.dim.yellow.bold.underline( 118 | time, 119 | )} [${color.green(this.context)}] ${message}`; 120 | break; 121 | case 'warn': 122 | result = `[${color.yellow('WARN')}] ${color.dim.yellow.bold.underline( 123 | time, 124 | )} [${color.green(this.context)}] ${message}`; 125 | break; 126 | } 127 | console.log(result); // TODO: DON'T remove this console.log 128 | 129 | this.logger.close(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/modules/post/post.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | Post, 8 | Put, 9 | } from '@nestjs/common'; 10 | import { Post as PostModel } from '@prisma/client'; 11 | import { ApiTags } from '@nestjs/swagger'; 12 | 13 | import { PostService } from './post.service'; 14 | 15 | @ApiTags('posts') 16 | @Controller('/posts') 17 | export class PostController { 18 | constructor(private postService: PostService) {} 19 | 20 | @Get('/') 21 | async getAllPosts(): Promise { 22 | return this.postService.findAll({}); 23 | } 24 | 25 | @Get('post/:id') 26 | async getPostById(@Param('id') id: string): Promise { 27 | return this.postService.findOne({ id: Number(id) }); 28 | } 29 | 30 | @Get('feed') 31 | async getPublishedPosts(): Promise { 32 | return this.postService.findAll({ 33 | where: { published: true }, 34 | }); 35 | } 36 | 37 | @Get('filtered-posts/:searchString') 38 | async getFilteredPosts( 39 | @Param('searchString') searchString: string, 40 | ): Promise { 41 | return this.postService.findAll({ 42 | where: { 43 | OR: [ 44 | { 45 | title: { contains: searchString }, 46 | }, 47 | { 48 | content: { contains: searchString }, 49 | }, 50 | ], 51 | }, 52 | }); 53 | } 54 | 55 | @Post('post') 56 | async createDraft( 57 | @Body() postData: { title: string; content?: string; authorEmail: string }, 58 | ): Promise { 59 | const { title, content, authorEmail } = postData; 60 | return this.postService.create({ 61 | title, 62 | content, 63 | User: { 64 | connect: { email: authorEmail }, 65 | }, 66 | }); 67 | } 68 | 69 | @Put('publish/:id') 70 | async publishPost(@Param('id') id: string): Promise { 71 | return this.postService.update({ 72 | where: { id: Number(id) }, 73 | data: { published: true }, 74 | }); 75 | } 76 | 77 | @Delete('post/:id') 78 | async deletePost(@Param('id') id: string): Promise { 79 | return this.postService.delete({ id: Number(id) }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/modules/post/post.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { PrismaService } from '../prisma/prisma.service'; 4 | 5 | import { PostService } from './post.service'; 6 | import { PostController } from './post.controller'; 7 | 8 | @Module({ 9 | imports: [], 10 | controllers: [PostController], 11 | providers: [PostService, PrismaService], 12 | exports: [PostService], 13 | }) 14 | export class PostModule {} 15 | -------------------------------------------------------------------------------- /src/modules/post/post.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Post, Prisma } from '@prisma/client'; 3 | 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | 6 | @Injectable() 7 | export class PostService { 8 | constructor(private prisma: PrismaService) {} 9 | 10 | async findOne( 11 | postWhereUniqueInput: Prisma.PostWhereUniqueInput, 12 | ): Promise { 13 | return this.prisma.post.findUnique({ 14 | where: postWhereUniqueInput, 15 | }); 16 | } 17 | 18 | async findAll(params: { 19 | skip?: number; 20 | take?: number; 21 | cursor?: Prisma.PostWhereUniqueInput; 22 | where?: Prisma.PostWhereInput; 23 | orderBy?: Prisma.PostOrderByWithRelationInput; 24 | }): Promise { 25 | const { skip, take, cursor, where, orderBy } = params; 26 | return this.prisma.post.findMany({ 27 | skip, 28 | take, 29 | cursor, 30 | where, 31 | orderBy, 32 | }); 33 | } 34 | 35 | async create(data: Prisma.PostCreateInput): Promise { 36 | return this.prisma.post.create({ 37 | data, 38 | }); 39 | } 40 | 41 | async update(params: { 42 | where: Prisma.PostWhereUniqueInput; 43 | data: Prisma.PostUpdateInput; 44 | }): Promise { 45 | const { data, where } = params; 46 | return this.prisma.post.update({ 47 | data, 48 | where, 49 | }); 50 | } 51 | 52 | async delete(where: Prisma.PostWhereUniqueInput): Promise { 53 | return this.prisma.post.delete({ 54 | where, 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/modules/prisma/prisma.config.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClientOptions } from '@prisma/client/runtime'; 2 | 3 | export type LogLevel = 'info' | 'query' | 'warn' | 'error'; 4 | export type LogDefinition = { 5 | level: LogLevel; 6 | emit: 'stdout' | 'event'; 7 | }; 8 | 9 | export const PRISMA_LOG_CONFIG: Array = [ 10 | { level: 'warn', emit: 'stdout' }, 11 | { level: 'info', emit: 'stdout' }, 12 | { level: 'error', emit: 'stdout' }, 13 | { level: 'query', emit: 'stdout' }, 14 | ]; 15 | 16 | export const PRISMA_CLIENT_OPTIONS: PrismaClientOptions = { 17 | log: PRISMA_LOG_CONFIG, 18 | rejectOnNotFound: true, 19 | __internal: { 20 | hooks: { 21 | // beforeRequest: (params) => { 22 | // // Do something 23 | // }, 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/modules/prisma/prisma.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { PrismaService } from './prisma.service'; 4 | 5 | @Module({ 6 | providers: [PrismaService], 7 | exports: [PrismaService], 8 | }) 9 | export class PrismaModule {} 10 | -------------------------------------------------------------------------------- /src/modules/prisma/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; 2 | import { Prisma, PrismaClient } from '@prisma/client'; 3 | 4 | import { UserListener } from '../user/user.listener'; 5 | 6 | import { PRISMA_CLIENT_OPTIONS } from './prisma.config'; 7 | 8 | @Injectable() 9 | export class PrismaService 10 | extends PrismaClient 11 | implements OnModuleInit, OnModuleDestroy { 12 | constructor() { 13 | super({ ...PRISMA_CLIENT_OPTIONS }); 14 | } 15 | 16 | async onModuleInit() { 17 | await this.$connect(); 18 | 19 | this.$on('error', (_e) => { 20 | // Do something 21 | }); 22 | 23 | this.$use(UserListener.onCreated); 24 | } 25 | 26 | async onModuleDestroy() { 27 | await this.$disconnect(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/sqs/sqs.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { SQSService } from './sqs.service'; 3 | 4 | @Controller() 5 | export class SqsController { 6 | constructor(private readonly sqsService: SQSService) {} 7 | } 8 | -------------------------------------------------------------------------------- /src/modules/sqs/sqs.module.ts: -------------------------------------------------------------------------------- 1 | import { Logger, Module } from '@nestjs/common'; 2 | import { SQSService } from './sqs.service'; 3 | import { SqsController } from './sqs.controller'; 4 | 5 | @Module({ 6 | imports: [], 7 | controllers: [SqsController], 8 | providers: [SQSService, Logger], 9 | exports: [SQSService], 10 | }) 11 | export class SqsModule {} 12 | -------------------------------------------------------------------------------- /src/modules/sqs/sqs.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { 3 | DeleteMessageCommand, 4 | ReceiveMessageCommand, 5 | SQSClient, 6 | } from '@aws-sdk/client-sqs'; 7 | 8 | @Injectable() 9 | export class SQSService { 10 | private readonly logger = new Logger(SQSService.name); 11 | private sqs: SQSClient; 12 | private indexingJobProgressQueue = 'queue_url'; 13 | 14 | constructor() { 15 | this.sqs = new SQSClient({ 16 | region: 'us-east-1', 17 | }); 18 | } 19 | 20 | public async deleteMessage(message) { 21 | const deleteParams = { 22 | QueueUrl: this.indexingJobProgressQueue, 23 | ReceiptHandle: message.ReceiptHandle, 24 | }; 25 | 26 | const deleteCommand = new DeleteMessageCommand(deleteParams); 27 | try { 28 | await this.sqs.send(deleteCommand); 29 | this.logger.log('Message deleted successfully.'); 30 | } catch (err) { 31 | this.logger.error('Error deleting message:', err); 32 | } 33 | } 34 | 35 | public async receiveMessages(callback) { 36 | const params = { 37 | QueueUrl: this.indexingJobProgressQueue, 38 | // MaxNumberOfMessages: 5, // Maximum number of messages to receive (1-10) 39 | WaitTimeSeconds: 1, // maximum 20 seconds 40 | }; 41 | 42 | const receiveCommand = new ReceiveMessageCommand(params); 43 | 44 | try { 45 | const response = await this.sqs.send(receiveCommand); 46 | const messages = response.Messages || []; 47 | 48 | if (messages.length === 0) { 49 | this.logger.log('No messages received.'); 50 | return; 51 | } 52 | 53 | messages.forEach((message) => { 54 | this.logger.log('Received message:', message.Body); 55 | callback(message); 56 | }); 57 | } catch (err) { 58 | this.logger.error('Error receiving messages:', err); 59 | } finally { 60 | await this.receiveMessages(callback); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/modules/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'; 2 | import { User } from '@prisma/client'; 3 | import { ApiTags } from '@nestjs/swagger'; 4 | 5 | import { JwtAuthGuard } from '../auth/auth.jwt.guard'; 6 | 7 | import { UserService } from './user.service'; 8 | 9 | @ApiTags('users') 10 | @Controller('/users') 11 | export class UserController { 12 | constructor(private userService: UserService) {} 13 | 14 | @Get() 15 | @UseGuards(JwtAuthGuard) 16 | async getAll(): Promise { 17 | return this.userService.users({}); 18 | } 19 | 20 | @Post('user') 21 | async signupUser( 22 | @Body() userData: { name?: string; email: string; password: string }, 23 | ): Promise { 24 | return this.userService.createUser(userData); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/user/user.listener.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { AuthHelpers } from '../../shared/helpers/auth.helpers'; 4 | 5 | @Injectable() 6 | export class UserListener { 7 | static async onCreated(params, next) { 8 | // Check incoming query type 9 | if (params.model == 'User') { 10 | if (params.action === 'create' || params.action === 'update') { 11 | const password = params.args['data'].password; 12 | 13 | const encryptedPass = await AuthHelpers.hash(password); 14 | 15 | params.args['data'] = { 16 | ...params.args['data'], 17 | password: encryptedPass, 18 | }; 19 | } 20 | } 21 | 22 | return next(params); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { PrismaService } from '../prisma/prisma.service'; 4 | import { PrismaModule } from '../prisma/prisma.module'; 5 | 6 | import { UserService } from './user.service'; 7 | import { UserController } from './user.controller'; 8 | import { UserListener } from './user.listener'; 9 | 10 | @Module({ 11 | imports: [PrismaModule], 12 | controllers: [UserController], 13 | providers: [UserService, PrismaService, UserListener], 14 | exports: [UserService], 15 | }) 16 | export class UserModule {} 17 | -------------------------------------------------------------------------------- /src/modules/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, User } from '@prisma/client'; 2 | import { Injectable } from '@nestjs/common'; 3 | 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | 6 | @Injectable() 7 | export class UserService { 8 | constructor(private prisma: PrismaService) {} 9 | 10 | async findUser( 11 | userWhereUniqueInput: Prisma.UserWhereUniqueInput, 12 | ): Promise { 13 | return this.prisma.user.findUnique({ 14 | where: userWhereUniqueInput, 15 | }); 16 | } 17 | 18 | async users(params: { 19 | skip?: number; 20 | take?: number; 21 | cursor?: Prisma.UserWhereUniqueInput; 22 | where?: Prisma.UserWhereInput; 23 | orderBy?: Prisma.UserOrderByWithRelationInput; 24 | }): Promise { 25 | const { skip, take, cursor, where, orderBy } = params; 26 | return this.prisma.user.findMany({ 27 | skip, 28 | take, 29 | cursor, 30 | where, 31 | orderBy, 32 | }); 33 | } 34 | 35 | async createUser(data: Prisma.UserCreateInput): Promise { 36 | return this.prisma.user.create({ 37 | data, 38 | }); 39 | } 40 | 41 | async updateUser(params: { 42 | where: Prisma.UserWhereUniqueInput; 43 | data: Prisma.UserUpdateInput; 44 | }): Promise { 45 | const { where, data } = params; 46 | return this.prisma.user.update({ 47 | data, 48 | where, 49 | }); 50 | } 51 | 52 | async deleteUser(where: Prisma.UserWhereUniqueInput): Promise { 53 | return this.prisma.user.delete({ 54 | where, 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/shared/constants/global.constants.ts: -------------------------------------------------------------------------------- 1 | //eslint-disable-next-line 2 | require('dotenv').config(); 3 | 4 | export const JWT_SECRET = process.env.JWT_SIGNATURE; 5 | export const JWT_EXPIRY_SECONDS = 3600; 6 | 7 | export enum ROLES_ENUM { 8 | ADMIN = 'admin', 9 | USER = 'user', 10 | } 11 | 12 | export const ROLES = { 13 | ADMIN: 'admin', 14 | USER: 'user', 15 | }; 16 | export const DEFAULT_PAGE_LIMIT = 10; 17 | export const MAX_PAGE_LIMIT = 100; 18 | 19 | export const DEFAULT_SORT_BY = 'id'; 20 | 21 | export const API_PREFIX = '/api/v1'; 22 | 23 | //Regex 24 | export const PHONE_REGEX = /^[0-9\s+-.()]+$/; 25 | 26 | export const SLUG_SEPARATOR = '-'; 27 | -------------------------------------------------------------------------------- /src/shared/constants/postgres.constants.ts: -------------------------------------------------------------------------------- 1 | export const PS_EXCEPTIONS = { 2 | '00000': 'successful_completion', 3 | '01000': 'warning', 4 | '0100C': 'dynamic_result_sets_returned', 5 | '01008': 'implicit_zero_bit_padding', 6 | '01003': 'null_value_eliminated_in_set_function', 7 | '01007': 'privilege_not_granted', 8 | '01006': 'privilege_not_revoked', 9 | '01004': 'string_data_right_truncation', 10 | '01P01': 'deprecated_feature', 11 | '02000': 'no_data', 12 | '02001': 'no_additional_dynamic_result_sets_returned', 13 | '03000': 'sql_statement_not_yet_complete', 14 | '08000': 'connection_exception', 15 | '08003': 'connection_does_not_exist', 16 | '08006': 'connection_failure', 17 | '08001': 'sqlclient_unable_to_establish_sqlconnection', 18 | '08004': 'sqlserver_rejected_establishment_of_sqlconnection', 19 | '08007': 'transaction_resolution_unknown', 20 | '08P01': 'protocol_violation', 21 | '09000': 'triggered_action_exception', 22 | '0A000': 'feature_not_supported', 23 | '0B000': 'invalid_transaction_initiation', 24 | '0F000': 'locator_exception', 25 | '0F001': 'invalid_locator_specification', 26 | '0L000': 'invalid_grantor', 27 | '0LP01': 'invalid_grant_operation', 28 | '0P000': 'invalid_role_specification', 29 | '0Z000': 'diagnostics_exception', 30 | '0Z002': 'stacked_diagnostics_accessed_without_active_handler', 31 | '20000': 'case_not_found', 32 | '21000': 'cardinality_violation', 33 | '22000': 'data_exception', 34 | '2202E': 'array_subscript_error', 35 | '22021': 'character_not_in_repertoire', 36 | '22008': 'datetime_field_overflow', 37 | '22012': 'division_by_zero', 38 | '22005': 'error_in_assignment', 39 | '2200B': 'escape_character_conflict', 40 | '22022': 'indicator_overflow', 41 | '22015': 'interval_field_overflow', 42 | '2201E': 'invalid_argument_for_logarithm', 43 | '22014': 'invalid_argument_for_ntile_function', 44 | '22016': 'invalid_argument_for_nth_value_function', 45 | '2201F': 'invalid_argument_for_power_function', 46 | '2201G': 'invalid_argument_for_width_bucket_function', 47 | '22018': 'invalid_character_value_for_cast', 48 | '22007': 'invalid_datetime_format', 49 | '22019': 'invalid_escape_character', 50 | '2200D': 'invalid_escape_octet', 51 | '22025': 'invalid_escape_sequence', 52 | '22P06': 'nonstandard_use_of_escape_character', 53 | '22010': 'invalid_indicator_parameter_value', 54 | '22023': 'invalid_parameter_value', 55 | '2201B': 'invalid_regular_expression', 56 | '2201W': 'invalid_row_count_in_limit_clause', 57 | '2201X': 'invalid_row_count_in_result_offset_clause', 58 | '2202H': 'invalid_tablesample_argument', 59 | '2202G': 'invalid_tablesample_repeat', 60 | '22009': 'invalid_time_zone_displacement_value', 61 | '2200C': 'invalid_use_of_escape_character', 62 | '2200G': 'most_specific_type_mismatch', 63 | '22004': 'null_value_not_allowed', 64 | '22002': 'null_value_no_indicator_parameter', 65 | '22003': 'numeric_value_out_of_range', 66 | '2200H': 'sequence_generator_limit_exceeded', 67 | '22026': 'string_data_length_mismatch', 68 | '22001': 'string_data_right_truncation', 69 | '22011': 'substring_error', 70 | '22027': 'trim_error', 71 | '22024': 'unterminated_c_string', 72 | '2200F': 'zero_length_character_string', 73 | '22P01': 'floating_point_exception', 74 | '22P02': 'invalid_text_representation', 75 | '22P03': 'invalid_binary_representation', 76 | '22P04': 'bad_copy_file_format', 77 | '22P05': 'untranslatable_character', 78 | '2200L': 'not_an_xml_document', 79 | '2200M': 'invalid_xml_document', 80 | '2200N': 'invalid_xml_content', 81 | '2200S': 'invalid_xml_comment', 82 | '2200T': 'invalid_xml_processing_instruction', 83 | '23000': 'integrity_constraint_violation', 84 | '23001': 'restrict_violation', 85 | '23502': 'not_null_violation', 86 | '23503': 'foreign_key_violation', 87 | '23505': 'unique_violation', 88 | '23514': 'check_violation', 89 | '23P01': 'exclusion_violation', 90 | '24000': 'invalid_cursor_state', 91 | '25000': 'invalid_transaction_state', 92 | '25001': 'active_sql_transaction', 93 | '25002': 'branch_transaction_already_active', 94 | '25008': 'held_cursor_requires_same_isolation_level', 95 | '25003': 'inappropriate_access_mode_for_branch_transaction', 96 | '25004': 'inappropriate_isolation_level_for_branch_transaction', 97 | '25005': 'no_active_sql_transaction_for_branch_transaction', 98 | '25006': 'read_only_sql_transaction', 99 | '25007': 'schema_and_data_statement_mixing_not_supported', 100 | '25P01': 'no_active_sql_transaction', 101 | '25P02': 'in_failed_sql_transaction', 102 | '25P03': 'idle_in_transaction_session_timeout', 103 | '26000': 'invalid_sql_statement_name', 104 | '27000': 'triggered_data_change_violation', 105 | '28000': 'invalid_authorization_specification', 106 | '28P01': 'invalid_password', 107 | '2B000': 'dependent_privilege_descriptors_still_exist', 108 | '2BP01': 'dependent_objects_still_exist', 109 | '2D000': 'invalid_transaction_termination', 110 | '2F000': 'sql_routine_exception', 111 | '2F005': 'function_executed_no_return_statement', 112 | '2F002': 'modifying_sql_data_not_permitted', 113 | '2F003': 'prohibited_sql_statement_attempted', 114 | '2F004': 'reading_sql_data_not_permitted', 115 | '34000': 'invalid_cursor_name', 116 | '38000': 'external_routine_exception', 117 | '38001': 'containing_sql_not_permitted', 118 | '38002': 'modifying_sql_data_not_permitted', 119 | '38003': 'prohibited_sql_statement_attempted', 120 | '38004': 'reading_sql_data_not_permitted', 121 | '39000': 'external_routine_invocation_exception', 122 | '39001': 'invalid_sqlstate_returned', 123 | '39004': 'null_value_not_allowed', 124 | '39P01': 'trigger_protocol_violated', 125 | '39P02': 'srf_protocol_violated', 126 | '39P03': 'event_trigger_protocol_violated', 127 | '3B000': 'savepoint_exception', 128 | '3B001': 'invalid_savepoint_specification', 129 | '3D000': 'invalid_catalog_name', 130 | '3F000': 'invalid_schema_name', 131 | '40000': 'transaction_rollback', 132 | '40002': 'transaction_integrity_constraint_violation', 133 | '40001': 'serialization_failure', 134 | '40003': 'statement_completion_unknown', 135 | '40P01': 'deadlock_detected', 136 | '42000': 'syntax_error_or_access_rule_violation', 137 | '42601': 'syntax_error', 138 | '42501': 'insufficient_privilege', 139 | '42846': 'cannot_coerce', 140 | '42803': 'grouping_error', 141 | '42P20': 'windowing_error', 142 | '42P19': 'invalid_recursion', 143 | '42830': 'invalid_foreign_key', 144 | '42602': 'invalid_name', 145 | '42622': 'name_too_long', 146 | '42939': 'reserved_name', 147 | '42804': 'datatype_mismatch', 148 | '42P18': 'indeterminate_datatype', 149 | '42P21': 'collation_mismatch', 150 | '42P22': 'indeterminate_collation', 151 | '42809': 'wrong_object_type', 152 | '428C9': 'generated_always', 153 | '42703': 'undefined_column', 154 | '42883': 'undefined_function', 155 | '42P01': 'undefined_table', 156 | '42P02': 'undefined_parameter', 157 | '42704': 'undefined_object', 158 | '42701': 'duplicate_column', 159 | '42P03': 'duplicate_cursor', 160 | '42P04': 'duplicate_database', 161 | '42723': 'duplicate_function', 162 | '42P05': 'duplicate_prepared_statement', 163 | '42P06': 'duplicate_schema', 164 | '42P07': 'duplicate_table', 165 | '42712': 'duplicate_alias', 166 | '42710': 'duplicate_object', 167 | '42702': 'ambiguous_column', 168 | '42725': 'ambiguous_function', 169 | '42P08': 'ambiguous_parameter', 170 | '42P09': 'ambiguous_alias', 171 | '42P10': 'invalid_column_reference', 172 | '42611': 'invalid_column_definition', 173 | '42P11': 'invalid_cursor_definition', 174 | '42P12': 'invalid_database_definition', 175 | '42P13': 'invalid_function_definition', 176 | '42P14': 'invalid_prepared_statement_definition', 177 | '42P15': 'invalid_schema_definition', 178 | '42P16': 'invalid_table_definition', 179 | '42P17': 'invalid_object_definition', 180 | '44000': 'with_check_option_violation', 181 | '53000': 'insufficient_resources', 182 | '53100': 'disk_full', 183 | '53200': 'out_of_memory', 184 | '53300': 'too_many_connections', 185 | '53400': 'configuration_limit_exceeded', 186 | '54000': 'program_limit_exceeded', 187 | '54001': 'statement_too_complex', 188 | '54011': 'too_many_columns', 189 | '54023': 'too_many_arguments', 190 | '55000': 'object_not_in_prerequisite_state', 191 | '55006': 'object_in_use', 192 | '55P02': 'cant_change_runtime_param', 193 | '55P03': 'lock_not_available', 194 | '57000': 'operator_intervention', 195 | '57014': 'query_canceled', 196 | '57P01': 'admin_shutdown', 197 | '57P02': 'crash_shutdown', 198 | '57P03': 'cannot_connect_now', 199 | '57P04': 'database_dropped', 200 | '58000': 'system_error', 201 | '58030': 'io_error', 202 | '58P01': 'undefined_file', 203 | '58P02': 'duplicate_file', 204 | '72000': 'snapshot_too_old', 205 | F0000: 'config_file_error', 206 | F0001: 'lock_file_exists', 207 | HV000: 'fdw_error', 208 | HV005: 'fdw_column_name_not_found', 209 | HV002: 'fdw_dynamic_parameter_value_needed', 210 | HV010: 'fdw_function_sequence_error', 211 | HV021: 'fdw_inconsistent_descriptor_information', 212 | HV024: 'fdw_invalid_attribute_value', 213 | HV007: 'fdw_invalid_column_name', 214 | HV008: 'fdw_invalid_column_number', 215 | HV004: 'fdw_invalid_data_type', 216 | HV006: 'fdw_invalid_data_type_descriptors', 217 | HV091: 'fdw_invalid_descriptor_field_identifier', 218 | HV00B: 'fdw_invalid_handle', 219 | HV00C: 'fdw_invalid_option_index', 220 | HV00D: 'fdw_invalid_option_name', 221 | HV090: 'fdw_invalid_string_length_or_buffer_length', 222 | HV00A: 'fdw_invalid_string_format', 223 | HV009: 'fdw_invalid_use_of_null_pointer', 224 | HV014: 'fdw_too_many_handles', 225 | HV001: 'fdw_out_of_memory', 226 | HV00P: 'fdw_no_schemas', 227 | HV00J: 'fdw_option_name_not_found', 228 | HV00K: 'fdw_reply_handle', 229 | HV00Q: 'fdw_schema_not_found', 230 | HV00R: 'fdw_table_not_found', 231 | HV00L: 'fdw_unable_to_create_execution', 232 | HV00M: 'fdw_unable_to_create_reply', 233 | HV00N: 'fdw_unable_to_establish_connection', 234 | P0000: 'plpgsql_error', 235 | P0001: 'raise_exception', 236 | P0002: 'no_data_found', 237 | P0003: 'too_many_rows', 238 | P0004: 'assert_failure', 239 | XX000: 'internal_error', 240 | XX001: 'data_corrupted', 241 | XX002: 'index_corrupted', 242 | }; 243 | -------------------------------------------------------------------------------- /src/shared/constants/prisma.constants.ts: -------------------------------------------------------------------------------- 1 | export const PRISMA_ERRORS = { 2 | P2002: 'Unique constraint failed on the {constraint}', 3 | // TODO: add other codes: 4 | // https://www.prisma.io/docs/reference/api-reference/error-reference#error-codes 5 | }; 6 | -------------------------------------------------------------------------------- /src/shared/constants/strings.ts: -------------------------------------------------------------------------------- 1 | export const INVALID_EMAIL = 'Missing or invalid email!'; 2 | -------------------------------------------------------------------------------- /src/shared/helpers/auth.helpers.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes, scrypt } from 'crypto'; 2 | 3 | const hash = (password) => { 4 | return new Promise((resolve, reject) => { 5 | const salt = randomBytes(8).toString('hex'); 6 | 7 | scrypt(password, salt, 64, (err, derivedKey) => { 8 | if (err) reject(err); 9 | resolve(salt + ':' + derivedKey.toString('hex')); 10 | }); 11 | }); 12 | }; 13 | 14 | const verify = (password, hash) => { 15 | return new Promise((resolve, reject) => { 16 | const [salt, key] = hash.split(':'); 17 | scrypt(password, salt, 64, (err, derivedKey) => { 18 | if (err) reject(err); 19 | resolve(key == derivedKey.toString('hex')); 20 | }); 21 | }); 22 | }; 23 | 24 | export const AuthHelpers = { 25 | hash, 26 | verify, 27 | }; 28 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { Test } from '@nestjs/testing'; 3 | import { INestApplication } from '@nestjs/common'; 4 | 5 | import { AppModule } from '../src/modules/app/app.module'; 6 | 7 | describe('AppController (e2e)', () => { 8 | let app: INestApplication; 9 | 10 | beforeAll(async () => { 11 | const moduleFixture = await Test.createTestingModule({ 12 | imports: [AppModule], 13 | }).compile(); 14 | 15 | app = moduleFixture.createNestApplication(); 16 | await app.init(); 17 | }); 18 | 19 | it('/ (GET)', () => { 20 | return request(app.getHttpServer()) 21 | .get('/') 22 | .expect(200) 23 | .expect('Hello World!'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "esModuleInterop": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "target": "es2017", 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "baseUrl": "./", 14 | "incremental": true 15 | } 16 | } 17 | --------------------------------------------------------------------------------