├── .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 |
4 |
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 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
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 |
--------------------------------------------------------------------------------