feat(staff): add password change for existing staff members

- Add "Đổi mật khẩu" toggle in staff edit form with new password
  and confirm password fields, validation (min 8 chars, match check)
- Add ResetStaffPasswordAsync() in PosDataService
- Add POST /api/bff/staff/reset-password BFF endpoint proxying to
  IAM service /api/v1/users/{userId}/reset-password
- Reset password state fields when opening edit form

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ho Ngoc Hai
2026-03-30 10:22:18 +07:00
parent 420100309b
commit ccb7716ba1
3 changed files with 90 additions and 1 deletions

View File

@@ -67,6 +67,22 @@
}
</div>
}
else
{
@* EN: Password change section for existing staff / VI: Phần đổi mật khẩu cho nhân viên đã có *@
<div style="margin-top:12px;padding:12px;border-radius:8px;background:rgba(139,92,246,0.05);border:1px solid rgba(139,92,246,0.2);">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;font-weight:600;">
<input type="checkbox" @bind="_changePassword" /> Đổi mật khẩu
</label>
@if (_changePassword)
{
<div style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div><label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Mật khẩu mới *</label><input type="password" @bind="_newStaffPassword" placeholder="Min 8 ký tự" style="width:100%;padding:8px 12px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:var(--admin-bg-elevated);color:var(--admin-text-primary);" /></div>
<div><label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Xác nhận mật khẩu *</label><input type="password" @bind="_confirmPassword" placeholder="Nhập lại mật khẩu" style="width:100%;padding:8px 12px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:var(--admin-bg-elevated);color:var(--admin-text-primary);" /></div>
</div>
}
</div>
}
<div style="display:flex;gap:8px;margin-top:16px;">
<button class="admin-btn-primary" @onclick="@(_editingStaffId.HasValue ? SaveStaffEdit : AddStaff)" style="display:inline-flex;align-items:center;gap:6px;"><i data-lucide="check" style="width:14px;height:14px;"></i>@(_editingStaffId.HasValue ? "Cập nhật" : "Lưu")</button>
<button @onclick="@(() => _showStaffForm = false)" style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-secondary);cursor:pointer;"><i data-lucide="x" style="width:14px;height:14px;"></i>Hủy</button>
@@ -147,6 +163,8 @@ else if (_staff.Any())
private string _newStaffFirstName = "";
private string _newStaffLastName = "";
private string _newStaffPassword = "";
private bool _changePassword;
private string _confirmPassword = "";
// Staff extended fields state
private string _newStaffAddress = "";
// Image upload state for staff docs
@@ -222,6 +240,9 @@ else if (_staff.Any())
_staffDocFrontPreview = s.DocumentFrontUrl;
_staffDocBackPreview = s.DocumentBackUrl;
_staffFormMessage = null;
_changePassword = false;
_newStaffPassword = "";
_confirmPassword = "";
_showStaffForm = true;
}
@@ -232,6 +253,18 @@ else if (_staff.Any())
{
_staffFormMessage = "Vui lòng nhập Mã NV."; _staffFormSuccess = false; return;
}
// EN: Validate password change if requested / VI: Validate đổi mật khẩu nếu yêu cầu
if (_changePassword)
{
if (string.IsNullOrWhiteSpace(_newStaffPassword) || _newStaffPassword.Length < 8)
{
_staffFormMessage = "Mật khẩu mới phải có ít nhất 8 ký tự."; _staffFormSuccess = false; return;
}
if (_newStaffPassword != _confirmPassword)
{
_staffFormMessage = "Mật khẩu xác nhận không khớp."; _staffFormSuccess = false; return;
}
}
try
{
var docFrontUrl = await UploadFileIfNeeded(_staffDocFrontFile) ?? (_staffDocFrontPreview?.StartsWith("data:") == true ? null : _staffDocFrontPreview);
@@ -239,8 +272,35 @@ else if (_staff.Any())
await DataService.UpdateStaffAsync(_editingStaffId.Value, new PosDataService.CreateStaffRequest(
_merchantId.Value, _newStaffCode, _newStaffPhone, _newStaffEmail, _newStaffRole,
_newStaffFirstName, _newStaffLastName, _newStaffAddress, null, docFrontUrl, docBackUrl));
_staffFormMessage = $"Đã cập nhật NV '{_newStaffCode}' thành công!"; _staffFormSuccess = true;
// EN: Change password if requested / VI: Đổi mật khẩu nếu yêu cầu
if (_changePassword && !string.IsNullOrWhiteSpace(_newStaffPassword))
{
var editingStaff = _staff.FirstOrDefault(s => s.Id == _editingStaffId);
if (editingStaff?.UserId != null)
{
var (pwOk, pwErr) = await DataService.ResetStaffPasswordAsync(editingStaff.UserId.Value, _newStaffPassword);
if (!pwOk)
{
_staffFormMessage = $"Đã cập nhật thông tin NV, nhưng đổi mật khẩu thất bại: {pwErr}";
_staffFormSuccess = false;
_staff = await DataService.GetStaffForShopAsync(ShopId);
return;
}
}
else
{
_staffFormMessage = "Đã cập nhật thông tin NV, nhưng nhân viên chưa có tài khoản IAM để đổi mật khẩu.";
_staffFormSuccess = false;
_staff = await DataService.GetStaffForShopAsync(ShopId);
return;
}
}
_staffFormMessage = $"Đã cập nhật NV '{_newStaffCode}' thành công!{(_changePassword ? " Mật khẩu đã được đổi." : "")}";
_staffFormSuccess = true;
_editingStaffId = null;
_changePassword = false; _newStaffPassword = ""; _confirmPassword = "";
_staffDocFrontFile = null; _staffDocBackFile = null; _staffDocFrontPreview = null; _staffDocBackPreview = null;
_staff = await DataService.GetStaffForShopAsync(ShopId);
}

View File

@@ -1191,6 +1191,19 @@ public class PosDataService
return (false, err);
}
/// <summary>
/// EN: Reset a staff member's password via IAM service (admin action).
/// VI: Đặt lại mật khẩu nhân viên qua IAM service (hành động admin).
/// </summary>
public async Task<(bool Ok, string? Error)> ResetStaffPasswordAsync(Guid userId, string newPassword)
{
var resp = await _http.PostAsJsonAsync("api/bff/staff/reset-password",
new { userId, newPassword }, _writeOptions);
if (resp.IsSuccessStatusCode) return (true, null);
var err = await TryExtractError(resp);
return (false, err);
}
// ═══ STORAGE / DRIVE ═══
public record StorageFileInfo(Guid Id, string FileName, string? ContentType, long FileSizeBytes, string? AccessLevel, DateTime UploadedAt);

View File

@@ -124,6 +124,22 @@ public class StaffController : ControllerBase
}
}
/// <summary>
/// EN: Reset a staff member's password via IAM service (admin action).
/// VI: Đặt lại mật khẩu nhân viên qua IAM service (hành động admin).
/// </summary>
[HttpPost("staff/reset-password")]
public async Task<IActionResult> ResetStaffPassword([FromBody] JsonElement body)
{
var userId = body.TryGetProperty("userId", out var uid) ? uid.GetString() : null;
var newPassword = body.TryGetProperty("newPassword", out var np) ? np.GetString() : null;
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(newPassword))
return BadRequest(new { success = false, message = "userId và newPassword là bắt buộc." });
var payload = new { userId, newPassword };
return await _iam.PostAsJsonAsync($"/api/v1/users/{userId}/reset-password", payload).ProxyAsync();
}
/// <summary>
/// EN: Update a staff member.
/// VI: Cập nhật nhân viên.