로그로그
This commit is contained in:
@@ -5,6 +5,14 @@ WORKDIR /app
|
||||
# 필요한 패키지 설치
|
||||
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 복사
|
||||
COPY package*.json ./
|
||||
|
||||
@@ -20,5 +28,5 @@ RUN npm run build
|
||||
# 포트 노출
|
||||
EXPOSE 4000
|
||||
|
||||
# 애플리케이션 실행
|
||||
CMD ["npm", "run", "start:prod"]
|
||||
# 애플리케이션 실행 (unbuffered stdout)
|
||||
CMD ["node", "--enable-source-maps", "dist/main.js"]
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
UnauthorizedException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { UserModel } from '../user/entities/user.entity';
|
||||
@@ -30,6 +31,8 @@ import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private readonly logger = new Logger(AuthService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(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> {
|
||||
const { userId, userPassword } = loginDto;
|
||||
this.logger.log(`[LOGIN] 로그인 시도 - userId: ${userId}`);
|
||||
|
||||
// 1. userId로 유저 찾기
|
||||
const user = await this.userRepository.findOne({
|
||||
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 (!isPasswordValid) {
|
||||
if (!user) {
|
||||
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('아이디 또는 비밀번호가 틀렸습니다');
|
||||
}
|
||||
|
||||
// 4. 탈퇴 여부 확인
|
||||
if (user.delDt !== null) {
|
||||
this.logger.warn(`[LOGIN] 탈퇴 계정 - userId: ${userId}`);
|
||||
throw new UnauthorizedException('탈퇴한 계정입니다');
|
||||
}
|
||||
// 6. JWT 토큰 생성
|
||||
|
||||
const payload = {
|
||||
userId: user.userId,
|
||||
userNo: user.pkUserNo,
|
||||
|
||||
};
|
||||
|
||||
// Access Token 생성 (기본 설정 사용)
|
||||
const accessToken = this.jwtService.sign(payload as any);
|
||||
|
||||
// Refresh Token 생성 (별도 secret과 만료시간 사용)
|
||||
const refreshOptions = {
|
||||
secret: this.configService.get<string>('JWT_REFRESH_SECRET')!,
|
||||
expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRES_IN') || '7d',
|
||||
};
|
||||
const refreshToken = this.jwtService.sign(payload as any, refreshOptions as any);
|
||||
|
||||
// 7. 로그인 응답 생성 (LoginResponseDto)
|
||||
this.logger.log(`[LOGIN] 로그인 성공 - userId: ${userId}`);
|
||||
|
||||
return {
|
||||
message: '로그인 성공',
|
||||
accessToken, // JWT 토큰 추가
|
||||
refreshToken, // JWT 토큰 추가
|
||||
accessToken,
|
||||
refreshToken,
|
||||
user: {
|
||||
pkUserNo: user.pkUserNo,
|
||||
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> {
|
||||
const { userId, userEmail, userPhone } = signupDto;
|
||||
this.logger.log(`[REGISTER] 회원가입 시도 - userId: ${userId}, email: ${userEmail}`);
|
||||
|
||||
// 0. 이메일 인증 확인 (Redis에 인증 완료 여부 체크)
|
||||
const verifiedKey = `signup-verified:${userEmail}`;
|
||||
const isEmailVerified = await this.verificationService.verifyCode(verifiedKey, 'true');
|
||||
|
||||
if (!isEmailVerified) {
|
||||
this.logger.warn(`[REGISTER] 이메일 미인증 - email: ${userEmail}`);
|
||||
throw new UnauthorizedException('이메일 인증이 완료되지 않았습니다');
|
||||
}
|
||||
|
||||
// 1. 중복 체크 (ID, 이메일, 전화번호, 사업자번호)
|
||||
const whereConditions = [{ userId }, { userEmail }, { userPhone }];
|
||||
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
where: whereConditions,
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
if (existingUser.userId === userId) {
|
||||
throw new ConflictException('이미 사용 중인 아이디입니다'); //HTTP 409 상태 코드 예외
|
||||
throw new ConflictException('이미 사용 중인 아이디입니다');
|
||||
}
|
||||
if (existingUser.userEmail === userEmail) {
|
||||
throw new ConflictException('이미 사용 중인 이메일입니다');
|
||||
@@ -137,28 +127,24 @@ export class AuthService {
|
||||
if (existingUser.userPhone === userPhone) {
|
||||
throw new ConflictException('이미 사용 중인 전화번호입니다');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 2. 비밀번호 해싱 (bcrypt)
|
||||
const saltRounds = parseInt(this.configService.get<string>('BCRYPT_SALT_ROUNDS') || '10', 10);
|
||||
const hashedPassword = await bcrypt.hash(signupDto.userPassword, saltRounds);
|
||||
// 3. 사용자 생성
|
||||
|
||||
const newUser = this.userRepository.create({
|
||||
userId: signupDto.userId,
|
||||
userPw: hashedPassword,
|
||||
userName: signupDto.userName,
|
||||
userPhone: signupDto.userPhone,
|
||||
userEmail: signupDto.userEmail,
|
||||
|
||||
regIp: clientIp, // 등록 ip
|
||||
regIp: clientIp,
|
||||
regUserId: signupDto.userId,
|
||||
});
|
||||
|
||||
// 4. DB에 저장
|
||||
const savedUser = await this.userRepository.save(newUser);
|
||||
this.logger.log(`[REGISTER] 회원가입 성공 - userId: ${savedUser.userId}`);
|
||||
|
||||
// 5. 응답 구조 생성 (SignupResponseDto 반환)
|
||||
return {
|
||||
message: '회원가입이 완료되었습니다',
|
||||
redirectUrl: '/dashboard',
|
||||
@@ -169,10 +155,6 @@ export class AuthService {
|
||||
|
||||
/**
|
||||
* 이메일 중복 체크
|
||||
*
|
||||
* @async
|
||||
* @param {string} userEmail
|
||||
* @returns {Promise<{ available: boolean; message: string }>}
|
||||
*/
|
||||
async checkEmailDuplicate(userEmail: string): Promise<{
|
||||
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<{
|
||||
success: boolean;
|
||||
@@ -208,42 +186,60 @@ export class AuthService {
|
||||
expiresIn: number;
|
||||
}> {
|
||||
const { userEmail } = dto;
|
||||
this.logger.log(`[SEND-CODE] ========== 인증번호 발송 시작 ==========`);
|
||||
this.logger.log(`[SEND-CODE] 이메일: ${userEmail}`);
|
||||
process.stdout.write(`[SEND-CODE] 이메일: ${userEmail}\n`);
|
||||
|
||||
try {
|
||||
// 1. 이메일 중복 체크
|
||||
this.logger.log(`[SEND-CODE] Step 1: 이메일 중복 체크`);
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
where: { userEmail },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
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();
|
||||
console.log(`[DEBUG] Generated code for ${userEmail}: ${code}`);
|
||||
this.logger.log(`[SEND-CODE] 생성된 인증번호: ${code}`);
|
||||
process.stdout.write(`[SEND-CODE] 생성된 인증번호: ${code}\n`);
|
||||
|
||||
// 3. Redis에 저장 (key: signup:이메일)
|
||||
// 3. Redis에 저장
|
||||
this.logger.log(`[SEND-CODE] Step 3: Redis 저장`);
|
||||
const key = `signup:${userEmail}`;
|
||||
await this.verificationService.saveCode(key, code);
|
||||
console.log(`[DEBUG] Saved code to Redis with key: ${key}`);
|
||||
this.logger.log(`[SEND-CODE] Redis 저장 완료 - key: ${key}`);
|
||||
|
||||
// 4. 이메일 발송
|
||||
this.logger.log(`[SEND-CODE] Step 4: 이메일 발송 시작`);
|
||||
await this.emailService.sendVerificationCode(userEmail, code);
|
||||
console.log(`[DEBUG] Email sent to: ${userEmail}`);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원가입 - 이메일 인증번호 검증
|
||||
*
|
||||
* @async
|
||||
* @param {VerifySignupCodeDto} dto
|
||||
* @returns {Promise<{ success: boolean; message: string; verified: boolean }>}
|
||||
*/
|
||||
async verifySignupCode(dto: VerifySignupCodeDto): Promise<{
|
||||
success: boolean;
|
||||
@@ -251,21 +247,21 @@ export class AuthService {
|
||||
verified: boolean;
|
||||
}> {
|
||||
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 isValid = await this.verificationService.verifyCode(key, code);
|
||||
console.log(`[DEBUG] Verification result: ${isValid}`);
|
||||
|
||||
if (!isValid) {
|
||||
this.logger.warn(`[VERIFY-CODE] 인증 실패 - email: ${userEmail}`);
|
||||
throw new UnauthorizedException('인증번호가 일치하지 않거나 만료되었습니다');
|
||||
}
|
||||
|
||||
// 검증 완료 표시 (5분간 유효)
|
||||
const verifiedKey = `signup-verified:${userEmail}`;
|
||||
await this.verificationService.saveCode(verifiedKey, 'true');
|
||||
|
||||
this.logger.log(`[VERIFY-CODE] 인증 성공 - email: ${userEmail}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '이메일 인증이 완료되었습니다',
|
||||
@@ -274,19 +270,14 @@ export class AuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 아이디 찾기 - 인증번호 발송 (이메일 인증)
|
||||
*
|
||||
* @async
|
||||
* @param {SendFindIdCodeDto} dto
|
||||
* @returns {Promise<{ success: boolean; message: string; expiresIn: number }>}
|
||||
* 아이디 찾기 - 인증번호 발송
|
||||
*/
|
||||
async sendFindIdCode(
|
||||
dto: SendFindIdCodeDto,
|
||||
): Promise<{ success: boolean; message: string; expiresIn: number }> {
|
||||
const { userName, userEmail } = dto;
|
||||
console.log(`[아이디 찾기] 인증번호 발송 요청 - 이름: ${userName}, 이메일: ${userEmail}`);
|
||||
this.logger.log(`[FIND-ID] 인증번호 발송 - name: ${userName}, email: ${userEmail}`);
|
||||
|
||||
// 1. 사용자 확인
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { userName, userEmail },
|
||||
});
|
||||
@@ -295,18 +286,12 @@ export class AuthService {
|
||||
throw new NotFoundException('일치하는 사용자 정보를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 2. 인증번호 생성
|
||||
const code = this.verificationService.generateCode();
|
||||
console.log(`[아이디 찾기] 생성된 인증번호: ${code} (이메일: ${userEmail})`);
|
||||
|
||||
// 3. Redis에 저장 (key: find-id:이메일)
|
||||
const key = `find-id:${userEmail}`;
|
||||
await this.verificationService.saveCode(key, code);
|
||||
console.log(`[아이디 찾기] Redis 저장 완료 - Key: ${key}`);
|
||||
|
||||
// 4. 이메일 발송
|
||||
await this.emailService.sendVerificationCode(userEmail, code);
|
||||
console.log(`[아이디 찾기] 이메일 발송 완료 - 수신자: ${userEmail}`);
|
||||
|
||||
this.logger.log(`[FIND-ID] 인증번호 발송 완료 - email: ${userEmail}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -317,26 +302,18 @@ export class AuthService {
|
||||
|
||||
/**
|
||||
* 아이디 찾기 - 인증번호 검증
|
||||
*
|
||||
* @async
|
||||
* @param {VerifyFindIdCodeDto} dto
|
||||
* @returns {Promise<FindIdResponseDto>}
|
||||
*/
|
||||
async verifyFindIdCode(dto: VerifyFindIdCodeDto): Promise<FindIdResponseDto> {
|
||||
const { userEmail, verificationCode } = dto;
|
||||
console.log(`[아이디 찾기] 인증번호 검증 요청 - 이메일: ${userEmail}, 입력 코드: ${verificationCode}`);
|
||||
this.logger.log(`[FIND-ID] 인증번호 검증 - email: ${userEmail}`);
|
||||
|
||||
// 1. 인증번호 검증
|
||||
const key = `find-id:${userEmail}`;
|
||||
console.log(`[아이디 찾기] 검증 Key: ${key}`);
|
||||
const isValid = await this.verificationService.verifyCode(key, verificationCode);
|
||||
console.log(`[아이디 찾기] 검증 결과: ${isValid}`);
|
||||
|
||||
if (!isValid) {
|
||||
throw new UnauthorizedException('인증번호가 일치하지 않거나 만료되었습니다');
|
||||
}
|
||||
|
||||
// 2. 사용자 정보 조회
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { userEmail },
|
||||
});
|
||||
@@ -345,7 +322,6 @@ export class AuthService {
|
||||
throw new NotFoundException('사용자를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 3. 아이디 마스킹
|
||||
const maskedUserId = this.maskUserId(user.userId);
|
||||
|
||||
return {
|
||||
@@ -355,13 +331,6 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 아이디 마스킹 (앞 4자리만 표시)
|
||||
*
|
||||
* @private
|
||||
* @param {string} userId
|
||||
* @returns {string}
|
||||
*/
|
||||
private maskUserId(userId: string): string {
|
||||
if (userId.length <= 4) {
|
||||
return userId;
|
||||
@@ -372,19 +341,14 @@ export class AuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 재설정 - 인증번호 발송 (이메일 인증)
|
||||
*
|
||||
* @async
|
||||
* @param {SendResetPasswordCodeDto} dto
|
||||
* @returns {Promise<{ success: boolean; message: string; expiresIn: number }>}
|
||||
* 비밀번호 재설정 - 인증번호 발송
|
||||
*/
|
||||
async sendResetPasswordCode(
|
||||
dto: SendResetPasswordCodeDto,
|
||||
): Promise<{ success: boolean; message: string; expiresIn: number }> {
|
||||
const { userId, userEmail } = dto;
|
||||
console.log(`[비밀번호 찾기] 인증번호 발송 요청 - 아이디: ${userId}, 이메일: ${userEmail}`);
|
||||
this.logger.log(`[RESET-PW] 인증번호 발송 - userId: ${userId}, email: ${userEmail}`);
|
||||
|
||||
// 1. 사용자 확인
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { userId, userEmail },
|
||||
});
|
||||
@@ -393,18 +357,12 @@ export class AuthService {
|
||||
throw new NotFoundException('일치하는 사용자 정보를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 2. 인증번호 생성
|
||||
const code = this.verificationService.generateCode();
|
||||
console.log(`[비밀번호 찾기] 생성된 인증번호: ${code} (이메일: ${userEmail})`);
|
||||
|
||||
// 3. Redis에 저장 (key: reset-pw:이메일)
|
||||
const key = `reset-pw:${userEmail}`;
|
||||
await this.verificationService.saveCode(key, code);
|
||||
console.log(`[비밀번호 찾기] Redis 저장 완료 - Key: ${key}`);
|
||||
|
||||
// 4. 이메일 발송
|
||||
await this.emailService.sendVerificationCode(userEmail, code);
|
||||
console.log(`[비밀번호 찾기] 이메일 발송 완료 - 수신자: ${userEmail}`);
|
||||
|
||||
this.logger.log(`[RESET-PW] 인증번호 발송 완료 - email: ${userEmail}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -414,29 +372,21 @@ export class AuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 재설정 - 인증번호 검증 및 재설정 토큰 발급
|
||||
*
|
||||
* @async
|
||||
* @param {VerifyResetPasswordCodeDto} dto
|
||||
* @returns {Promise<{ success: boolean; message: string; resetToken: string }>}
|
||||
* 비밀번호 재설정 - 인증번호 검증
|
||||
*/
|
||||
async verifyResetPasswordCode(
|
||||
dto: VerifyResetPasswordCodeDto,
|
||||
): Promise<{ success: boolean; message: string; resetToken: string }> {
|
||||
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}`;
|
||||
console.log(`[비밀번호 찾기] 검증 Key: ${key}`);
|
||||
const isValid = await this.verificationService.verifyCode(key, verificationCode);
|
||||
console.log(`[비밀번호 찾기] 검증 결과: ${isValid}`);
|
||||
|
||||
if (!isValid) {
|
||||
throw new UnauthorizedException('인증번호가 일치하지 않거나 만료되었습니다');
|
||||
}
|
||||
|
||||
// 2. 사용자 확인
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { userId, userEmail },
|
||||
});
|
||||
@@ -445,7 +395,6 @@ export class AuthService {
|
||||
throw new NotFoundException('사용자를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 3. 비밀번호 재설정 토큰 생성 및 저장 (30분 유효)
|
||||
const resetToken = await this.verificationService.generateResetToken(userId);
|
||||
|
||||
return {
|
||||
@@ -457,22 +406,17 @@ export class AuthService {
|
||||
|
||||
/**
|
||||
* 비밀번호 재설정 - 새 비밀번호로 변경
|
||||
*
|
||||
* @async
|
||||
* @param {ResetPasswordDto} dto
|
||||
* @returns {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);
|
||||
|
||||
if (!userId) {
|
||||
throw new UnauthorizedException('유효하지 않거나 만료된 토큰입니다');
|
||||
}
|
||||
|
||||
// 2. 사용자 조회
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { userId },
|
||||
});
|
||||
@@ -481,14 +425,14 @@ export class AuthService {
|
||||
throw new NotFoundException('사용자를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 3. 새 비밀번호 해싱
|
||||
const saltRounds = parseInt(this.configService.get<string>('BCRYPT_SALT_ROUNDS') || '10', 10);
|
||||
const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
|
||||
|
||||
// 4. 비밀번호 업데이트
|
||||
user.userPw = hashedPassword;
|
||||
await this.userRepository.save(user);
|
||||
|
||||
this.logger.log(`[RESET-PW] 비밀번호 변경 완료 - userId: ${userId}`);
|
||||
|
||||
return {
|
||||
message: '비밀번호가 변경되었습니다',
|
||||
};
|
||||
|
||||
@@ -9,11 +9,7 @@ import {
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
/**
|
||||
* 모든 예외 필터
|
||||
*
|
||||
* @description
|
||||
* HTTP 예외뿐만 아니라 모든 예외를 잡아서 처리합니다.
|
||||
* 예상치 못한 에러도 일관된 형식으로 응답합니다.
|
||||
* 모든 예외 필터 - Docker 환경에서 로그 출력 보장
|
||||
*/
|
||||
@Catch()
|
||||
export class AllExceptionsFilter implements ExceptionFilter {
|
||||
@@ -40,11 +36,7 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
||||
}
|
||||
} else {
|
||||
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
message = '서버 내부 오류가 발생했습니다.';
|
||||
|
||||
if (exception instanceof Error) {
|
||||
message = exception.message;
|
||||
}
|
||||
message = exception instanceof Error ? exception.message : '서버 내부 오류가 발생했습니다.';
|
||||
}
|
||||
|
||||
const errorResponse = {
|
||||
@@ -61,32 +53,41 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
||||
(errorResponse as any).stack = exception.stack;
|
||||
}
|
||||
|
||||
// ========== 상세 로깅 ==========
|
||||
this.logger.error(
|
||||
`========== EXCEPTION ==========`
|
||||
);
|
||||
this.logger.error(
|
||||
`[${request.method}] ${request.url} - Status: ${status}`
|
||||
);
|
||||
this.logger.error(
|
||||
`Message: ${JSON.stringify(message)}`
|
||||
);
|
||||
// ========== 상세 로깅 (stdout으로 즉시 출력) ==========
|
||||
const logMessage = [
|
||||
'',
|
||||
'╔══════════════════════════════════════════════════════════════╗',
|
||||
'║ EXCEPTION OCCURRED ║',
|
||||
'╚══════════════════════════════════════════════════════════════╝',
|
||||
` Timestamp : ${errorResponse.timestamp}`,
|
||||
` Method : ${request.method}`,
|
||||
` Path : ${request.url}`,
|
||||
` Status : ${status}`,
|
||||
` Message : ${JSON.stringify(message)}`,
|
||||
];
|
||||
|
||||
if (exception instanceof Error) {
|
||||
this.logger.error(`Error Name: ${exception.name}`);
|
||||
this.logger.error(`Error Message: ${exception.message}`);
|
||||
this.logger.error(`Stack Trace:\n${exception.stack}`);
|
||||
} else {
|
||||
this.logger.error(`Exception: ${JSON.stringify(exception)}`);
|
||||
logMessage.push(` Error Name: ${exception.name}`);
|
||||
logMessage.push(` Stack :`);
|
||||
logMessage.push(exception.stack || 'No stack trace');
|
||||
}
|
||||
|
||||
// Request Body 로깅 (민감정보 마스킹)
|
||||
const safeBody = { ...request.body };
|
||||
if (safeBody.password) safeBody.password = '****';
|
||||
if (safeBody.token) safeBody.token = '****';
|
||||
this.logger.error(`Request Body: ${JSON.stringify(safeBody)}`);
|
||||
logMessage.push(` Body : ${JSON.stringify(safeBody)}`);
|
||||
logMessage.push('══════════════════════════════════════════════════════════════════');
|
||||
logMessage.push('');
|
||||
|
||||
this.logger.error(`===============================`);
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -3,50 +3,42 @@ import {
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { Request } from 'express';
|
||||
|
||||
/**
|
||||
* 로깅 인터셉터
|
||||
*
|
||||
* @description
|
||||
* API 요청/응답을 로깅하고 실행 시간을 측정합니다.
|
||||
*
|
||||
* @example
|
||||
* // main.ts에서 전역 적용
|
||||
* app.useGlobalInterceptors(new LoggingInterceptor());
|
||||
*
|
||||
* @export
|
||||
* @class LoggingInterceptor
|
||||
* @implements {NestInterceptor}
|
||||
* 로깅 인터셉터 - Docker 환경에서 로그 출력 보장
|
||||
*/
|
||||
@Injectable()
|
||||
export class LoggingInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger('HTTP');
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const { method, url, ip } = request;
|
||||
const userAgent = request.get('user-agent') || '';
|
||||
const now = Date.now();
|
||||
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] Incoming Request: ${method} ${url} - ${ip} - ${userAgent}`,
|
||||
);
|
||||
const incomingLog = `[REQUEST] ${method} ${url} - IP: ${ip}`;
|
||||
this.logger.log(incomingLog);
|
||||
process.stdout.write(`${new Date().toISOString()} - ${incomingLog}\n`);
|
||||
|
||||
return next.handle().pipe(
|
||||
tap({
|
||||
next: (data) => {
|
||||
next: () => {
|
||||
const responseTime = Date.now() - now;
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] Response: ${method} ${url} - ${responseTime}ms`,
|
||||
);
|
||||
const successLog = `[RESPONSE] ${method} ${url} - ${responseTime}ms - SUCCESS`;
|
||||
this.logger.log(successLog);
|
||||
process.stdout.write(`${new Date().toISOString()} - ${successLog}\n`);
|
||||
},
|
||||
error: (error) => {
|
||||
const responseTime = Date.now() - now;
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] Error Response: ${method} ${url} - ${responseTime}ms - ${error.message}`,
|
||||
);
|
||||
const errorLog = `[RESPONSE] ${method} ${url} - ${responseTime}ms - ERROR: ${error.message}`;
|
||||
this.logger.error(errorLog);
|
||||
process.stdout.write(`${new Date().toISOString()} - ${errorLog}\n`);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,30 +1,66 @@
|
||||
import { NestFactory, Reflector } from '@nestjs/core';
|
||||
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 { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
|
||||
import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
|
||||
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
|
||||
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로 자동 변환
|
||||
// 1700 = numeric type OID
|
||||
types.setTypeParser(1700, parseFloat);
|
||||
|
||||
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`);
|
||||
|
||||
try {
|
||||
// 로거 활성화 (Docker에서 모든 로그 출력)
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
logger: ['error', 'warn', 'log', 'debug', 'verbose'],
|
||||
bufferLogs: false,
|
||||
});
|
||||
|
||||
logger.log('NestFactory.create() 완료');
|
||||
|
||||
// CORS 추가
|
||||
app.enableCors({
|
||||
origin: (origin, callback) => {
|
||||
// origin이 없는 경우 (서버 간 요청, Postman 등) 허용
|
||||
if (!origin) return callback(null, true);
|
||||
|
||||
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
|
||||
/^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));
|
||||
@@ -33,32 +69,49 @@ async function bootstrap() {
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
// ValidationPipe 추가 (Body 파싱과 Dto 유효성 global 설정)
|
||||
// ValidationPipe 추가
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
whitelist: false, // nested object를 위해 whitelist 비활성화
|
||||
whitelist: false,
|
||||
transformOptions: {
|
||||
enableImplicitConversion: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// 전역 필터 적용 - 모든 예외를 잡아서 일관된 형식으로 응답
|
||||
// 전역 필터 적용
|
||||
app.useGlobalFilters(new AllExceptionsFilter());
|
||||
|
||||
// 전역 인터셉터 적용
|
||||
app.useGlobalInterceptors(
|
||||
new LoggingInterceptor(), // 요청/응답 로깅
|
||||
new TransformInterceptor(), // 일관된 응답 변환 (success, data, timestamp)
|
||||
//backend\src\common\interceptors\transform.interceptor.ts 구현체
|
||||
new LoggingInterceptor(),
|
||||
new TransformInterceptor(),
|
||||
);
|
||||
|
||||
// 전역 JWT 인증 가드 적용 (@Public 데코레이터가 있는 엔드포인트는 제외)
|
||||
// 전역 JWT 인증 가드 적용
|
||||
const reflector = app.get(Reflector);
|
||||
app.useGlobalGuards(new JwtAuthGuard(reflector));
|
||||
|
||||
await app.listen(process.env.PORT ?? 4000); // 로컬 개발환경
|
||||
//await app.listen(process.env.PORT ?? 4000, '0.0.0.0'); // 모든 네트워크 외부 바인딩
|
||||
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();
|
||||
|
||||
@@ -1,41 +1,59 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
|
||||
/**
|
||||
* 이메일 발송 서비스
|
||||
*
|
||||
* @export
|
||||
* @class EmailService
|
||||
*/
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
private transporter; // nodemailer 전송 객체
|
||||
private readonly logger = new Logger(EmailService.name);
|
||||
private transporter;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
// SMTP 서버 설정 (AWS SES, Gmail 등)
|
||||
this.transporter = nodemailer.createTransport({ // .env 파일 EMAIL CONFIGURATION
|
||||
host: this.configService.get('SMTP_HOST'),
|
||||
port: parseInt(this.configService.get('SMTP_PORT')),
|
||||
secure: this.configService.get('SMTP_PORT') === '465',
|
||||
const smtpHost = this.configService.get('SMTP_HOST');
|
||||
const smtpPort = this.configService.get('SMTP_PORT');
|
||||
const smtpUser = this.configService.get('SMTP_USER');
|
||||
const smtpPass = this.configService.get('SMTP_PASS');
|
||||
|
||||
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: {
|
||||
user: this.configService.get('SMTP_USER'),
|
||||
pass: this.configService.get('SMTP_PASS'),
|
||||
user: smtpUser,
|
||||
pass: smtpPass,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증번호 이메일 발송
|
||||
*
|
||||
* @async
|
||||
* @param {string} email - 수신자 이메일
|
||||
* @param {string} code - 6자리 인증번호
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async sendVerificationCode(email: string, code: string): Promise<void> {
|
||||
await this.transporter.sendMail({
|
||||
from: this.configService.get('FROM_EMAIL'),
|
||||
const fromEmail = this.configService.get('FROM_EMAIL');
|
||||
|
||||
this.logger.log(`[EMAIL] ========== 이메일 발송 시작 ==========`);
|
||||
this.logger.log(`[EMAIL] From: ${fromEmail}`);
|
||||
this.logger.log(`[EMAIL] To: ${email}`);
|
||||
this.logger.log(`[EMAIL] Code: ${code}`);
|
||||
process.stdout.write(`[EMAIL] Sending to: ${email}, code: ${code}\n`);
|
||||
|
||||
try {
|
||||
const result = await this.transporter.sendMail({
|
||||
from: fromEmail,
|
||||
to: email,
|
||||
subject: '[한우 유전능력 시스템] 인증번호 안내',
|
||||
html: `
|
||||
@@ -51,5 +69,20 @@ export class EmailService {
|
||||
</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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRedis } from '@nestjs-modules/ioredis';
|
||||
import Redis from 'ioredis';
|
||||
import * as crypto from 'crypto';
|
||||
@@ -6,92 +6,138 @@ import { VERIFICATION_CONFIG } from 'src/common/config/VerificationConfig';
|
||||
|
||||
/**
|
||||
* 인증번호 생성 및 검증 서비스 (Redis 기반)
|
||||
*
|
||||
* @export
|
||||
* @class VerificationService
|
||||
*/
|
||||
@Injectable()
|
||||
export class VerificationService {
|
||||
constructor(@InjectRedis() private readonly redis: Redis) {}
|
||||
private readonly logger = new Logger(VerificationService.name);
|
||||
|
||||
/**
|
||||
* 6자리 인증번호 생성
|
||||
*
|
||||
* @returns {string} 6자리 숫자
|
||||
*/
|
||||
generateCode(): string {
|
||||
return Math.floor(100000 + Math.random() * 900000).toString();
|
||||
constructor(@InjectRedis() private readonly redis: Redis) {
|
||||
this.logger.log(`[REDIS] VerificationService 초기화`);
|
||||
process.stdout.write(`[REDIS] VerificationService initialized\n`);
|
||||
|
||||
// Redis 연결 상태 로깅
|
||||
this.checkRedisConnection();
|
||||
}
|
||||
|
||||
private async checkRedisConnection(): Promise<void> {
|
||||
try {
|
||||
const pong = await this.redis.ping();
|
||||
this.logger.log(`[REDIS] 연결 상태: ${pong}`);
|
||||
process.stdout.write(`[REDIS] Connection status: ${pong}\n`);
|
||||
} catch (error) {
|
||||
this.logger.error(`[REDIS] 연결 실패: ${error.message}`);
|
||||
process.stdout.write(`[REDIS] Connection FAILED: ${error.message}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증번호 저장 (Redis 3분 후 자동 삭제)
|
||||
*
|
||||
* @async
|
||||
* @param {string} key - Redis 키 (예: find-id:test@example.com)
|
||||
* @param {string} code - 인증번호
|
||||
* @returns {Promise<void>}
|
||||
* 6자리 인증번호 생성
|
||||
*/
|
||||
generateCode(): string {
|
||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
this.logger.log(`[REDIS] 인증번호 생성: ${code}`);
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증번호 저장 (Redis)
|
||||
*/
|
||||
async saveCode(key: string, code: string): Promise<void> {
|
||||
await this.redis.set(key, code, 'EX', VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS);
|
||||
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`);
|
||||
|
||||
try {
|
||||
const result = await this.redis.set(key, code, 'EX', VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증번호 검증
|
||||
*
|
||||
* @async
|
||||
* @param {string} key - Redis 키
|
||||
* @param {string} code - 사용자가 입력한 인증번호
|
||||
* @returns {Promise<boolean>} 검증 성공 여부
|
||||
*/
|
||||
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);
|
||||
console.log(`[DEBUG VerificationService] Key: ${key}, Input code: ${code}, Saved code: ${savedCode}`);
|
||||
this.logger.log(`[REDIS] Saved Code: ${savedCode}`);
|
||||
process.stdout.write(`[REDIS] Saved code: ${savedCode}\n`);
|
||||
|
||||
if (!savedCode) {
|
||||
console.log(`[DEBUG VerificationService] No saved code found for key: ${key}`);
|
||||
return false; // 인증번호 없음 (만료 또는 미발급)
|
||||
this.logger.warn(`[REDIS] 저장된 코드 없음 (만료 또는 미발급)`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (savedCode !== code) {
|
||||
console.log(`[DEBUG VerificationService] Code mismatch - Saved: ${savedCode}, Input: ${code}`);
|
||||
return false; // 인증번호 불일치
|
||||
this.logger.warn(`[REDIS] 코드 불일치 - Saved: ${savedCode}, Input: ${code}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 검증 성공 시 Redis에서 삭제 (1회용)
|
||||
await this.redis.del(key);
|
||||
console.log(`[DEBUG VerificationService] Code verified successfully!`);
|
||||
this.logger.log(`[REDIS] 검증 성공, 코드 삭제됨`);
|
||||
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
|
||||
* @param {string} userId - 사용자 ID
|
||||
* @returns {Promise<string>} 재설정 토큰
|
||||
*/
|
||||
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
|
||||
* @param {string} token - 재설정 토큰
|
||||
* @returns {Promise<string | null>} 사용자 ID 또는 null
|
||||
*/
|
||||
async verifyResetToken(token: string): Promise<string | null> {
|
||||
this.logger.log(`[REDIS] 리셋 토큰 검증`);
|
||||
|
||||
try {
|
||||
const userId = await this.redis.get(`reset:${token}`);
|
||||
|
||||
if (!userId) {
|
||||
return null; // 토큰 없음 (만료 또는 미발급)
|
||||
this.logger.warn(`[REDIS] 리셋 토큰 없음 또는 만료`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 검증 성공 시 토큰 삭제 (1회용)
|
||||
await this.redis.del(`reset:${token}`);
|
||||
this.logger.log(`[REDIS] 리셋 토큰 검증 성공 - userId: ${userId}`);
|
||||
return userId;
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`[REDIS] 리셋 토큰 검증 실패: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user