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" } }