ERP-node/PHASE3.14_AUTH_SERVICE_MIGR...

11 KiB

📋 Phase 3.14: AuthService Raw Query 전환 계획

📋 개요

AuthService는 5개의 Prisma 호출이 있으며, 사용자 인증 및 권한 관리를 담당하는 서비스입니다.

📊 기본 정보

항목 내용
파일 위치 backend-node/src/services/authService.ts
파일 크기 335 라인
Prisma 호출 0개 (이미 Phase 1.5에서 전환 완료)
현재 진행률 5/5 (100%) 전환 완료
복잡도 높음 (보안, 암호화, 세션 관리)
우선순위 🟡 중간 (Phase 3.14)
상태 완료 (Phase 1.5에서 이미 완료)

🎯 전환 목표

  • 5개 모든 Prisma 호출을 db.tsquery(), queryOne() 함수로 교체
  • 사용자 인증 기능 정상 동작
  • 비밀번호 암호화/검증 유지
  • 세션 관리 기능 유지
  • 권한 검증 기능 유지
  • TypeScript 컴파일 성공
  • Prisma import 완전 제거

🔍 예상 Prisma 사용 패턴

주요 기능 (5개 예상)

1. 사용자 로그인 (인증)

  • findFirst or findUnique
  • 이메일/사용자명으로 조회
  • 비밀번호 검증

2. 사용자 정보 조회

  • findUnique
  • user_id 기준
  • 권한 정보 포함

3. 사용자 생성 (회원가입)

  • create
  • 비밀번호 암호화
  • 중복 검사

4. 비밀번호 변경

  • update
  • 기존 비밀번호 검증
  • 새 비밀번호 암호화

5. 세션 관리

  • create, update, delete
  • 세션 토큰 저장/조회

💡 전환 전략

1단계: 인증 관련 전환 (2개)

  • login() - 사용자 조회 + 비밀번호 검증
  • getUserInfo() - 사용자 정보 조회

2단계: 사용자 관리 전환 (2개)

  • createUser() - 사용자 생성
  • changePassword() - 비밀번호 변경

3단계: 세션 관리 전환 (1개)

  • manageSession() - 세션 CRUD

💻 전환 예시

예시 1: 로그인 (비밀번호 검증)

변경 전:

async login(username: string, password: string) {
  const user = await prisma.users.findFirst({
    where: {
      OR: [
        { username: username },
        { email: username },
      ],
      is_active: true,
    },
  });

  if (!user) {
    throw new Error("User not found");
  }

  const isPasswordValid = await bcrypt.compare(password, user.password_hash);

  if (!isPasswordValid) {
    throw new Error("Invalid password");
  }

  return user;
}

변경 후:

async login(username: string, password: string) {
  const user = await queryOne<any>(
    `SELECT * FROM users
     WHERE (username = $1 OR email = $1)
     AND is_active = $2`,
    [username, true]
  );

  if (!user) {
    throw new Error("User not found");
  }

  const isPasswordValid = await bcrypt.compare(password, user.password_hash);

  if (!isPasswordValid) {
    throw new Error("Invalid password");
  }

  return user;
}

예시 2: 사용자 생성 (비밀번호 암호화)

변경 전:

async createUser(userData: CreateUserDto) {
  // 중복 검사
  const existing = await prisma.users.findFirst({
    where: {
      OR: [
        { username: userData.username },
        { email: userData.email },
      ],
    },
  });

  if (existing) {
    throw new Error("User already exists");
  }

  // 비밀번호 암호화
  const passwordHash = await bcrypt.hash(userData.password, 10);

  // 사용자 생성
  const user = await prisma.users.create({
    data: {
      username: userData.username,
      email: userData.email,
      password_hash: passwordHash,
      company_code: userData.company_code,
    },
  });

  return user;
}

변경 후:

async createUser(userData: CreateUserDto) {
  // 중복 검사
  const existing = await queryOne<any>(
    `SELECT * FROM users
     WHERE username = $1 OR email = $2`,
    [userData.username, userData.email]
  );

  if (existing) {
    throw new Error("User already exists");
  }

  // 비밀번호 암호화
  const passwordHash = await bcrypt.hash(userData.password, 10);

  // 사용자 생성
  const user = await queryOne<any>(
    `INSERT INTO users
     (username, email, password_hash, company_code, created_at, updated_at)
     VALUES ($1, $2, $3, $4, NOW(), NOW())
     RETURNING *`,
    [userData.username, userData.email, passwordHash, userData.company_code]
  );

  return user;
}

예시 3: 비밀번호 변경

변경 전:

async changePassword(
  userId: number,
  oldPassword: string,
  newPassword: string
) {
  const user = await prisma.users.findUnique({
    where: { user_id: userId },
  });

  if (!user) {
    throw new Error("User not found");
  }

  const isOldPasswordValid = await bcrypt.compare(
    oldPassword,
    user.password_hash
  );

  if (!isOldPasswordValid) {
    throw new Error("Invalid old password");
  }

  const newPasswordHash = await bcrypt.hash(newPassword, 10);

  await prisma.users.update({
    where: { user_id: userId },
    data: { password_hash: newPasswordHash },
  });
}

변경 후:

