Protect finance cockpit with login
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
@using TrafagSalesExporter.Services
|
||||
@inject IFinanceCockpitAccessService FinanceAccess
|
||||
@inject ISnackbar Snackbar
|
||||
@inject NavigationManager Navigation
|
||||
@inject IUiTextService UiText
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-4">@T("Finance Cockpit", "Finance Cockpit")</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mb-4" Elevation="1" Style="max-width:520px;">
|
||||
<MudStack Spacing="3">
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined">
|
||||
@T("Finance Cockpit ist geschuetzt. Bitte separat anmelden.", "Finance Cockpit is protected. Please sign in separately.")
|
||||
</MudAlert>
|
||||
@if (!FinanceAccess.IsConfigured)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Variant="Variant.Filled">
|
||||
@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.")
|
||||
</MudAlert>
|
||||
}
|
||||
<MudTextField @bind-Value="_username" Label="@T("Name", "Name")" Disabled="@(!FinanceAccess.IsConfigured)" />
|
||||
<MudTextField @bind-Value="_password" Label="@T("Passwort", "Password")" InputType="InputType.Password" Disabled="@(!FinanceAccess.IsConfigured)" />
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="UnlockAsync"
|
||||
StartIcon="@Icons.Material.Filled.LockOpen" Disabled="@(!FinanceAccess.IsConfigured)">
|
||||
@T("Finance Cockpit entsperren", "Unlock Finance Cockpit")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
@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);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
@using TrafagSalesExporter.Security
|
||||
@inject TrafagSalesExporter.Services.IUiTextService UiText
|
||||
@inject TrafagSalesExporter.Services.IFinanceCockpitAccessService FinanceAccess
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<MudNavMenu>
|
||||
<MudNavGroup Title="@T("Finance Cockpit", "Finance Cockpit")" Icon="@Icons.Material.Filled.Analytics" Expanded="true">
|
||||
@@ -12,10 +14,6 @@
|
||||
<MudNavLink Href="/finance-cockpit/vergleich" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.CompareArrows">
|
||||
@T("Soll/Ist Vergleich", "Actual/reference comparison")
|
||||
</MudNavLink>
|
||||
</MudNavGroup>
|
||||
<MudNavLink Href="/hr-kpi" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Groups">
|
||||
@T("HR KPI (Login)", "HR KPI (login)")
|
||||
</MudNavLink>
|
||||
<MudNavGroup Title="@T("Admin", "Admin")" Icon="@Icons.Material.Filled.AdminPanelSettings">
|
||||
<AuthorizeView Policy="@SecurityPolicies.AdminOnly">
|
||||
<Authorized>
|
||||
@@ -34,8 +32,25 @@
|
||||
@T("Logs", "Logs")
|
||||
</MudNavLink>
|
||||
</MudNavGroup>
|
||||
@if (FinanceAccess.IsEnabled && FinanceAccess.IsUnlocked)
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Color="Color.Secondary" Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.Lock" OnClick="LockFinanceCockpit" Class="ml-3">
|
||||
@T("Finance sperren", "Lock finance")
|
||||
</MudButton>
|
||||
}
|
||||
</MudNavGroup>
|
||||
<MudNavLink Href="/hr-kpi" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Groups">
|
||||
@T("HR KPI (Login)", "HR KPI (login)")
|
||||
</MudNavLink>
|
||||
</MudNavMenu>
|
||||
|
||||
@code {
|
||||
private void LockFinanceCockpit()
|
||||
{
|
||||
FinanceAccess.Lock();
|
||||
Navigation.NavigateTo("/");
|
||||
}
|
||||
|
||||
private string T(string german, string english) => UiText.Text(german, english);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@inject NavigationManager Navigation
|
||||
@inject TrafagSalesExporter.Services.IFinanceCockpitAccessService FinanceAccess
|
||||
|
||||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
@if (RequiresFinanceUnlock() && FinanceAccess.IsEnabled && !FinanceAccess.IsUnlocked)
|
||||
{
|
||||
<LayoutView Layout="typeof(Layout.MainLayout)">
|
||||
<FinanceCockpitUnlockPanel />
|
||||
</LayoutView>
|
||||
}
|
||||
else
|
||||
{
|
||||
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
|
||||
<NotAuthorized>
|
||||
<LayoutView Layout="typeof(Layout.MainLayout)">
|
||||
@@ -17,7 +27,27 @@
|
||||
</LayoutView>
|
||||
</Authorizing>
|
||||
</AuthorizeRouteView>
|
||||
}
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
</CascadingAuthenticationState>
|
||||
|
||||
@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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
@using Microsoft.JSInterop
|
||||
@using MudBlazor
|
||||
@using TrafagSalesExporter.Components
|
||||
@using TrafagSalesExporter.Components.FinanceCockpit
|
||||
@using TrafagSalesExporter.Components.Layout
|
||||
@using TrafagSalesExporter.Models
|
||||
|
||||
@@ -46,6 +46,7 @@ builder.Services.AddMudServices();
|
||||
builder.Services.AddHttpClient(nameof(ExchangeRateImportService));
|
||||
builder.Services.Configure<HrKpiDataSourceOptions>(builder.Configuration.GetSection(HrKpiDataSourceOptions.SectionName));
|
||||
builder.Services.Configure<HrKpiAccessOptions>(builder.Configuration.GetSection(HrKpiAccessOptions.SectionName));
|
||||
builder.Services.Configure<FinanceCockpitAccessOptions>(builder.Configuration.GetSection(FinanceCockpitAccessOptions.SectionName));
|
||||
|
||||
builder.Services.AddDbContextFactory<AppDbContext>(options =>
|
||||
options.UseSqlite("Data Source=trafag_exporter.db;Default Timeout=60"));
|
||||
@@ -106,6 +107,7 @@ builder.Services.AddScoped<IDashboardPageService, DashboardPageService>();
|
||||
builder.Services.AddScoped<ILogsPageService, LogsPageService>();
|
||||
builder.Services.AddScoped<ITransformationsPageService, TransformationsPageService>();
|
||||
builder.Services.AddScoped<IHrKpiAccessService, HrKpiAccessService>();
|
||||
builder.Services.AddScoped<IFinanceCockpitAccessService, FinanceCockpitAccessService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<FinanceCockpitAccessOptions> 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);
|
||||
}
|
||||
}
|
||||
@@ -29,5 +29,10 @@
|
||||
"Enabled": true,
|
||||
"Username": "hr",
|
||||
"PasswordHash": "A8AF253007750E0C2986CBD0BC570530B4AE2417AAC59067591E708547834AE4"
|
||||
},
|
||||
"FinanceCockpitAccess": {
|
||||
"Enabled": true,
|
||||
"Username": "finance",
|
||||
"PasswordHash": "A8AF253007750E0C2986CBD0BC570530B4AE2417AAC59067591E708547834AE4"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user