From e81e87ff9f26c22d31e9cefe996c1170c6dce8ab Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Fri, 26 Jun 2026 12:27:52 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20Auth:=20user=20t=E1=BB=B1=20=C4=91?= =?UTF-8?q?=E1=BB=95i=20m=E1=BA=ADt=20kh=E1=BA=A9u=20(self-service=20chang?= =?UTF-8?q?e-password)=20=E2=80=94=20BE=20+=202=20FE=20+=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Go-live request anh Kiệt FDC (Zalo "user chưa tự đổi pass được hả em"): trước đây chỉ admin Reset hộ (POST users/{id}/reset-password), user KHÔNG tự đổi được. Thêm self-service: - BE: ChangePasswordCommand + POST /api/auth/change-password ([Authorize]). Lấy user qua ICurrentUser.UserId; UserManager.ChangePasswordAsync verify mật khẩu HIỆN TẠI; sai → ValidationException field CurrentPassword "Mật khẩu hiện tại không đúng."; rule mật khẩu mới ≥ 12 + khác mật khẩu cũ (validator); sau đổi vô hiệu refresh token (mirror ResetPassword). Qualify SolutionErp...ValidationException (tránh CS0104 clash với FluentValidation). - FE 2 app (SHA-mirror): ChangePasswordDialog (3 ô current/new/confirm + validate client: ≥12, khớp, khác cũ) wire vào menu tài khoản TopBar — "Đổi mật khẩu" trên "Đăng xuất". - Test: +ChangePasswordCommandTests (đúng/sai pass cũ keyed-field + msg, <12 boundary, ==cũ, chưa-auth/UserId-null/user-not-found, RefreshToken cleared). Full suite 431 PASS (45D + 386I). Build: BE 0 warn/0 err · fe-user + fe-admin build OK. Co-Authored-By: Claude Opus 4.8 --- .../src/components/ChangePasswordDialog.tsx | 115 +++++++++ fe-admin/src/components/TopBar.tsx | 15 +- .../src/components/ChangePasswordDialog.tsx | 115 +++++++++ fe-user/src/components/TopBar.tsx | 15 +- .../Controllers/AuthController.cs | 10 + .../ChangePassword/ChangePasswordCommand.cs | 58 +++++ .../Application/ChangePasswordCommandTests.cs | 243 ++++++++++++++++++ 7 files changed, 569 insertions(+), 2 deletions(-) create mode 100644 fe-admin/src/components/ChangePasswordDialog.tsx create mode 100644 fe-user/src/components/ChangePasswordDialog.tsx create mode 100644 src/Backend/SolutionErp.Application/Auth/Commands/ChangePassword/ChangePasswordCommand.cs create mode 100644 tests/SolutionErp.Infrastructure.Tests/Application/ChangePasswordCommandTests.cs diff --git a/fe-admin/src/components/ChangePasswordDialog.tsx b/fe-admin/src/components/ChangePasswordDialog.tsx new file mode 100644 index 0000000..35ea66b --- /dev/null +++ b/fe-admin/src/components/ChangePasswordDialog.tsx @@ -0,0 +1,115 @@ +import { useState } from 'react' +import { useMutation } from '@tanstack/react-query' +import { toast } from 'sonner' +import { KeyRound } from 'lucide-react' +import { Dialog } from '@/components/ui/Dialog' +import { Button } from '@/components/ui/Button' +import { Input } from '@/components/ui/Input' +import { Label } from '@/components/ui/Label' +import { api } from '@/lib/api' +import { getErrorMessage } from '@/lib/apiError' + +type Props = { open: boolean; onClose: () => void } + +// Self-service đổi mật khẩu — user tự đổi (nhập mật khẩu hiện tại để xác thực). +// BE: POST /auth/change-password (Identity verify pass cũ). Khác admin Reset. +export function ChangePasswordDialog({ open, onClose }: Props) { + const [current, setCurrent] = useState('') + const [next, setNext] = useState('') + const [confirm, setConfirm] = useState('') + + const close = () => { + setCurrent('') + setNext('') + setConfirm('') + onClose() + } + + const mutation = useMutation({ + mutationFn: () => + api.post('/auth/change-password', { currentPassword: current, newPassword: next }), + onSuccess: () => { + toast.success('Đổi mật khẩu thành công. Lần đăng nhập sau dùng mật khẩu mới.') + close() + }, + onError: err => toast.error(getErrorMessage(err)), + }) + + const newTooShort = next.length > 0 && next.length < 12 + const sameAsOld = next.length > 0 && next === current + const mismatch = confirm.length > 0 && next !== confirm + const canSubmit = + current.length > 0 && next.length >= 12 && next === confirm && next !== current && !mutation.isPending + + const submit = () => { + if (canSubmit) mutation.mutate() + } + + return ( + + Đổi mật khẩu + + } + footer={ + <> + + + + } + > +
{ + e.preventDefault() + submit() + }} + > +
+ + setCurrent(e.target.value)} + /> +
+
+ + setNext(e.target.value)} + /> + {newTooShort &&

Mật khẩu mới phải có ít nhất 12 ký tự.

} + {sameAsOld &&

Mật khẩu mới phải khác mật khẩu hiện tại.

} +
+
+ + setConfirm(e.target.value)} + /> + {mismatch &&

Mật khẩu nhập lại không khớp.

} +
+

Mật khẩu cần tối thiểu 12 ký tự.

+ {/* cho phép submit bằng Enter */} +
+ ) +} diff --git a/fe-admin/src/components/TopBar.tsx b/fe-admin/src/components/TopBar.tsx index 43a0777..c99b9aa 100644 --- a/fe-admin/src/components/TopBar.tsx +++ b/fe-admin/src/components/TopBar.tsx @@ -1,12 +1,14 @@ import { useEffect, useRef, useState } from 'react' -import { ChevronDown, LogOut } from 'lucide-react' +import { ChevronDown, LogOut, KeyRound } from 'lucide-react' import { useAuth } from '@/contexts/AuthContext' +import { ChangePasswordDialog } from '@/components/ChangePasswordDialog' import { NotificationBell } from '@/components/NotificationBell' import { cn } from '@/lib/cn' function UserMenu() { const { user, logout } = useAuth() const [open, setOpen] = useState(false) + const [pwOpen, setPwOpen] = useState(false) const ref = useRef(null) useEffect(() => { @@ -53,6 +55,16 @@ function UserMenu() { )} + + + + } + > +
{ + e.preventDefault() + submit() + }} + > +
+ + setCurrent(e.target.value)} + /> +
+
+ + setNext(e.target.value)} + /> + {newTooShort &&

Mật khẩu mới phải có ít nhất 12 ký tự.

} + {sameAsOld &&

Mật khẩu mới phải khác mật khẩu hiện tại.

} +
+
+ + setConfirm(e.target.value)} + /> + {mismatch &&

Mật khẩu nhập lại không khớp.

} +
+

Mật khẩu cần tối thiểu 12 ký tự.

+ {/* cho phép submit bằng Enter */} +