로그로그

This commit is contained in:
2025-12-15 09:55:43 +09:00
parent ef5f921e21
commit 6b8ea7e74c
7 changed files with 429 additions and 352 deletions

View File

@@ -5,6 +5,14 @@ WORKDIR /app
# 필요한 패키지 설치 # 필요한 패키지 설치
RUN apk add --no-cache curl RUN apk add --no-cache curl
# Node.js 로그 버퍼링 비활성화 (Docker에서 실시간 로그 출력)
ENV NODE_ENV=production
ENV NODE_OPTIONS="--enable-source-maps"
# stdout/stderr 버퍼링 비활성화
ENV PYTHONUNBUFFERED=1
ENV NODE_NO_WARNINGS=1
# package.json 복사 # package.json 복사
COPY package*.json ./ COPY package*.json ./
@@ -20,5 +28,5 @@ RUN npm run build
# 포트 노출 # 포트 노출
EXPOSE 4000 EXPOSE 4000
# 애플리케이션 실행 # 애플리케이션 실행 (unbuffered stdout)
CMD ["npm", "run", "start:prod"] CMD ["node", "--enable-source-maps", "dist/main.js"]

View File

@@ -3,6 +3,7 @@ import {
Injectable, Injectable,
NotFoundException, NotFoundException,
UnauthorizedException, UnauthorizedException,
Logger,
} from '@nestjs/common'; } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { UserModel } from '../user/entities/user.entity'; import { UserModel } from '../user/entities/user.entity';
@@ -30,6 +31,8 @@ import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
private readonly logger = new Logger(AuthService.name);
constructor( constructor(
@InjectRepository(UserModel) @InjectRepository(UserModel)
private readonly userRepository: Repository<UserModel>, private readonly userRepository: Repository<UserModel>,
@@ -41,57 +44,49 @@ export class AuthService {
/** /**
* 유저 로그인 * 유저 로그인
*
* @async
* @param {LoginDto} loginDto
* @returns {Promise<LoginResponseDto>}
*/ */
async login(loginDto: LoginDto): Promise<LoginResponseDto> { async login(loginDto: LoginDto): Promise<LoginResponseDto> {
const { userId, userPassword } = loginDto; const { userId, userPassword } = loginDto;
this.logger.log(`[LOGIN] 로그인 시도 - userId: ${userId}`);
// 1. userId로 유저 찾기
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({
where: { userId }, where: { userId },
}); });
// 2. user 없으면 에러
if (!user) {
throw new UnauthorizedException('아이디 또는 비밀번호가 틀렸습니다'); //HTTP 401 상태 코드 예외
}
// 3. 비밀번호 비교 (bcrypt)
const tempHash = await bcrypt.hash(userPassword, 10);
console.log('=========input password bcrypt hash========:', tempHash);
const isPasswordValid = await bcrypt.compare(userPassword, user.userPw); if (!user) {
if (!isPasswordValid) { this.logger.warn(`[LOGIN] 사용자 없음 - userId: ${userId}`);
throw new UnauthorizedException('아이디 또는 비밀번호가 틀렸습니다');
}
const isPasswordValid = await bcrypt.compare(userPassword, user.userPw);
if (!isPasswordValid) {
this.logger.warn(`[LOGIN] 비밀번호 불일치 - userId: ${userId}`);
throw new UnauthorizedException('아이디 또는 비밀번호가 틀렸습니다'); throw new UnauthorizedException('아이디 또는 비밀번호가 틀렸습니다');
} }
// 4. 탈퇴 여부 확인
if (user.delDt !== null) { if (user.delDt !== null) {
this.logger.warn(`[LOGIN] 탈퇴 계정 - userId: ${userId}`);
throw new UnauthorizedException('탈퇴한 계정입니다'); throw new UnauthorizedException('탈퇴한 계정입니다');
} }
// 6. JWT 토큰 생성
const payload = { const payload = {
userId: user.userId, userId: user.userId,
userNo: user.pkUserNo, userNo: user.pkUserNo,
}; };
// Access Token 생성 (기본 설정 사용)
const accessToken = this.jwtService.sign(payload as any); const accessToken = this.jwtService.sign(payload as any);
// Refresh Token 생성 (별도 secret과 만료시간 사용)
const refreshOptions = { const refreshOptions = {
secret: this.configService.get<string>('JWT_REFRESH_SECRET')!, secret: this.configService.get<string>('JWT_REFRESH_SECRET')!,
expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRES_IN') || '7d', expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRES_IN') || '7d',
}; };
const refreshToken = this.jwtService.sign(payload as any, refreshOptions as any); const refreshToken = this.jwtService.sign(payload as any, refreshOptions as any);
// 7. 로그인 응답 생성 (LoginResponseDto) this.logger.log(`[LOGIN] 로그인 성공 - userId: ${userId}`);
return { return {
message: '로그인 성공', message: '로그인 성공',
accessToken, // JWT 토큰 추가 accessToken,
refreshToken, // JWT 토큰 추가 refreshToken,
user: { user: {
pkUserNo: user.pkUserNo, pkUserNo: user.pkUserNo,
userId: user.userId, userId: user.userId,
@@ -104,32 +99,27 @@ export class AuthService {
/** /**
* 회원가입 * 회원가입
*
* @async
* @param {SignupDto} signupDto
* @returns {Promise<SignupResponseDto>}
*/ */
async register(signupDto: SignupDto, clientIp: string): Promise<SignupResponseDto> { async register(signupDto: SignupDto, clientIp: string): Promise<SignupResponseDto> {
const { userId, userEmail, userPhone } = signupDto; const { userId, userEmail, userPhone } = signupDto;
this.logger.log(`[REGISTER] 회원가입 시도 - userId: ${userId}, email: ${userEmail}`);
// 0. 이메일 인증 확인 (Redis에 인증 완료 여부 체크)
const verifiedKey = `signup-verified:${userEmail}`; const verifiedKey = `signup-verified:${userEmail}`;
const isEmailVerified = await this.verificationService.verifyCode(verifiedKey, 'true'); const isEmailVerified = await this.verificationService.verifyCode(verifiedKey, 'true');
if (!isEmailVerified) { if (!isEmailVerified) {
this.logger.warn(`[REGISTER] 이메일 미인증 - email: ${userEmail}`);
throw new UnauthorizedException('이메일 인증이 완료되지 않았습니다'); throw new UnauthorizedException('이메일 인증이 완료되지 않았습니다');
} }
// 1. 중복 체크 (ID, 이메일, 전화번호, 사업자번호)
const whereConditions = [{ userId }, { userEmail }, { userPhone }]; const whereConditions = [{ userId }, { userEmail }, { userPhone }];
const existingUser = await this.userRepository.findOne({ const existingUser = await this.userRepository.findOne({
where: whereConditions, where: whereConditions,
}); });
if (existingUser) { if (existingUser) {
if (existingUser.userId === userId) { if (existingUser.userId === userId) {
throw new ConflictException('이미 사용 중인 아이디입니다'); //HTTP 409 상태 코드 예외 throw new ConflictException('이미 사용 중인 아이디입니다');
} }
if (existingUser.userEmail === userEmail) { if (existingUser.userEmail === userEmail) {
throw new ConflictException('이미 사용 중인 이메일입니다'); throw new ConflictException('이미 사용 중인 이메일입니다');
@@ -137,28 +127,24 @@ export class AuthService {
if (existingUser.userPhone === userPhone) { if (existingUser.userPhone === userPhone) {
throw new ConflictException('이미 사용 중인 전화번호입니다'); throw new ConflictException('이미 사용 중인 전화번호입니다');
} }
} }
// 2. 비밀번호 해싱 (bcrypt)
const saltRounds = parseInt(this.configService.get<string>('BCRYPT_SALT_ROUNDS') || '10', 10); const saltRounds = parseInt(this.configService.get<string>('BCRYPT_SALT_ROUNDS') || '10', 10);
const hashedPassword = await bcrypt.hash(signupDto.userPassword, saltRounds); const hashedPassword = await bcrypt.hash(signupDto.userPassword, saltRounds);
// 3. 사용자 생성
const newUser = this.userRepository.create({ const newUser = this.userRepository.create({
userId: signupDto.userId, userId: signupDto.userId,
userPw: hashedPassword, userPw: hashedPassword,
userName: signupDto.userName, userName: signupDto.userName,
userPhone: signupDto.userPhone, userPhone: signupDto.userPhone,
userEmail: signupDto.userEmail, userEmail: signupDto.userEmail,
regIp: clientIp,
regIp: clientIp, // 등록 ip
regUserId: signupDto.userId, regUserId: signupDto.userId,
}); });
// 4. DB에 저장
const savedUser = await this.userRepository.save(newUser); const savedUser = await this.userRepository.save(newUser);
this.logger.log(`[REGISTER] 회원가입 성공 - userId: ${savedUser.userId}`);
// 5. 응답 구조 생성 (SignupResponseDto 반환)
return { return {
message: '회원가입이 완료되었습니다', message: '회원가입이 완료되었습니다',
redirectUrl: '/dashboard', redirectUrl: '/dashboard',
@@ -169,10 +155,6 @@ export class AuthService {
/** /**
* 이메일 중복 체크 * 이메일 중복 체크
*
* @async
* @param {string} userEmail
* @returns {Promise<{ available: boolean; message: string }>}
*/ */
async checkEmailDuplicate(userEmail: string): Promise<{ async checkEmailDuplicate(userEmail: string): Promise<{
available: boolean; available: boolean;
@@ -197,10 +179,6 @@ export class AuthService {
/** /**
* 회원가입 - 이메일 인증번호 발송 * 회원가입 - 이메일 인증번호 발송
*
* @async
* @param {SendSignupCodeDto} dto
* @returns {Promise<{ success: boolean; message: string; expiresIn: number }>}
*/ */
async sendSignupCode(dto: SendSignupCodeDto): Promise<{ async sendSignupCode(dto: SendSignupCodeDto): Promise<{
success: boolean; success: boolean;
@@ -208,42 +186,60 @@ export class AuthService {
expiresIn: number; expiresIn: number;
}> { }> {
const { userEmail } = dto; const { userEmail } = dto;
this.logger.log(`[SEND-CODE] ========== 인증번호 발송 시작 ==========`);
this.logger.log(`[SEND-CODE] 이메일: ${userEmail}`);
process.stdout.write(`[SEND-CODE] 이메일: ${userEmail}\n`);
// 1. 이메일 중복 체크 try {
const existingUser = await this.userRepository.findOne({ // 1. 이메일 중복 체크
where: { userEmail }, this.logger.log(`[SEND-CODE] Step 1: 이메일 중복 체크`);
}); const existingUser = await this.userRepository.findOne({
where: { userEmail },
});
if (existingUser) { if (existingUser) {
throw new ConflictException('이미 사용 중인 이메일입니다'); this.logger.warn(`[SEND-CODE] 이메일 중복 - ${userEmail}`);
throw new ConflictException('이미 사용 중인 이메일입니다');
}
this.logger.log(`[SEND-CODE] Step 1 완료: 중복 없음`);
// 2. 인증번호 생성
this.logger.log(`[SEND-CODE] Step 2: 인증번호 생성`);
const code = this.verificationService.generateCode();
this.logger.log(`[SEND-CODE] 생성된 인증번호: ${code}`);
process.stdout.write(`[SEND-CODE] 생성된 인증번호: ${code}\n`);
// 3. Redis에 저장
this.logger.log(`[SEND-CODE] Step 3: Redis 저장`);
const key = `signup:${userEmail}`;
await this.verificationService.saveCode(key, code);
this.logger.log(`[SEND-CODE] Redis 저장 완료 - key: ${key}`);
// 4. 이메일 발송
this.logger.log(`[SEND-CODE] Step 4: 이메일 발송 시작`);
await this.emailService.sendVerificationCode(userEmail, code);
this.logger.log(`[SEND-CODE] 이메일 발송 완료`);
this.logger.log(`[SEND-CODE] ========== 인증번호 발송 성공 ==========`);
return {
success: true,
message: '인증번호가 이메일로 발송되었습니다',
expiresIn: VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS,
};
} catch (error) {
this.logger.error(`[SEND-CODE] ========== 에러 발생 ==========`);
this.logger.error(`[SEND-CODE] Error Name: ${error.name}`);
this.logger.error(`[SEND-CODE] Error Message: ${error.message}`);
this.logger.error(`[SEND-CODE] Stack: ${error.stack}`);
process.stdout.write(`[SEND-CODE] ERROR: ${error.message}\n`);
process.stdout.write(`[SEND-CODE] STACK: ${error.stack}\n`);
throw error;
} }
// 2. 인증번호 생성
const code = this.verificationService.generateCode();
console.log(`[DEBUG] Generated code for ${userEmail}: ${code}`);
// 3. Redis에 저장 (key: signup:이메일)
const key = `signup:${userEmail}`;
await this.verificationService.saveCode(key, code);
console.log(`[DEBUG] Saved code to Redis with key: ${key}`);
// 4. 이메일 발송
await this.emailService.sendVerificationCode(userEmail, code);
console.log(`[DEBUG] Email sent to: ${userEmail}`);
return {
success: true,
message: '인증번호가 이메일로 발송되었습니다',
expiresIn: VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS,
};
} }
/** /**
* 회원가입 - 이메일 인증번호 검증 * 회원가입 - 이메일 인증번호 검증
*
* @async
* @param {VerifySignupCodeDto} dto
* @returns {Promise<{ success: boolean; message: string; verified: boolean }>}
*/ */
async verifySignupCode(dto: VerifySignupCodeDto): Promise<{ async verifySignupCode(dto: VerifySignupCodeDto): Promise<{
success: boolean; success: boolean;
@@ -251,21 +247,21 @@ export class AuthService {
verified: boolean; verified: boolean;
}> { }> {
const { userEmail, code } = dto; const { userEmail, code } = dto;
console.log(`[DEBUG] Verifying code for ${userEmail}: ${code}`); this.logger.log(`[VERIFY-CODE] 인증번호 검증 - email: ${userEmail}`);
// Redis에서 검증
const key = `signup:${userEmail}`; const key = `signup:${userEmail}`;
const isValid = await this.verificationService.verifyCode(key, code); const isValid = await this.verificationService.verifyCode(key, code);
console.log(`[DEBUG] Verification result: ${isValid}`);
if (!isValid) { if (!isValid) {
this.logger.warn(`[VERIFY-CODE] 인증 실패 - email: ${userEmail}`);
throw new UnauthorizedException('인증번호가 일치하지 않거나 만료되었습니다'); throw new UnauthorizedException('인증번호가 일치하지 않거나 만료되었습니다');
} }
// 검증 완료 표시 (5분간 유효)
const verifiedKey = `signup-verified:${userEmail}`; const verifiedKey = `signup-verified:${userEmail}`;
await this.verificationService.saveCode(verifiedKey, 'true'); await this.verificationService.saveCode(verifiedKey, 'true');
this.logger.log(`[VERIFY-CODE] 인증 성공 - email: ${userEmail}`);
return { return {
success: true, success: true,
message: '이메일 인증이 완료되었습니다', message: '이메일 인증이 완료되었습니다',
@@ -274,19 +270,14 @@ export class AuthService {
} }
/** /**
* 아이디 찾기 - 인증번호 발송 (이메일 인증) * 아이디 찾기 - 인증번호 발송
*
* @async
* @param {SendFindIdCodeDto} dto
* @returns {Promise<{ success: boolean; message: string; expiresIn: number }>}
*/ */
async sendFindIdCode( async sendFindIdCode(
dto: SendFindIdCodeDto, dto: SendFindIdCodeDto,
): Promise<{ success: boolean; message: string; expiresIn: number }> { ): Promise<{ success: boolean; message: string; expiresIn: number }> {
const { userName, userEmail } = dto; const { userName, userEmail } = dto;
console.log(`[아이디 찾기] 인증번호 발송 요청 - 이름: ${userName}, 이메일: ${userEmail}`); this.logger.log(`[FIND-ID] 인증번호 발송 - name: ${userName}, email: ${userEmail}`);
// 1. 사용자 확인
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({
where: { userName, userEmail }, where: { userName, userEmail },
}); });
@@ -295,18 +286,12 @@ export class AuthService {
throw new NotFoundException('일치하는 사용자 정보를 찾을 수 없습니다'); throw new NotFoundException('일치하는 사용자 정보를 찾을 수 없습니다');
} }
// 2. 인증번호 생성
const code = this.verificationService.generateCode(); const code = this.verificationService.generateCode();
console.log(`[아이디 찾기] 생성된 인증번호: ${code} (이메일: ${userEmail})`);
// 3. Redis에 저장 (key: find-id:이메일)
const key = `find-id:${userEmail}`; const key = `find-id:${userEmail}`;
await this.verificationService.saveCode(key, code); await this.verificationService.saveCode(key, code);
console.log(`[아이디 찾기] Redis 저장 완료 - Key: ${key}`);
// 4. 이메일 발송
await this.emailService.sendVerificationCode(userEmail, code); await this.emailService.sendVerificationCode(userEmail, code);
console.log(`[아이디 찾기] 이메일 발송 완료 - 수신자: ${userEmail}`);
this.logger.log(`[FIND-ID] 인증번호 발송 완료 - email: ${userEmail}`);
return { return {
success: true, success: true,
@@ -317,26 +302,18 @@ export class AuthService {
/** /**
* 아이디 찾기 - 인증번호 검증 * 아이디 찾기 - 인증번호 검증
*
* @async
* @param {VerifyFindIdCodeDto} dto
* @returns {Promise<FindIdResponseDto>}
*/ */
async verifyFindIdCode(dto: VerifyFindIdCodeDto): Promise<FindIdResponseDto> { async verifyFindIdCode(dto: VerifyFindIdCodeDto): Promise<FindIdResponseDto> {
const { userEmail, verificationCode } = dto; const { userEmail, verificationCode } = dto;
console.log(`[아이디 찾기] 인증번호 검증 요청 - 이메일: ${userEmail}, 입력 코드: ${verificationCode}`); this.logger.log(`[FIND-ID] 인증번호 검증 - email: ${userEmail}`);
// 1. 인증번호 검증
const key = `find-id:${userEmail}`; const key = `find-id:${userEmail}`;
console.log(`[아이디 찾기] 검증 Key: ${key}`);
const isValid = await this.verificationService.verifyCode(key, verificationCode); const isValid = await this.verificationService.verifyCode(key, verificationCode);
console.log(`[아이디 찾기] 검증 결과: ${isValid}`);
if (!isValid) { if (!isValid) {
throw new UnauthorizedException('인증번호가 일치하지 않거나 만료되었습니다'); throw new UnauthorizedException('인증번호가 일치하지 않거나 만료되었습니다');
} }
// 2. 사용자 정보 조회
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({
where: { userEmail }, where: { userEmail },
}); });
@@ -345,7 +322,6 @@ export class AuthService {
throw new NotFoundException('사용자를 찾을 수 없습니다'); throw new NotFoundException('사용자를 찾을 수 없습니다');
} }
// 3. 아이디 마스킹
const maskedUserId = this.maskUserId(user.userId); const maskedUserId = this.maskUserId(user.userId);
return { return {
@@ -355,13 +331,6 @@ export class AuthService {
}; };
} }
/**
* 아이디 마스킹 (앞 4자리만 표시)
*
* @private
* @param {string} userId
* @returns {string}
*/
private maskUserId(userId: string): string { private maskUserId(userId: string): string {
if (userId.length <= 4) { if (userId.length <= 4) {
return userId; return userId;
@@ -372,19 +341,14 @@ export class AuthService {
} }
/** /**
* 비밀번호 재설정 - 인증번호 발송 (이메일 인증) * 비밀번호 재설정 - 인증번호 발송
*
* @async
* @param {SendResetPasswordCodeDto} dto
* @returns {Promise<{ success: boolean; message: string; expiresIn: number }>}
*/ */
async sendResetPasswordCode( async sendResetPasswordCode(
dto: SendResetPasswordCodeDto, dto: SendResetPasswordCodeDto,
): Promise<{ success: boolean; message: string; expiresIn: number }> { ): Promise<{ success: boolean; message: string; expiresIn: number }> {
const { userId, userEmail } = dto; const { userId, userEmail } = dto;
console.log(`[비밀번호 찾기] 인증번호 발송 요청 - 아이디: ${userId}, 이메일: ${userEmail}`); this.logger.log(`[RESET-PW] 인증번호 발송 - userId: ${userId}, email: ${userEmail}`);
// 1. 사용자 확인
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({
where: { userId, userEmail }, where: { userId, userEmail },
}); });
@@ -393,18 +357,12 @@ export class AuthService {
throw new NotFoundException('일치하는 사용자 정보를 찾을 수 없습니다'); throw new NotFoundException('일치하는 사용자 정보를 찾을 수 없습니다');
} }
// 2. 인증번호 생성
const code = this.verificationService.generateCode(); const code = this.verificationService.generateCode();
console.log(`[비밀번호 찾기] 생성된 인증번호: ${code} (이메일: ${userEmail})`);
// 3. Redis에 저장 (key: reset-pw:이메일)
const key = `reset-pw:${userEmail}`; const key = `reset-pw:${userEmail}`;
await this.verificationService.saveCode(key, code); await this.verificationService.saveCode(key, code);
console.log(`[비밀번호 찾기] Redis 저장 완료 - Key: ${key}`);
// 4. 이메일 발송
await this.emailService.sendVerificationCode(userEmail, code); await this.emailService.sendVerificationCode(userEmail, code);
console.log(`[비밀번호 찾기] 이메일 발송 완료 - 수신자: ${userEmail}`);
this.logger.log(`[RESET-PW] 인증번호 발송 완료 - email: ${userEmail}`);
return { return {
success: true, success: true,
@@ -414,29 +372,21 @@ export class AuthService {
} }
/** /**
* 비밀번호 재설정 - 인증번호 검증 및 재설정 토큰 발급 * 비밀번호 재설정 - 인증번호 검증
*
* @async
* @param {VerifyResetPasswordCodeDto} dto
* @returns {Promise<{ success: boolean; message: string; resetToken: string }>}
*/ */
async verifyResetPasswordCode( async verifyResetPasswordCode(
dto: VerifyResetPasswordCodeDto, dto: VerifyResetPasswordCodeDto,
): Promise<{ success: boolean; message: string; resetToken: string }> { ): Promise<{ success: boolean; message: string; resetToken: string }> {
const { userId, userEmail, verificationCode } = dto; const { userId, userEmail, verificationCode } = dto;
console.log(`[비밀번호 찾기] 인증번호 검증 요청 - 아이디: ${userId}, 이메일: ${userEmail}, 입력 코드: ${verificationCode}`); this.logger.log(`[RESET-PW] 인증번호 검증 - userId: ${userId}, email: ${userEmail}`);
// 1. 인증번호 검증
const key = `reset-pw:${userEmail}`; const key = `reset-pw:${userEmail}`;
console.log(`[비밀번호 찾기] 검증 Key: ${key}`);
const isValid = await this.verificationService.verifyCode(key, verificationCode); const isValid = await this.verificationService.verifyCode(key, verificationCode);
console.log(`[비밀번호 찾기] 검증 결과: ${isValid}`);
if (!isValid) { if (!isValid) {
throw new UnauthorizedException('인증번호가 일치하지 않거나 만료되었습니다'); throw new UnauthorizedException('인증번호가 일치하지 않거나 만료되었습니다');
} }
// 2. 사용자 확인
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({
where: { userId, userEmail }, where: { userId, userEmail },
}); });
@@ -445,7 +395,6 @@ export class AuthService {
throw new NotFoundException('사용자를 찾을 수 없습니다'); throw new NotFoundException('사용자를 찾을 수 없습니다');
} }
// 3. 비밀번호 재설정 토큰 생성 및 저장 (30분 유효)
const resetToken = await this.verificationService.generateResetToken(userId); const resetToken = await this.verificationService.generateResetToken(userId);
return { return {
@@ -457,22 +406,17 @@ export class AuthService {
/** /**
* 비밀번호 재설정 - 새 비밀번호로 변경 * 비밀번호 재설정 - 새 비밀번호로 변경
*
* @async
* @param {ResetPasswordDto} dto
* @returns {Promise<ResetPasswordResponseDto>}
*/ */
async resetPassword(dto: ResetPasswordDto): Promise<ResetPasswordResponseDto> { async resetPassword(dto: ResetPasswordDto): Promise<ResetPasswordResponseDto> {
const { resetToken, newPassword } = dto; // 요청 const { resetToken, newPassword } = dto;
this.logger.log(`[RESET-PW] 비밀번호 변경 시도`);
// 1. 재설정 토큰 검증
const userId = await this.verificationService.verifyResetToken(resetToken); const userId = await this.verificationService.verifyResetToken(resetToken);
if (!userId) { if (!userId) {
throw new UnauthorizedException('유효하지 않거나 만료된 토큰입니다'); throw new UnauthorizedException('유효하지 않거나 만료된 토큰입니다');
} }
// 2. 사용자 조회
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({
where: { userId }, where: { userId },
}); });
@@ -481,14 +425,14 @@ export class AuthService {
throw new NotFoundException('사용자를 찾을 수 없습니다'); throw new NotFoundException('사용자를 찾을 수 없습니다');
} }
// 3. 새 비밀번호 해싱
const saltRounds = parseInt(this.configService.get<string>('BCRYPT_SALT_ROUNDS') || '10', 10); const saltRounds = parseInt(this.configService.get<string>('BCRYPT_SALT_ROUNDS') || '10', 10);
const hashedPassword = await bcrypt.hash(newPassword, saltRounds); const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
// 4. 비밀번호 업데이트
user.userPw = hashedPassword; user.userPw = hashedPassword;
await this.userRepository.save(user); await this.userRepository.save(user);
this.logger.log(`[RESET-PW] 비밀번호 변경 완료 - userId: ${userId}`);
return { return {
message: '비밀번호가 변경되었습니다', message: '비밀번호가 변경되었습니다',
}; };

View File

@@ -9,11 +9,7 @@ import {
import { Request, Response } from 'express'; import { Request, Response } from 'express';
/** /**
* 모든 예외 필터 * 모든 예외 필터 - Docker 환경에서 로그 출력 보장
*
* @description
* HTTP 예외뿐만 아니라 모든 예외를 잡아서 처리합니다.
* 예상치 못한 에러도 일관된 형식으로 응답합니다.
*/ */
@Catch() @Catch()
export class AllExceptionsFilter implements ExceptionFilter { export class AllExceptionsFilter implements ExceptionFilter {
@@ -40,11 +36,7 @@ export class AllExceptionsFilter implements ExceptionFilter {
} }
} else { } else {
status = HttpStatus.INTERNAL_SERVER_ERROR; status = HttpStatus.INTERNAL_SERVER_ERROR;
message = '서버 내부 오류가 발생했습니다.'; message = exception instanceof Error ? exception.message : '서버 내부 오류가 발생했습니다.';
if (exception instanceof Error) {
message = exception.message;
}
} }
const errorResponse = { const errorResponse = {
@@ -61,32 +53,41 @@ export class AllExceptionsFilter implements ExceptionFilter {
(errorResponse as any).stack = exception.stack; (errorResponse as any).stack = exception.stack;
} }
// ========== 상세 로깅 ========== // ========== 상세 로깅 (stdout으로 즉시 출력) ==========
this.logger.error( const logMessage = [
`========== EXCEPTION ==========` '',
); '╔══════════════════════════════════════════════════════════════╗',
this.logger.error( '║ EXCEPTION OCCURRED ║',
`[${request.method}] ${request.url} - Status: ${status}` '╚══════════════════════════════════════════════════════════════╝',
); ` Timestamp : ${errorResponse.timestamp}`,
this.logger.error( ` Method : ${request.method}`,
`Message: ${JSON.stringify(message)}` ` Path : ${request.url}`,
); ` Status : ${status}`,
` Message : ${JSON.stringify(message)}`,
];
if (exception instanceof Error) { if (exception instanceof Error) {
this.logger.error(`Error Name: ${exception.name}`); logMessage.push(` Error Name: ${exception.name}`);
this.logger.error(`Error Message: ${exception.message}`); logMessage.push(` Stack :`);
this.logger.error(`Stack Trace:\n${exception.stack}`); logMessage.push(exception.stack || 'No stack trace');
} else {
this.logger.error(`Exception: ${JSON.stringify(exception)}`);
} }
// Request Body 로깅 (민감정보 마스킹) // Request Body 로깅 (민감정보 마스킹)
const safeBody = { ...request.body }; const safeBody = { ...request.body };
if (safeBody.password) safeBody.password = '****'; if (safeBody.password) safeBody.password = '****';
if (safeBody.token) safeBody.token = '****'; if (safeBody.token) safeBody.token = '****';
this.logger.error(`Request Body: ${JSON.stringify(safeBody)}`); logMessage.push(` Body : ${JSON.stringify(safeBody)}`);
logMessage.push('══════════════════════════════════════════════════════════════════');
this.logger.error(`===============================`); logMessage.push('');
// NestJS Logger 사용 (Docker stdout으로 출력)
this.logger.error(logMessage.join('\n'));
// 추가로 console.error도 출력 (백업)
console.error(logMessage.join('\n'));
// process.stdout으로 직접 출력 (버퍼링 우회)
process.stdout.write(logMessage.join('\n') + '\n');
response.status(status).json(errorResponse); response.status(status).json(errorResponse);
} }

View File

@@ -3,50 +3,42 @@ import {
NestInterceptor, NestInterceptor,
ExecutionContext, ExecutionContext,
CallHandler, CallHandler,
Logger,
} from '@nestjs/common'; } from '@nestjs/common';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { Request } from 'express'; import { Request } from 'express';
/** /**
* 로깅 인터셉터 * 로깅 인터셉터 - Docker 환경에서 로그 출력 보장
*
* @description
* API 요청/응답을 로깅하고 실행 시간을 측정합니다.
*
* @example
* // main.ts에서 전역 적용
* app.useGlobalInterceptors(new LoggingInterceptor());
*
* @export
* @class LoggingInterceptor
* @implements {NestInterceptor}
*/ */
@Injectable() @Injectable()
export class LoggingInterceptor implements NestInterceptor { export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest<Request>(); const request = context.switchToHttp().getRequest<Request>();
const { method, url, ip } = request; const { method, url, ip } = request;
const userAgent = request.get('user-agent') || ''; const userAgent = request.get('user-agent') || '';
const now = Date.now(); const now = Date.now();
console.log( const incomingLog = `[REQUEST] ${method} ${url} - IP: ${ip}`;
`[${new Date().toISOString()}] Incoming Request: ${method} ${url} - ${ip} - ${userAgent}`, this.logger.log(incomingLog);
); process.stdout.write(`${new Date().toISOString()} - ${incomingLog}\n`);
return next.handle().pipe( return next.handle().pipe(
tap({ tap({
next: (data) => { next: () => {
const responseTime = Date.now() - now; const responseTime = Date.now() - now;
console.log( const successLog = `[RESPONSE] ${method} ${url} - ${responseTime}ms - SUCCESS`;
`[${new Date().toISOString()}] Response: ${method} ${url} - ${responseTime}ms`, this.logger.log(successLog);
); process.stdout.write(`${new Date().toISOString()} - ${successLog}\n`);
}, },
error: (error) => { error: (error) => {
const responseTime = Date.now() - now; const responseTime = Date.now() - now;
console.error( const errorLog = `[RESPONSE] ${method} ${url} - ${responseTime}ms - ERROR: ${error.message}`;
`[${new Date().toISOString()}] Error Response: ${method} ${url} - ${responseTime}ms - ${error.message}`, this.logger.error(errorLog);
); process.stdout.write(`${new Date().toISOString()} - ${errorLog}\n`);
}, },
}), }),
); );

View File

@@ -1,64 +1,117 @@
import { NestFactory, Reflector } from '@nestjs/core'; import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common'; import { ValidationPipe, Logger } from '@nestjs/common';
import { JwtAuthGuard } from './common/guards/jwt-auth.guard'; import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter'; import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
import { LoggingInterceptor } from './common/interceptors/logging.interceptor'; import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
import { TransformInterceptor } from './common/interceptors/transform.interceptor'; import { TransformInterceptor } from './common/interceptors/transform.interceptor';
import { types } from 'pg'; import { types } from 'pg';
// ========== 전역 에러 핸들러 (NestJS 도달 전 에러 캐치) ==========
process.on('uncaughtException', (error) => {
console.error('╔══════════════════════════════════════════════════════════════╗');
console.error('║ UNCAUGHT EXCEPTION ║');
console.error('╚══════════════════════════════════════════════════════════════╝');
console.error(`Timestamp: ${new Date().toISOString()}`);
console.error(`Error: ${error.message}`);
console.error(`Stack: ${error.stack}`);
process.stdout.write(`[FATAL] Uncaught Exception: ${error.message}\n`);
process.stdout.write(`[FATAL] Stack: ${error.stack}\n`);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('╔══════════════════════════════════════════════════════════════╗');
console.error('║ UNHANDLED REJECTION ║');
console.error('╚══════════════════════════════════════════════════════════════╝');
console.error(`Timestamp: ${new Date().toISOString()}`);
console.error(`Reason: ${reason}`);
process.stdout.write(`[FATAL] Unhandled Rejection: ${reason}\n`);
});
// PostgreSQL numeric/decimal 타입을 JavaScript number로 자동 변환 // PostgreSQL numeric/decimal 타입을 JavaScript number로 자동 변환
// 1700 = numeric type OID
types.setTypeParser(1700, parseFloat); types.setTypeParser(1700, parseFloat);
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const logger = new Logger('Bootstrap');
logger.log('========================================');
logger.log('Application starting...');
logger.log(`NODE_ENV: ${process.env.NODE_ENV}`);
logger.log(`REDIS_URL: ${process.env.REDIS_URL}`);
logger.log(`POSTGRES_HOST: ${process.env.POSTGRES_HOST}`);
logger.log('========================================');
process.stdout.write(`[BOOTSTRAP] Starting application...\n`);
// CORS 추가 try {
app.enableCors({ // 로거 활성화 (Docker에서 모든 로그 출력)
origin: (origin, callback) => { const app = await NestFactory.create(AppModule, {
// origin이 없는 경우 (서버 간 요청, Postman 등) 허용 logger: ['error', 'warn', 'log', 'debug', 'verbose'],
if (!origin) return callback(null, true); bufferLogs: false,
});
const allowedPatterns = [
/^http:\/\/localhost:\d+$/, // localhost 모든 포트
/^http:\/\/127\.0\.0\.1:\d+$/, // 127.0.0.1 모든 포트
/^http:\/\/192\.168\.11\.\d+:\d+$/, // 192.168.11.* 대역
/^https?:\/\/.*\.turbosoft\.kr$/, // *.turbosoft.kr
];
const isAllowed = allowedPatterns.some(pattern => pattern.test(origin));
callback(null, isAllowed);
},
credentials: true,
});
// ValidationPipe 추가 (Body 파싱과 Dto 유효성 global 설정) logger.log('NestFactory.create() 완료');
app.useGlobalPipes(
new ValidationPipe({ // CORS 추가
transform: true, app.enableCors({
whitelist: false, // nested object를 위해 whitelist 비활성화 origin: (origin, callback) => {
transformOptions: { if (!origin) return callback(null, true);
enableImplicitConversion: true,
const allowedPatterns = [
/^http:\/\/localhost:\d+$/,
/^http:\/\/127\.0\.0\.1:\d+$/,
/^http:\/\/192\.168\.11\.\d+:\d+$/,
/^https?:\/\/.*\.turbosoft\.kr$/,
];
const isAllowed = allowedPatterns.some(pattern => pattern.test(origin));
callback(null, isAllowed);
}, },
}), credentials: true,
); });
// 전역 필터 적용 - 모든 예외를 잡아서 일관된 형식으로 응답 // ValidationPipe 추가
app.useGlobalFilters(new AllExceptionsFilter()); app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: false,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
// 전역 인터셉터 적용 // 전역 터 적용
app.useGlobalInterceptors( app.useGlobalFilters(new AllExceptionsFilter());
new LoggingInterceptor(), // 요청/응답 로깅
new TransformInterceptor(), // 일관된 응답 변환 (success, data, timestamp)
//backend\src\common\interceptors\transform.interceptor.ts 구현체
);
// 전역 JWT 인증 가드 적용 (@Public 데코레이터가 있는 엔드포인트는 제외) // 전역 인터셉터 적용
const reflector = app.get(Reflector); app.useGlobalInterceptors(
app.useGlobalGuards(new JwtAuthGuard(reflector)); new LoggingInterceptor(),
new TransformInterceptor(),
);
await app.listen(process.env.PORT ?? 4000); // 로컬 개발환경 // 전역 JWT 인증 가드 적용
//await app.listen(process.env.PORT ?? 4000, '0.0.0.0'); // 모든 네트워크 외부 바인딩 const reflector = app.get(Reflector);
app.useGlobalGuards(new JwtAuthGuard(reflector));
const port = process.env.PORT ?? 4000;
await app.listen(port, '0.0.0.0');
logger.log('========================================');
logger.log(`Application running on port ${port}`);
logger.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
logger.log('========================================');
process.stdout.write(`[BOOTSTRAP] Server listening on port ${port}\n`);
} catch (error) {
logger.error('========================================');
logger.error('Application failed to start!');
logger.error(`Error: ${error.message}`);
logger.error(`Stack: ${error.stack}`);
logger.error('========================================');
process.stdout.write(`[BOOTSTRAP] STARTUP FAILED: ${error.message}\n`);
process.stdout.write(`[BOOTSTRAP] STACK: ${error.stack}\n`);
process.exit(1);
}
} }
bootstrap();
bootstrap();

View File

@@ -1,55 +1,88 @@
import { Injectable } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import * as nodemailer from 'nodemailer'; import * as nodemailer from 'nodemailer';
/** /**
* 이메일 발송 서비스 * 이메일 발송 서비스
*
* @export
* @class EmailService
*/ */
@Injectable() @Injectable()
export class EmailService { export class EmailService {
private transporter; // nodemailer 전송 객체 private readonly logger = new Logger(EmailService.name);
private transporter;
constructor(private configService: ConfigService) { constructor(private configService: ConfigService) {
// SMTP 서버 설정 (AWS SES, Gmail 등) const smtpHost = this.configService.get('SMTP_HOST');
this.transporter = nodemailer.createTransport({ // .env 파일 EMAIL CONFIGURATION const smtpPort = this.configService.get('SMTP_PORT');
host: this.configService.get('SMTP_HOST'), const smtpUser = this.configService.get('SMTP_USER');
port: parseInt(this.configService.get('SMTP_PORT')), const smtpPass = this.configService.get('SMTP_PASS');
secure: this.configService.get('SMTP_PORT') === '465',
this.logger.log(`[EMAIL] SMTP 설정 초기화`);
this.logger.log(`[EMAIL] Host: ${smtpHost}`);
this.logger.log(`[EMAIL] Port: ${smtpPort}`);
this.logger.log(`[EMAIL] User: ${smtpUser}`);
this.logger.log(`[EMAIL] Pass: ${smtpPass ? '****설정됨' : '미설정!!'}`);
process.stdout.write(`[EMAIL] SMTP Config - Host: ${smtpHost}, Port: ${smtpPort}, User: ${smtpUser}\n`);
if (!smtpHost || !smtpPort || !smtpUser || !smtpPass) {
this.logger.error(`[EMAIL] SMTP 설정 누락! 환경변수를 확인하세요.`);
process.stdout.write(`[EMAIL] ERROR: SMTP 설정 누락!\n`);
}
this.transporter = nodemailer.createTransport({
host: smtpHost,
port: parseInt(smtpPort || '587'),
secure: smtpPort === '465',
auth: { auth: {
user: this.configService.get('SMTP_USER'), user: smtpUser,
pass: this.configService.get('SMTP_PASS'), pass: smtpPass,
}, },
}); });
} }
/** /**
* 인증번호 이메일 발송 * 인증번호 이메일 발송
*
* @async
* @param {string} email - 수신자 이메일
* @param {string} code - 6자리 인증번호
* @returns {Promise<void>}
*/ */
async sendVerificationCode(email: string, code: string): Promise<void> { async sendVerificationCode(email: string, code: string): Promise<void> {
await this.transporter.sendMail({ const fromEmail = this.configService.get('FROM_EMAIL');
from: this.configService.get('FROM_EMAIL'),
to: email, this.logger.log(`[EMAIL] ========== 이메일 발송 시작 ==========`);
subject: '[한우 유전능력 시스템] 인증번호 안내', this.logger.log(`[EMAIL] From: ${fromEmail}`);
html: ` this.logger.log(`[EMAIL] To: ${email}`);
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;"> this.logger.log(`[EMAIL] Code: ${code}`);
<h2 style="color: #333;">인증번호 안내</h2> process.stdout.write(`[EMAIL] Sending to: ${email}, code: ${code}\n`);
<p>아래 인증번호를 입력해주세요.</p>
<div style="background-color: #f5f5f5; padding: 20px; text-align: center; margin: 20px 0;"> try {
<h1 style="color: #4CAF50; font-size: 32px; margin: 0;">${code}</h1> const result = await this.transporter.sendMail({
from: fromEmail,
to: email,
subject: '[한우 유전능력 시스템] 인증번호 안내',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #333;">인증번호 안내</h2>
<p>아래 인증번호를 입력해주세요.</p>
<div style="background-color: #f5f5f5; padding: 20px; text-align: center; margin: 20px 0;">
<h1 style="color: #4CAF50; font-size: 32px; margin: 0;">${code}</h1>
</div>
<p style="color: #666;">인증번호는 3분간 유효합니다.</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="color: #999; font-size: 12px;">본 메일은 발신 전용입니다.</p>
</div> </div>
<p style="color: #666;">인증번호는 3분간 유효합니다.</p> `,
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;"> });
<p style="color: #999; font-size: 12px;">본 메일은 발신 전용입니다.</p>
</div> this.logger.log(`[EMAIL] 발송 성공 - MessageId: ${result.messageId}`);
`, this.logger.log(`[EMAIL] Response: ${JSON.stringify(result)}`);
}); process.stdout.write(`[EMAIL] SUCCESS - MessageId: ${result.messageId}\n`);
} catch (error) {
this.logger.error(`[EMAIL] ========== 발송 실패 ==========`);
this.logger.error(`[EMAIL] Error Name: ${error.name}`);
this.logger.error(`[EMAIL] Error Message: ${error.message}`);
this.logger.error(`[EMAIL] Error Code: ${error.code}`);
this.logger.error(`[EMAIL] Stack: ${error.stack}`);
process.stdout.write(`[EMAIL] ERROR: ${error.message}\n`);
process.stdout.write(`[EMAIL] STACK: ${error.stack}\n`);
throw error;
}
} }
} }

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { InjectRedis } from '@nestjs-modules/ioredis'; import { InjectRedis } from '@nestjs-modules/ioredis';
import Redis from 'ioredis'; import Redis from 'ioredis';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
@@ -6,92 +6,138 @@ import { VERIFICATION_CONFIG } from 'src/common/config/VerificationConfig';
/** /**
* 인증번호 생성 및 검증 서비스 (Redis 기반) * 인증번호 생성 및 검증 서비스 (Redis 기반)
*
* @export
* @class VerificationService
*/ */
@Injectable() @Injectable()
export class VerificationService { export class VerificationService {
constructor(@InjectRedis() private readonly redis: Redis) {} private readonly logger = new Logger(VerificationService.name);
/** constructor(@InjectRedis() private readonly redis: Redis) {
* 6자리 인증번호 생성 this.logger.log(`[REDIS] VerificationService 초기화`);
* process.stdout.write(`[REDIS] VerificationService initialized\n`);
* @returns {string} 6자리 숫자
*/ // Redis 연결 상태 로깅
generateCode(): string { this.checkRedisConnection();
return Math.floor(100000 + Math.random() * 900000).toString(); }
}
/** private async checkRedisConnection(): Promise<void> {
* 인증번호 저장 (Redis 3분 후 자동 삭제) try {
* const pong = await this.redis.ping();
* @async this.logger.log(`[REDIS] 연결 상태: ${pong}`);
* @param {string} key - Redis 키 (예: find-id:test@example.com) process.stdout.write(`[REDIS] Connection status: ${pong}\n`);
* @param {string} code - 인증번호 } catch (error) {
* @returns {Promise<void>} this.logger.error(`[REDIS] 연결 실패: ${error.message}`);
*/ process.stdout.write(`[REDIS] Connection FAILED: ${error.message}\n`);
async saveCode(key: string, code: string): Promise<void> { }
await this.redis.set(key, code, 'EX', VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS); }
}
/** /**
* 인증번호 검증 * 6자리 인증번호 생성
* */
* @async generateCode(): string {
* @param {string} key - Redis 키 const code = Math.floor(100000 + Math.random() * 900000).toString();
* @param {string} code - 사용자가 입력한 인증번호 this.logger.log(`[REDIS] 인증번호 생성: ${code}`);
* @returns {Promise<boolean>} 검증 성공 여부 return code;
*/ }
async verifyCode(key: string, code: string): Promise<boolean> {
const savedCode = await this.redis.get(key);
console.log(`[DEBUG VerificationService] Key: ${key}, Input code: ${code}, Saved code: ${savedCode}`);
if (!savedCode) { /**
console.log(`[DEBUG VerificationService] No saved code found for key: ${key}`); * 인증번호 저장 (Redis)
return false; // 인증번호 없음 (만료 또는 미발급) */
} async saveCode(key: string, code: string): Promise<void> {
this.logger.log(`[REDIS] ========== 코드 저장 시작 ==========`);
this.logger.log(`[REDIS] Key: ${key}`);
this.logger.log(`[REDIS] Code: ${code}`);
this.logger.log(`[REDIS] TTL: ${VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS}`);
process.stdout.write(`[REDIS] Saving - Key: ${key}, Code: ${code}\n`);
if (savedCode !== code) { try {
console.log(`[DEBUG VerificationService] Code mismatch - Saved: ${savedCode}, Input: ${code}`); const result = await this.redis.set(key, code, 'EX', VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS);
return false; // 인증번호 불일치 this.logger.log(`[REDIS] 저장 결과: ${result}`);
} process.stdout.write(`[REDIS] Save result: ${result}\n`);
} catch (error) {
this.logger.error(`[REDIS] ========== 저장 실패 ==========`);
this.logger.error(`[REDIS] Error: ${error.message}`);
this.logger.error(`[REDIS] Stack: ${error.stack}`);
process.stdout.write(`[REDIS] SAVE ERROR: ${error.message}\n`);
process.stdout.write(`[REDIS] STACK: ${error.stack}\n`);
throw error;
}
}
// 검증 성공 시 Redis에서 삭제 (1회용) /**
await this.redis.del(key); * 인증번호 검증
console.log(`[DEBUG VerificationService] Code verified successfully!`); */
return true; async verifyCode(key: string, code: string): Promise<boolean> {
} this.logger.log(`[REDIS] ========== 코드 검증 시작 ==========`);
this.logger.log(`[REDIS] Key: ${key}`);
this.logger.log(`[REDIS] Input Code: ${code}`);
process.stdout.write(`[REDIS] Verifying - Key: ${key}, Input: ${code}\n`);
/** try {
* 비밀번호 재설정 토큰 생성 및 저장 const savedCode = await this.redis.get(key);
* this.logger.log(`[REDIS] Saved Code: ${savedCode}`);
* @async process.stdout.write(`[REDIS] Saved code: ${savedCode}\n`);
* @param {string} userId - 사용자 ID
* @returns {Promise<string>} 재설정 토큰
*/
async generateResetToken(userId: string): Promise<string> {
const token = crypto.randomBytes(VERIFICATION_CONFIG.TOKEN_BYTES_LENGTH).toString('hex');
await this.redis.set(`reset:${token}`, userId, 'EX', VERIFICATION_CONFIG.RESET_TOKEN_EXPIRY_SECONDS);
return token;
}
/** if (!savedCode) {
* 비밀번호 재설정 토큰 검증 this.logger.warn(`[REDIS] 저장된 코드 없음 (만료 또는 미발급)`);
* return false;
* @async }
* @param {string} token - 재설정 토큰
* @returns {Promise<string | null>} 사용자 ID 또는 null
*/
async verifyResetToken(token: string): Promise<string | null> {
const userId = await this.redis.get(`reset:${token}`);
if (!userId) { if (savedCode !== code) {
return null; // 토큰 없음 (만료 또는 미발급) this.logger.warn(`[REDIS] 코드 불일치 - Saved: ${savedCode}, Input: ${code}`);
} return false;
}
// 검증 성공 시 토큰 삭제 (1회용) await this.redis.del(key);
await this.redis.del(`reset:${token}`); this.logger.log(`[REDIS] 검증 성공, 코드 삭제됨`);
return userId; return true;
}
} catch (error) {
this.logger.error(`[REDIS] ========== 검증 실패 ==========`);
this.logger.error(`[REDIS] Error: ${error.message}`);
this.logger.error(`[REDIS] Stack: ${error.stack}`);
process.stdout.write(`[REDIS] VERIFY ERROR: ${error.message}\n`);
throw error;
}
}
/**
* 비밀번호 재설정 토큰 생성 및 저장
*/
async generateResetToken(userId: string): Promise<string> {
this.logger.log(`[REDIS] 리셋 토큰 생성 - userId: ${userId}`);
try {
const token = crypto.randomBytes(VERIFICATION_CONFIG.TOKEN_BYTES_LENGTH).toString('hex');
await this.redis.set(`reset:${token}`, userId, 'EX', VERIFICATION_CONFIG.RESET_TOKEN_EXPIRY_SECONDS);
this.logger.log(`[REDIS] 리셋 토큰 저장 완료`);
return token;
} catch (error) {
this.logger.error(`[REDIS] 리셋 토큰 생성 실패: ${error.message}`);
throw error;
}
}
/**
* 비밀번호 재설정 토큰 검증
*/
async verifyResetToken(token: string): Promise<string | null> {
this.logger.log(`[REDIS] 리셋 토큰 검증`);
try {
const userId = await this.redis.get(`reset:${token}`);
if (!userId) {
this.logger.warn(`[REDIS] 리셋 토큰 없음 또는 만료`);
return null;
}
await this.redis.del(`reset:${token}`);
this.logger.log(`[REDIS] 리셋 토큰 검증 성공 - userId: ${userId}`);
return userId;
} catch (error) {
this.logger.error(`[REDIS] 리셋 토큰 검증 실패: ${error.message}`);
throw error;
}
}
} }