kintex/_workspace/04_backend_public_auth.md
zio 7c830ddea0 feat(auth): 공개 인증 엔드포인트 — 회원가입·비밀번호 찾기/초기화 + V9
- POST /api/auth/register(BCrypt·중복 EMAIL_TAKEN·초대코드 best-effort event_member)
- POST /api/auth/password/forgot(사용자 열거 방지·항상 200·OTP 해시 저장 10분 1회성)
- POST /api/auth/password/reset(코드 검증·시도제한·비번 갱신·잠금 리셋)
- V9: password_reset 테이블 + app_user.company_name(V1~V8 불변·멱등)
- SecurityConfig permitAll 3경로, 비번/코드/해시/스택트레이스 미노출. 기존 auth/2FA/M2~M5 불변

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 20:23:54 +09:00

4.1 KiB

킨텍스 — 공개 인증 API 계약 (회원가입·비밀번호 찾기/초기화)

작성: kintex-backend-dev · 근거: docs/design.md(SCR-01) 프론트 계약 정합 + _workspace/01_backend_contracts.md(§0 공통규약 준수) 기존 인증(JWT/RBAC auth)·Phase B(security: TotpService·LoginAttempt·app_user otp/lock)는 재사용·불변. 본 3종은 순증. 전부 공개(SecurityConfig permitAll 추가). 응답은 표준 ApiResponse<T> 봉투. 비밀번호·재설정 코드·해시는 응답/로그 미노출(계약 §0-3).


1. POST /api/auth/register — 회원가입 (공개)

요청

{ "email": "a@x.com", "displayName": "홍길동", "password": "min8chars",
  "companyName": "지오인포(선택)", "inviteCode": "evt-2026(선택)" }
  • 검증: email @Email, displayName 필수(≤120), password 8~100자. companyName·inviteCode 선택.

응답 200

{ "success": true, "data": {
    "userId": "usr-xxxxxxxxxxxxxxxxxxxx", "email": "a@x.com",
    "displayName": "홍길동", "joinedEvent": "evt-2026"   // inviteCode 매핑 성공 시 eventId, 아니면 null
  }, "error": null }
  • 생성 계정: role_code='USER', status='ACTIVE'(즉시 로그인 가능), verify_method='EMAIL', hall_manager=false. 비밀번호 BCrypt 해시 저장.
  • inviteCode: 행사 식별자(event.id)로 해석 → 활성 행사면 event_member(role EXHIBITOR) best-effort 매핑. 매칭 실패/에러는 무시(가입은 성립, joinedEvent=null).
  • companyName: app_user.company_name(V9 순증 컬럼)에 보존(선택).
  • 응답에 password_hash·otp 등 민감정보 제외.

오류

상황 code HTTP
email 중복 EMAIL_TAKEN 409
검증 실패 VALIDATION 400

2. POST /api/auth/password/forgot — 비밀번호 찾기 (공개)

요청 { "email": "a@x.com" }

응답 항상 200(사용자 열거 방지 — 존재 여부와 무관하게 동일 응답)

{ "success": true, "data": {
    "message": "입력하신 이메일이 등록되어 있으면 비밀번호 재설정 안내를 보내드립니다." },
  "error": null }
  • 활성 계정 존재 시에만 6자리 재설정 코드 생성 → BCrypt 해시로 password_reset 저장(만료 10분·1회성).
  • 이메일 발송 인프라 부재 → dev 로깅으로 대체. 로그에는 코드 마스킹(1****6)·이메일 마스킹만. 응답에 코드 미포함.

3. POST /api/auth/password/reset — 비밀번호 초기화 (공개)

요청 { "email": "a@x.com", "code": "123456", "newPassword": "min8chars" }

응답 200 { "success": true, "data": { "message": "비밀번호가 변경되었습니다. 새 비밀번호로 로그인해 주세요." }, "error": null }

  • 검증: 최신 미사용 코드 대조(만료·1회성·시도제한 max 5). 성공 시 password_hash 갱신 + failed_login_count=0·locked_until=NULL 리셋 + 코드 used=true.
  • 실패(코드 불일치/만료/시도초과/없음): OTP_INVALID (401), 메시지 일반화("재설정 코드가 올바르지 않거나 만료되었습니다.").

4. 스키마 — 마이그레이션 V9 (V1~V8 불변·멱등·순증)

V9__public_auth_password_reset.sql

  • ALTER TABLE app_user ADD COLUMN IF NOT EXISTS company_name varchar(200);
  • CREATE TABLE IF NOT EXISTS password_reset (id bigserial PK, email, code_hash BCrypt, expires_at, used, attempts, created_at) + idx_password_reset_email(email, used, created_at DESC).
  • code_hash는 저장/검증 전용 — 어떤 조회에서도 응답 SELECT 금지.

5. 코드 산출물

  • 컨트롤러 auth/PublicAuthController (신규, /api/auth 순증 — 기존 AuthController·TwoFactorController 불변)
  • 서비스 auth/PublicAuthService
  • DTO auth/dto/{RegisterRequest,RegisterResponse,ForgotPasswordRequest,ResetPasswordRequest,MessageResponse}
  • 매퍼 auth/mapper/PublicAuthMapper + mybatis/mapper/PublicAuthMapper.xml
  • ErrorCode.EMAIL_TAKEN(409) 추가 / SecurityConfig permitAll 3경로 추가 / OTP_INVALID 재사용
  • 미변경: M2~M5, 기존 auth 로그인, Phase B 2FA, 프론트. ./gradlew build -x test BUILD SUCCESSFUL.