Skip to content

feat: #259 로그인 상태에서 비밀번호 변경#269

Merged
FLYLIKEB merged 1 commit into
mainfrom
feature/issue-259-change-password
Mar 16, 2026
Merged

feat: #259 로그인 상태에서 비밀번호 변경#269
FLYLIKEB merged 1 commit into
mainfrom
feature/issue-259-change-password

Conversation

@FLYLIKEB
Copy link
Copy Markdown
Owner

@FLYLIKEB FLYLIKEB commented Mar 16, 2026

Summary

Changes

Backend

  • ChangePasswordDto: 현재/새/확인 비밀번호 유효성 검사 (8자 이상, 영문+숫자)
  • AuthService.changePassword(): 현재 비밀번호 검증 → 해시 저장 → 리프레시 토큰 revoke
  • AuthController: PATCH /auth/change-password JWT 인증 필요 엔드포인트 추가
  • E2E 테스트: 인증없음(401), 비밀번호불일치(401), 확인불일치(400), 규칙미달(400), 정상변경(200)

Frontend

  • authApi.changePassword(): 새 API 메서드 추가
  • Settings 페이지 계정 연동 섹션의 이메일 행에 "비밀번호 변경" 버튼 추가
  • Dialog 모달: 현재/새/새확인 비밀번호 입력 폼 (클라이언트 사이드 유효성 검사)
  • 변경 성공 시 toast.success 후 자동 로그아웃 및 /login 리다이렉트

Test plan

  • 프론트엔드 빌드 (npm run build) 성공
  • Settings 테스트 7개 통과 (npm run test:run)
  • E2E 테스트 스위트 작성 (DB 연결 시 실행)

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 설정 페이지에서 비밀번호 변경 기능이 추가되었습니다. 사용자는 현재 비밀번호를 입력하여 새로운 비밀번호로 변경할 수 있습니다. 비밀번호는 최소 8자 이상이며 영문과 숫자를 포함해야 합니다.
  • 테스트

    • 비밀번호 변경 기능에 대한 포괄적인 테스트가 추가되었습니다.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cha-log Ready Ready Preview, Comment Mar 16, 2026 4:23am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 16, 2026

Walkthrough

로그인 상태에서 비밀번호를 변경할 수 있는 기능을 추가합니다. 백엔드에 JWT 인증 기반의 PATCH 엔드포인트를 구현하고, 프론트엔드의 Settings 페이지에 비밀번호 변경 모달 UI를 추가합니다.

Changes

Cohort / File(s) Summary
Backend API & DTO
backend/src/auth/auth.controller.ts, backend/src/auth/auth.service.ts, backend/src/auth/dto/change-password.dto.ts
PATCH /auth/change-password 엔드포인트 추가. 현재 비밀번호 검증, 신규 비밀번호 해싱, 기존 refresh token 폐기 로직 구현. ChangePasswordDto에서 최소 8자, 영문+숫자 포함 규칙 검증.
Backend Tests
backend/test/app.e2e-spec.ts, backend/test/suites/auth-change-password.e2e-spec.ts
E2E 테스트 스위트 추가. 미인증(401), 현재 비밀번호 오류, 비밀번호 확인 불일치, 정책 검증 실패, 성공 케이스 및 기존 비밀번호 무효화 시나리오 포함.
Frontend API
src/lib/api/auth.api.ts
changePassword 메서드 추가. /auth/change-password로 PATCH 요청을 수행하고 { message: string } 응답 반환.
Frontend UI & State Management
src/pages/Settings.tsx
비밀번호 변경 다이얼로그, 폼 필드(현재 비밀번호, 신규 비밀번호, 확인), 클라이언트 측 검증 추가. 이메일 계정 행에 "비밀번호 변경" 버튼 표시. 성공 시 로그아웃 및 로그인 페이지로 리다이렉트.
Frontend Tests
src/pages/__tests__/Settings.test.tsx
authApi.changePassword 및 useAuth 목업 추가. "비밀번호 변경" 버튼 표시 및 모달 열림 검증 테스트 추가.