async changePassword(
  userId: number,
  oldPassword: string,
  newPassword: string
) {
  const user = await queryOne<any>(
    `SELECT * FROM users WHERE user_id = $1`,
    [userId]
  );

  if (!user) {
    throw new Error("User not found");
  }

  const isOldPasswordValid = await bcrypt.compare(
    oldPassword,
    user.password_hash
  );

  if (!isOldPasswordValid) {
    throw new Error("Invalid old password");
  }

  const newPasswordHash = await bcrypt.hash(newPassword, 10);

  await query(
    `UPDATE users
     SET password_hash = $1, updated_at = NOW()
     WHERE user_id = $2`,
    [newPasswordHash, userId]
  );
}

🔧 기술적 고려사항

1. 비밀번호 보안

import bcrypt from "bcrypt";

// 비밀번호 해싱 (회원가입, 비밀번호 변경)
const SALT_ROUNDS = 10;
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);

// 비밀번호 검증 (로그인)
const isValid = await bcrypt.compare(plainPassword, passwordHash);

2. SQL 인젝션 방지

// ❌ 위험: 직접 문자열 결합
const sql = `SELECT * FROM users WHERE username = '${username}'`;

// ✅ 안전: 파라미터 바인딩
const user = await queryOne(`SELECT * FROM users WHERE username = $1`, [
  username,
]);

3. 세션 토큰 관리

import crypto from "crypto";

// 세션 토큰 생성
const sessionToken = crypto.randomBytes(32).toString("hex");

// 세션 저장
await query(
  `INSERT INTO user_sessions (user_id, session_token, expires_at)
   VALUES ($1, $2, NOW() + INTERVAL '1 day')`,
  [userId, sessionToken]
);

4. 권한 검증

async checkPermission(userId: number, permission: string): Promise<boolean> {
  const result = await queryOne<{ has_permission: boolean }>(
    `SELECT EXISTS (
       SELECT 1 FROM user_permissions up
       JOIN permissions p ON up.permission_id = p.permission_id
       WHERE up.user_id = $1 AND p.permission_name = $2
     ) as has_permission`,
    [userId, permission]
  );

  return result?.has_permission || false;
}

전환 완료 내역 (Phase 1.5에서 이미 완료됨)

AuthService는 Phase 1.5에서 이미 Raw Query로 전환이 완료되었습니다.

전환된 Prisma 호출 (5개)

  1. loginPwdCheck() - 로그인 비밀번호 검증

    • user_info 테이블에서 비밀번호 조회
    • EncryptUtil을 활용한 비밀번호 검증
    • 마스터 패스워드 지원
  2. insertLoginAccessLog() - 로그인 로그 기록

    • login_access_log 테이블에 INSERT
    • 로그인 시간, IP 주소 등 기록
  3. getUserInfo() - 사용자 정보 조회

    • user_info 테이블 조회
    • PersonBean 객체로 반환
  4. updateLastLoginDate() - 마지막 로그인 시간 업데이트

    • user_info 테이블 UPDATE
    • last_login_date 갱신
  5. checkUserPermission() - 사용자 권한 확인

    • user_auth 테이블 조회
    • 권한 코드 검증

주요 기술적 특징

  • 보안: EncryptUtil을 활용한 안전한 비밀번호 검증
  • JWT 토큰: JwtUtils를 활용한 토큰 생성 및 검증
  • 로깅: 상세한 로그인 이력 기록
  • 에러 처리: 안전한 에러 메시지 반환

코드 상태

  • Prisma import 없음
  • query 함수 사용 중
  • TypeScript 컴파일 성공
  • 보안 로직 유지

📝 원본 전환 체크리스트

1단계: Prisma 호출 전환 ( Phase 1.5에서 완료)

  • login() - 사용자 조회 + 비밀번호 검증 (findFirst)
  • getUserInfo() - 사용자 정보 조회 (findUnique)
  • createUser() - 사용자 생성 (create with 중복 검사)
  • changePassword() - 비밀번호 변경 (findUnique + update)
  • manageSession() - 세션 관리 (create/update/delete)

2단계: 보안 검증

  • 비밀번호 해싱 로직 유지 (bcrypt)
  • SQL 인젝션 방지 확인
  • 세션 토큰 보안 확인
  • 중복 계정 방지 확인

3단계: 테스트

  • 단위 테스트 작성 (5개)
    • 로그인 성공/실패 테스트
    • 사용자 생성 테스트
    • 비밀번호 변경 테스트
    • 세션 관리 테스트
    • 권한 검증 테스트
  • 보안 테스트
    • SQL 인젝션 테스트
    • 비밀번호 강도 테스트
    • 세션 탈취 방지 테스트
  • 통합 테스트 작성 (2개)

4단계: 문서화

  • 전환 완료 문서 업데이트
  • 보안 가이드 업데이트

🎯 예상 난이도 및 소요 시간

  • 난이도: (높음)
    • 보안 크리티컬 (비밀번호, 세션)
    • SQL 인젝션 방지 필수
    • 철저한 테스트 필요
  • 예상 소요 시간: 1.5~2시간
    • Prisma 호출 전환: 40분
    • 보안 검증: 40분
    • 테스트: 40분

⚠️ 주의사항

보안 필수 체크리스트

  1. 모든 사용자 입력은 파라미터 바인딩 사용
  2. 비밀번호는 절대 평문 저장 금지 (bcrypt 사용)
  3. 세션 토큰은 충분히 길고 랜덤해야 함
  4. 비밀번호 실패 시 구체적 오류 메시지 금지 ("User not found" vs "Invalid credentials")
  5. 로그인 실패 횟수 제한 (Brute Force 방지)

상태: 대기 중
특이사항: 보안 크리티컬, 비밀번호 암호화, 세션 관리 포함
⚠️ 주의: 이 서비스는 보안에 매우 중요하므로 신중한 테스트 필수!