Files
pos-system/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/AuthService.cs
Ho Ngoc Hai deffb9de4a fix: resolve attendance staffName display and token conflict between staff/admin sessions
1. Attendance API now joins with MerchantStaff to return staffName instead of showing truncated staffId
2. AuthService uses role-suffixed localStorage keys (aPOS_token_owner, aPOS_token_staff) to prevent
   staff and admin tokens from overwriting each other on the same origin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:38:01 +07:00

243 lines
9.7 KiB
C#

// EN: Authentication service — handles register, login, token management.
// VI: Service xác thực — xử lý đăng ký, đăng nhập, quản lý token.
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.JSInterop;
using WebClientTpos.Shared.DTOs;
using WebClientTpos.Shared;
namespace WebClientTpos.Client.Services;
/// <summary>
/// EN: Authentication service for Duende IdentityServer integration.
/// VI: Service xác thực tích hợp Duende IdentityServer.
/// </summary>
public class AuthService
{
private readonly HttpClient _http;
private readonly IJSRuntime _js;
private readonly AuthStateService _authState;
// EN: Role-based localStorage keys to prevent token conflict between admin/staff sessions.
// VI: Dùng key localStorage theo role để tránh xung đột token giữa admin/staff.
private const string TokenKeyPrefix = "aPOS_token";
private const string UserEmailKeyPrefix = "aPOS_email";
private const string UserNameKeyPrefix = "aPOS_name";
// EN: Legacy keys for backward compatibility (read-only fallback).
// VI: Key cũ để tương thích ngược (chỉ đọc fallback).
private const string LegacyTokenKey = "aPOS_token";
private const string LegacyEmailKey = "aPOS_email";
private static string TokenKey(string role) => $"{TokenKeyPrefix}_{role}";
private static string UserEmailKey(string role) => $"{UserEmailKeyPrefix}_{role}";
private static string UserNameKey(string role) => $"{UserNameKeyPrefix}_{role}";
// EN: Duende IdentityServer client config
// VI: Cấu hình client Duende IdentityServer
private const string ClientId = "password-client";
private const string ClientSecret = "password-client-secret";
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true
};
public AuthService(HttpClient http, IJSRuntime js, AuthStateService authState)
{
_http = http;
_js = js;
_authState = authState;
}
/// <summary>
/// EN: Register a new user account.
/// VI: Đăng ký tài khoản mới.
/// </summary>
public async Task<(bool Success, string? Error)> RegisterAsync(RegisterDto dto)
{
try
{
// EN: Build payload with FirstName/LastName from form (IAM requires these)
// VI: Tạo payload với FirstName/LastName từ form (IAM yêu cầu)
var payload = new
{
dto.Email,
dto.Password,
dto.FirstName,
dto.LastName
};
// EN: Use PascalCase serialization to match backend RegisterUserCommand record params
// VI: Dùng PascalCase serialization để khớp với record params backend
var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = null };
var jsonContent = JsonContent.Create(payload, options: jsonOptions);
var response = await _http.PostAsync("/api/auth/register", jsonContent);
if (response.IsSuccessStatusCode)
{
return (true, null);
}
var content = await response.Content.ReadAsStringAsync();
try
{
// EN: Try to parse structured validation errors from IAM
// VI: Parse lỗi validation có cấu trúc từ IAM
using var doc = JsonDocument.Parse(content);
if (doc.RootElement.TryGetProperty("errors", out var errors))
{
var msgs = new List<string>();
foreach (var prop in errors.EnumerateObject())
{
foreach (var err in prop.Value.EnumerateArray())
msgs.Add(err.GetString() ?? prop.Name);
}
return (false, string.Join("; ", msgs));
}
if (doc.RootElement.TryGetProperty("title", out var title))
return (false, title.GetString());
return (false, content.Length > 200 ? "Đăng ký thất bại" : content);
}
catch
{
return (false, "Đăng ký thất bại");
}
}
catch (Exception ex)
{
return (false, $"Lỗi ({ex.GetType().Name})");
}
}
/// <summary>
/// EN: Login via Duende IdentityServer password grant.
/// VI: Đăng nhập qua Duende IdentityServer password grant.
/// </summary>
public async Task<(bool Success, string? Error)> LoginAsync(string email, string password, string role = "owner")
{
try
{
// EN: Build form-urlencoded body for token endpoint
// VI: Tạo body form-urlencoded cho token endpoint
var formData = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "password"),
new KeyValuePair<string, string>("client_id", ClientId),
new KeyValuePair<string, string>("client_secret", ClientSecret),
new KeyValuePair<string, string>("username", email),
new KeyValuePair<string, string>("password", password),
new KeyValuePair<string, string>("scope", "openid profile email api"),
});
var response = await _http.PostAsync("/api/iam/connect/token", formData);
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
var token = JsonSerializer.Deserialize<TokenResponse>(json, JsonOptions);
if (token != null && !string.IsNullOrEmpty(token.AccessToken))
{
// EN: Save token with role-specific key to prevent admin/staff conflict.
// VI: Lưu token với key theo role để tránh xung đột admin/staff.
await _js.InvokeVoidAsync("localStorage.setItem", TokenKey(role), token.AccessToken);
await _js.InvokeVoidAsync("localStorage.setItem", UserEmailKey(role), email);
_authState.Login(email, token.AccessToken, role);
return (true, null);
}
return (false, "Token không hợp lệ");
}
var errorContent = await response.Content.ReadAsStringAsync();
if (errorContent.Contains("invalid_grant"))
{
return (false, "Email hoặc mật khẩu không đúng");
}
return (false, $"Đăng nhập thất bại ({response.StatusCode})");
}
catch (Exception ex)
{
return (false, $"Lỗi kết nối: {ex.Message}");
}
}
/// <summary>
/// EN: Get stored access token.
/// VI: Lấy access token đã lưu.
/// </summary>
public async Task<string?> GetTokenAsync(string role = "owner")
{
try
{
var token = await _js.InvokeAsync<string?>("localStorage.getItem", TokenKey(role));
if (string.IsNullOrEmpty(token))
token = await _js.InvokeAsync<string?>("localStorage.getItem", LegacyTokenKey);
return token;
}
catch
{
return _authState.Token;
}
}
/// <summary>
/// EN: Try to restore session from localStorage on app start.
/// VI: Khôi phục session từ localStorage khi app khởi động.
/// </summary>
/// <summary>
/// EN: Try to restore session from localStorage. Uses role-specific keys, falls back to legacy keys.
/// VI: Khôi phục session từ localStorage. Dùng key theo role, fallback sang key cũ.
/// </summary>
public async Task TryRestoreSessionAsync(string role = "owner")
{
try
{
// EN: Try role-specific keys first, then fall back to legacy keys.
// VI: Thử key theo role trước, rồi fallback sang key cũ.
var token = await _js.InvokeAsync<string?>("localStorage.getItem", TokenKey(role));
var email = await _js.InvokeAsync<string?>("localStorage.getItem", UserEmailKey(role));
if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(email))
{
// EN: Fallback to legacy keys for backward compatibility.
// VI: Fallback sang key cũ để tương thích ngược.
token = await _js.InvokeAsync<string?>("localStorage.getItem", LegacyTokenKey);
email = await _js.InvokeAsync<string?>("localStorage.getItem", LegacyEmailKey);
}
if (!string.IsNullOrEmpty(token) && !string.IsNullOrEmpty(email))
{
_authState.Login(email, token, role);
}
}
catch { /* localStorage not available */ }
}
/// <summary>
/// EN: Logout and clear stored data.
/// VI: Đăng xuất và xóa dữ liệu đã lưu.
/// </summary>
public async Task LogoutAsync(string role = "owner")
{
try
{
await _js.InvokeVoidAsync("localStorage.removeItem", TokenKey(role));
await _js.InvokeVoidAsync("localStorage.removeItem", UserEmailKey(role));
await _js.InvokeVoidAsync("localStorage.removeItem", UserNameKey(role));
// EN: Also clear legacy keys on logout.
// VI: Cũng xóa key cũ khi logout.
await _js.InvokeVoidAsync("localStorage.removeItem", LegacyTokenKey);
await _js.InvokeVoidAsync("localStorage.removeItem", LegacyEmailKey);
}
catch { }
_authState.Logout();
}
}