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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user