Sequence Diagram

sequenceDiagram
    participant User as 사용자
    participant Client as 프론트엔드
    participant Auth as AuthController
    participant Service as AuthService
    participant DB as Database

    User->>Client: 현재/신규 비밀번호 입력 후 제출
    Client->>Client: 클라이언트 측 검증 (8자, 영문+숫자, 일치 확인)
    Client->>Auth: PATCH /auth/change-password + JWT<br/>(currentPassword, newPassword, confirmPassword)
    Auth->>Auth: JWT 인증 & userId 검증
    Auth->>Service: changePassword(userId, dto)
    Service->>Service: 신규/확인 비밀번호 일치 검증
    Service->>DB: userId로 email 인증 레코드 조회
    Service->>Service: 현재 비밀번호 해싱값과 DB값 비교
    alt 비밀번호 일치
        Service->>Service: 신규 비밀번호 해싱 (bcrypt)
        Service->>DB: 레코드 업데이트 (새 해시)
        Service->>DB: 사용자의 모든 refresh token 폐기
        Service-->>Auth: { message: "재로그인 필요" }
        Auth-->>Client: 200 + message
        Client->>Client: 성공 알림 표시
        Client->>Client: 로그아웃 처리
        Client->>User: 로그인 페이지로 리다이렉트
    else 비밀번호 불일치
        Service-->>Auth: 401 Unauthorized
        Auth-->>Client: 401
        Client->>User: 오류 메시지 표시
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 주요 변경 사항(로그인 상태에서 비밀번호 변경 기능)을 명확하고 간결하게 요약하고 있으며, 이슈 번호(#259)도 포함되어 있습니다.
Linked Issues check ✅ Passed PR이 이슈 #259의 모든 주요 요구사항을 충족합니다: 백엔드 엔드포인트(PATCH /auth/change-password), DTO 유효성 검사(8자 이상, 영문+숫자), 현재 비밀번호 검증, 리프레시 토큰 폐기, 프론트엔드 UI 및 모달, 성공 시 로그아웃 및 리다이렉트 구현.
Out of Scope Changes check ✅ Passed 모든 변경사항이 이슈 #259의 비밀번호 변경 기능 구현과 관련되어 있으며, 범위를 벗어난 변경사항이 없습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/issue-259-change-password
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/pages/__tests__/Settings.test.tsx (1)

157-201: 비밀번호 변경 “제출 성공 경로” 단언을 추가하면 더 안전합니다.

현재는 버튼/모달 표시까지만 검증합니다. authApi.changePassword 호출, logout 호출, /login 이동까지 확인하는 테스트를 추가하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/__tests__/Settings.test.tsx` around lines 157 - 201, Add a test
that verifies the successful password-change flow: mock authApi.changePassword
to resolve, fill in and submit the password-change form in the Settings
component (use the existing renderWithRouter and userEvent setup), then assert
authApi.changePassword was called with the current and new password values,
assert mockLogout was called, and assert the app navigated to '/login' (check
router history or the presence of the login route) to confirm logout+redirect.
Locate the form and submit button via labels/headings already used in the tests
and reuse useAuth/mockLogout to validate side effects.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/src/auth/auth.service.ts`:
- Around line 285-287: Wrap the password update and token revocation in a single
database transaction so both succeed or both roll back: inside the AuthService
method that sets auth.credential, calls this.userAuthRepository.save(auth) and
this.revokeAllRefreshTokens(userId), start a transaction (e.g., via a
QueryRunner or TypeORM transactionalEntityManager), perform the bcrypt hash
assignment to auth.credential, save using the transactional manager (not
this.userAuthRepository directly), call revokeAllRefreshTokens(userId) using the
same transaction context (or convert it to accept the transactional manager),
and commit; on error rollback and rethrow the error to ensure atomicity.

In `@backend/test/app.e2e-spec.ts`:
- Around line 15-17: The test bootstrap imports the same suite twice
("./suites/cellar.e2e-spec"), causing duplicate test execution; remove the
redundant import so each suite is imported only once (keep one import of
"./suites/cellar.e2e-spec" and delete the other), ensuring the remaining imports
(including "./suites/auth-change-password.e2e-spec") stay unchanged.

In `@backend/test/suites/auth-change-password.e2e-spec.ts`:
- Around line 101-150: Add an E2E that verifies refresh-token revocation on
password change: create a user with context.testHelper.createUser and capture
its issued refresh token (from the login response or cookie) before calling
PATCH /auth/change-password (as in the existing tests), then after asserting 200
on the password change attempt, call the refresh endpoint (e.g., POST
/auth/refresh or the route your app exposes) using the previously captured
refresh token and assert it fails (401/403 or the app's expected error). Use the
same helper methods generateUniqueEmail/createUser and existing routes
'/auth/change-password' and '/auth/login' to locate where to add the test and
ensure you reuse the original user.token if needed to authorize the change.

In `@src/pages/Settings.tsx`:
- Around line 534-535: The password form state is only cleared on success,
leaving sensitive inputs in memory when the Dialog is closed by the user; update
the Dialog with open={isChangePasswordOpen} onOpenChange={...} to detect when it
closes (open becomes false) and call the form-reset routine (e.g.,
resetChangePasswordForm or setCurrentPassword/setNewPassword/setConfirmPassword
to empty strings) so all password-related state is cleared immediately; apply
the same change to the other modal referenced (the Dialog at the earlier block
around lines 277-278) so both modals clear sensitive inputs on close.

---

Nitpick comments:
In `@src/pages/__tests__/Settings.test.tsx`:
- Around line 157-201: Add a test that verifies the successful password-change
flow: mock authApi.changePassword to resolve, fill in and submit the
password-change form in the Settings component (use the existing
renderWithRouter and userEvent setup), then assert authApi.changePassword was
called with the current and new password values, assert mockLogout was called,
and assert the app navigated to '/login' (check router history or the presence
of the login route) to confirm logout+redirect. Locate the form and submit
button via labels/headings already used in the tests and reuse
useAuth/mockLogout to validate side effects.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2b54c8aa-c5ed-4359-a9b3-b338beed9d50

📥 Commits

Reviewing files that changed from the base of the PR and between 65d4be2 and 791398f.

📒 Files selected for processing (8)
  • backend/src/auth/auth.controller.ts
  • backend/src/auth/auth.service.ts
  • backend/src/auth/dto/change-password.dto.ts
  • backend/test/app.e2e-spec.ts
  • backend/test/suites/auth-change-password.e2e-spec.ts
  • src/lib/api/auth.api.ts
  • src/pages/Settings.tsx
  • src/pages/__tests__/Settings.test.tsx

Comment on lines +285 to +287
auth.credential = await bcrypt.hash(dto.newPassword, 10);
await this.userAuthRepository.save(auth);
await this.revokeAllRefreshTokens(userId);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

비밀번호 갱신과 토큰 폐기는 트랜잭션으로 묶어야 합니다.

현재는 비밀번호 저장 후 토큰 폐기가 실패하면 부분 반영 상태가 됩니다(비밀번호는 바뀌었는데 기존 리프레시 토큰은 유효). 두 작업을 원자적으로 처리해 주세요.

🔒 제안 수정안
-    auth.credential = await bcrypt.hash(dto.newPassword, 10);
-    await this.userAuthRepository.save(auth);
-    await this.revokeAllRefreshTokens(userId);
+    await this.userAuthRepository.manager.transaction(async (manager) => {
+      auth.credential = await bcrypt.hash(dto.newPassword, 10);
+      await manager.save(UserAuthentication, auth);
+      await manager.update(
+        RefreshToken,
+        { userId, isRevoked: false },
+        { isRevoked: true },
+      );
+    });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
auth.credential = await bcrypt.hash(dto.newPassword, 10);
await this.userAuthRepository.save(auth);
await this.revokeAllRefreshTokens(userId);
await this.userAuthRepository.manager.transaction(async (manager) => {
auth.credential = await bcrypt.hash(dto.newPassword, 10);
await manager.save(UserAuthentication, auth);
await manager.update(
RefreshToken,
{ userId, isRevoked: false },
{ isRevoked: true },
);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/auth/auth.service.ts` around lines 285 - 287, Wrap the password
update and token revocation in a single database transaction so both succeed or
both roll back: inside the AuthService method that sets auth.credential, calls
this.userAuthRepository.save(auth) and this.revokeAllRefreshTokens(userId),
start a transaction (e.g., via a QueryRunner or TypeORM
transactionalEntityManager), perform the bcrypt hash assignment to
auth.credential, save using the transactional manager (not
this.userAuthRepository directly), call revokeAllRefreshTokens(userId) using the
same transaction context (or convert it to accept the transactional manager),
and commit; on error rollback and rethrow the error to ensure atomicity.

Comment on lines 15 to +17
import './suites/cellar.e2e-spec';
import './suites/cellar.e2e-spec';
import './suites/auth-change-password.e2e-spec';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

중복된 테스트 스위트 import를 제거해주세요.

./suites/cellar.e2e-spec가 두 번 import되어 해당 스위트가 중복 실행될 수 있습니다.

🔧 제안 수정안
 import './suites/notes-schemas.e2e-spec';
 import './suites/cellar.e2e-spec';
-import './suites/cellar.e2e-spec';
 import './suites/auth-change-password.e2e-spec';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/test/app.e2e-spec.ts` around lines 15 - 17, The test bootstrap
imports the same suite twice ("./suites/cellar.e2e-spec"), causing duplicate
test execution; remove the redundant import so each suite is imported only once
(keep one import of "./suites/cellar.e2e-spec" and delete the other), ensuring
the remaining imports (including "./suites/auth-change-password.e2e-spec") stay
unchanged.

Comment on lines +101 to +150
it('PATCH /auth/change-password - 정상 변경 후 200 및 새 비밀번호로 로그인 성공', async () => {
const uniqueEmail = context.testHelper.generateUniqueEmail('change-pw');
const user = await context.testHelper.createUser('Change Password Success', uniqueEmail, 'password123');

await request(context.app.getHttpServer())
.patch('/auth/change-password')
.set('Authorization', `Bearer ${user.token}`)
.send({
currentPassword: 'password123',
newPassword: 'newPassword456',
confirmPassword: 'newPassword456',
})
.expect(200)
.expect((res) => {
expect(res.body).toHaveProperty('message');
});

// 새 비밀번호로 로그인 성공 확인
return request(context.app.getHttpServer())
.post('/auth/login')
.send({
email: uniqueEmail,
password: 'newPassword456',
})
.expect(201);
});

it('PATCH /auth/change-password - 변경 후 이전 비밀번호로 로그인 실패', async () => {
const uniqueEmail = context.testHelper.generateUniqueEmail('change-pw-old');
const user = await context.testHelper.createUser('Change Password Old', uniqueEmail, 'password123');

await request(context.app.getHttpServer())
.patch('/auth/change-password')
.set('Authorization', `Bearer ${user.token}`)
.send({
currentPassword: 'password123',
newPassword: 'newPassword456',
confirmPassword: 'newPassword456',
})
.expect(200);

// 이전 비밀번호로 로그인 실패 확인
return request(context.app.getHttpServer())
.post('/auth/login')
.send({
email: uniqueEmail,
password: 'password123',
})
.expect(401);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

리프레시 토큰 전체 폐기 요구사항을 검증하는 E2E가 필요합니다.

현재 성공/실패 로그인은 검증하지만, “비밀번호 변경 후 기존 refresh token 사용 불가”는 직접 검증하지 않습니다. 이 케이스를 추가해 주세요.

🧪 추가 테스트 예시
+  it('PATCH /auth/change-password - 변경 후 기존 refresh token으로 재발급 실패', async () => {
+    const email = context.testHelper.generateUniqueEmail('change-pw-refresh');
+    await context.testHelper.createUser('Change Password Refresh', email, 'password123');
+
+    const loginRes = await request(context.app.getHttpServer())
+      .post('/auth/login')
+      .send({ email, password: 'password123' })
+      .expect(201);
+
+    const accessToken = loginRes.body.access_token;
+    const refreshCookie = (loginRes.headers['set-cookie'] || []).find((c: string) =>
+      c.startsWith('refresh_token='),
+    );
+    expect(refreshCookie).toBeDefined();
+
+    await request(context.app.getHttpServer())
+      .patch('/auth/change-password')
+      .set('Authorization', `Bearer ${accessToken}`)
+      .send({
+        currentPassword: 'password123',
+        newPassword: 'newPassword456',
+        confirmPassword: 'newPassword456',
+      })
+      .expect(200);
+
+    await request(context.app.getHttpServer())
+      .post('/auth/refresh')
+      .set('Cookie', refreshCookie as string)
+      .expect(401);
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/test/suites/auth-change-password.e2e-spec.ts` around lines 101 - 150,
Add an E2E that verifies refresh-token revocation on password change: create a
user with context.testHelper.createUser and capture its issued refresh token
(from the login response or cookie) before calling PATCH /auth/change-password
(as in the existing tests), then after asserting 200 on the password change
attempt, call the refresh endpoint (e.g., POST /auth/refresh or the route your
app exposes) using the previously captured refresh token and assert it fails
(401/403 or the app's expected error). Use the same helper methods
generateUniqueEmail/createUser and existing routes '/auth/change-password' and
'/auth/login' to locate where to add the test and ensure you reuse the original
user.token if needed to authorize the change.

Comment thread src/pages/Settings.tsx
Comment on lines +534 to +535
<Dialog open={isChangePasswordOpen} onOpenChange={setIsChangePasswordOpen}>
<DialogContent>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

모달 닫기 시 비밀번호 입력값을 즉시 초기화해주세요.

현재는 성공 시에만 폼을 비우고, 사용자가 닫기(X/외부 클릭)로 종료하면 민감 입력값이 state에 남습니다.

🧹 제안 수정안
-      <Dialog open={isChangePasswordOpen} onOpenChange={setIsChangePasswordOpen}>
+      <Dialog
+        open={isChangePasswordOpen}
+        onOpenChange={(open) => {
+          setIsChangePasswordOpen(open);
+          if (!open) {
+            setChangePasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' });
+          }
+        }}
+      >

Also applies to: 277-278

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Settings.tsx` around lines 534 - 535, The password form state is
only cleared on success, leaving sensitive inputs in memory when the Dialog is
closed by the user; update the Dialog with open={isChangePasswordOpen}
onOpenChange={...} to detect when it closes (open becomes false) and call the
form-reset routine (e.g., resetChangePasswordForm or
setCurrentPassword/setNewPassword/setConfirmPassword to empty strings) so all
password-related state is cleared immediately; apply the same change to the
other modal referenced (the Dialog at the earlier block around lines 277-278) so
both modals clear sensitive inputs on close.

@FLYLIKEB FLYLIKEB merged commit 24a4817 into main Mar 16, 2026
3 checks passed
@FLYLIKEB FLYLIKEB deleted the feature/issue-259-change-password branch March 16, 2026 04:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 로그인 상태에서 비밀번호 변경

1 participant