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
|
@using TrafagSalesExporter.Security
|
||||||
@inject TrafagSalesExporter.Services.IUiTextService UiText
|
@inject TrafagSalesExporter.Services.IUiTextService UiText
|
||||||
|
@inject TrafagSalesExporter.Services.IFinanceCockpitAccessService FinanceAccess
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
<MudNavMenu>
|
<MudNavMenu>
|
||||||
<MudNavGroup Title="@T("Finance Cockpit", "Finance Cockpit")" Icon="@Icons.Material.Filled.Analytics" Expanded="true">
|
<MudNavGroup Title="@T("Finance Cockpit", "Finance Cockpit")" Icon="@Icons.Material.Filled.Analytics" Expanded="true">
|
||||||
@@ -12,30 +14,43 @@
|
|||||||
<MudNavLink Href="/finance-cockpit/vergleich" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.CompareArrows">
|
<MudNavLink Href="/finance-cockpit/vergleich" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.CompareArrows">
|
||||||
@T("Soll/Ist Vergleich", "Actual/reference comparison")
|
@T("Soll/Ist Vergleich", "Actual/reference comparison")
|
||||||
</MudNavLink>
|
</MudNavLink>
|
||||||
|
<MudNavGroup Title="@T("Admin", "Admin")" Icon="@Icons.Material.Filled.AdminPanelSettings">
|
||||||
|
<AuthorizeView Policy="@SecurityPolicies.AdminOnly">
|
||||||
|
<Authorized>
|
||||||
|
<MudNavLink Href="/standorte" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.LocationOn">
|
||||||
|
@T("Standorte", "Sites")
|
||||||
|
</MudNavLink>
|
||||||
|
<MudNavLink Href="/transformations" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Transform">
|
||||||
|
@T("Transformationen", "Transformations")
|
||||||
|
</MudNavLink>
|
||||||
|
<MudNavLink Href="/settings" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Settings">
|
||||||
|
@T("Settings", "Settings")
|
||||||
|
</MudNavLink>
|
||||||
|
</Authorized>
|
||||||
|
</AuthorizeView>
|
||||||
|
<MudNavLink Href="/logs" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">
|
||||||
|
@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>
|
</MudNavGroup>
|
||||||
<MudNavLink Href="/hr-kpi" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Groups">
|
<MudNavLink Href="/hr-kpi" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Groups">
|
||||||
@T("HR KPI (Login)", "HR KPI (login)")
|
@T("HR KPI (Login)", "HR KPI (login)")
|
||||||
</MudNavLink>
|
</MudNavLink>
|
||||||
<MudNavGroup Title="@T("Admin", "Admin")" Icon="@Icons.Material.Filled.AdminPanelSettings">
|
|
||||||
<AuthorizeView Policy="@SecurityPolicies.AdminOnly">
|
|
||||||
<Authorized>
|
|
||||||
<MudNavLink Href="/standorte" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.LocationOn">
|
|
||||||
@T("Standorte", "Sites")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavLink Href="/transformations" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Transform">
|
|
||||||
@T("Transformationen", "Transformations")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavLink Href="/settings" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Settings">
|
|
||||||
@T("Settings", "Settings")
|
|
||||||
</MudNavLink>
|
|
||||||
</Authorized>
|
|
||||||
</AuthorizeView>
|
|
||||||
<MudNavLink Href="/logs" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">
|
|
||||||
@T("Logs", "Logs")
|
|
||||||
</MudNavLink>
|
|
||||||
</MudNavGroup>
|
|
||||||
</MudNavMenu>
|
</MudNavMenu>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
private void LockFinanceCockpit()
|
||||||
|
{
|
||||||
|
FinanceAccess.Lock();
|
||||||
|
Navigation.NavigateTo("/");
|
||||||
|
}
|
||||||
|
|
||||||
private string T(string german, string english) => UiText.Text(german, english);
|
private string T(string german, string english) => UiText.Text(german, english);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,53 @@
|
|||||||
@using Microsoft.AspNetCore.Components.Authorization
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject TrafagSalesExporter.Services.IFinanceCockpitAccessService FinanceAccess
|
||||||
|
|
||||||
<CascadingAuthenticationState>
|
<CascadingAuthenticationState>
|
||||||
<Router AppAssembly="typeof(Program).Assembly">
|
<Router AppAssembly="typeof(Program).Assembly">
|
||||||
<Found Context="routeData">
|
<Found Context="routeData">
|
||||||
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
|
@if (RequiresFinanceUnlock() && FinanceAccess.IsEnabled && !FinanceAccess.IsUnlocked)
|
||||||
<NotAuthorized>
|
{
|
||||||
<LayoutView Layout="typeof(Layout.MainLayout)">
|
<LayoutView Layout="typeof(Layout.MainLayout)">
|
||||||
<MudAlert Severity="Severity.Error" Variant="Variant.Outlined">
|
<FinanceCockpitUnlockPanel />
|
||||||
Zugriff verweigert. Bitte mit einem berechtigten Windows-/Domain-Benutzer anmelden.
|
</LayoutView>
|
||||||
</MudAlert>
|
}
|
||||||
</LayoutView>
|
else
|
||||||
</NotAuthorized>
|
{
|
||||||
<Authorizing>
|
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
|
||||||
<LayoutView Layout="typeof(Layout.MainLayout)">
|
<NotAuthorized>
|
||||||
<MudProgressCircular Indeterminate="true" />
|
<LayoutView Layout="typeof(Layout.MainLayout)">
|
||||||
</LayoutView>
|
<MudAlert Severity="Severity.Error" Variant="Variant.Outlined">
|
||||||
</Authorizing>
|
Zugriff verweigert. Bitte mit einem berechtigten Windows-/Domain-Benutzer anmelden.
|
||||||
</AuthorizeRouteView>
|
</MudAlert>
|
||||||
|
</LayoutView>
|
||||||
|
</NotAuthorized>
|
||||||
|
<Authorizing>
|
||||||
|
<LayoutView Layout="typeof(Layout.MainLayout)">
|
||||||
|
<MudProgressCircular Indeterminate="true" />
|
||||||
|
</LayoutView>
|
||||||
|
</Authorizing>
|
||||||
|
</AuthorizeRouteView>
|
||||||
|
}
|
||||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||||
</Found>
|
</Found>
|
||||||
</Router>
|
</Router>
|
||||||
</CascadingAuthenticationState>
|
</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 Microsoft.JSInterop
|
||||||
@using MudBlazor
|
@using MudBlazor
|
||||||
@using TrafagSalesExporter.Components
|
@using TrafagSalesExporter.Components
|
||||||
|
@using TrafagSalesExporter.Components.FinanceCockpit
|
||||||
@using TrafagSalesExporter.Components.Layout
|
@using TrafagSalesExporter.Components.Layout
|
||||||
@using TrafagSalesExporter.Models
|
@using TrafagSalesExporter.Models
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ builder.Services.AddMudServices();
|
|||||||
builder.Services.AddHttpClient(nameof(ExchangeRateImportService));
|
builder.Services.AddHttpClient(nameof(ExchangeRateImportService));
|
||||||
builder.Services.Configure<HrKpiDataSourceOptions>(builder.Configuration.GetSection(HrKpiDataSourceOptions.SectionName));
|
builder.Services.Configure<HrKpiDataSourceOptions>(builder.Configuration.GetSection(HrKpiDataSourceOptions.SectionName));
|
||||||
builder.Services.Configure<HrKpiAccessOptions>(builder.Configuration.GetSection(HrKpiAccessOptions.SectionName));
|
builder.Services.Configure<HrKpiAccessOptions>(builder.Configuration.GetSection(HrKpiAccessOptions.SectionName));
|
||||||
|
builder.Services.Configure<FinanceCockpitAccessOptions>(builder.Configuration.GetSection(FinanceCockpitAccessOptions.SectionName));
|
||||||
|
|
||||||
builder.Services.AddDbContextFactory<AppDbContext>(options =>
|
builder.Services.AddDbContextFactory<AppDbContext>(options =>
|
||||||
options.UseSqlite("Data Source=trafag_exporter.db;Default Timeout=60"));
|
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<ILogsPageService, LogsPageService>();
|
||||||
builder.Services.AddScoped<ITransformationsPageService, TransformationsPageService>();
|
builder.Services.AddScoped<ITransformationsPageService, TransformationsPageService>();
|
||||||
builder.Services.AddScoped<IHrKpiAccessService, HrKpiAccessService>();
|
builder.Services.AddScoped<IHrKpiAccessService, HrKpiAccessService>();
|
||||||
|
builder.Services.AddScoped<IFinanceCockpitAccessService, FinanceCockpitAccessService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
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,
|
"Enabled": true,
|
||||||
"Username": "hr",
|
"Username": "hr",
|
||||||
"PasswordHash": "A8AF253007750E0C2986CBD0BC570530B4AE2417AAC59067591E708547834AE4"
|
"PasswordHash": "A8AF253007750E0C2986CBD0BC570530B4AE2417AAC59067591E708547834AE4"
|
||||||
|
},
|
||||||
|
"FinanceCockpitAccess": {
|
||||||
|
"Enabled": true,
|
||||||
|
"Username": "finance",
|
||||||
|
"PasswordHash": "A8AF253007750E0C2986CBD0BC570530B4AE2417AAC59067591E708547834AE4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user