├── .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 |
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 |
--------------------------------------------------------------------------------