INIT
This commit is contained in:
195
backend/src/auth/auth.controller.ts
Normal file
195
backend/src/auth/auth.controller.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { Body, Controller, Get, Post, Query, Req } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { LoginResponseDto } from './dto/login-response.dto';
|
||||
import { AuthService } from './auth.service';
|
||||
import { SignupDto } from './dto/signup.dto';
|
||||
import { SignupResponseDto } from './dto/signup-response.dto';
|
||||
import { SendFindIdCodeDto } from './dto/send-find-id-code.dto';
|
||||
import { VerifyFindIdCodeDto } from './dto/verify-find-id-code.dto';
|
||||
import { FindIdResponseDto } from './dto/find-id-response.dto';
|
||||
import { SendResetPasswordCodeDto } from './dto/send-reset-password-code.dto';
|
||||
import { VerifyResetPasswordCodeDto } from './dto/verify-reset-password-code.dto';
|
||||
import { ResetPasswordDto } from './dto/reset-password.dto';
|
||||
import { ResetPasswordResponseDto } from './dto/reset-password-response.dto';
|
||||
import { SendSignupCodeDto } from './dto/send-signup-code.dto';
|
||||
import { VerifySignupCodeDto } from './dto/verify-signup-code.dto';
|
||||
import { Public } from '../common/decorators/public.decorator';
|
||||
|
||||
/**
|
||||
* 인증 관련 컨트롤러
|
||||
*
|
||||
* @description
|
||||
* 로그인, 회원가입, 아이디 찾기, 비밀번호 재설정 등 인증 관련 API
|
||||
*
|
||||
* @export
|
||||
* @class AuthController
|
||||
* @typedef {AuthController}
|
||||
*/
|
||||
@Controller('auth')
|
||||
@Public() // 모든 엔드포인트가 공개 (인증 불필요)
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
/**
|
||||
* POST /auth/login - 사용자 로그인 처리
|
||||
*
|
||||
* @async
|
||||
* @param {LoginDto} loginDto
|
||||
* @returns {Promise<LoginResponseDto>}
|
||||
*/
|
||||
@Post('login')
|
||||
async login(@Body() loginDto: LoginDto): Promise<LoginResponseDto> {
|
||||
return this.authService.login(loginDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /auth/check-email - 이메일 중복 체크
|
||||
*
|
||||
* @async
|
||||
* @param {string} email
|
||||
* @returns {Promise<{ available: boolean; message: string }>}
|
||||
*/
|
||||
@Get('check-email')
|
||||
async checkEmail(
|
||||
@Query('email') email: string,
|
||||
): Promise<{ available: boolean; message: string }> {
|
||||
return this.authService.checkEmailDuplicate(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/signup/send-code - 회원가입 이메일 인증번호 발송
|
||||
*
|
||||
* @async
|
||||
* @param {SendSignupCodeDto} dto
|
||||
* @returns {Promise<{ success: boolean; message: string; expiresIn: number }>}
|
||||
*/
|
||||
@Post('signup/send-code')
|
||||
async sendSignupCode(
|
||||
@Body() dto: SendSignupCodeDto,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
expiresIn: number;
|
||||
}> {
|
||||
return this.authService.sendSignupCode(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/signup/verify-code - 회원가입 이메일 인증번호 검증
|
||||
*
|
||||
* @async
|
||||
* @param {VerifySignupCodeDto} dto
|
||||
* @returns {Promise<{ success: boolean; message: string; verified: boolean }>}
|
||||
*/
|
||||
@Post('signup/verify-code')
|
||||
async verifySignupCode(
|
||||
@Body() dto: VerifySignupCodeDto,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
verified: boolean;
|
||||
}> {
|
||||
return this.authService.verifySignupCode(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/register - 회원가입
|
||||
*
|
||||
* @description
|
||||
* 이메일 인증이 완료된 후에만 회원가입이 가능합니다.
|
||||
* 먼저 /auth/signup/send-code로 인증번호를 받고,
|
||||
* /auth/signup/verify-code로 인증을 완료한 후 호출하세요.
|
||||
*
|
||||
* @async
|
||||
* @param {SignupDto} signupDto
|
||||
* @param {Request} req
|
||||
* @returns {Promise<SignupResponseDto>}
|
||||
*/
|
||||
@Post('register')
|
||||
async register(
|
||||
@Body() signupDto: SignupDto,
|
||||
@Req() req: Request,
|
||||
): Promise<SignupResponseDto> {
|
||||
const clientIp = req.ip || req.socket.remoteAddress || 'unknown';
|
||||
return this.authService.register(signupDto, clientIp);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/find-id/send-code - 아이디 찾기 인증번호 발송
|
||||
*
|
||||
* @async
|
||||
* @param {SendFindIdCodeDto} dto
|
||||
* @returns {Promise<{ success: boolean; message: string; expiresIn: number }>}
|
||||
*/
|
||||
@Post('find-id/send-code')
|
||||
async sendFindIdCode(
|
||||
@Body() dto: SendFindIdCodeDto,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
expiresIn: number;
|
||||
}> {
|
||||
return this.authService.sendFindIdCode(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/find-id/verify-code - 아이디 찾기 인증번호 검증
|
||||
*
|
||||
* @async
|
||||
* @param {VerifyFindIdCodeDto} dto
|
||||
* @returns {Promise<FindIdResponseDto>}
|
||||
*/
|
||||
@Post('find-id/verify-code')
|
||||
async verifyFindIdCode(
|
||||
@Body() dto: VerifyFindIdCodeDto,
|
||||
): Promise<FindIdResponseDto> {
|
||||
return this.authService.verifyFindIdCode(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/reset-password/send-code - 비밀번호 재설정 인증번호 발송
|
||||
*
|
||||
* @async
|
||||
* @param {SendResetPasswordCodeDto} dto
|
||||
* @returns {Promise<{ success: boolean; message: string; expiresIn: number }>}
|
||||
*/
|
||||
@Post('reset-password/send-code')
|
||||
async sendResetPasswordCode(
|
||||
@Body() dto: SendResetPasswordCodeDto,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
expiresIn: number;
|
||||
}> {
|
||||
return this.authService.sendResetPasswordCode(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/reset-password/verify-code - 비밀번호 재설정 인증번호 검증
|
||||
*
|
||||
* @async
|
||||
* @param {VerifyResetPasswordCodeDto} dto
|
||||
* @returns {Promise<{ success: boolean; message: string; resetToken: string }>}
|
||||
*/
|
||||
@Post('reset-password/verify-code')
|
||||
async verifyResetPasswordCode(
|
||||
@Body() dto: VerifyResetPasswordCodeDto,
|
||||
): Promise<{ success: boolean; message: string; resetToken: string }> {
|
||||
return this.authService.verifyResetPasswordCode(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/reset-password - 비밀번호 재설정 실행
|
||||
*
|
||||
* @async
|
||||
* @param {ResetPasswordDto} dto
|
||||
* @returns {Promise<ResetPasswordResponseDto>}
|
||||
*/
|
||||
@Post('reset-password')
|
||||
async resetPassword(
|
||||
@Body() dto: ResetPasswordDto,
|
||||
): Promise<ResetPasswordResponseDto> {
|
||||
return this.authService.resetPassword(dto);
|
||||
}
|
||||
}
|
||||
25
backend/src/auth/auth.module.ts
Normal file
25
backend/src/auth/auth.module.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { UserModel } from '../user/entities/user.entity';
|
||||
import { JwtModule } from 'src/common/jwt/jwt.module';
|
||||
import { EmailModule } from 'src/shared/email/email.module';
|
||||
import { VerificationModule } from 'src/shared/verification/verification.module';
|
||||
|
||||
/**
|
||||
* 인증 모듈
|
||||
* 로그인, 회원가입, 비밀번호 재설정 등 인증 관련 기능 제공
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([UserModel]),
|
||||
JwtModule,
|
||||
EmailModule,
|
||||
VerificationModule,
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
493
backend/src/auth/auth.service.ts
Normal file
493
backend/src/auth/auth.service.ts
Normal file
@@ -0,0 +1,493 @@
|
||||
import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { UserModel } from '../user/entities/user.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { LoginResponseDto } from './dto/login-response.dto';
|
||||
import { SignupDto } from './dto/signup.dto';
|
||||
import { SignupResponseDto } from './dto/signup-response.dto';
|
||||
import { SendFindIdCodeDto } from './dto/send-find-id-code.dto';
|
||||
import { VerifyFindIdCodeDto } from './dto/verify-find-id-code.dto';
|
||||
import { FindIdResponseDto } from './dto/find-id-response.dto';
|
||||
import { SendResetPasswordCodeDto } from './dto/send-reset-password-code.dto';
|
||||
import { VerifyResetPasswordCodeDto } from './dto/verify-reset-password-code.dto';
|
||||
import { ResetPasswordDto } from './dto/reset-password.dto';
|
||||
import { ResetPasswordResponseDto } from './dto/reset-password-response.dto';
|
||||
import { SendSignupCodeDto } from './dto/send-signup-code.dto';
|
||||
import { VerifySignupCodeDto } from './dto/verify-signup-code.dto';
|
||||
|
||||
import { EmailService } from 'src/shared/email/email.service';
|
||||
import { VerificationService } from 'src/shared/verification/verification.service';
|
||||
import { VERIFICATION_CONFIG } from 'src/common/config/VerificationConfig';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
@InjectRepository(UserModel)
|
||||
private readonly userRepository: Repository<UserModel>,
|
||||
private readonly emailService: EmailService,
|
||||
private readonly verificationService: VerificationService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 유저 로그인
|
||||
*
|
||||
* @async
|
||||
* @param {LoginDto} loginDto
|
||||
* @returns {Promise<LoginResponseDto>}
|
||||
*/
|
||||
async login(loginDto: LoginDto): Promise<LoginResponseDto> {
|
||||
const { userId, userPassword } = loginDto;
|
||||
|
||||
// 1. userId로 유저 찾기
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { userId },
|
||||
});
|
||||
// 2. user 없으면 에러
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('아이디 또는 비밀번호가 틀렸습니다'); //HTTP 401 상태 코드 예외
|
||||
}
|
||||
// 3. 비밀번호 비교 (bcrypt)
|
||||
const isPasswordValid = await bcrypt.compare(userPassword, user.userPw);
|
||||
if (!isPasswordValid) {
|
||||
throw new UnauthorizedException('아이디 또는 비밀번호가 틀렸습니다');
|
||||
}
|
||||
|
||||
// 4. 탈퇴 여부 확인
|
||||
if (user.delDt !== null) {
|
||||
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)
|
||||
return {
|
||||
message: '로그인 성공',
|
||||
accessToken, // JWT 토큰 추가
|
||||
refreshToken, // JWT 토큰 추가
|
||||
user: {
|
||||
pkUserNo: user.pkUserNo,
|
||||
userId: user.userId,
|
||||
userName: user.userName,
|
||||
userEmail: user.userEmail,
|
||||
userRole: user.userRole || 'USER',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원가입
|
||||
*
|
||||
* @async
|
||||
* @param {SignupDto} signupDto
|
||||
* @returns {Promise<SignupResponseDto>}
|
||||
*/
|
||||
async register(signupDto: SignupDto, clientIp: string): Promise<SignupResponseDto> {
|
||||
const { userId, userEmail, userPhone } = signupDto;
|
||||
|
||||
// 0. 이메일 인증 확인 (Redis에 인증 완료 여부 체크)
|
||||
const verifiedKey = `signup-verified:${userEmail}`;
|
||||
const isEmailVerified = await this.verificationService.verifyCode(verifiedKey, 'true');
|
||||
|
||||
if (!isEmailVerified) {
|
||||
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 상태 코드 예외
|
||||
}
|
||||
if (existingUser.userEmail === userEmail) {
|
||||
throw new ConflictException('이미 사용 중인 이메일입니다');
|
||||
}
|
||||
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
|
||||
regUserId: signupDto.userId,
|
||||
});
|
||||
|
||||
// 4. DB에 저장
|
||||
const savedUser = await this.userRepository.save(newUser);
|
||||
|
||||
// 5. 응답 구조 생성 (SignupResponseDto 반환)
|
||||
return {
|
||||
message: '회원가입이 완료되었습니다',
|
||||
redirectUrl: '/dashboard',
|
||||
userId: savedUser.userId,
|
||||
userNo: savedUser.pkUserNo,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 이메일 중복 체크
|
||||
*
|
||||
* @async
|
||||
* @param {string} userEmail
|
||||
* @returns {Promise<{ available: boolean; message: string }>}
|
||||
*/
|
||||
async checkEmailDuplicate(userEmail: string): Promise<{
|
||||
available: boolean;
|
||||
message: string;
|
||||
}> {
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
where: { userEmail },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return {
|
||||
available: false,
|
||||
message: '이미 사용 중인 이메일입니다',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
available: true,
|
||||
message: '사용 가능한 이메일입니다',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원가입 - 이메일 인증번호 발송
|
||||
*
|
||||
* @async
|
||||
* @param {SendSignupCodeDto} dto
|
||||
* @returns {Promise<{ success: boolean; message: string; expiresIn: number }>}
|
||||
*/
|
||||
async sendSignupCode(dto: SendSignupCodeDto): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
expiresIn: number;
|
||||
}> {
|
||||
const { userEmail } = dto;
|
||||
|
||||
// 1. 이메일 중복 체크
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
where: { userEmail },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
throw new ConflictException('이미 사용 중인 이메일입니다');
|
||||
}
|
||||
|
||||
// 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<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
verified: boolean;
|
||||
}> {
|
||||
const { userEmail, code } = dto;
|
||||
console.log(`[DEBUG] Verifying code for ${userEmail}: ${code}`);
|
||||
|
||||
// Redis에서 검증
|
||||
const key = `signup:${userEmail}`;
|
||||
const isValid = await this.verificationService.verifyCode(key, code);
|
||||
console.log(`[DEBUG] Verification result: ${isValid}`);
|
||||
|
||||
if (!isValid) {
|
||||
throw new UnauthorizedException('인증번호가 일치하지 않거나 만료되었습니다');
|
||||
}
|
||||
|
||||
// 검증 완료 표시 (5분간 유효)
|
||||
const verifiedKey = `signup-verified:${userEmail}`;
|
||||
await this.verificationService.saveCode(verifiedKey, 'true');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '이메일 인증이 완료되었습니다',
|
||||
verified: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 아이디 찾기 - 인증번호 발송 (이메일 인증)
|
||||
*
|
||||
* @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}`);
|
||||
|
||||
// 1. 사용자 확인
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { userName, userEmail },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
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}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '인증번호가 이메일로 발송되었습니다',
|
||||
expiresIn: VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 아이디 찾기 - 인증번호 검증
|
||||
*
|
||||
* @async
|
||||
* @param {VerifyFindIdCodeDto} dto
|
||||
* @returns {Promise<FindIdResponseDto>}
|
||||
*/
|
||||
async verifyFindIdCode(dto: VerifyFindIdCodeDto): Promise<FindIdResponseDto> {
|
||||
const { userEmail, verificationCode } = dto;
|
||||
console.log(`[아이디 찾기] 인증번호 검증 요청 - 이메일: ${userEmail}, 입력 코드: ${verificationCode}`);
|
||||
|
||||
// 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 },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('사용자를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 3. 아이디 마스킹
|
||||
const maskedUserId = this.maskUserId(user.userId);
|
||||
|
||||
return {
|
||||
message: '인증이 완료되었습니다',
|
||||
userId: user.userId,
|
||||
maskedUserId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 아이디 마스킹 (앞 4자리만 표시)
|
||||
*
|
||||
* @private
|
||||
* @param {string} userId
|
||||
* @returns {string}
|
||||
*/
|
||||
private maskUserId(userId: string): string {
|
||||
if (userId.length <= 4) {
|
||||
return userId;
|
||||
}
|
||||
const visiblePart = userId.substring(0, 4);
|
||||
const maskedPart = '*'.repeat(userId.length - 4);
|
||||
return visiblePart + maskedPart;
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 재설정 - 인증번호 발송 (이메일 인증)
|
||||
*
|
||||
* @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}`);
|
||||
|
||||
// 1. 사용자 확인
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { userId, userEmail },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
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}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '인증번호가 이메일로 발송되었습니다',
|
||||
expiresIn: VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 재설정 - 인증번호 검증 및 재설정 토큰 발급
|
||||
*
|
||||
* @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}`);
|
||||
|
||||
// 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 },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('사용자를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 3. 비밀번호 재설정 토큰 생성 및 저장 (30분 유효)
|
||||
const resetToken = await this.verificationService.generateResetToken(userId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '인증이 완료되었습니다',
|
||||
resetToken,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 재설정 - 새 비밀번호로 변경
|
||||
*
|
||||
* @async
|
||||
* @param {ResetPasswordDto} dto
|
||||
* @returns {Promise<ResetPasswordResponseDto>}
|
||||
*/
|
||||
async resetPassword(dto: ResetPasswordDto): Promise<ResetPasswordResponseDto> {
|
||||
const { resetToken, newPassword } = dto; // 요청
|
||||
|
||||
// 1. 재설정 토큰 검증
|
||||
const userId = await this.verificationService.verifyResetToken(resetToken);
|
||||
|
||||
if (!userId) {
|
||||
throw new UnauthorizedException('유효하지 않거나 만료된 토큰입니다');
|
||||
}
|
||||
|
||||
// 2. 사용자 조회
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
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);
|
||||
|
||||
return {
|
||||
message: '비밀번호가 변경되었습니다',
|
||||
};
|
||||
}
|
||||
}
|
||||
28
backend/src/auth/dto/find-id-response.dto.ts
Normal file
28
backend/src/auth/dto/find-id-response.dto.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 아이디 찾기 응답 DTO
|
||||
* 표준화된 응답 구조 정의
|
||||
*
|
||||
* @export
|
||||
* @class FindIdResponseDto
|
||||
* @typedef {FindIdResponseDto}
|
||||
*/
|
||||
export class FindIdResponseDto {
|
||||
/**
|
||||
* 응답 메시지
|
||||
* @type {string}
|
||||
*/
|
||||
message: string;
|
||||
|
||||
/**
|
||||
* 찾은 사용자 ID
|
||||
* @type {string}
|
||||
*/
|
||||
userId: string;
|
||||
|
||||
/**
|
||||
* 마스킹된 사용자 ID (선택 - 보안 강화)
|
||||
* 예: "testuser" -> "test****"
|
||||
* @type {string}
|
||||
*/
|
||||
maskedUserId?: string;
|
||||
}
|
||||
15
backend/src/auth/dto/login-response.dto.ts
Normal file
15
backend/src/auth/dto/login-response.dto.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 로그인 응답 DTO
|
||||
*/
|
||||
export class LoginResponseDto {
|
||||
message: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
user: {
|
||||
pkUserNo: number;
|
||||
userId: string;
|
||||
userName: string;
|
||||
userEmail: string;
|
||||
userRole: 'USER' | 'ADMIN';
|
||||
};
|
||||
}
|
||||
16
backend/src/auth/dto/login.dto.ts
Normal file
16
backend/src/auth/dto/login.dto.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { IsString } from "class-validator";
|
||||
|
||||
/**
|
||||
* 클라이언트 로그인 데이터 검증(Validation)
|
||||
*
|
||||
* @export
|
||||
* @class LoginDto
|
||||
* @typedef {LoginDto}
|
||||
*/
|
||||
export class LoginDto {
|
||||
@IsString()
|
||||
userId: string; // 사용자 ID (로그인 ID)
|
||||
|
||||
@IsString()
|
||||
userPassword: string; // 비밀번호
|
||||
}
|
||||
21
backend/src/auth/dto/reset-password-response.dto.ts
Normal file
21
backend/src/auth/dto/reset-password-response.dto.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 비밀번호 재설정 응답 DTO
|
||||
* 표준화된 응답 구조 정의
|
||||
*
|
||||
* @export
|
||||
* @class ResetPasswordResponseDto
|
||||
* @typedef {ResetPasswordResponseDto}
|
||||
*/
|
||||
export class ResetPasswordResponseDto {
|
||||
/**
|
||||
* 응답 메시지
|
||||
* @type {string}
|
||||
*/
|
||||
message: string;
|
||||
|
||||
/**
|
||||
* 임시 비밀번호 (개발 환경에서만 반환, 실무에서는 SMS 발송)
|
||||
* @type {string}
|
||||
*/
|
||||
tempPassword?: string;
|
||||
}
|
||||
18
backend/src/auth/dto/reset-password.dto.ts
Normal file
18
backend/src/auth/dto/reset-password.dto.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||
|
||||
/**
|
||||
* 비밀번호 재설정 요청 DTO
|
||||
*
|
||||
* @export
|
||||
* @class ResetPasswordDto
|
||||
*/
|
||||
export class ResetPasswordDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
resetToken: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MinLength(6)
|
||||
newPassword: string;
|
||||
}
|
||||
17
backend/src/auth/dto/send-find-id-code.dto.ts
Normal file
17
backend/src/auth/dto/send-find-id-code.dto.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
/**
|
||||
* 아이디 찾기 인증번호 발송 요청 DTO
|
||||
*
|
||||
* @export
|
||||
* @class SendFindIdCodeDto
|
||||
*/
|
||||
export class SendFindIdCodeDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
userName: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
userEmail: string;
|
||||
}
|
||||
17
backend/src/auth/dto/send-reset-password-code.dto.ts
Normal file
17
backend/src/auth/dto/send-reset-password-code.dto.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
/**
|
||||
* 비밀번호 재설정 인증번호 발송 요청 DTO
|
||||
*
|
||||
* @export
|
||||
* @class SendResetPasswordCodeDto
|
||||
*/
|
||||
export class SendResetPasswordCodeDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
userId: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
userEmail: string;
|
||||
}
|
||||
13
backend/src/auth/dto/send-signup-code.dto.ts
Normal file
13
backend/src/auth/dto/send-signup-code.dto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { IsEmail, IsNotEmpty } from 'class-validator';
|
||||
|
||||
/**
|
||||
* 회원가입 인증번호 발송 DTO
|
||||
*
|
||||
* @export
|
||||
* @class SendSignupCodeDto
|
||||
*/
|
||||
export class SendSignupCodeDto {
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
userEmail: string;
|
||||
}
|
||||
33
backend/src/auth/dto/signup-response.dto.ts
Normal file
33
backend/src/auth/dto/signup-response.dto.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 회원가입 응답 DTO
|
||||
* 표준화된 응답 구조 정의
|
||||
*
|
||||
* @export
|
||||
* @class SignupResponseDto
|
||||
* @typedef {SignupResponseDto}
|
||||
*/
|
||||
export class SignupResponseDto {
|
||||
/**
|
||||
* 응답 메시지
|
||||
* @type {string}
|
||||
*/
|
||||
message: string;
|
||||
|
||||
/**
|
||||
* 리다이렉트 URL
|
||||
* @type {string}
|
||||
*/
|
||||
redirectUrl: string;
|
||||
|
||||
/**
|
||||
* 생성된 사용자 ID (선택)
|
||||
* @type {string}
|
||||
*/
|
||||
userId?: string;
|
||||
|
||||
/**
|
||||
* 생성된 사용자 번호 (선택)
|
||||
* @type {number}
|
||||
*/
|
||||
userNo?: number;
|
||||
}
|
||||
52
backend/src/auth/dto/signup.dto.ts
Normal file
52
backend/src/auth/dto/signup.dto.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { IsEmail, IsEnum, IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator';
|
||||
import { UserSeType } from 'src/common/const/UserSeType';
|
||||
|
||||
/**
|
||||
* 클라이언트 회원가입 데이터 검증(Validation)
|
||||
*
|
||||
* @export
|
||||
* @class SignupDto
|
||||
* @typedef {SignupDto}
|
||||
*/
|
||||
export class SignupDto {
|
||||
@IsEnum(UserSeType)
|
||||
@IsNotEmpty()
|
||||
userSe: UserSeType; // 사용자 구분 (FARM/CNSLT/ORGAN)
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
userInstName?: string; // 농장명/기관명
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MinLength(4)
|
||||
userId: string; // 사용자 ID
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MinLength(6)
|
||||
userPassword: string; // 비밀번호
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
userName: string; // 이름
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
userPhone: string; // 휴대폰 번호
|
||||
|
||||
@IsOptional()
|
||||
userBirth?: Date; // 생년월일
|
||||
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
userEmail: string; // 이메일
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
userAddress?: string; // 주소
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
userBizNo?: string; // 사업자등록번호
|
||||
}
|
||||
18
backend/src/auth/dto/verify-find-id-code.dto.ts
Normal file
18
backend/src/auth/dto/verify-find-id-code.dto.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { IsEmail, IsNotEmpty, IsString, Length } from 'class-validator';
|
||||
|
||||
/**
|
||||
* 아이디 찾기 인증번호 검증 요청 DTO
|
||||
*
|
||||
* @export
|
||||
* @class VerifyFindIdCodeDto
|
||||
*/
|
||||
export class VerifyFindIdCodeDto {
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
userEmail: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(6, 6)
|
||||
verificationCode: string;
|
||||
}
|
||||
22
backend/src/auth/dto/verify-reset-password-code.dto.ts
Normal file
22
backend/src/auth/dto/verify-reset-password-code.dto.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { IsEmail, IsNotEmpty, IsString, Length } from 'class-validator';
|
||||
|
||||
/**
|
||||
* 비밀번호 재설정 인증번호 검증 요청 DTO
|
||||
*
|
||||
* @export
|
||||
* @class VerifyResetPasswordCodeDto
|
||||
*/
|
||||
export class VerifyResetPasswordCodeDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
userId: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
userEmail: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(6, 6)
|
||||
verificationCode: string;
|
||||
}
|
||||
17
backend/src/auth/dto/verify-signup-code.dto.ts
Normal file
17
backend/src/auth/dto/verify-signup-code.dto.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
/**
|
||||
* 회원가입 인증번호 검증 DTO
|
||||
*
|
||||
* @export
|
||||
* @class VerifySignupCodeDto
|
||||
*/
|
||||
export class VerifySignupCodeDto {
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
userEmail: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
code: string;
|
||||
}
|
||||
Reference in New Issue
Block a user