diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 83ee857..ee7270a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,6 +12,7 @@ model User { email String @unique name String nickname String + password String? auth_provider String profile_url String? role_id Int diff --git a/src/chat/chat.module.ts b/src/chat/chat.module.ts index 9fef970..261283e 100644 --- a/src/chat/chat.module.ts +++ b/src/chat/chat.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { ChatService } from './chat.service'; import { PrismaModule } from '@src/prisma/prisma.module'; -import { AuthModule } from '@modules/auth/auth.module'; +import { AuthModule } from '@modules/auth/auth.module'; // AuthModule 추가 @Module({ imports: [PrismaModule, AuthModule], diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 54c9525..945ded4 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -8,13 +8,13 @@ import { Put, HttpException, Res, + HttpStatus, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { AuthService } from '@src/modules/auth/auth.service'; import { JwtAuthGuard } from '@src/modules/auth/guards/jwt-auth.guard'; import { JwtService } from '@nestjs/jwt'; import { ApiResponse } from '@common/dto/response.dto'; -import { ErrorMessages } from '@common/constants/error-messages'; import { HttpStatusCodes } from '@common/constants/http-status-code'; import { Response } from 'express'; @Controller('auth') @@ -35,30 +35,18 @@ export class AuthController { // Authorization Code 교환 및 사용자 정보 가져오기 const { user, accessToken, refreshToken, isExistingUser } = await this.authService.handleGoogleCallback(code); - console.log('accessToken: ' + accessToken); - console.log('refreshToken: ' + refreshToken); - // 리프레시 토큰을 HTTP-Only 쿠키로 설정 - res.cookie('refreshToken', refreshToken, { - httpOnly: false, - secure: false, - sameSite: 'none', - path: '/', - maxAge: 7 * 24 * 60 * 60 * 1000, - }); - - return res.status(HttpStatusCodes.OK).json( - new ApiResponse(HttpStatusCodes.OK, 'Google 로그인 성공', { - user, - accessToken, - refreshToken, - isExistingUser, - }) - ); - // return new ApiResponse(HttpStatusCodes.OK, 'Google 로그인 성공', { - // user, - // accessToken, - // isExistingUser, - // }); + console.log('refreshToken: ', refreshToken); + const response = { + message: { + code: 200, + message: 'Google 로그인 성공', + }, + user, + accessToken, + isExistingUser, + }; + console.log(response); + return res.status(HttpStatusCodes.OK).json(response); } @Get('github') @@ -72,22 +60,24 @@ export class AuthController { // Authorization Code 교환 및 사용자 정보 가져오기 const { user, accessToken, refreshToken, isExistingUser } = await this.authService.handleGithubCallback(code); - - // 리프레시 토큰을 HTTP-Only 쿠키로 설정 - res.cookie('refreshToken', refreshToken, { - httpOnly: true, - secure: true, - sameSite: 'strict', // CSRF 방지 - maxAge: 7 * 24 * 60 * 60, // 7일 - }); - - return new ApiResponse(HttpStatusCodes.OK, 'Google 로그인 성공', { + console.log(refreshToken); + const response = { + message: { + code: 200, + message: 'Github 로그인 성공', + }, user, accessToken, isExistingUser, - }); + }; + console.log(response); + return res.status(HttpStatusCodes.OK).json(response); } + // @Post('login') + // async login( + // @Body() + // ) // Role 선택 API @Put('roleselect') @UseGuards(JwtAuthGuard) @@ -110,131 +100,66 @@ export class AuthController { return res.status(HttpStatusCodes.OK).json(responseBody); } - // @Post('refresh') - // async refreshAccessToken(@Req() req: any, @Res() res: Response) { - // console.log('Refresh Token API 호출'); - // const refreshToken = req.cookies['refreshToken']; - // - // if (!refreshToken) { - // throw new HttpException( - // ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.text, - // ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.code - // ); - // } - // //const userId = req.user?.user_id; - // const userId = this.authService.getUserIdFromRefreshToken(refreshToken); - // console.log('user_id: ' + userId); - // const isValid = await this.authService.validateRefreshToken( - // userId, - // refreshToken - // ); - // if (!isValid) { - // const error = ErrorMessages.AUTH.INVALID_REFRESH_TOKEN; - // throw new HttpException(error.text, error.code); - // } - // - // const newAccessToken = this.authService.generateAccessToken(userId); - // const newRefreshToken = this.authService.generateRefreshToken(userId); - // res.cookie('refreshToken', newRefreshToken, { - // httpOnly: true, - // secure: false, - // sameSite: 'none', - // maxAge: 7 * 24 * 60 * 60 * 1000, - // }); - // return res.status(HttpStatusCodes.OK).json( - // new ApiResponse( - // HttpStatusCodes.OK, - // '액세스 토큰이 성공적으로 갱신되었습니다.', - // { - // accessToken: newAccessToken, - // } - // ) - // ); - // } - @Post('refresh') - async refreshAccessToken(@Req() req: any, @Res() res: Response) { - console.log('Refresh Token API 호출'); - const refreshToken = req.cookies['refreshToken']; - console.log('refreshToken from Cookie: ' + refreshToken); - // 1. Refresh Token이 없는 경우 예외 처리 - if (!refreshToken) { - console.error('리프레쉬 토큰 없음'); - throw new HttpException( - ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.text, - ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.code - ); + async refreshAccessToken( + @Body('user_id') userId: number, // user_id는 숫자로 받음 + @Res() res: Response + ) { + if (userId === undefined || userId === null) { + throw new HttpException('User ID is required', HttpStatus.BAD_REQUEST); } - console.log('요청된 Refresh Token:', refreshToken); - + console.log(userId); try { - // 2. Refresh Token에서 User ID 추출 - const userId = this.authService.getUserIdFromRefreshToken(refreshToken); - if (!userId) { - console.error('Refresh Token으로부터 User ID 추출 실패'); - throw new HttpException( - ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.text, - ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.code - ); - } - console.log('추출된 User ID:', userId); - - // 3. Refresh Token 유효성 검증 - const isValid = await this.authService.validateRefreshToken( - userId, - refreshToken - ); - console.log('Refresh Token 유효성 검증 결과:', isValid); - - if (!isValid) { - console.error('Refresh Token이 Redis와 일치하지 않음'); - throw new HttpException( - ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.text, - ErrorMessages.AUTH.INVALID_REFRESH_TOKEN.code - ); - } - - // 4. 새로운 Access Token 및 Refresh Token 생성 - const newAccessToken = this.authService.generateAccessToken(userId); - const newRefreshToken = this.authService.generateRefreshToken(userId); - - console.log('새로운 Access Token:', newAccessToken); - console.log('새로운 Refresh Token:', newRefreshToken); - - // 5. Redis에 새로운 Refresh Token 저장 - await this.authService.storeRefreshToken(userId, newRefreshToken); - - // 6. Refresh Token을 쿠키에 저장 - res.cookie('refreshToken', refreshToken, { - httpOnly: false, - secure: false, - sameSite: 'none', - path: '/', - maxAge: 7 * 24 * 60 * 60 * 1000, - }); - - return res.status(HttpStatusCodes.OK).json( - new ApiResponse( - HttpStatusCodes.OK, - '액세스 토큰이 성공적으로 갱신되었습니다.', - { - accessToken: newAccessToken, - } - ) - ); + // 리프레쉬 토큰 확인 및 액세스 토큰 재발급 + const newAccessToken = await this.authService.renewAccessToken(userId); + + // 성공 메시지와 새로운 액세스 토큰 반환 + const response = { + message: { + code: 200, + text: 'Access token이 성공적으로 갱신 되었습니다.', + }, + access_token: newAccessToken, + }; + + return res.status(200).json(response); } catch (error) { - console.error('Refresh Token 갱신 중 오류 발생:', error.message); - return res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({ - message: 'Internal Server Error', - }); + //throw new HttpException(error.message, HttpStatus.UNAUTHORIZED); + console.log(error); } } - @Post('logout') + @UseGuards(JwtAuthGuard) async logout(@Req() req: any, @Res() res: Response) { - res.clearCookie('refreshToken'); // HTTP-Only 쿠키 삭제 + const userId = req.user?.user_id; + await this.authService.deleteRefreshToken(userId); return res .status(HttpStatusCodes.OK) .json(new ApiResponse(HttpStatusCodes.OK, '로그아웃 성공')); } + + @Post('signup') + async signup( + @Body() body: { email: string; nickname: string; password: string } + ) { + const result = await this.authService.signup( + body.email, + body.nickname, + body.password + ); + return { + message: 'Signup successful', + user: result, + }; + } + + @Post('login') + async login(@Body() body: { email: string; password: string }) { + const result = await this.authService.login(body.email, body.password); + return { + message: 'Login successful', + user: result.user, + access_token: result.accessToken, + }; + } } diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index f53f3d8..01a067d 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,4 +1,4 @@ -import { Injectable, HttpException } from '@nestjs/common'; +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { RedisService } from '@modules/redis/redis.service'; import { PrismaService } from '@src/prisma/prisma.service'; @@ -124,16 +124,17 @@ export class AuthService { auth_provider: 'github', }); - const jwt = this.generateAccessToken(user.id); - const refreshToken = await this.generateRefreshToken(user.id); // 리프레시 토큰 생성 + const accessToken = this.generateAccessToken(user.id); + const refreshToken = this.generateRefreshToken(user.id); // 리프레시 토큰 생성 // Redis에 리프레시 토큰 저장 await this.storeRefreshToken(user.id, refreshToken); const responseUser = this.filterUserFields(user); + return { user: responseUser, - accessToken: jwt, - refreshToken: refreshToken, // 리프레시 토큰 반환 + accessToken, + refreshToken, isExistingUser, }; } catch (error) { @@ -152,10 +153,18 @@ export class AuthService { } // 사용자 찾기 또는 생성 async findOrCreateUser(profile: AuthUserDto) { - return this.prisma.user.upsert({ + // 이메일로 유저 찾기 + const existingUser = await this.prisma.user.findUnique({ where: { email: profile.email }, - update: {}, // 이미 존재하면 아무것도 업데이트하지 않음 - create: { + }); + + if (existingUser) { + return existingUser; // 기존 유저 반환 + } + + // 새 유저 생성 + return this.prisma.user.create({ + data: { email: profile.email, name: profile.name, nickname: profile.nickname, @@ -170,6 +179,61 @@ export class AuthService { }); } + // 회원가입 로직 + async signup(email: string, nickname: string, password: string) { + // 이메일 중복 확인 + const existingUser = await this.prisma.user.findUnique({ + where: { email }, + }); + if (existingUser) { + throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST); + } + + // 새로운 사용자 생성 + const newUser = await this.prisma.user.create({ + data: { + email, + name: nickname, + nickname, + password, + auth_provider: 'pad', // 소셜 로그인과 구분 + role: { connect: { id: 1 } }, + status: { connect: { id: 1 } }, + }, + }); + + return { + user_id: newUser.id, + email: newUser.email, + nickname: newUser.nickname, + }; + } + + // 로그인 로직 + async login(email: string, password: string) { + // 사용자 조회 + const user = await this.prisma.user.findUnique({ + where: { email }, + }); + if (!user || user.password !== password) { + throw new HttpException( + 'Invalid email or password', + HttpStatus.UNAUTHORIZED + ); + } + + // 액세스 토큰 및 리프레시 토큰 생성 + const accessToken = this.generateAccessToken(user.id); + const refreshToken = this.generateRefreshToken(user.id); + + // Redis에 리프레시 토큰 저장 + await this.storeRefreshToken(user.id, refreshToken); + const responseUser = this.filterUserFields(user); + return { + user: responseUser, + accessToken, + }; + } private filterUserFields(user: any) { return { user_id: user.id, @@ -186,7 +250,7 @@ export class AuthService { console.log(`Access Token 생성: userId=${userId}`); return this.jwtService.sign( { userId }, - { expiresIn: '1m', secret: process.env.ACCESS_TOKEN_SECRET } + { expiresIn: '7d', secret: process.env.ACCESS_TOKEN_SECRET } ); } @@ -275,4 +339,50 @@ export class AuthService { }; return result; } + + async renewAccessToken(userId: number): Promise { + const redisKey = `refresh_token:${userId}`; + const refreshToken = await this.redisService.get(redisKey); + + if (!refreshToken) { + console.error(`No refresh token found for Redis key: ${redisKey}`); + throw new Error('Refresh token not found for user'); + } + + console.log(`Retrieved refresh token from Redis: ${refreshToken}`); + + try { + const payload = this.jwtService.verify(refreshToken, { + secret: process.env.REFRESH_TOKEN_SECRET, + algorithms: ['HS256'], // 생성 시와 동일한 알고리즘 + }); + console.log(`Decoded payload:`, payload); + + if (payload.userId !== userId) { + console.error( + `Token userId mismatch. Expected: ${userId}, Got: ${payload.userId}` + ); + throw new Error('Invalid refresh token'); + } + } catch (error) { + if (error.name === 'TokenExpiredError') { + throw new HttpException( + 'Refresh token has expired', + HttpStatus.BAD_REQUEST + ); + } + throw new HttpException( + 'Refresh token validation failed', + HttpStatus.UNAUTHORIZED + ); + } + + const newAccessToken = this.jwtService.sign( + { userId }, + { expiresIn: '15m', secret: process.env.ACCESS_TOKEN_SECRET } + ); + + console.log(`Generated new access token: ${newAccessToken}`); + return newAccessToken; + } } diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts index ac9ef55..f2a9816 100644 --- a/src/modules/auth/strategies/jwt.strategy.ts +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -12,7 +12,6 @@ export class JwtStrategy extends PassportStrategy(Strategy) { } async validate(payload: any) { - console.log('JWT_payload: ', payload); // req.user에 설정될 사용자 정보 반환 return { user_id: payload.userId, email: payload.email }; }