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

77 lines
4.1 KiB
Markdown

# 킨텍스 — 공개 인증 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 — 회원가입 (공개)
요청
```json
{ "email": "a@x.com", "displayName": "홍길동", "password": "min8chars",
"companyName": "지오인포(선택)", "inviteCode": "evt-2026(선택)" }
```
- 검증: email @Email, displayName 필수(≤120), password 8~100자. companyName·inviteCode 선택.
응답 200
```json
{ "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**(사용자 열거 방지 — 존재 여부와 무관하게 동일 응답)
```json
{ "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.