├── .prettierrc ├── nest-cli.json ├── src ├── authentication │ ├── user.model.ts │ ├── authentication.strategy.ts │ ├── strategy │ │ ├── fake.strategy.ts │ │ ├── keycloak.strategy.ts │ │ └── manual.strategy.ts │ ├── authentication.module.ts │ ├── authentication.service.ts │ └── authentication.guard.ts ├── app.service.ts ├── app.module.ts ├── app.controller.ts ├── main.ts └── e2e.spec.ts ├── tsconfig.build.json ├── .env.example ├── README.md ├── .idea ├── $CACHE_FILE$ ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── vcs.xml ├── misc.xml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── nest-template-example.iml └── runConfigurations │ ├── tests.xml │ └── tests_watch.xml ├── .editorconfig ├── tsconfig.json ├── test └── helpers │ └── authentication │ └── create-token.ts ├── .eslintrc.js ├── package.json └── .gitignore /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /src/authentication/user.model.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: string; 3 | username: string; 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PORT=3000 3 | KEYCLOAK_BASE_URL=http://localhost:8080 4 | KEYCLOAK_REALM=my_awesome_app 5 | KEYCLOAK_CLIENT_ID=backend 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NestJS Authentication example 2 | 3 | Code supporting the Medium article [here](https://medium.com/@paztek/protecting-your-nestjs-api-with-keycloak-8236e0998233) 4 | 5 | -------------------------------------------------------------------------------- /.idea/$CACHE_FILE$: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # 4 space indentation 12 | indent_style = space 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/nest-template-example.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | import { AuthenticationModule } from './authentication/authentication.module'; 6 | 7 | @Module({ 8 | imports: [ 9 | AuthenticationModule, 10 | ], 11 | controllers: [ 12 | AppController, 13 | ], 14 | providers: [ 15 | AppService, 16 | ], 17 | }) 18 | export class AppModule {} 19 | -------------------------------------------------------------------------------- /src/authentication/authentication.strategy.ts: -------------------------------------------------------------------------------- 1 | export const AUTHENTICATION_STRATEGY_TOKEN = 'AuthenticationStrategy'; 2 | 3 | export interface KeycloakUserInfoResponse { 4 | sub: string; 5 | email_verified: boolean; 6 | name: string; 7 | preferred_username: string; 8 | given_name: string; 9 | family_name: string, 10 | email: string; 11 | } 12 | 13 | export interface AuthenticationStrategy { 14 | authenticate(accessToken: string): Promise; 15 | } 16 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, UseGuards } from '@nestjs/common'; 2 | 3 | import { AppService } from './app.service'; 4 | import { AuthenticationGuard } from './authentication/authentication.guard'; 5 | 6 | @UseGuards(AuthenticationGuard) 7 | @Controller() 8 | export class AppController { 9 | 10 | constructor( 11 | private readonly appService: AppService 12 | ) {} 13 | 14 | @Get() 15 | getHello(): string { 16 | return this.appService.getHello(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { Logger } from '@nestjs/common'; 3 | 4 | import { AppModule } from './app.module'; 5 | 6 | async function bootstrap() { 7 | const logger = new Logger('App'); 8 | 9 | logger.log('Starting app...'); 10 | const app = await NestFactory.create(AppModule); 11 | 12 | const port = parseInt(process.env.NODE_PORT, 10) || 3000; 13 | await app.listen(port); 14 | 15 | logger.log(`App started on port ${port}...`); 16 | } 17 | bootstrap(); 18 | -------------------------------------------------------------------------------- /.idea/runConfigurations/tests.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/runConfigurations/tests_watch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/authentication/strategy/fake.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import * as jwt from 'jsonwebtoken'; 3 | 4 | import { AuthenticationStrategy, KeycloakUserInfoResponse } from '../authentication.strategy'; 5 | 6 | @Injectable() 7 | export class FakeAuthenticationStrategy implements AuthenticationStrategy { 8 | 9 | /** 10 | * Blindly trust the JWT, assume it has the Keycloak structure and return the decoded payload 11 | */ 12 | public authenticate(accessToken: string): Promise { 13 | return Promise.resolve(jwt.decode(accessToken)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/helpers/authentication/create-token.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import { v4 as uuid } from 'uuid'; 3 | 4 | import { KeycloakUserInfoResponse } from '../../../src/authentication/authentication.strategy'; 5 | 6 | export function createToken(id: string = uuid(), username = 'john.doe'): string { 7 | const userInfos: KeycloakUserInfoResponse = { 8 | sub: id, 9 | email: `${username}@example.com`, 10 | email_verified: true, 11 | name: 'John doe', 12 | preferred_username: username, 13 | given_name: 'John', 14 | family_name: 'Doe', 15 | }; 16 | 17 | return jwt.sign(userInfos, 'secret'); 18 | } 19 | -------------------------------------------------------------------------------- /.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'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier', 12 | 'prettier/@typescript-eslint', 13 | ], 14 | root: true, 15 | env: { 16 | node: true, 17 | jest: true, 18 | }, 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/authentication/authentication.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule, Module } from '@nestjs/common'; 2 | 3 | import { AuthenticationGuard } from './authentication.guard'; 4 | import { AuthenticationService } from './authentication.service'; 5 | import { AUTHENTICATION_STRATEGY_TOKEN } from './authentication.strategy'; 6 | import { ManualAuthenticationStrategy } from './strategy/manual.strategy'; 7 | 8 | @Module({ 9 | imports: [ 10 | HttpModule, 11 | ], 12 | providers: [ 13 | AuthenticationGuard, 14 | AuthenticationService, 15 | { 16 | provide: AUTHENTICATION_STRATEGY_TOKEN, 17 | useClass: ManualAuthenticationStrategy, 18 | }, 19 | ], 20 | exports: [ 21 | AuthenticationService, 22 | ], 23 | }) 24 | export class AuthenticationModule {} 25 | -------------------------------------------------------------------------------- /src/authentication/authentication.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, Logger } from '@nestjs/common'; 2 | 3 | import { User } from './user.model'; 4 | import { AUTHENTICATION_STRATEGY_TOKEN, AuthenticationStrategy } from './authentication.strategy'; 5 | 6 | export class AuthenticationError extends Error {} 7 | 8 | @Injectable() 9 | export class AuthenticationService { 10 | 11 | private logger = new Logger(AuthenticationService.name); 12 | 13 | constructor( 14 | @Inject(AUTHENTICATION_STRATEGY_TOKEN) private readonly strategy: AuthenticationStrategy, 15 | ) {} 16 | 17 | async authenticate(accessToken: string): Promise { 18 | try { 19 | const userInfos = await this.strategy.authenticate(accessToken); 20 | 21 | const user = { 22 | id: userInfos.sub, 23 | username: userInfos.preferred_username, 24 | }; 25 | 26 | /** 27 | * Perform any addition business logic with the user: 28 | * - insert user in "users" table on first authentication, 29 | * - etc. 30 | */ 31 | 32 | return user; 33 | } catch (e) { 34 | this.logger.error(e.message, e.stackTrace); 35 | throw new AuthenticationError(e.message); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/authentication/strategy/keycloak.strategy.ts: -------------------------------------------------------------------------------- 1 | import { HttpService, Injectable } from '@nestjs/common'; 2 | 3 | import { AuthenticationStrategy, KeycloakUserInfoResponse } from '../authentication.strategy'; 4 | 5 | @Injectable() 6 | export class KeycloakAuthenticationStrategy implements AuthenticationStrategy { 7 | 8 | private readonly baseURL: string; 9 | private readonly realm: string; 10 | 11 | constructor( 12 | private readonly httpService: HttpService, 13 | ) { 14 | this.baseURL = process.env.KEYCLOAK_BASE_URL; 15 | this.realm = process.env.KEYCLOAK_REALM; 16 | } 17 | 18 | /** 19 | * Call the OpenId Connect UserInfo endpoint on Keycloak: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo 20 | * 21 | * If it succeeds, the token is valid and we get the user infos in the response 22 | * If it fails, the token is invalid or expired 23 | */ 24 | async authenticate(accessToken: string): Promise { 25 | const url = `${this.baseURL}/realms/${this.realm}/protocol/openid-connect/userinfo`; 26 | 27 | const response = await this.httpService.get(url, { 28 | headers: { 29 | authorization: `Bearer ${accessToken}`, 30 | }, 31 | }).toPromise(); 32 | 33 | return response.data; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/authentication/authentication.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable } from '@nestjs/common'; 2 | 3 | import { AuthenticationService } from './authentication.service'; 4 | import { Request } from 'express'; 5 | 6 | @Injectable() 7 | export class AuthenticationGuard implements CanActivate { 8 | 9 | constructor( 10 | private readonly authenticationService: AuthenticationService, 11 | ) {} 12 | 13 | async canActivate(context: ExecutionContext): Promise { 14 | const request: Request = context.switchToHttp().getRequest(); 15 | 16 | const header = request.header('Authorization'); 17 | if (!header) { 18 | throw new HttpException('Authorization: Bearer header missing', HttpStatus.UNAUTHORIZED); 19 | } 20 | 21 | const parts = header.split(' '); 22 | if (parts.length !== 2 || parts[0] !== 'Bearer') { 23 | throw new HttpException('Authorization: Bearer header invalid', HttpStatus.UNAUTHORIZED); 24 | } 25 | 26 | const token = parts[1]; 27 | 28 | try { 29 | // Store the user on the request object if we want to retrieve it from the controllers 30 | request['user'] = await this.authenticationService.authenticate(token); 31 | return true; 32 | } catch (e) { 33 | throw new HttpException(e.message, HttpStatus.UNAUTHORIZED); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/e2e.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | 4 | import { AppModule } from './app.module'; 5 | import { INestApplication } from '@nestjs/common'; 6 | import { createToken } from '../test/helpers/authentication/create-token'; 7 | import { AUTHENTICATION_STRATEGY_TOKEN } from './authentication/authentication.strategy'; 8 | import { FakeAuthenticationStrategy } from './authentication/strategy/fake.strategy'; 9 | 10 | describe('Hello E2E', () => { 11 | let app: INestApplication; 12 | let server: any; 13 | 14 | const token = createToken(); 15 | 16 | beforeEach(async () => { 17 | const module = await Test.createTestingModule({ 18 | imports: [ 19 | AppModule 20 | ], 21 | }) 22 | .overrideProvider(AUTHENTICATION_STRATEGY_TOKEN).useClass(FakeAuthenticationStrategy) 23 | .compile(); 24 | 25 | app = module.createNestApplication(); 26 | await app.init(); 27 | 28 | server = app.getHttpServer(); 29 | }); 30 | 31 | describe('GET /', () => { 32 | 33 | it('requires authentication', async () => { 34 | const response = await request(server) 35 | .get('/'); 36 | 37 | expect(response.status).toEqual(401); 38 | }); 39 | 40 | it('returns "Hello World!"', async () => { 41 | const response = await request(server) 42 | .get('/') 43 | .set('Authorization', `Bearer ${token}`); 44 | 45 | expect(response.status).toEqual(200); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-authentication-example", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^7.0.0", 25 | "@nestjs/core": "^7.0.0", 26 | "@nestjs/platform-express": "^7.0.0", 27 | "jsonwebtoken": "^8.5.1", 28 | "reflect-metadata": "^0.1.13", 29 | "rimraf": "^3.0.2", 30 | "rxjs": "^6.5.4", 31 | "uuid": "^8.3.0" 32 | }, 33 | "devDependencies": { 34 | "@nestjs/cli": "^7.0.0", 35 | "@nestjs/schematics": "^7.0.0", 36 | "@nestjs/testing": "^7.0.0", 37 | "@types/express": "^4.17.3", 38 | "@types/jest": "25.2.3", 39 | "@types/node": "^13.9.1", 40 | "@types/supertest": "^2.0.8", 41 | "@typescript-eslint/eslint-plugin": "3.0.2", 42 | "@typescript-eslint/parser": "3.0.2", 43 | "eslint": "7.1.0", 44 | "eslint-config-prettier": "^6.10.0", 45 | "eslint-plugin-import": "^2.20.1", 46 | "jest": "26.0.1", 47 | "prettier": "^1.19.1", 48 | "supertest": "^4.0.2", 49 | "ts-jest": "26.1.0", 50 | "ts-loader": "^6.2.1", 51 | "ts-node": "^8.6.2", 52 | "tsconfig-paths": "^3.9.0", 53 | "typescript": "^3.7.4" 54 | }, 55 | "jest": { 56 | "moduleFileExtensions": [ 57 | "js", 58 | "json", 59 | "ts" 60 | ], 61 | "rootDir": "src", 62 | "testRegex": ".spec.ts$", 63 | "transform": { 64 | "^.+\\.(t|j)s$": "ts-jest" 65 | }, 66 | "coverageDirectory": "../coverage", 67 | "testEnvironment": "node" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 23 | 24 | 25 | 27 | 28 | 29 | 36 | 37 | 44 | 45 | -------------------------------------------------------------------------------- /src/authentication/strategy/manual.strategy.ts: -------------------------------------------------------------------------------- 1 | import { HttpService, Injectable } from '@nestjs/common'; 2 | import { map } from 'rxjs/operators'; 3 | import * as jwt from 'jsonwebtoken'; 4 | 5 | import { AuthenticationStrategy, KeycloakUserInfoResponse } from '../authentication.strategy'; 6 | 7 | export class InvalidTokenPublicKeyId extends Error { 8 | constructor(keyId: string) { 9 | super(`Invalid public key ID ${keyId}`); 10 | } 11 | } 12 | 13 | /** 14 | * Format of the keys returned in the JSON response from Keycloak for the list of public keys 15 | */ 16 | interface KeycloakCertsResponse { 17 | keys: KeycloakKey[]; 18 | } 19 | interface KeycloakKey { 20 | kid: KeyId; 21 | x5c: PublicKey; 22 | } 23 | type KeyId = string; 24 | type PublicKey = string; 25 | 26 | @Injectable() 27 | export class ManualAuthenticationStrategy implements AuthenticationStrategy { 28 | 29 | /** 30 | * Keep an in-memory map of the known public keys to avoid calling Keycloak every time 31 | */ 32 | private readonly keysMap: Map = new Map(); 33 | 34 | private readonly baseURL: string; 35 | private readonly realm: string; 36 | 37 | constructor( 38 | private httpService: HttpService, 39 | ) { 40 | this.baseURL = process.env.KEYCLOAK_BASE_URL; 41 | this.realm = process.env.KEYCLOAK_REALM; 42 | } 43 | 44 | public async authenticate(accessToken: string): Promise { 45 | const token = jwt.decode(accessToken, { complete: true }); // For once, we'd like to have the header and not just the payload 46 | 47 | const keyId = token.header.kid; 48 | 49 | const publicKey = await this.getPublicKey(keyId); 50 | 51 | return jwt.verify(accessToken, publicKey); 52 | } 53 | 54 | private async getPublicKey(keyId: KeyId): Promise { 55 | if (this.keysMap.has(keyId)) { 56 | return this.keysMap.get(keyId); 57 | } else { 58 | const keys = await this.httpService.get(`${this.baseURL}/realms/${this.realm}/protocol/openid-connect/certs`) 59 | .pipe(map((response) => response.data.keys)) 60 | .toPromise(); 61 | 62 | const key = keys.find((k) => k.kid === keyId); 63 | 64 | if (key) { 65 | const publicKey = 66 | ` 67 | -----BEGIN CERTIFICATE----- 68 | ${key.x5c} 69 | -----END CERTIFICATE----- 70 | `; 71 | this.keysMap.set(keyId, publicKey); 72 | 73 | return publicKey; 74 | } else { 75 | // Token is probably so old, Keycloak doesn't even advertise the corresponding public key anymore 76 | throw new InvalidTokenPublicKeyId(keyId); 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 3 | 4 | # User-specific stuff 5 | .idea/**/workspace.xml 6 | .idea/**/tasks.xml 7 | .idea/**/usage.statistics.xml 8 | .idea/**/dictionaries 9 | .idea/**/shelf 10 | 11 | # Generated files 12 | .idea/**/contentModel.xml 13 | 14 | # Sensitive or high-churn files 15 | .idea/**/dataSources/ 16 | .idea/**/dataSources.ids 17 | .idea/**/dataSources.local.xml 18 | .idea/**/sqlDataSources.xml 19 | .idea/**/dynamic.xml 20 | .idea/**/uiDesigner.xml 21 | .idea/**/dbnavigator.xml 22 | 23 | # Gradle 24 | .idea/**/gradle.xml 25 | .idea/**/libraries 26 | 27 | # Gradle and Maven with auto-import 28 | # When using Gradle or Maven with auto-import, you should exclude module files, 29 | # since they will be recreated, and may cause churn. Uncomment if using 30 | # auto-import. 31 | # .idea/artifacts 32 | # .idea/compiler.xml 33 | # .idea/jarRepositories.xml 34 | # .idea/modules.xml 35 | # .idea/*.iml 36 | # .idea/modules 37 | # *.iml 38 | # *.ipr 39 | 40 | # CMake 41 | cmake-build-*/ 42 | 43 | # Mongo Explorer plugin 44 | .idea/**/mongoSettings.xml 45 | 46 | # File-based project format 47 | *.iws 48 | 49 | # IntelliJ 50 | out/ 51 | 52 | # mpeltonen/sbt-idea plugin 53 | .idea_modules/ 54 | 55 | # JIRA plugin 56 | atlassian-ide-plugin.xml 57 | 58 | # Cursive Clojure plugin 59 | .idea/replstate.xml 60 | 61 | # Crashlytics plugin (for Android Studio and IntelliJ) 62 | com_crashlytics_export_strings.xml 63 | crashlytics.properties 64 | crashlytics-build.properties 65 | fabric.properties 66 | 67 | # Editor-based Rest Client 68 | .idea/httpRequests 69 | 70 | # Android studio 3.1+ serialized cache file 71 | .idea/caches/build_file_checksums.ser 72 | 73 | # Logs 74 | logs 75 | *.log 76 | npm-debug.log* 77 | yarn-debug.log* 78 | yarn-error.log* 79 | lerna-debug.log* 80 | 81 | # Diagnostic reports (https://nodejs.org/api/report.html) 82 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 83 | 84 | # Runtime data 85 | pids 86 | *.pid 87 | *.seed 88 | *.pid.lock 89 | 90 | # Directory for instrumented libs generated by jscoverage/JSCover 91 | lib-cov 92 | 93 | # Coverage directory used by tools like istanbul 94 | coverage 95 | *.lcov 96 | 97 | # nyc test coverage 98 | .nyc_output 99 | 100 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 101 | .grunt 102 | 103 | # Bower dependency directory (https://bower.io/) 104 | bower_components 105 | 106 | # node-waf configuration 107 | .lock-wscript 108 | 109 | # Compiled binary addons (https://nodejs.org/api/addons.html) 110 | build/Release 111 | 112 | # Dependency directories 113 | node_modules/ 114 | jspm_packages/ 115 | 116 | # Snowpack dependency directory (https://snowpack.dev/) 117 | web_modules/ 118 | 119 | # TypeScript cache 120 | *.tsbuildinfo 121 | 122 | # Optional npm cache directory 123 | .npm 124 | 125 | # Optional eslint cache 126 | .eslintcache 127 | 128 | # Microbundle cache 129 | .rpt2_cache/ 130 | .rts2_cache_cjs/ 131 | .rts2_cache_es/ 132 | .rts2_cache_umd/ 133 | 134 | # Optional REPL history 135 | .node_repl_history 136 | 137 | # Output of 'npm pack' 138 | *.tgz 139 | 140 | # Yarn Integrity file 141 | .yarn-integrity 142 | 143 | # dotenv environment variables file 144 | .env 145 | .env.test 146 | 147 | # parcel-bundler cache (https://parceljs.org/) 148 | .cache 149 | .parcel-cache 150 | 151 | # Next.js build output 152 | .next 153 | out 154 | 155 | # Nuxt.js build / generate output 156 | .nuxt 157 | dist 158 | 159 | # Gatsby files 160 | .cache/ 161 | # Comment in the public line in if your project uses Gatsby and not Next.js 162 | # https://nextjs.org/blog/next-9-1#public-directory-support 163 | # public 164 | 165 | # vuepress build output 166 | .vuepress/dist 167 | 168 | # Serverless directories 169 | .serverless/ 170 | 171 | # FuseBox cache 172 | .fusebox/ 173 | 174 | # DynamoDB Local files 175 | .dynamodb/ 176 | 177 | # TernJS port file 178 | .tern-port 179 | 180 | # Stores VSCode versions used for testing VSCode extensions 181 | .vscode-test 182 | 183 | # yarn v2 184 | .yarn/cache 185 | .yarn/unplugged 186 | .yarn/build-state.yml 187 | .yarn/install-state.gz 188 | .pnp.* 189 | --------------------------------------------------------------------------------