redis 제거
This commit is contained in:
61
backend/.env
Normal file
61
backend/.env
Normal file
@@ -0,0 +1,61 @@
|
||||
# ==============================================
|
||||
# DATABASE CONFIGURATION
|
||||
# ==============================================
|
||||
POSTGRES_HOST=192.168.11.46
|
||||
POSTGRES_USER=genome
|
||||
POSTGRES_PASSWORD=genome1@3
|
||||
POSTGRES_DB=genome_db
|
||||
POSTGRES_PORT=5431
|
||||
POSTGRES_SYNCHRONIZE=true
|
||||
POSTGRES_LOGGING=true
|
||||
|
||||
# ==============================================
|
||||
# BACKEND CONFIGURATION
|
||||
# ==============================================
|
||||
BACKEND_PORT=4000
|
||||
NODE_ENV=development
|
||||
|
||||
# ==============================================
|
||||
# JWT AUTHENTICATION
|
||||
# ==============================================
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
|
||||
JWT_EXPIRES_IN=24h
|
||||
JWT_REFRESH_SECRET=your-refresh-token-secret
|
||||
JWT_REFRESH_EXPIRES_IN=7d
|
||||
|
||||
# ==============================================
|
||||
# CORS CONFIGURATION
|
||||
# ==============================================
|
||||
CORS_ORIGIN=http://localhost:3000,http://192.168.11.46:3000,http://123.143.174.11:5244
|
||||
CORS_CREDENTIALS=true
|
||||
|
||||
# ==============================================
|
||||
# SECURITY SETTINGS
|
||||
# ==============================================
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_MAX_REQUESTS=100
|
||||
BCRYPT_SALT_ROUNDS=12
|
||||
|
||||
# ==============================================
|
||||
# FILE UPLOAD
|
||||
# ==============================================
|
||||
MAX_FILE_SIZE=10485760
|
||||
UPLOAD_DESTINATION=./uploads
|
||||
ALLOWED_FILE_TYPES=jpg,jpeg,png,gif,pdf,doc,docx
|
||||
|
||||
# ==============================================
|
||||
# EMAIL CONFIGURATION (SMTP)
|
||||
# ==============================================
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=turbosoft11@gmail.com
|
||||
SMTP_PASS="kojl sxbx pdfi yhxz"
|
||||
FROM_EMAIL=turbosoft11@gmail.com
|
||||
|
||||
# ==============================================
|
||||
# LOGGING
|
||||
# ==============================================
|
||||
LOG_LEVEL=debug
|
||||
LOG_FORMAT=dev
|
||||
LOG_FILE_ENABLED=true
|
||||
LOG_FILE_PATH=./logs
|
||||
6
backend/.gitignore
vendored
6
backend/.gitignore
vendored
@@ -13,12 +13,6 @@ package-lock.json
|
||||
# ==============================================
|
||||
# Environment Variables (민감정보)
|
||||
# ==============================================
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
# .env.example은 커밋 가능
|
||||
|
||||
# ==============================================
|
||||
# Logs
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs-modules/ioredis": "^2.0.2",
|
||||
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
@@ -37,7 +37,7 @@
|
||||
"bcrypt": "^6.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"ioredis": "^5.8.1",
|
||||
|
||||
"multer": "^2.0.2",
|
||||
"nodemailer": "^7.0.9",
|
||||
"passport": "^0.7.0",
|
||||
|
||||
@@ -3,7 +3,6 @@ import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { RedisModule } from './redis/redis.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { UserModule } from './user/user.module';
|
||||
import { CommonModule } from './common/common.module';
|
||||
@@ -22,49 +21,48 @@ import { GeneModule } from './gene/gene.module';
|
||||
import { SystemModule } from './system/system.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
type: 'postgres',
|
||||
host: configService.get('POSTGRES_HOST'),
|
||||
port: configService.get('POSTGRES_PORT'),
|
||||
username: configService.get('POSTGRES_USER'),
|
||||
password: configService.get('POSTGRES_PASSWORD'),
|
||||
database: configService.get('POSTGRES_DB'),
|
||||
synchronize: configService.get('POSTGRES_SYNCHRONIZE'),
|
||||
logging: configService.get('POSTGRES_LOGGING'),
|
||||
autoLoadEntities: true,
|
||||
entities: [],
|
||||
}),
|
||||
}),
|
||||
// 인프라 모듈
|
||||
RedisModule,
|
||||
JwtModule,
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
|
||||
// 인증/사용자 모듈
|
||||
AuthModule,
|
||||
UserModule,
|
||||
|
||||
// 비즈니스 모듈
|
||||
FarmModule,
|
||||
CowModule,
|
||||
GenomeModule,
|
||||
GeneModule,
|
||||
MptModule,
|
||||
DashboardModule,
|
||||
|
||||
// 기타
|
||||
HelpModule,
|
||||
SystemModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService, JwtStrategy],
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
type: 'postgres',
|
||||
host: configService.get('POSTGRES_HOST'),
|
||||
port: configService.get('POSTGRES_PORT'),
|
||||
username: configService.get('POSTGRES_USER'),
|
||||
password: configService.get('POSTGRES_PASSWORD'),
|
||||
database: configService.get('POSTGRES_DB'),
|
||||
synchronize: configService.get('POSTGRES_SYNCHRONIZE'),
|
||||
logging: configService.get('POSTGRES_LOGGING'),
|
||||
autoLoadEntities: true,
|
||||
entities: [],
|
||||
}),
|
||||
}),
|
||||
// 인프라 모듈
|
||||
JwtModule,
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
|
||||
// 인증/사용자 모듈
|
||||
AuthModule,
|
||||
UserModule,
|
||||
|
||||
// 비즈니스 모듈
|
||||
FarmModule,
|
||||
CowModule,
|
||||
GenomeModule,
|
||||
GeneModule,
|
||||
MptModule,
|
||||
DashboardModule,
|
||||
|
||||
// 기타
|
||||
HelpModule,
|
||||
SystemModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService, JwtStrategy],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { RedisModule as NestRedisModule } from '@nestjs-modules/ioredis';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
|
||||
/**
|
||||
* RedisModule
|
||||
*
|
||||
* @description
|
||||
* Redis 연결 설정을 담당하는 전역 모듈입니다.
|
||||
* 캐시, 세션 관리, 인증번호 저장 등에 사용됩니다.
|
||||
*
|
||||
* @Global 데코레이터로 전역에서 사용 가능하므로,
|
||||
* 필요한 서비스에서 @InjectRedis()로 바로 주입할 수 있습니다.
|
||||
*
|
||||
* @export
|
||||
* @class RedisModule
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
NestRedisModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
type: 'single',
|
||||
url: configService.get('REDIS_URL'),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
exports: [NestRedisModule],
|
||||
})
|
||||
export class RedisModule {}
|
||||
@@ -6,20 +6,15 @@ import { VerificationService } from './verification.service';
|
||||
*
|
||||
* @description
|
||||
* 인증번호 생성 및 검증 기능을 제공하는 모듈입니다.
|
||||
* Redis를 사용하여 인증번호를 임시 저장하고 검증합니다.
|
||||
* 메모리 Map을 사용하여 인증번호를 임시 저장하고 검증합니다.
|
||||
*
|
||||
* 사용 예:
|
||||
* - 아이디 찾기 인증번호 발송
|
||||
* - 비밀번호 재설정 인증번호 발송
|
||||
* - 회원가입 이메일 인증
|
||||
*
|
||||
* RedisModule이 @Global로 설정되어 있어 자동으로 주입됩니다.
|
||||
*
|
||||
* @export
|
||||
* @class VerificationModule
|
||||
*/
|
||||
@Module({
|
||||
providers: [VerificationService],
|
||||
exports: [VerificationService],
|
||||
providers: [VerificationService],
|
||||
exports: [VerificationService],
|
||||
})
|
||||
export class VerificationModule {}
|
||||
|
||||
@@ -1,32 +1,40 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRedis } from '@nestjs-modules/ioredis';
|
||||
import Redis from 'ioredis';
|
||||
import * as crypto from 'crypto';
|
||||
import { VERIFICATION_CONFIG } from 'src/common/config/VerificationConfig';
|
||||
|
||||
/**
|
||||
* 인증번호 생성 및 검증 서비스 (Redis 기반)
|
||||
* 인증번호 생성 및 검증 서비스 (메모리 기반)
|
||||
*/
|
||||
@Injectable()
|
||||
export class VerificationService {
|
||||
private readonly logger = new Logger(VerificationService.name);
|
||||
|
||||
// 메모리 저장소 (key -> { code, expiresAt })
|
||||
private readonly store = new Map<string, { value: string; expiresAt: number }>();
|
||||
|
||||
constructor(@InjectRedis() private readonly redis: Redis) {
|
||||
this.logger.log(`[REDIS] VerificationService 초기화`);
|
||||
process.stdout.write(`[REDIS] VerificationService initialized\n`);
|
||||
constructor() {
|
||||
this.logger.log(`[VERIFY] VerificationService 초기화 (메모리 모드)`);
|
||||
|
||||
// Redis 연결 상태 로깅
|
||||
this.checkRedisConnection();
|
||||
// 만료된 항목 정리 (1분마다)
|
||||
setInterval(() => this.cleanup(), 60000);
|
||||
}
|
||||
|
||||
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`);
|
||||
/**
|
||||
* 만료된 항목 정리
|
||||
*/
|
||||
private cleanup(): void {
|
||||
const now = Date.now();
|
||||
let cleaned = 0;
|
||||
|
||||
for (const [key, data] of this.store.entries()) {
|
||||
if (data.expiresAt < now) {
|
||||
this.store.delete(key);
|
||||
cleaned++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleaned > 0) {
|
||||
this.logger.debug(`[VERIFY] 만료된 ${cleaned}개 항목 정리됨`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,109 +43,89 @@ export class VerificationService {
|
||||
*/
|
||||
generateCode(): string {
|
||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
this.logger.log(`[REDIS] 인증번호 생성: ${code}`);
|
||||
this.logger.log(`[VERIFY] 인증번호 생성: ${code}`);
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증번호 저장 (Redis)
|
||||
* 인증번호 저장 (TTL 적용)
|
||||
*/
|
||||
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`);
|
||||
|
||||
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;
|
||||
}
|
||||
const expiresAt = Date.now() + (VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS * 1000);
|
||||
|
||||
this.store.set(key, { value: code, expiresAt });
|
||||
|
||||
this.logger.log(`[VERIFY] 코드 저장 - Key: ${key}, TTL: ${VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS}초`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증번호 검증
|
||||
*/
|
||||
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`);
|
||||
this.logger.log(`[VERIFY] 코드 검증 - Key: ${key}, Input: ${code}`);
|
||||
|
||||
try {
|
||||
const savedCode = await this.redis.get(key);
|
||||
this.logger.log(`[REDIS] Saved Code: ${savedCode}`);
|
||||
process.stdout.write(`[REDIS] Saved code: ${savedCode}\n`);
|
||||
const data = this.store.get(key);
|
||||
|
||||
if (!savedCode) {
|
||||
this.logger.warn(`[REDIS] 저장된 코드 없음 (만료 또는 미발급)`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (savedCode !== code) {
|
||||
this.logger.warn(`[REDIS] 코드 불일치 - Saved: ${savedCode}, Input: ${code}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.redis.del(key);
|
||||
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;
|
||||
if (!data) {
|
||||
this.logger.warn(`[VERIFY] 저장된 코드 없음 (만료 또는 미발급)`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 만료 체크
|
||||
if (data.expiresAt < Date.now()) {
|
||||
this.logger.warn(`[VERIFY] 코드 만료됨`);
|
||||
this.store.delete(key);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data.value !== code) {
|
||||
this.logger.warn(`[VERIFY] 코드 불일치 - Saved: ${data.value}, Input: ${code}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 검증 성공 시 삭제 (1회용)
|
||||
this.store.delete(key);
|
||||
this.logger.log(`[VERIFY] 검증 성공, 코드 삭제됨`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 재설정 토큰 생성 및 저장
|
||||
*/
|
||||
async generateResetToken(userId: string): Promise<string> {
|
||||
this.logger.log(`[REDIS] 리셋 토큰 생성 - userId: ${userId}`);
|
||||
this.logger.log(`[VERIFY] 리셋 토큰 생성 - 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;
|
||||
}
|
||||
const token = crypto.randomBytes(VERIFICATION_CONFIG.TOKEN_BYTES_LENGTH).toString('hex');
|
||||
const expiresAt = Date.now() + (VERIFICATION_CONFIG.RESET_TOKEN_EXPIRY_SECONDS * 1000);
|
||||
|
||||
this.store.set(`reset:${token}`, { value: userId, expiresAt });
|
||||
|
||||
this.logger.log(`[VERIFY] 리셋 토큰 저장 완료`);
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 재설정 토큰 검증
|
||||
*/
|
||||
async verifyResetToken(token: string): Promise<string | null> {
|
||||
this.logger.log(`[REDIS] 리셋 토큰 검증`);
|
||||
this.logger.log(`[VERIFY] 리셋 토큰 검증`);
|
||||
|
||||
try {
|
||||
const userId = await this.redis.get(`reset:${token}`);
|
||||
const key = `reset:${token}`;
|
||||
const data = this.store.get(key);
|
||||
|
||||
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;
|
||||
if (!data) {
|
||||
this.logger.warn(`[VERIFY] 리셋 토큰 없음 또는 만료`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data.expiresAt < Date.now()) {
|
||||
this.logger.warn(`[VERIFY] 리셋 토큰 만료됨`);
|
||||
this.store.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.store.delete(key);
|
||||
this.logger.log(`[VERIFY] 리셋 토큰 검증 성공 - userId: ${data.value}`);
|
||||
return data.value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { InjectRedis } from '@nestjs-modules/ioredis';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
export interface SystemHealthResponse {
|
||||
status: 'ok' | 'error';
|
||||
@@ -17,12 +15,6 @@ export interface SystemHealthResponse {
|
||||
status: 'connected' | 'disconnected';
|
||||
error?: string;
|
||||
};
|
||||
redis: {
|
||||
host: string;
|
||||
port: number;
|
||||
status: 'connected' | 'disconnected';
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -30,19 +22,16 @@ export class SystemService {
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
@InjectDataSource() private dataSource: DataSource,
|
||||
@InjectRedis() private redis: Redis,
|
||||
) {}
|
||||
|
||||
async getHealth(): Promise<SystemHealthResponse> {
|
||||
const dbHealth = await this.checkDatabase();
|
||||
const redisHealth = await this.checkRedis();
|
||||
|
||||
return {
|
||||
status: dbHealth.status === 'connected' && redisHealth.status === 'connected' ? 'ok' : 'error',
|
||||
status: dbHealth.status === 'connected' ? 'ok' : 'error',
|
||||
timestamp: new Date().toISOString(),
|
||||
environment: this.configService.get('NODE_ENV') || 'development',
|
||||
database: dbHealth,
|
||||
redis: redisHealth,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,29 +50,4 @@ export class SystemService {
|
||||
return { ...config, status: 'disconnected' as const, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
private async checkRedis() {
|
||||
const redisUrl = this.configService.get('REDIS_URL') || '';
|
||||
let host = this.configService.get('REDIS_HOST') || 'unknown';
|
||||
let port = parseInt(this.configService.get('REDIS_PORT')) || 6379;
|
||||
|
||||
if (redisUrl) {
|
||||
try {
|
||||
const url = new URL(redisUrl);
|
||||
host = url.hostname;
|
||||
port = parseInt(url.port) || 6379;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
try {
|
||||
const pong = await this.redis.ping();
|
||||
return {
|
||||
host,
|
||||
port,
|
||||
status: pong === 'PONG' ? 'connected' as const : 'disconnected' as const,
|
||||
};
|
||||
} catch (error) {
|
||||
return { host, port, status: 'disconnected' as const, error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user