From 18f5445376df6deca6d1d3e63216559d0436254c Mon Sep 17 00:00:00 2001 From: yubinquitous Date: Thu, 2 Nov 2023 14:30:33 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20auth=20controller=20=EC=B6=94=EA=B0=80,?= =?UTF-8?q?=2042=20access=20token=20=EB=B0=9C=EA=B8=89=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api-gateway-config | 2 +- package-lock.json | 61 +++++++++++++++++-- package.json | 2 + src/app.module.ts | 12 +++- src/auth/auth-fortytwo.service.ts | 52 ++++++++++++++++ src/auth/auth.controller.spec.ts | 20 ++++++ src/auth/auth.controller.ts | 25 ++++++++ src/auth/auth.module.ts | 10 +++ src/auth/auth.service.spec.ts | 18 ++++++ src/auth/auth.service.ts | 15 +++++ src/auth/dto/create-auth.dto.ts | 1 + src/auth/dto/fortytwo-user.dto.ts | 4 ++ src/auth/dto/update-auth.dto.ts | 4 ++ src/auth/entities/auth.entity.ts | 1 + src/common/interceptors/logger.interceptor.ts | 20 ++++++ .../interceptors/transform.interceptor.ts | 29 +++++++++ src/common/middlewares/logger.middleware.ts | 18 ++++++ 17 files changed, 285 insertions(+), 9 deletions(-) create mode 100644 src/auth/auth-fortytwo.service.ts create mode 100644 src/auth/auth.controller.spec.ts create mode 100644 src/auth/auth.controller.ts create mode 100644 src/auth/auth.module.ts create mode 100644 src/auth/auth.service.spec.ts create mode 100644 src/auth/auth.service.ts create mode 100644 src/auth/dto/create-auth.dto.ts create mode 100644 src/auth/dto/fortytwo-user.dto.ts create mode 100644 src/auth/dto/update-auth.dto.ts create mode 100644 src/auth/entities/auth.entity.ts create mode 100644 src/common/interceptors/logger.interceptor.ts create mode 100644 src/common/interceptors/transform.interceptor.ts create mode 100644 src/common/middlewares/logger.middleware.ts diff --git a/api-gateway-config b/api-gateway-config index d1d4a50..6308234 160000 --- a/api-gateway-config +++ b/api-gateway-config @@ -1 +1 @@ -Subproject commit d1d4a50b1fcc0fe676fb20ddfdb9fccd14087088 +Subproject commit 6308234c4e4557e13c98e6c3a4811f9a12152912 diff --git a/package-lock.json b/package-lock.json index 37a498b..beb0dda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,9 @@ "dependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", + "@nestjs/mapped-types": "*", "@nestjs/platform-express": "^10.0.0", + "axios": "^1.6.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1" }, @@ -1528,6 +1530,25 @@ } } }, + "node_modules/@nestjs/mapped-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.2.tgz", + "integrity": "sha512-V0izw6tWs6fTp9+KiiPUbGHWALy563Frn8X6Bm87ANLRuE46iuBMD5acKBDP5lKL/75QFvrzSJT7HkCbB0jTpg==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "10.2.7", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.7.tgz", @@ -2584,8 +2605,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.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "node_modules/babel-jest": { "version": "29.7.0", @@ -3219,7 +3249,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" }, @@ -3619,7 +3648,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" } @@ -4398,6 +4426,25 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", @@ -4430,7 +4477,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", @@ -6721,6 +6767,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/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", diff --git a/package.json b/package.json index 1ff7d5b..725ea3b 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "dependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", + "@nestjs/mapped-types": "*", "@nestjs/platform-express": "^10.0.0", + "axios": "^1.6.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1" }, diff --git a/src/app.module.ts b/src/app.module.ts index 8662803..103caa0 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,10 +1,16 @@ -import { Module } from '@nestjs/common'; +import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { AuthModule } from './auth/auth.module'; +import { LoggerMiddleware } from './common/middlewares/logger.middleware'; @Module({ - imports: [], + imports: [AuthModule], controllers: [AppController], providers: [AppService], }) -export class AppModule {} +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(LoggerMiddleware).forRoutes('*'); + } +} diff --git a/src/auth/auth-fortytwo.service.ts b/src/auth/auth-fortytwo.service.ts new file mode 100644 index 0000000..acd7e9b --- /dev/null +++ b/src/auth/auth-fortytwo.service.ts @@ -0,0 +1,52 @@ +import { Injectable, Logger, UnauthorizedException } from '@nestjs/common'; +import axios from 'axios'; +import { FortyTwoUserDto } from './dto/fortytwo-user.dto'; + +@Injectable() +export class AuthFortyTwoService { + private readonly logger = new Logger(AuthFortyTwoService.name); + private readonly baseUrl = 'https://api.intra.42.fr'; + + async getAccessToken(code: string): Promise { + try { + const response = await axios.post( + `${this.baseUrl}/oauth/token`, + { + grant_type: 'authorization_code', + client_id: process.env.FORTYTWO_CLIENT_ID, + client_secret: process.env.FORTYTWO_CLIEND_SECRET, + code, + redirect_uri: process.env.FORTYTWO_REDIRECT_URI, + }, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + return response.data.access_token; + } catch (error) { + this.logger.error(error); + throw new UnauthorizedException('Invalid code'); + } + } + + async getUserData(accessToken: string): Promise { + try { + const response = await axios.get(`${this.baseUrl}/v2/me`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + const userData: FortyTwoUserDto = { + nickname: response.data.login, + email: response.data.email, + }; + return userData; + } catch (error) { + this.logger.error(error); + throw new UnauthorizedException('Invalid access token'); + } + } +} diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts new file mode 100644 index 0000000..58dee31 --- /dev/null +++ b/src/auth/auth.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; + +describe('AuthController', () => { + let controller: AuthController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + providers: [AuthService], + }).compile(); + + controller = module.get(AuthController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts new file mode 100644 index 0000000..e2f4e8b --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,25 @@ +import { Body, Controller, Post } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { AuthFortyTwoService } from './auth-fortytwo.service'; +import { FortyTwoUserDto } from './dto/fortytwo-user.dto'; + +@Controller('auth') +export class AuthController { + constructor( + private readonly authService: AuthService, + private readonly authFortyTwoService: AuthFortyTwoService, + ) {} + + @Post('signin') + async signin(@Body() code: string) { + // code를 이용해 access token을 받아온다. + const fortyTwoAccessToken = + await this.authFortyTwoService.getAccessToken(code); + // access token을 이용해 42API에서 유저 정보를 받아온다. + const fortyTwoUserDto: FortyTwoUserDto = + await this.authFortyTwoService.getUserData(fortyTwoAccessToken); + + // 유저 정보를 이용해 유저를 찾는다. + const user = await this.authService.validateUser(fortyTwoUserDto); + } +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..f144104 --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; +import { AuthFortyTwoService } from './auth-fortytwo.service'; + +@Module({ + controllers: [AuthController], + providers: [AuthService, AuthFortyTwoService], +}) +export class AuthModule {} diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts new file mode 100644 index 0000000..800ab66 --- /dev/null +++ b/src/auth/auth.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthService], + }).compile(); + + service = module.get(AuthService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts new file mode 100644 index 0000000..8f3c0f5 --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,15 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { FortyTwoUserDto } from './dto/fortytwo-user.dto'; + +@Injectable() +export class AuthService { + private readonly logger = new Logger(AuthService.name); + + // constructor(private readonly connection: Connection) {} + + async validateUser(userData: FortyTwoUserDto) { + // const queryRunner = this.connection.createQueryRunner(); + // 유저 정보를 이용해 유저를 찾는다. + // const user = await this. + } +} diff --git a/src/auth/dto/create-auth.dto.ts b/src/auth/dto/create-auth.dto.ts new file mode 100644 index 0000000..00ef00f --- /dev/null +++ b/src/auth/dto/create-auth.dto.ts @@ -0,0 +1 @@ +export class CreateAuthDto {} diff --git a/src/auth/dto/fortytwo-user.dto.ts b/src/auth/dto/fortytwo-user.dto.ts new file mode 100644 index 0000000..cca3dd1 --- /dev/null +++ b/src/auth/dto/fortytwo-user.dto.ts @@ -0,0 +1,4 @@ +export class FortyTwoUserDto { + nickname: string; + email: string; +} diff --git a/src/auth/dto/update-auth.dto.ts b/src/auth/dto/update-auth.dto.ts new file mode 100644 index 0000000..100de4f --- /dev/null +++ b/src/auth/dto/update-auth.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateAuthDto } from './create-auth.dto'; + +export class UpdateAuthDto extends PartialType(CreateAuthDto) {} diff --git a/src/auth/entities/auth.entity.ts b/src/auth/entities/auth.entity.ts new file mode 100644 index 0000000..15f15a8 --- /dev/null +++ b/src/auth/entities/auth.entity.ts @@ -0,0 +1 @@ +export class Auth {} diff --git a/src/common/interceptors/logger.interceptor.ts b/src/common/interceptors/logger.interceptor.ts new file mode 100644 index 0000000..cd48671 --- /dev/null +++ b/src/common/interceptors/logger.interceptor.ts @@ -0,0 +1,20 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + console.log('Before...'); + + const now = Date.now(); + return next + .handle() + .pipe(tap(() => console.log(`After... ${Date.now() - now}ms`))); + } +} diff --git a/src/common/interceptors/transform.interceptor.ts b/src/common/interceptors/transform.interceptor.ts new file mode 100644 index 0000000..2f6daa8 --- /dev/null +++ b/src/common/interceptors/transform.interceptor.ts @@ -0,0 +1,29 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +export interface Response { + data: T; +} + +@Injectable() +export class TransformInterceptor + implements NestInterceptor> +{ + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable> { + return next.handle().pipe( + map((data) => ({ + success: true, + data, + })), + ); + } +} diff --git a/src/common/middlewares/logger.middleware.ts b/src/common/middlewares/logger.middleware.ts new file mode 100644 index 0000000..27fec9d --- /dev/null +++ b/src/common/middlewares/logger.middleware.ts @@ -0,0 +1,18 @@ +import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; +import { NextFunction } from 'express'; + +@Injectable() +export class LoggerMiddleware implements NestMiddleware { + private logger = new Logger('HTTP'); + + use(req: any, res: any, next: NextFunction) { + res.on('finish', () => { + this.logger.log( + `${req.ip} ${req.method} ${res.statusCode}`, + req.originalUrl, + ); + }); + + next(); + } +}