diff --git a/DbFirst.BlazorWebApp/AppSettings.cs b/DbFirst.BlazorWebApp/AppSettings.cs
index 39ee9c9..3841630 100644
--- a/DbFirst.BlazorWebApp/AppSettings.cs
+++ b/DbFirst.BlazorWebApp/AppSettings.cs
@@ -3,4 +3,5 @@
public class AppSettings
{
public string ApiBaseUrl { get; set; } = string.Empty;
+ public string DataApiBaseUrl { get; set; } = string.Empty;
}
\ No newline at end of file
diff --git a/DbFirst.BlazorWebApp/Components/App.razor b/DbFirst.BlazorWebApp/Components/App.razor
index 3a4ea34..1fd5afc 100644
--- a/DbFirst.BlazorWebApp/Components/App.razor
+++ b/DbFirst.BlazorWebApp/Components/App.razor
@@ -31,6 +31,18 @@
else
document.documentElement.classList.remove('dx-dark');
};
+
+ window.authStorage = {
+ get: function (key) { return sessionStorage.getItem(key); },
+ set: function (username, cookie) {
+ sessionStorage.setItem('auth_user', username);
+ sessionStorage.setItem('auth_cookie', cookie);
+ },
+ clear: function () {
+ sessionStorage.removeItem('auth_user');
+ sessionStorage.removeItem('auth_cookie');
+ }
+ };
diff --git a/DbFirst.BlazorWebApp/Components/Layout/LoginLayout.razor b/DbFirst.BlazorWebApp/Components/Layout/LoginLayout.razor
new file mode 100644
index 0000000..56830dd
--- /dev/null
+++ b/DbFirst.BlazorWebApp/Components/Layout/LoginLayout.razor
@@ -0,0 +1,10 @@
+@inherits LayoutComponentBase
+@inject ThemeState ThemeState
+
+
diff --git a/DbFirst.BlazorWebApp/Components/Layout/MainLayout.razor b/DbFirst.BlazorWebApp/Components/Layout/MainLayout.razor
index f7ce51c..8f8573d 100644
--- a/DbFirst.BlazorWebApp/Components/Layout/MainLayout.razor
+++ b/DbFirst.BlazorWebApp/Components/Layout/MainLayout.razor
@@ -2,6 +2,9 @@
@implements IDisposable
@inject ThemeState ThemeState
@inject IJSRuntime JS
+@inject AuthService AuthService
+@inject IAuthApiClient AuthApiClient
+@inject NavigationManager Navigation
- @Body
+ @if (_checkingAuth)
+ {
+
+ Wird geladen…
+
+ }
+ else
+ {
+ @Body
+ }
@@ -35,10 +56,12 @@
@code {
private bool _isInteractive;
+ private bool _checkingAuth = true;
protected override void OnInitialized()
{
ThemeState.OnChange += OnThemeChanged;
+ AuthService.OnChange += OnAuthChanged;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -46,17 +69,71 @@
if (firstRender)
{
_isInteractive = true;
+
+ if (!AuthService.IsAuthenticated)
+ {
+ try
+ {
+ var username = await JS.InvokeAsync("authStorage.get", "auth_user");
+ var cookie = await JS.InvokeAsync("authStorage.get", "auth_cookie");
+
+ if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(cookie))
+ {
+ var restored = await AuthApiClient.RestoreAsync(username, cookie);
+ if (!restored)
+ {
+ await JS.InvokeVoidAsync("authStorage.clear");
+ Navigation.NavigateTo("/login", replace: true);
+ return;
+ }
+ }
+ else
+ {
+ Navigation.NavigateTo("/login", replace: true);
+ return;
+ }
+ }
+ catch
+ {
+ Navigation.NavigateTo("/login", replace: true);
+ return;
+ }
+ }
+
+ _checkingAuth = false;
+ StateHasChanged();
}
+
await ApplyDxDarkOverrideAsync();
}
+ private async Task LogoutAsync()
+ {
+ try
+ {
+ await AuthApiClient.LogoutAsync();
+ await JS.InvokeVoidAsync("authStorage.clear");
+ }
+ catch
+ {
+ // Fehler beim Abmelden ignorieren, trotzdem weiterleiten
+ }
+
+ Navigation.NavigateTo("/login", replace: true);
+ }
+
+ private void OnAuthChanged()
+ {
+ InvokeAsync(StateHasChanged);
+ }
+
private void OnThemeChanged()
{
InvokeAsync(async () =>
{
StateHasChanged();
if (_isInteractive)
- await ApplyDxDarkOverrideAsync();
+ await ApplyDxDarkOverrideAsync();
});
}
@@ -82,5 +159,6 @@
public void Dispose()
{
ThemeState.OnChange -= OnThemeChanged;
+ AuthService.OnChange -= OnAuthChanged;
}
}
diff --git a/DbFirst.BlazorWebApp/Components/Pages/Login.razor b/DbFirst.BlazorWebApp/Components/Pages/Login.razor
new file mode 100644
index 0000000..6dde548
--- /dev/null
+++ b/DbFirst.BlazorWebApp/Components/Pages/Login.razor
@@ -0,0 +1,106 @@
+@page "/login"
+@layout DbFirst.BlazorWebApp.Components.Layout.LoginLayout
+@rendermode InteractiveServer
+
+@inject IAuthApiClient AuthApiClient
+@inject AuthService AuthService
+@inject NavigationManager Navigation
+@inject IJSRuntime JS
+
+Anmelden – DbFirst
+
+
+
+@code {
+ private string _username = string.Empty;
+ private string _password = string.Empty;
+ private string _errorMessage = string.Empty;
+ private bool _isLoading;
+
+ protected override void OnInitialized()
+ {
+ if (AuthService.IsAuthenticated)
+ Navigation.NavigateTo("/");
+ }
+
+ private async Task HandleLoginAsync()
+ {
+ if (_isLoading) return;
+ _errorMessage = string.Empty;
+
+ if (string.IsNullOrWhiteSpace(_username) || string.IsNullOrWhiteSpace(_password))
+ {
+ _errorMessage = "Bitte Benutzername und Passwort eingeben.";
+ return;
+ }
+
+ _isLoading = true;
+ try
+ {
+ var success = await AuthApiClient.LoginAsync(_username, _password);
+ if (success)
+ {
+ await JS.InvokeVoidAsync("authStorage.set", AuthService.UserName, AuthService.RawCookieHeader);
+ Navigation.NavigateTo("/");
+ }
+ else
+ {
+ _errorMessage = "Anmeldung fehlgeschlagen. Bitte Benutzerdaten prüfen.";
+ }
+ }
+ catch
+ {
+ _errorMessage = "Verbindungsfehler. Bitte später erneut versuchen.";
+ }
+ finally
+ {
+ _isLoading = false;
+ }
+ }
+}
diff --git a/DbFirst.BlazorWebApp/Components/Pages/Login.razor.css b/DbFirst.BlazorWebApp/Components/Pages/Login.razor.css
new file mode 100644
index 0000000..30106d9
--- /dev/null
+++ b/DbFirst.BlazorWebApp/Components/Pages/Login.razor.css
@@ -0,0 +1,111 @@
+.login-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: calc(100vh - 2rem);
+}
+
+.login-card {
+ width: 100%;
+ max-width: 400px;
+ padding: 2rem;
+ border-radius: 8px;
+ border: 1px solid var(--band-editor-border, #dee2e6);
+ background-color: var(--band-editor-bg, #fff);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
+}
+
+.login-header {
+ text-align: center;
+ margin-bottom: 1.75rem;
+}
+
+.login-brand {
+ font-size: 1.8rem;
+ font-weight: 700;
+ color: #1b6ec2;
+ letter-spacing: 0.02em;
+}
+
+.login-subtitle {
+ margin-top: 0.35rem;
+ margin-bottom: 0;
+ font-size: 0.9rem;
+ color: #6c757d;
+}
+
+.login-field {
+ margin-bottom: 1rem;
+}
+
+.login-label {
+ display: block;
+ font-size: 0.85rem;
+ font-weight: 500;
+ margin-bottom: 0.3rem;
+}
+
+.login-actions {
+ margin-top: 1.25rem;
+}
+
+/* Gemeinsamer Stil für Text- und Passwort-Eingabefelder */
+.login-text-input {
+ display: block;
+ width: 100%;
+ height: 2.25rem;
+ padding: 0.25rem 0.65rem;
+ font-size: 1rem;
+ font-family: inherit;
+ line-height: 1.5;
+ color: inherit;
+ background-color: transparent;
+ border: 1px solid #ced4da;
+ border-radius: 4px;
+ box-sizing: border-box;
+ outline: none;
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+ .login-text-input:focus {
+ border-color: #86b7fe;
+ box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
+ }
+
+/* Submit-Button */
+.login-submit-btn {
+ display: block;
+ width: 100%;
+ padding: 0.45rem 1rem;
+ font-size: 1rem;
+ font-family: inherit;
+ font-weight: 500;
+ color: #fff;
+ background-color: #1b6ec2;
+ border: 1px solid #1861ac;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
+}
+
+ .login-submit-btn:hover:not(:disabled) {
+ background-color: #1558a0;
+ border-color: #1456a0;
+ }
+
+ .login-submit-btn:disabled {
+ opacity: 0.65;
+ cursor: not-allowed;
+ }
+
+/* Dark-Mode */
+.app-dark .login-text-input {
+ border-color: #555;
+ color: #e8e8e8;
+ background-color: #2d2d2d;
+}
+
+ .app-dark .login-text-input:focus {
+ border-color: #6cb6ff;
+ box-shadow: 0 0 0 0.2rem rgba(108, 182, 255, 0.25);
+ }
diff --git a/DbFirst.BlazorWebApp/Program.cs b/DbFirst.BlazorWebApp/Program.cs
index ef6ed42..0f7c66f 100644
--- a/DbFirst.BlazorWebApp/Program.cs
+++ b/DbFirst.BlazorWebApp/Program.cs
@@ -2,6 +2,7 @@ using DbFirst.BlazorWebApp;
using DbFirst.BlazorWebApp.Components;
using DbFirst.BlazorWebApp.Services;
using DevExpress.Blazor;
+using System.Net;
var builder = WebApplication.CreateBuilder(args);
@@ -12,19 +13,45 @@ builder.Services.AddRazorComponents()
builder.Services.AddDevExpressBlazor(options => options.BootstrapVersion = BootstrapVersion.v5);
builder.Services.AddScoped();
builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
builder.Services.Configure(builder.Configuration);
var appSettings = builder.Configuration.Get() ?? new AppSettings();
-void ConfigureClient(HttpClient client)
+
+// Alle API-Clients teilen sich denselben scoped CookieContainer (pro Blazor-Circuit),
+// damit das Auth-Cookie nach dem Login automatisch an alle Folgeanfragen angehängt wird.
+static HttpClient CreateHttpClientWithCookies(CookieContainer cookieContainer, string? baseUrl)
{
- if (!string.IsNullOrWhiteSpace(appSettings.ApiBaseUrl))
- client.BaseAddress = new Uri(appSettings.ApiBaseUrl);
+ var handler = new HttpClientHandler { CookieContainer = cookieContainer, UseCookies = true };
+ var client = new HttpClient(handler);
+ if (!string.IsNullOrWhiteSpace(baseUrl))
+ client.BaseAddress = new Uri(baseUrl);
+ return client;
}
-builder.Services.AddHttpClient(ConfigureClient);
-builder.Services.AddHttpClient(ConfigureClient);
-builder.Services.AddHttpClient(ConfigureClient);
-builder.Services.AddHttpClient(ConfigureClient);
+builder.Services.AddScoped(sp =>
+{
+ var cc = sp.GetRequiredService();
+ var client = CreateHttpClientWithCookies(cc, appSettings.ApiBaseUrl);
+ return new AuthApiClient(client, sp.GetRequiredService(), cc);
+});
+
+var dataApiBaseUrl = !string.IsNullOrWhiteSpace(appSettings.DataApiBaseUrl)
+ ? appSettings.DataApiBaseUrl
+ : appSettings.ApiBaseUrl;
+
+builder.Services.AddScoped(sp =>
+ new CatalogApiClient(CreateHttpClientWithCookies(sp.GetRequiredService(), dataApiBaseUrl)));
+
+builder.Services.AddScoped(sp =>
+ new DashboardApiClient(CreateHttpClientWithCookies(sp.GetRequiredService(), dataApiBaseUrl)));
+
+builder.Services.AddScoped(sp =>
+ new MassDataApiClient(CreateHttpClientWithCookies(sp.GetRequiredService(), dataApiBaseUrl)));
+
+builder.Services.AddScoped(sp =>
+ new LayoutApiClient(CreateHttpClientWithCookies(sp.GetRequiredService(), dataApiBaseUrl)));
var app = builder.Build();
diff --git a/DbFirst.BlazorWebApp/Services/AuthApiClient.cs b/DbFirst.BlazorWebApp/Services/AuthApiClient.cs
new file mode 100644
index 0000000..cc950e4
--- /dev/null
+++ b/DbFirst.BlazorWebApp/Services/AuthApiClient.cs
@@ -0,0 +1,64 @@
+using System.Net;
+
+namespace DbFirst.BlazorWebApp.Services;
+
+public class AuthApiClient(HttpClient httpClient, AuthService authService, CookieContainer cookieContainer) : IAuthApiClient
+{
+ private const string LoginEndpoint = "api/Auth/db-first/login";
+ private const string LogoutEndpoint = "api/Auth/logout";
+ private const string CheckEndpoint = "api/Auth/check";
+
+ public async Task LoginAsync(string username, string password, CancellationToken ct = default)
+ {
+ var content = new MultipartFormDataContent();
+ content.Add(new StringContent(username), "Username");
+ content.Add(new StringContent(password), "Password");
+ content.Add(new StringContent(string.Empty), "UserId");
+
+ var response = await httpClient.PostAsync(LoginEndpoint, content, ct);
+ if (!response.IsSuccessStatusCode)
+ return false;
+
+ var rawCookie = ExtractCookies();
+ authService.SetAuthenticated(username, rawCookie);
+ return true;
+ }
+
+ public async Task LogoutAsync(CancellationToken ct = default)
+ {
+ await httpClient.PostAsync(LogoutEndpoint, null, ct);
+ authService.SetUnauthenticated();
+ }
+
+ public async Task RestoreAsync(string username, string rawCookieHeader, CancellationToken ct = default)
+ {
+ RestoreCookies(rawCookieHeader);
+ var response = await httpClient.GetAsync(CheckEndpoint, ct);
+ if (response.IsSuccessStatusCode)
+ {
+ authService.SetAuthenticated(username, rawCookieHeader);
+ return true;
+ }
+
+ authService.SetUnauthenticated();
+ return false;
+ }
+
+ private string ExtractCookies()
+ {
+ if (httpClient.BaseAddress is null) return string.Empty;
+ var cookies = cookieContainer.GetCookies(httpClient.BaseAddress);
+ return string.Join("; ", cookies.Cast().Select(c => $"{c.Name}={c.Value}"));
+ }
+
+ private void RestoreCookies(string rawCookieHeader)
+ {
+ if (httpClient.BaseAddress is null) return;
+ foreach (var part in rawCookieHeader.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
+ {
+ var eqIdx = part.IndexOf('=');
+ if (eqIdx > 0)
+ cookieContainer.Add(httpClient.BaseAddress, new Cookie(part[..eqIdx].Trim(), part[(eqIdx + 1)..].Trim()));
+ }
+ }
+}
diff --git a/DbFirst.BlazorWebApp/Services/AuthService.cs b/DbFirst.BlazorWebApp/Services/AuthService.cs
new file mode 100644
index 0000000..b55ef81
--- /dev/null
+++ b/DbFirst.BlazorWebApp/Services/AuthService.cs
@@ -0,0 +1,26 @@
+namespace DbFirst.BlazorWebApp.Services;
+
+public class AuthService
+{
+ public bool IsAuthenticated { get; private set; }
+ public string UserName { get; private set; } = string.Empty;
+ public string? RawCookieHeader { get; private set; }
+
+ public event Action? OnChange;
+
+ public void SetAuthenticated(string userName, string rawCookieHeader)
+ {
+ IsAuthenticated = true;
+ UserName = userName;
+ RawCookieHeader = rawCookieHeader;
+ OnChange?.Invoke();
+ }
+
+ public void SetUnauthenticated()
+ {
+ IsAuthenticated = false;
+ UserName = string.Empty;
+ RawCookieHeader = null;
+ OnChange?.Invoke();
+ }
+}
diff --git a/DbFirst.BlazorWebApp/Services/IAuthApiClient.cs b/DbFirst.BlazorWebApp/Services/IAuthApiClient.cs
new file mode 100644
index 0000000..25d5532
--- /dev/null
+++ b/DbFirst.BlazorWebApp/Services/IAuthApiClient.cs
@@ -0,0 +1,8 @@
+namespace DbFirst.BlazorWebApp.Services;
+
+public interface IAuthApiClient
+{
+ Task LoginAsync(string username, string password, CancellationToken ct = default);
+ Task LogoutAsync(CancellationToken ct = default);
+ Task RestoreAsync(string username, string rawCookieHeader, CancellationToken ct = default);
+}
diff --git a/DbFirst.BlazorWebApp/appsettings.Development.json b/DbFirst.BlazorWebApp/appsettings.Development.json
index be563e8..545137b 100644
--- a/DbFirst.BlazorWebApp/appsettings.Development.json
+++ b/DbFirst.BlazorWebApp/appsettings.Development.json
@@ -5,7 +5,8 @@
"Microsoft.AspNetCore": "Warning"
}
},
- "ApiBaseUrl": "https://localhost:7204/",
+ "ApiBaseUrl": "http://172.24.12.39:9090/",
+ "DataApiBaseUrl": "https://localhost:7204/",
"BrowserLink": {
"Enabled": false
},
diff --git a/DbFirst.BlazorWebApp/appsettings.json b/DbFirst.BlazorWebApp/appsettings.json
index 73c9700..7779dac 100644
--- a/DbFirst.BlazorWebApp/appsettings.json
+++ b/DbFirst.BlazorWebApp/appsettings.json
@@ -5,6 +5,7 @@
"Microsoft.AspNetCore": "Warning"
}
},
- "ApiBaseUrl": "https://localhost:7204/",
+ "ApiBaseUrl": "http://172.24.12.39:9090/",
+ "DataApiBaseUrl": "https://localhost:7204/",
"AllowedHosts": "*"
}
diff --git a/DbFirst.BlazorWebApp/wwwroot/app.css b/DbFirst.BlazorWebApp/wwwroot/app.css
index 72224cc..fe87cc6 100644
--- a/DbFirst.BlazorWebApp/wwwroot/app.css
+++ b/DbFirst.BlazorWebApp/wwwroot/app.css
@@ -294,4 +294,10 @@ html.dx-dark .dxbl-grid > .dxbl-grid-top-panel {
.top-row .btn-gap {
margin-left: 8px;
+}
+
+.top-row-username {
+ font-size: 0.85rem;
+ color: #6c757d;
+ white-space: nowrap;
}
\ No newline at end of file