feat: 비밀번호 재설정 플로우 (이메일 링크 기반)

백엔드:
- User 엔티티에 passwordResetTokenHash/passwordResetExpiresAt(select:false) 추가
- UserService: setPasswordResetToken/findByPasswordResetTokenHash/updatePassword
- MailService: sendPasswordResetEmail (dev=로그 출력, prod=수신자만)
- AuthService: requestPasswordReset(존재 비노출·로컬계정만·1시간 토큰),
  resetPassword(토큰 해시 검증→새 비번 저장→전 세션 폐기)
- POST /auth/forgot-password(3/분)·/auth/reset-password(5/분), 가입과 동일 비번 규칙
- OAuth 콜백의 잔여 /repos 리다이렉트를 /projects 로 교정(리팩터 누락분)

프론트:
- useAuth: forgotPassword/resetPassword
- ForgotPasswordPage(/forgot-password), ResetPasswordPage(/reset-password, 토큰=쿼리)
- LoginPage '비밀번호 찾기' 링크 연결 + 재설정 완료(?reset=1) 배너

런타임 검증: 재설정→새 비번 로그인 200·구 비번 거부·토큰 재사용 거부·약한 비번 VAL_001·
없는 이메일 동일응답(비노출). 백엔드 build/lint 0·14 tests, 프론트 type-check/lint/build 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 20:50:34 +09:00
parent cb6aa07ebc
commit 52b70363d9
12 changed files with 745 additions and 7 deletions
+19 -1
View File
@@ -20,6 +20,16 @@ export function useAuth() {
await api.post('/auth/resend-verification', { email })
}
// 비밀번호 재설정 요청 — 재설정 메일 발송(존재 여부 비노출)
async function forgotPassword(email: string): Promise<void> {
await api.post('/auth/forgot-password', { email })
}
// 비밀번호 재설정 — 메일 링크의 토큰 + 새 비밀번호
async function resetPassword(token: string, password: string): Promise<void> {
await api.post('/auth/reset-password', { token, password })
}
// 로그인
async function login(payload: LoginPayload): Promise<AuthUser> {
return (await api.post('/auth/login', payload)) as unknown as AuthUser
@@ -36,5 +46,13 @@ export function useAuth() {
return (await api.get('/auth/me', { silent: true })) as unknown as AuthUser
}
return { signup, resendVerification, login, logout, fetchMe }
return {
signup,
resendVerification,
forgotPassword,
resetPassword,
login,
logout,
fetchMe,
}
}