// 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; /// /// EN: Authentication service for Duende IdentityServer integration. /// VI: Service xác thực tích hợp Duende IdentityServer. /// 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; } /// /// EN: Register a new user account. /// VI: Đăng ký tài khoản mới. /// 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(); 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})"); } } /// /// EN: Login via Duende IdentityServer password grant. /// VI: Đăng nhập qua Duende IdentityServer password grant. /// 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("grant_type", "password"), new KeyValuePair("client_id", ClientId), new KeyValuePair("client_secret", ClientSecret), new KeyValuePair("username", email), new KeyValuePair("password", password), new KeyValuePair("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(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}"); } } /// /// EN: Get stored access token. /// VI: Lấy access token đã lưu. /// public async Task GetTokenAsync(string role = "owner") { try { var token = await _js.InvokeAsync("localStorage.getItem", TokenKey(role)); if (string.IsNullOrEmpty(token)) token = await _js.InvokeAsync("localStorage.getItem", LegacyTokenKey); return token; } catch { return _authState.Token; } } /// /// EN: Try to restore session from localStorage on app start. /// VI: Khôi phục session từ localStorage khi app khởi động. /// /// /// 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ũ. /// 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("localStorage.getItem", TokenKey(role)); var email = await _js.InvokeAsync("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("localStorage.getItem", LegacyTokenKey); email = await _js.InvokeAsync("localStorage.getItem", LegacyEmailKey); } if (!string.IsNullOrEmpty(token) && !string.IsNullOrEmpty(email)) { _authState.Login(email, token, role); } } catch { /* localStorage not available */ } } /// /// EN: Logout and clear stored data. /// VI: Đăng xuất và xóa dữ liệu đã lưu. /// 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(); } }