fix: migrate to IDbContextFactory and remove direct AppDbContext from DI (#11)

Reviewed-on: #11
Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Co-committed-by: Marek Jasiński <jasins.marek@gmail.com>
This commit was merged in pull request #11.
This commit is contained in:
2026-05-07 16:39:21 +00:00
committed by Marek Jaisński
parent 3faecbb639
commit 2248a2b757
35 changed files with 983 additions and 215 deletions
@@ -59,6 +59,13 @@
<div class="auth-error">@_errorMessage</div>
}
<div class="auth-options">
<label class="remember-me">
<InputCheckbox @bind-Value="_loginModel.RememberMe" />
<span>Zapamiętaj mnie</span>
</label>
</div>
<button type="submit" class="btn-submit-auth" disabled="@_isSubmitting">
@if (_isSubmitting)
{
@@ -95,7 +102,7 @@
try
{
var success = await IdentityService.LoginAsync(_loginModel.Email, _loginModel.Password);
var success = await IdentityService.LoginAsync(_loginModel.Email, _loginModel.Password, _loginModel.RememberMe);
if (success) NavigationManager.NavigateTo("/");
else _errorMessage = "Nieprawidłowy e-mail lub hasło.";
}
@@ -114,5 +121,7 @@
[System.ComponentModel.DataAnnotations.Required]
public string Password { get; set; } = string.Empty;
public bool RememberMe { get; set; }
}
}
@@ -6,7 +6,7 @@ namespace NexusReader.UI.Shared.Services;
public interface IIdentityService
{
Task<bool> RegisterAsync(string email, string password);
Task<bool> LoginAsync(string email, string password);
Task<bool> LoginAsync(string email, string password, bool rememberMe = false);
Task LogoutAsync();
Task<UserProfile?> GetProfileAsync();
Task<bool> RefreshTokenAsync();
@@ -45,7 +45,7 @@ public class IdentityService : IIdentityService
return response.IsSuccessStatusCode;
}
public async Task<bool> LoginAsync(string email, string password)
public async Task<bool> LoginAsync(string email, string password, bool rememberMe = false)
{
var response = await _httpClient.PostAsJsonAsync("identity/login", new { email, password });
@@ -59,7 +59,22 @@ public class IdentityService : IIdentityService
{
await _storageService.SaveSecureString(RefreshTokenKey, result.RefreshToken);
}
_authStateProvider.NotifyUserAuthentication(result.AccessToken);
// Option A: Fetch profile to get claims
var profile = await GetProfileAsync();
if (profile != null)
{
await _storageService.SaveSecureString("nexus_user_email", profile.Email);
await _storageService.SaveSecureString("nexus_user_tenant", profile.TenantId.ToString());
_authStateProvider.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString());
}
else
{
// Fallback if profile fetch fails
_authStateProvider.NotifyUserAuthentication(email, "unknown");
}
return true;
}
}
@@ -71,6 +86,8 @@ public class IdentityService : IIdentityService
{
_storageService.RemoveSecure(TokenKey);
_storageService.RemoveSecure(RefreshTokenKey);
_storageService.RemoveSecure("nexus_user_email");
_storageService.RemoveSecure("nexus_user_tenant");
_authStateProvider.NotifyUserLogout();
}
@@ -105,7 +122,15 @@ public class IdentityService : IIdentityService
{
await _storageService.SaveSecureString(RefreshTokenKey, loginResult.RefreshToken);
}
_authStateProvider.NotifyUserAuthentication(loginResult.AccessToken);
var profile = await GetProfileAsync();
if (profile != null)
{
await _storageService.SaveSecureString("nexus_user_email", profile.Email);
await _storageService.SaveSecureString("nexus_user_tenant", profile.TenantId.ToString());
_authStateProvider.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString());
}
return true;
}
}
@@ -19,15 +19,30 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider
{
try
{
var result = await _storageService.GetSecureString(TokenKey);
var token = result.IsSuccess ? result.Value : null;
var tokenResult = await _storageService.GetSecureString(TokenKey);
var token = tokenResult.IsSuccess ? tokenResult.Value : null;
if (string.IsNullOrWhiteSpace(token))
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
var identity = new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt");
// For opaque tokens, we read the user info that was stored during login
var emailResult = await _storageService.GetSecureString("nexus_user_email");
var tenantIdResult = await _storageService.GetSecureString("nexus_user_tenant");
var claims = new List<Claim>();
if (emailResult.IsSuccess && !string.IsNullOrEmpty(emailResult.Value))
{
claims.Add(new Claim(ClaimTypes.Name, emailResult.Value));
claims.Add(new Claim(ClaimTypes.Email, emailResult.Value));
}
if (tenantIdResult.IsSuccess && !string.IsNullOrEmpty(tenantIdResult.Value))
{
claims.Add(new Claim("TenantId", tenantIdResult.Value));
}
var identity = new ClaimsIdentity(claims, "OpaqueBearer");
var user = new ClaimsPrincipal(identity);
return new AuthenticationState(user);
@@ -38,9 +53,16 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider
}
}
public void NotifyUserAuthentication(string token)
public void NotifyUserAuthentication(string email, string tenantId)
{
var identity = new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt");
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, email),
new Claim(ClaimTypes.Email, email),
new Claim("TenantId", tenantId)
};
var identity = new ClaimsIdentity(claims, "OpaqueBearer");
var user = new ClaimsPrincipal(identity);
var authState = Task.FromResult(new AuthenticationState(user));
NotifyAuthenticationStateChanged(authState);
@@ -52,30 +74,4 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider
var authState = Task.FromResult(new AuthenticationState(guest));
NotifyAuthenticationStateChanged(authState);
}
private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var claims = new List<Claim>();
var payload = jwt.Split('.')[1];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
if (keyValuePairs != null)
{
claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString() ?? string.Empty)));
}
return claims;
}
private byte[] ParseBase64WithoutPadding(string base64)
{
switch (base64.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
return Convert.FromBase64String(base64);
}
}
@@ -74,6 +74,28 @@
.toggle-visibility { position: absolute; right: 16px; background: none; border: none; color: var(--nexus-text-muted); cursor: pointer; padding: 4px; z-index: 5; }
.auth-options {
display: flex;
justify-content: flex-start;
padding: 0 4px;
}
.remember-me {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 0.85rem;
color: var(--nexus-text-muted);
user-select: none;
}
.remember-me input {
accent-color: var(--nexus-primary);
width: 14px;
height: 14px;
}
.btn-submit-auth {
width: 100%; padding: 14px; background: var(--nexus-primary); border: none; border-radius: 12px;
color: #000; font-size: 0.95rem; font-weight: 700; cursor: pointer; transition: all 0.2s;