From d3543f9e0ba7ec8d27011e06cf435b5dcb4ef7b6 Mon Sep 17 00:00:00 2001 From: siwonpada Date: Sat, 9 Mar 2024 13:22:51 +0900 Subject: [PATCH 1/5] setting: user modules --- package-lock.json | 52 ++++++++++++++++++++++++++++++++---- package.json | 2 ++ src/prisma/prisma.service.ts | 8 ++++++ src/user/user.controller.ts | 4 --- src/user/user.module.ts | 8 +++--- src/user/user.repository.ts | 7 +++++ src/user/user.service.ts | 9 ++++++- 7 files changed, 77 insertions(+), 13 deletions(-) delete mode 100644 src/user/user.controller.ts create mode 100644 src/user/user.repository.ts diff --git a/package-lock.json b/package-lock.json index 62cc219..64b4e6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,14 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@nestjs/axios": "^3.0.2", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.0", "@prisma/client": "^5.10.2", + "axios": "^1.6.7", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" }, @@ -1680,6 +1682,16 @@ "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==" }, + "node_modules/@nestjs/axios": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.2.tgz", + "integrity": "sha512-Z6GuOUdNQjP7FX+OuV2Ybyamse+/e0BFdTWBX5JxpBDKA+YkdLynDgG6HTF04zy6e9zPa19UX0WA2VDoehwhXQ==", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "10.3.2", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz", @@ -3021,8 +3033,17 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "node_modules/babel-jest": { "version": "29.7.0", @@ -3622,7 +3643,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3895,7 +3915,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -4781,6 +4800,25 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -4851,7 +4889,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -7204,6 +7241,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 3577f19..5776a36 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,14 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@nestjs/axios": "^3.0.2", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.0", "@prisma/client": "^5.10.2", + "axios": "^1.6.7", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" }, diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts index 6f1dc42..e8601e4 100644 --- a/src/prisma/prisma.service.ts +++ b/src/prisma/prisma.service.ts @@ -17,10 +17,18 @@ export class PrismaService }); } + /** + * This method is called when the application is on the bootstrap phase. + * And it's the right place to connect to the database. + */ async onModuleInit() { await this.$connect(); } + /** + * This method is called when the application is shutting down. + * And it's the right place to close the database connection. + */ async onModuleDestroy() { await this.$disconnect(); } diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts deleted file mode 100644 index ad8c2a6..0000000 --- a/src/user/user.controller.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Controller } from '@nestjs/common'; - -@Controller('user') -export class UserController {} diff --git a/src/user/user.module.ts b/src/user/user.module.ts index 60024cf..5a5177d 100644 --- a/src/user/user.module.ts +++ b/src/user/user.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common'; import { UserService } from './user.service'; -import { UserController } from './user.controller'; +import { HttpModule } from '@nestjs/axios'; +import { PrismaModule } from 'src/prisma/prisma.module'; +import { UserRepository } from './user.repository'; @Module({ - providers: [UserService], - controllers: [UserController] + imports: [HttpModule, PrismaModule], + providers: [UserService, UserRepository], }) export class UserModule {} diff --git a/src/user/user.repository.ts b/src/user/user.repository.ts new file mode 100644 index 0000000..9edd88d --- /dev/null +++ b/src/user/user.repository.ts @@ -0,0 +1,7 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from 'src/prisma/prisma.service'; + +@Injectable() +export class UserRepository { + constructor(private readonly prismaService: PrismaService) {} +} diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 668a7d6..17fd247 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -1,4 +1,11 @@ +import { HttpService } from '@nestjs/axios'; import { Injectable } from '@nestjs/common'; +import { UserRepository } from './user.repository'; @Injectable() -export class UserService {} +export class UserService { + constructor( + private readonly httpService: HttpService, + private readonly userRepository: UserRepository, + ) {} +} From 3181b770d0eba58b336bef8a3823bd2c7a534697 Mon Sep 17 00:00:00 2001 From: siwonpada Date: Sat, 9 Mar 2024 14:17:26 +0900 Subject: [PATCH 2/5] feat: idp and user --- package-lock.json | 8 ++++- package.json | 3 +- src/app.module.ts | 2 ++ src/idp/idp.module.ts | 11 +++++++ src/idp/idp.service.ts | 57 ++++++++++++++++++++++++++++++++++ src/idp/types/userInfo.type.ts | 15 +++++++++ src/user/user.module.ts | 4 +-- src/user/user.repository.ts | 35 ++++++++++++++++++++- src/user/user.service.ts | 21 +++++++++++-- test/idp/idp.service.spec.ts | 18 +++++++++++ 10 files changed, 166 insertions(+), 8 deletions(-) create mode 100644 src/idp/idp.module.ts create mode 100644 src/idp/idp.service.ts create mode 100644 src/idp/types/userInfo.type.ts create mode 100644 test/idp/idp.service.spec.ts diff --git a/package-lock.json b/package-lock.json index 64b4e6e..e085859 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "@prisma/client": "^5.10.2", "axios": "^1.6.7", "reflect-metadata": "^0.2.0", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "ts-case-convert": "^2.0.7" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -8446,6 +8447,11 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-case-convert": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/ts-case-convert/-/ts-case-convert-2.0.7.tgz", + "integrity": "sha512-Kqj8wrkuduWsKUOUNRczrkdHCDt4ZNNd6HKjVw42EnMIGHQUABS4pqfy0acETVLwUTppc1fzo/yi11+uMTaqzw==" + }, "node_modules/ts-jest": { "version": "29.1.2", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", diff --git a/package.json b/package.json index 5776a36..ce1fbac 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "@prisma/client": "^5.10.2", "axios": "^1.6.7", "reflect-metadata": "^0.2.0", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "ts-case-convert": "^2.0.7" }, "devDependencies": { "@nestjs/cli": "^10.0.0", diff --git a/src/app.module.ts b/src/app.module.ts index 8455ac1..d906ed8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,6 +5,7 @@ import { GroupModule } from './group/group.module'; import { MemberModule } from './member/member.module'; import { UserModule } from './user/user.module'; import { RoleModule } from './role/role.module'; +import { IdpModule } from './idp/idp.module'; @Module({ imports: [ @@ -15,6 +16,7 @@ import { RoleModule } from './role/role.module'; MemberModule, UserModule, RoleModule, + IdpModule, ], controllers: [AppController], }) diff --git a/src/idp/idp.module.ts b/src/idp/idp.module.ts new file mode 100644 index 0000000..695c241 --- /dev/null +++ b/src/idp/idp.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { IdpService } from './idp.service'; +import { HttpService } from '@nestjs/axios'; +import { ConfigModule } from '@nestjs/config'; + +@Module({ + imports: [HttpService, ConfigModule], + providers: [IdpService], + exports: [IdpService], +}) +export class IdpModule {} diff --git a/src/idp/idp.service.ts b/src/idp/idp.service.ts new file mode 100644 index 0000000..8d81e3b --- /dev/null +++ b/src/idp/idp.service.ts @@ -0,0 +1,57 @@ +import { HttpService } from '@nestjs/axios'; +import { + Injectable, + InternalServerErrorException, + Logger, + UnauthorizedException, +} from '@nestjs/common'; +import { UserInfo, UserInfoResponse } from './types/userInfo.type'; +import { ConfigService } from '@nestjs/config'; +import { catchError, firstValueFrom } from 'rxjs'; +import { AxiosError } from 'axios'; +import { objectToCamel } from 'ts-case-convert'; + +@Injectable() +export class IdpService { + private readonly logger = new Logger(IdpService.name); + private idpUrl: string; + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) { + this.idpUrl = this.configService.getOrThrow('IDP_URL'); + } + + /** + * Get user info from IDP and the handing exceptions + * @param accessToken it is the idp access token + * @returns object of the UserInfo type + */ + async getUserInfo(accessToken: string): Promise { + this.logger.log('Fetching user info from IDP'); + const url = this.idpUrl + '/userinfo'; + const userInfoResponse = await firstValueFrom( + this.httpService + .get(url, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + .pipe( + catchError((error) => { + if (error instanceof AxiosError) { + if (error.response?.status === 401) { + this.logger.debug('Invalid access token'); + throw new UnauthorizedException('Invalid access token'); + } + this.logger.error('IDP error', error); + throw new InternalServerErrorException('IDP error'); + } + this.logger.error('Unknown error', error); + throw new InternalServerErrorException('Unknown error'); + }), + ), + ); + return objectToCamel(userInfoResponse.data); + } +} diff --git a/src/idp/types/userInfo.type.ts b/src/idp/types/userInfo.type.ts new file mode 100644 index 0000000..e01dfa9 --- /dev/null +++ b/src/idp/types/userInfo.type.ts @@ -0,0 +1,15 @@ +export type UserInfoResponse = { + user_uuid: string; + user_email_id: string; + user_name: string; + user_phone_number: string; + student_number: string; +}; + +export type UserInfo = { + userUuid: string; + userEmailId: string; + userName: string; + userPhoneNumber: string; + studentNumber: string; +}; diff --git a/src/user/user.module.ts b/src/user/user.module.ts index 5a5177d..e04ebdb 100644 --- a/src/user/user.module.ts +++ b/src/user/user.module.ts @@ -1,11 +1,11 @@ import { Module } from '@nestjs/common'; import { UserService } from './user.service'; -import { HttpModule } from '@nestjs/axios'; import { PrismaModule } from 'src/prisma/prisma.module'; import { UserRepository } from './user.repository'; +import { IdpModule } from 'src/idp/idp.module'; @Module({ - imports: [HttpModule, PrismaModule], + imports: [PrismaModule, IdpModule], providers: [UserService, UserRepository], }) export class UserModule {} diff --git a/src/user/user.repository.ts b/src/user/user.repository.ts index 9edd88d..204ff5a 100644 --- a/src/user/user.repository.ts +++ b/src/user/user.repository.ts @@ -1,7 +1,40 @@ -import { Injectable } from '@nestjs/common'; +import { + ConflictException, + Injectable, + InternalServerErrorException, + Logger, +} from '@nestjs/common'; +import { User } from '@prisma/client'; +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; import { PrismaService } from 'src/prisma/prisma.service'; @Injectable() export class UserRepository { + private readonly logger = new Logger(UserRepository.name); constructor(private readonly prismaService: PrismaService) {} + + async getUserByUuid({ uuid }: Pick): Promise { + this.logger.log('Fetching user by uuid'); + return this.prismaService.user.findUnique({ + where: { uuid }, + }); + } + + async createUser({ uuid }: Pick): Promise { + this.logger.log('Creating user'); + return this.prismaService.user + .create({ + data: { uuid }, + }) + .catch((error) => { + if (error instanceof PrismaClientKnownRequestError) { + if (error.code === 'P2002') { + this.logger.debug('User already exists'); + throw new ConflictException('User already exists'); + } + } + this.logger.error('Unknown database error', error); + throw new InternalServerErrorException('Unknown database error'); + }); + } } diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 17fd247..7dc61fe 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -1,11 +1,26 @@ -import { HttpService } from '@nestjs/axios'; -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { UserRepository } from './user.repository'; +import { IdpService } from 'src/idp/idp.service'; +import { User } from '@prisma/client'; @Injectable() export class UserService { + private readonly logger = new Logger(UserService.name); constructor( - private readonly httpService: HttpService, + private readonly idpService: IdpService, private readonly userRepository: UserRepository, ) {} + + async validateUser(accessToken: string): Promise { + this.logger.log('Validating user'); + const userInfo = await this.idpService.getUserInfo(accessToken); + const user = await this.userRepository.getUserByUuid({ + uuid: userInfo.userUuid, + }); + if (!user) { + this.logger.log('User not found, creating user'); + return this.userRepository.createUser({ uuid: userInfo.userUuid }); + } + return user; + } } diff --git a/test/idp/idp.service.spec.ts b/test/idp/idp.service.spec.ts new file mode 100644 index 0000000..b75b7ff --- /dev/null +++ b/test/idp/idp.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { IdpService } from '../../src/idp/idp.service'; + +describe('IdpService', () => { + let service: IdpService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [IdpService], + }).compile(); + + service = module.get(IdpService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); From 25d878081d8597e25987214b53944d16695042b7 Mon Sep 17 00:00:00 2001 From: siwonpada Date: Sat, 9 Mar 2024 14:22:17 +0900 Subject: [PATCH 3/5] comment: user module --- src/user/user.repository.ts | 10 ++++++++++ src/user/user.service.ts | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/src/user/user.repository.ts b/src/user/user.repository.ts index 204ff5a..1c3d475 100644 --- a/src/user/user.repository.ts +++ b/src/user/user.repository.ts @@ -13,6 +13,11 @@ export class UserRepository { private readonly logger = new Logger(UserRepository.name); constructor(private readonly prismaService: PrismaService) {} + /** + * this function gets the user by uuid, if not found returns null. So, the null validation is needed in the service + * @param param0 object which has the uuid of the user + * @returns User type or null + */ async getUserByUuid({ uuid }: Pick): Promise { this.logger.log('Fetching user by uuid'); return this.prismaService.user.findUnique({ @@ -20,6 +25,11 @@ export class UserRepository { }); } + /** + * this function creates the user, if database throw unique constraint error, it throws conflict exception + * @param param0 object which has the uuid of the user + * @returns User type + */ async createUser({ uuid }: Pick): Promise { this.logger.log('Creating user'); return this.prismaService.user diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 7dc61fe..0d5e49e 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -11,6 +11,11 @@ export class UserService { private readonly userRepository: UserRepository, ) {} + /** + * this function validates the user by idp and creates the user if not found + * @param accessToken the access token from idp + * @returns User type + */ async validateUser(accessToken: string): Promise { this.logger.log('Validating user'); const userInfo = await this.idpService.getUserInfo(accessToken); From a1a484bfb5bc9da2312727ebc030fb7bd7094a83 Mon Sep 17 00:00:00 2001 From: siwonpada Date: Sat, 9 Mar 2024 14:32:09 +0900 Subject: [PATCH 4/5] feat: user guard --- package-lock.json | 139 ++++++++++++++++++++++++++++++++ package.json | 3 + src/user/guard/user.guard.ts | 5 ++ src/user/guard/user.strategy.ts | 21 +++++ src/user/user.module.ts | 5 +- 5 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 src/user/guard/user.guard.ts create mode 100644 src/user/guard/user.strategy.ts diff --git a/package-lock.json b/package-lock.json index e085859..7fbaf69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,12 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.0", "@prisma/client": "^5.10.2", "axios": "^1.6.7", + "passport-http-bearer": "^1.0.1", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "ts-case-convert": "^2.0.7" @@ -28,6 +30,7 @@ "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/passport-http-bearer": "^1.0.41", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", @@ -1909,6 +1912,15 @@ } } }, + "node_modules/@nestjs/passport": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz", + "integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, "node_modules/@nestjs/platform-express": { "version": "10.3.3", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.3.tgz", @@ -2194,6 +2206,15 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2254,12 +2275,30 @@ "@types/node": "*" } }, + "node_modules/@types/content-disposition": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.8.tgz", + "integrity": "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==", + "dev": true + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", "dev": true }, + "node_modules/@types/cookies": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz", + "integrity": "sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "8.56.5", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.5.tgz", @@ -2319,6 +2358,12 @@ "@types/node": "*" } }, + "node_modules/@types/http-assert": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.5.tgz", + "integrity": "sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==", + "dev": true + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -2365,6 +2410,37 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/keygrip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", + "dev": true + }, + "node_modules/@types/koa": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", + "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", + "dev": true, + "dependencies": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "node_modules/@types/koa-compose": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.8.tgz", + "integrity": "sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==", + "dev": true, + "dependencies": { + "@types/koa": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -2386,6 +2462,26 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/passport": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz", + "integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-http-bearer": { + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/@types/passport-http-bearer/-/passport-http-bearer-1.0.41.tgz", + "integrity": "sha512-ecW+9e8C+0id5iz3YZ+uIarsk/vaRPkKSajt1i1Am66t0mC9gDfQDKXZz9fnPOW2xKUufbmCSou4005VM94Feg==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/koa": "*", + "@types/passport": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.12", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.12.tgz", @@ -6962,6 +7058,43 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "peer": true, + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-http-bearer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/passport-http-bearer/-/passport-http-bearer-1.0.1.tgz", + "integrity": "sha512-SELQM+dOTuMigr9yu8Wo4Fm3ciFfkMq5h/ZQ8ffi4ELgZrX1xh9PlglqZdcUZ1upzJD/whVyt+YWF62s3U6Ipw==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7034,6 +7167,12 @@ "node": ">=8" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==", + "peer": true + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", diff --git a/package.json b/package.json index ce1fbac..3030c72 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,12 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.0", "@prisma/client": "^5.10.2", "axios": "^1.6.7", + "passport-http-bearer": "^1.0.1", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "ts-case-convert": "^2.0.7" @@ -39,6 +41,7 @@ "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/passport-http-bearer": "^1.0.41", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", diff --git a/src/user/guard/user.guard.ts b/src/user/guard/user.guard.ts new file mode 100644 index 0000000..50409d5 --- /dev/null +++ b/src/user/guard/user.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class UserGuard extends AuthGuard('user') {} diff --git a/src/user/guard/user.strategy.ts b/src/user/guard/user.strategy.ts new file mode 100644 index 0000000..67a6ab9 --- /dev/null +++ b/src/user/guard/user.strategy.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport-http-bearer'; +import { UserService } from '../user.service'; +import { User } from '@prisma/client'; + +@Injectable() +export class UserStrategy extends PassportStrategy(Strategy, 'user') { + constructor(private readonly userService: UserService) { + super(); + } + + /** + * this function validates the user by token + * @param token the token from the request + * @returns User type, it will be contained to the request object + */ + async validate(token: string): Promise { + return this.userService.validateUser(token); + } +} diff --git a/src/user/user.module.ts b/src/user/user.module.ts index e04ebdb..3ca5c2b 100644 --- a/src/user/user.module.ts +++ b/src/user/user.module.ts @@ -3,9 +3,12 @@ import { UserService } from './user.service'; import { PrismaModule } from 'src/prisma/prisma.module'; import { UserRepository } from './user.repository'; import { IdpModule } from 'src/idp/idp.module'; +import { UserStrategy } from './guard/user.strategy'; +import { UserGuard } from './guard/user.guard'; @Module({ imports: [PrismaModule, IdpModule], - providers: [UserService, UserRepository], + providers: [UserService, UserRepository, UserStrategy, UserGuard], + exports: [UserService, UserGuard], }) export class UserModule {} From 138ba834daa4eb7c19668026d3c6fc9eb66d2234 Mon Sep 17 00:00:00 2001 From: siwonpada Date: Sat, 9 Mar 2024 14:41:13 +0900 Subject: [PATCH 5/5] add user guard --- src/user/decorator/getUser.decorator.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/user/decorator/getUser.decorator.ts diff --git a/src/user/decorator/getUser.decorator.ts b/src/user/decorator/getUser.decorator.ts new file mode 100644 index 0000000..3acd574 --- /dev/null +++ b/src/user/decorator/getUser.decorator.ts @@ -0,0 +1,8 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { User } from '@prisma/client'; + +export const GetUser = createParamDecorator( + (data, ctx: ExecutionContext): User => { + return ctx.switchToHttp().getRequest().user; + }, +);