All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m10s
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 <noreply@anthropic.com>
116 lines
3.9 KiB
TypeScript
116 lines
3.9 KiB
TypeScript
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 (
|
|
<Dialog
|
|
open={open}
|
|
onClose={close}
|
|
size="sm"
|
|
title={
|
|
<span className="flex items-center gap-2">
|
|
<KeyRound className="h-4 w-4" /> Đổi mật khẩu
|
|
</span>
|
|
}
|
|
footer={
|
|
<>
|
|
<Button variant="outline" onClick={close} disabled={mutation.isPending}>
|
|
Hủy
|
|
</Button>
|
|
<Button onClick={submit} disabled={!canSubmit}>
|
|
{mutation.isPending ? 'Đang lưu…' : 'Đổi mật khẩu'}
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
<form
|
|
className="space-y-3"
|
|
onSubmit={e => {
|
|
e.preventDefault()
|
|
submit()
|
|
}}
|
|
>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="cp-current">Mật khẩu hiện tại</Label>
|
|
<Input
|
|
id="cp-current"
|
|
type="password"
|
|
autoComplete="current-password"
|
|
value={current}
|
|
onChange={e => setCurrent(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="cp-new">Mật khẩu mới</Label>
|
|
<Input
|
|
id="cp-new"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
value={next}
|
|
onChange={e => setNext(e.target.value)}
|
|
/>
|
|
{newTooShort && <p className="text-xs text-red-600">Mật khẩu mới phải có ít nhất 12 ký tự.</p>}
|
|
{sameAsOld && <p className="text-xs text-red-600">Mật khẩu mới phải khác mật khẩu hiện tại.</p>}
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="cp-confirm">Nhập lại mật khẩu mới</Label>
|
|
<Input
|
|
id="cp-confirm"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
value={confirm}
|
|
onChange={e => setConfirm(e.target.value)}
|
|
/>
|
|
{mismatch && <p className="text-xs text-red-600">Mật khẩu nhập lại không khớp.</p>}
|
|
</div>
|
|
<p className="text-xs text-slate-500">Mật khẩu cần tối thiểu 12 ký tự.</p>
|
|
{/* cho phép submit bằng Enter */}
|
|
<button type="submit" className="hidden" aria-hidden tabIndex={-1} />
|
|
</form>
|
|
</Dialog>
|
|
)
|
|
}
|