diff --git a/TrafagSalesExporter/Components/FinanceCockpit/FinanceCockpitUnlockPanel.razor b/TrafagSalesExporter/Components/FinanceCockpit/FinanceCockpitUnlockPanel.razor
new file mode 100644
index 0000000..d960431
--- /dev/null
+++ b/TrafagSalesExporter/Components/FinanceCockpit/FinanceCockpitUnlockPanel.razor
@@ -0,0 +1,47 @@
+@using TrafagSalesExporter.Services
+@inject IFinanceCockpitAccessService FinanceAccess
+@inject ISnackbar Snackbar
+@inject NavigationManager Navigation
+@inject IUiTextService UiText
+
+@T("Finance Cockpit", "Finance Cockpit")
+
+
+
+
+ @T("Finance Cockpit ist geschuetzt. Bitte separat anmelden.", "Finance Cockpit is protected. Please sign in separately.")
+
+ @if (!FinanceAccess.IsConfigured)
+ {
+
+ @T("Finance-Cockpit-Zugang ist noch nicht konfiguriert. Bitte Username und PasswordHash in FinanceCockpitAccess konfigurieren.", "Finance Cockpit access is not configured yet. Please configure Username and PasswordHash in FinanceCockpitAccess.")
+
+ }
+
+
+
+ @T("Finance Cockpit entsperren", "Unlock Finance Cockpit")
+
+
+
+
+@code {
+ private string? _username;
+ private string? _password;
+
+ private Task UnlockAsync()
+ {
+ if (!FinanceAccess.TryUnlock(_username ?? string.Empty, _password ?? string.Empty))
+ {
+ Snackbar.Add(T("Finance-Cockpit-Anmeldung fehlgeschlagen.", "Finance Cockpit sign-in failed."), Severity.Error);
+ return Task.CompletedTask;
+ }
+
+ _password = string.Empty;
+ Navigation.Refresh(forceReload: false);
+ return Task.CompletedTask;
+ }
+
+ private string T(string german, string english) => UiText.Text(german, english);
+}
diff --git a/TrafagSalesExporter/Components/Layout/NavMenu.razor b/TrafagSalesExporter/Components/Layout/NavMenu.razor
index 1ecee67..72bc637 100644
--- a/TrafagSalesExporter/Components/Layout/NavMenu.razor
+++ b/TrafagSalesExporter/Components/Layout/NavMenu.razor
@@ -1,5 +1,7 @@
@using TrafagSalesExporter.Security
@inject TrafagSalesExporter.Services.IUiTextService UiText
+@inject TrafagSalesExporter.Services.IFinanceCockpitAccessService FinanceAccess
+@inject NavigationManager Navigation
@@ -12,30 +14,43 @@
@T("Soll/Ist Vergleich", "Actual/reference comparison")
+
+
+
+
+ @T("Standorte", "Sites")
+
+
+ @T("Transformationen", "Transformations")
+
+
+ @T("Settings", "Settings")
+
+
+
+
+ @T("Logs", "Logs")
+
+
+ @if (FinanceAccess.IsEnabled && FinanceAccess.IsUnlocked)
+ {
+
+ @T("Finance sperren", "Lock finance")
+
+ }
@T("HR KPI (Login)", "HR KPI (login)")
-
-
-
-
- @T("Standorte", "Sites")
-
-
- @T("Transformationen", "Transformations")
-
-
- @T("Settings", "Settings")
-
-
-
-
- @T("Logs", "Logs")
-
-
@code {
+ private void LockFinanceCockpit()
+ {
+ FinanceAccess.Lock();
+ Navigation.NavigateTo("/");
+ }
+
private string T(string german, string english) => UiText.Text(german, english);
}
diff --git a/TrafagSalesExporter/Components/Routes.razor b/TrafagSalesExporter/Components/Routes.razor
index 35b3aef..3283b10 100644
--- a/TrafagSalesExporter/Components/Routes.razor
+++ b/TrafagSalesExporter/Components/Routes.razor
@@ -1,23 +1,53 @@
@using Microsoft.AspNetCore.Components.Authorization
+@inject NavigationManager Navigation
+@inject TrafagSalesExporter.Services.IFinanceCockpitAccessService FinanceAccess
-
-
-
-
- Zugriff verweigert. Bitte mit einem berechtigten Windows-/Domain-Benutzer anmelden.
-
-
-
-
-
-
-
-
-
+ @if (RequiresFinanceUnlock() && FinanceAccess.IsEnabled && !FinanceAccess.IsUnlocked)
+ {
+
+
+
+ }
+ else
+ {
+
+
+
+
+ Zugriff verweigert. Bitte mit einem berechtigten Windows-/Domain-Benutzer anmelden.
+
+
+
+
+
+
+
+
+
+ }
+
+@code {
+ private bool RequiresFinanceUnlock()
+ {
+ var path = Navigation.ToBaseRelativePath(Navigation.Uri)
+ .Split('?', '#')[0]
+ .Trim('/')
+ .ToLowerInvariant();
+
+ return path is "" or
+ "management-cockpit" or
+ "finance-cockpit/vergleich" or
+ "standorte" or
+ "transformations" or
+ "settings" or
+ "logs" or
+ "source-viewer";
+ }
+}
diff --git a/TrafagSalesExporter/Components/_Imports.razor b/TrafagSalesExporter/Components/_Imports.razor
index c4d2f34..b7f26b4 100644
--- a/TrafagSalesExporter/Components/_Imports.razor
+++ b/TrafagSalesExporter/Components/_Imports.razor
@@ -7,5 +7,6 @@
@using Microsoft.JSInterop
@using MudBlazor
@using TrafagSalesExporter.Components
+@using TrafagSalesExporter.Components.FinanceCockpit
@using TrafagSalesExporter.Components.Layout
@using TrafagSalesExporter.Models
diff --git a/TrafagSalesExporter/Program.cs b/TrafagSalesExporter/Program.cs
index 56dc36a..81724c8 100644
--- a/TrafagSalesExporter/Program.cs
+++ b/TrafagSalesExporter/Program.cs
@@ -46,6 +46,7 @@ builder.Services.AddMudServices();
builder.Services.AddHttpClient(nameof(ExchangeRateImportService));
builder.Services.Configure(builder.Configuration.GetSection(HrKpiDataSourceOptions.SectionName));
builder.Services.Configure(builder.Configuration.GetSection(HrKpiAccessOptions.SectionName));
+builder.Services.Configure(builder.Configuration.GetSection(FinanceCockpitAccessOptions.SectionName));
builder.Services.AddDbContextFactory(options =>
options.UseSqlite("Data Source=trafag_exporter.db;Default Timeout=60"));
@@ -106,6 +107,7 @@ builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
+builder.Services.AddScoped();
var app = builder.Build();
diff --git a/TrafagSalesExporter/Security/FinanceCockpitAccessOptions.cs b/TrafagSalesExporter/Security/FinanceCockpitAccessOptions.cs
new file mode 100644
index 0000000..448ea49
--- /dev/null
+++ b/TrafagSalesExporter/Security/FinanceCockpitAccessOptions.cs
@@ -0,0 +1,11 @@
+namespace TrafagSalesExporter.Security;
+
+public sealed class FinanceCockpitAccessOptions
+{
+ public const string SectionName = "FinanceCockpitAccess";
+
+ public bool Enabled { get; set; } = true;
+ public string Username { get; set; } = "finance";
+ public string PasswordHash { get; set; } = string.Empty;
+ public string Password { get; set; } = string.Empty;
+}
diff --git a/TrafagSalesExporter/Services/FinanceCockpitAccessService.cs b/TrafagSalesExporter/Services/FinanceCockpitAccessService.cs
new file mode 100644
index 0000000..71db8c7
--- /dev/null
+++ b/TrafagSalesExporter/Services/FinanceCockpitAccessService.cs
@@ -0,0 +1,74 @@
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.Extensions.Options;
+using TrafagSalesExporter.Security;
+
+namespace TrafagSalesExporter.Services;
+
+public interface IFinanceCockpitAccessService
+{
+ bool IsEnabled { get; }
+ bool IsConfigured { get; }
+ bool IsUnlocked { get; }
+ bool TryUnlock(string username, string password);
+ void Lock();
+}
+
+public sealed class FinanceCockpitAccessService : IFinanceCockpitAccessService
+{
+ private readonly FinanceCockpitAccessOptions _options;
+
+ public FinanceCockpitAccessService(IOptions options)
+ {
+ _options = options.Value;
+ }
+
+ public bool IsEnabled => _options.Enabled;
+
+ public bool IsConfigured =>
+ !IsEnabled ||
+ !string.IsNullOrWhiteSpace(_options.Username) &&
+ (!string.IsNullOrWhiteSpace(_options.PasswordHash) || !string.IsNullOrEmpty(_options.Password));
+
+ public bool IsUnlocked { get; private set; }
+
+ public bool TryUnlock(string username, string password)
+ {
+ if (!IsEnabled)
+ {
+ IsUnlocked = true;
+ return true;
+ }
+
+ if (!IsConfigured ||
+ string.IsNullOrWhiteSpace(username) ||
+ string.IsNullOrEmpty(password) ||
+ !FixedEquals(username.Trim(), _options.Username.Trim()))
+ {
+ return false;
+ }
+
+ var valid = !string.IsNullOrWhiteSpace(_options.PasswordHash)
+ ? VerifyPasswordHash(password, _options.PasswordHash)
+ : FixedEquals(password, _options.Password);
+
+ IsUnlocked = valid;
+ return valid;
+ }
+
+ public void Lock() => IsUnlocked = false;
+
+ private static bool VerifyPasswordHash(string password, string configuredHash)
+ {
+ var passwordHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(password)));
+ return FixedEquals(passwordHash, configuredHash.Trim());
+ }
+
+ private static bool FixedEquals(string left, string right)
+ {
+ var leftBytes = Encoding.UTF8.GetBytes(left);
+ var rightBytes = Encoding.UTF8.GetBytes(right);
+ return leftBytes.Length == rightBytes.Length &&
+ CryptographicOperations.FixedTimeEquals(leftBytes, rightBytes);
+ }
+}
diff --git a/TrafagSalesExporter/appsettings.json b/TrafagSalesExporter/appsettings.json
index 7ac8233..44519de 100644
--- a/TrafagSalesExporter/appsettings.json
+++ b/TrafagSalesExporter/appsettings.json
@@ -29,5 +29,10 @@
"Enabled": true,
"Username": "hr",
"PasswordHash": "A8AF253007750E0C2986CBD0BC570530B4AE2417AAC59067591E708547834AE4"
+ },
+ "FinanceCockpitAccess": {
+ "Enabled": true,
+ "Username": "finance",
+ "PasswordHash": "A8AF253007750E0C2986CBD0BC570530B4AE2417AAC59067591E708547834AE4"
}
}