Add AD auth and B1 currency fields
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
@using System.Security.Claims
|
||||||
@inject TrafagSalesExporter.Services.IUiTextService UiText
|
@inject TrafagSalesExporter.Services.IUiTextService UiText
|
||||||
|
|
||||||
<MudThemeProvider Theme="_theme" />
|
<MudThemeProvider Theme="_theme" />
|
||||||
@@ -23,6 +24,11 @@
|
|||||||
<MudSelectItem Value="@("de")">DE</MudSelectItem>
|
<MudSelectItem Value="@("de")">DE</MudSelectItem>
|
||||||
<MudSelectItem Value="@("en")">EN</MudSelectItem>
|
<MudSelectItem Value="@("en")">EN</MudSelectItem>
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
|
<AuthorizeView>
|
||||||
|
<Authorized Context="authState">
|
||||||
|
<MudText Typo="Typo.caption" Class="mr-3">@ShortName(authState.User)</MudText>
|
||||||
|
</Authorized>
|
||||||
|
</AuthorizeView>
|
||||||
<img src="trafag.jpg" alt="Trafag" class="app-logo" />
|
<img src="trafag.jpg" alt="Trafag" class="app-logo" />
|
||||||
</MudAppBar>
|
</MudAppBar>
|
||||||
|
|
||||||
@@ -67,6 +73,13 @@
|
|||||||
|
|
||||||
private string T(string german, string english) => UiText.Text(german, english);
|
private string T(string german, string english) => UiText.Text(german, english);
|
||||||
|
|
||||||
|
private static string ShortName(ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
var name = user.Identity?.Name ?? string.Empty;
|
||||||
|
var separator = name.LastIndexOf('\\');
|
||||||
|
return separator >= 0 && separator < name.Length - 1 ? name[(separator + 1)..] : name;
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
UiText.Changed -= HandleLanguageChanged;
|
UiText.Changed -= HandleLanguageChanged;
|
||||||
|
|||||||
@@ -1,21 +1,30 @@
|
|||||||
|
@using TrafagSalesExporter.Security
|
||||||
@inject TrafagSalesExporter.Services.IUiTextService UiText
|
@inject TrafagSalesExporter.Services.IUiTextService UiText
|
||||||
|
|
||||||
<MudNavMenu>
|
<MudNavMenu>
|
||||||
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">
|
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">
|
||||||
@T("Dashboard", "Dashboard")
|
@T("Dashboard", "Dashboard")
|
||||||
</MudNavLink>
|
</MudNavLink>
|
||||||
|
<AuthorizeView Policy="@SecurityPolicies.AdminOnly">
|
||||||
|
<Authorized>
|
||||||
<MudNavLink Href="/standorte" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.LocationOn">
|
<MudNavLink Href="/standorte" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.LocationOn">
|
||||||
@T("Standorte", "Sites")
|
@T("Standorte", "Sites")
|
||||||
</MudNavLink>
|
</MudNavLink>
|
||||||
<MudNavLink Href="/transformations" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Transform">
|
<MudNavLink Href="/transformations" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Transform">
|
||||||
@T("Transformationen", "Transformations")
|
@T("Transformationen", "Transformations")
|
||||||
</MudNavLink>
|
</MudNavLink>
|
||||||
|
</Authorized>
|
||||||
|
</AuthorizeView>
|
||||||
<MudNavLink Href="/management-cockpit" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Analytics">
|
<MudNavLink Href="/management-cockpit" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Analytics">
|
||||||
@T("Management Cockpit", "Management Cockpit")
|
@T("Management Cockpit", "Management Cockpit")
|
||||||
</MudNavLink>
|
</MudNavLink>
|
||||||
|
<AuthorizeView Policy="@SecurityPolicies.AdminOnly">
|
||||||
|
<Authorized>
|
||||||
<MudNavLink Href="/settings" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Settings">
|
<MudNavLink Href="/settings" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Settings">
|
||||||
@T("Settings", "Settings")
|
@T("Settings", "Settings")
|
||||||
</MudNavLink>
|
</MudNavLink>
|
||||||
|
</Authorized>
|
||||||
|
</AuthorizeView>
|
||||||
<MudNavLink Href="/logs" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">
|
<MudNavLink Href="/logs" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">
|
||||||
@T("Logs", "Logs")
|
@T("Logs", "Logs")
|
||||||
</MudNavLink>
|
</MudNavLink>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@page "/settings"
|
@page "/settings"
|
||||||
|
@attribute [Authorize(Policy = TrafagSalesExporter.Security.SecurityPolicies.AdminOnly)]
|
||||||
@using TrafagSalesExporter.Models
|
@using TrafagSalesExporter.Models
|
||||||
@using TrafagSalesExporter.Services
|
@using TrafagSalesExporter.Services
|
||||||
@inject ISettingsPageService SettingsPageActions
|
@inject ISettingsPageService SettingsPageActions
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@page "/standorte"
|
@page "/standorte"
|
||||||
|
@attribute [Authorize(Policy = TrafagSalesExporter.Security.SecurityPolicies.AdminOnly)]
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@using System.Text.Json
|
@using System.Text.Json
|
||||||
@using System.Reflection
|
@using System.Reflection
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@page "/transformations"
|
@page "/transformations"
|
||||||
|
@attribute [Authorize(Policy = TrafagSalesExporter.Security.SecurityPolicies.AdminOnly)]
|
||||||
@using System.Reflection
|
@using System.Reflection
|
||||||
@using TrafagSalesExporter.Models
|
@using TrafagSalesExporter.Models
|
||||||
@using TrafagSalesExporter.Services
|
@using TrafagSalesExporter.Services
|
||||||
|
|||||||
@@ -1,6 +1,23 @@
|
|||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
|
|
||||||
|
<CascadingAuthenticationState>
|
||||||
<Router AppAssembly="typeof(Program).Assembly">
|
<Router AppAssembly="typeof(Program).Assembly">
|
||||||
<Found Context="routeData">
|
<Found Context="routeData">
|
||||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
|
||||||
|
<NotAuthorized>
|
||||||
|
<LayoutView Layout="typeof(Layout.MainLayout)">
|
||||||
|
<MudAlert Severity="Severity.Error" Variant="Variant.Outlined">
|
||||||
|
Zugriff verweigert. Bitte mit einem berechtigten Windows-/Domain-Benutzer anmelden.
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
@using System.Net.Http
|
@using System.Net.Http
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
@using Microsoft.AspNetCore.Components.Routing
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
@using Microsoft.AspNetCore.Components.Web
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using MudBlazor
|
@using MudBlazor
|
||||||
@using TrafagSalesExporter.Components
|
@using TrafagSalesExporter.Components
|
||||||
|
|||||||
@@ -2,6 +2,171 @@
|
|||||||
|
|
||||||
Stand: 2026-04-15
|
Stand: 2026-04-15
|
||||||
|
|
||||||
|
## Nachtrag 2026-04-29 B1-Belegwaehrungsfelder aus HANA
|
||||||
|
|
||||||
|
Der HANA/B1-Export wurde um Beleg- und Hauswaehrungsfelder erweitert.
|
||||||
|
|
||||||
|
Grund:
|
||||||
|
|
||||||
|
- `p.StockPrice` muss fachlich in der B1-Hauswaehrung bewertet werden
|
||||||
|
- die Hauswaehrung kommt aus `OADM.MainCurncy`
|
||||||
|
- bisher wurde `StandardCostCurrency` aus `p.Currency` bzw. `h.DocCur` abgeleitet
|
||||||
|
- fuer Power-BI-/Cockpit-Gegenpruefung muessen Belegwaehrung, Hauswaehrung, Netto-/Steuerbetraege und Kurs sichtbar sein
|
||||||
|
|
||||||
|
Neue Felder in `SalesRecord` und `CentralSalesRecord`:
|
||||||
|
|
||||||
|
- `DocumentCurrency`
|
||||||
|
- `DocumentTotalForeignCurrency`
|
||||||
|
- `DocumentTotalLocalCurrency`
|
||||||
|
- `VatSumForeignCurrency`
|
||||||
|
- `VatSumLocalCurrency`
|
||||||
|
- `DocumentRate`
|
||||||
|
- `CompanyCurrency`
|
||||||
|
|
||||||
|
B1-Feldmapping:
|
||||||
|
|
||||||
|
- `DocumentCurrency` = `OINV/ORIN.DocCur`
|
||||||
|
- `DocumentTotalForeignCurrency` = `OINV/ORIN.DocTotalFC`
|
||||||
|
- `DocumentTotalLocalCurrency` = `OINV/ORIN.DocTotal`
|
||||||
|
- `VatSumForeignCurrency` = `OINV/ORIN.VatSumFC`
|
||||||
|
- `VatSumLocalCurrency` = `OINV/ORIN.VatSum`
|
||||||
|
- `DocumentRate` = `OINV/ORIN.DocRate`
|
||||||
|
- `CompanyCurrency` = `OADM.MainCurncy`
|
||||||
|
- `StandardCostCurrency` = `OADM.MainCurncy`
|
||||||
|
|
||||||
|
Technische Umsetzung:
|
||||||
|
|
||||||
|
- `HanaQueryService` liest `OADM` jetzt per `CROSS JOIN`
|
||||||
|
- Invoice- und Credit-Note-Query liefern die neuen Felder
|
||||||
|
- bei Gutschriften werden Dokument- und Steuerbetraege mit negativem Vorzeichen uebernommen
|
||||||
|
- `CentralSalesRecords`-Schema wurde erweitert
|
||||||
|
- bestehende SQLite-DBs erhalten die neuen Spalten per `DatabaseSchemaMaintenanceService`
|
||||||
|
- `CentralSalesRecordService` persistiert und liest die neuen Felder
|
||||||
|
- `ExcelExportService` schreibt die neuen Spalten in Standort- und `Sales_All_*.xlsx`-Dateien
|
||||||
|
- `ManualExcelImportService` kann die neuen Spalten wieder einlesen
|
||||||
|
- `ConfigTransferService` erhaelt die neuen Felder beim Remapping zentraler Laufzeitdaten
|
||||||
|
|
||||||
|
Wichtig fuer Power BI:
|
||||||
|
|
||||||
|
- die neuen `DocumentTotal*`- und `VatSum*`-Felder sind Belegkopfwerte
|
||||||
|
- sie werden in der positionsbasierten Excel pro Positionszeile wiederholt
|
||||||
|
- diese Felder duerfen daher nicht blind positionsweise summiert werden
|
||||||
|
- fuer Belegkopfsummen in Power BI zuerst nach `DocumentType`, `Invoice Number`, `TSC` und ggf. `Land` deduplizieren
|
||||||
|
- positionsbasierte Auswertungen sollen weiterhin mit positionsbezogenen Feldern wie `Sales Price/Value`, `Quantity` oder `Standard cost` arbeiten
|
||||||
|
|
||||||
|
Verifikation:
|
||||||
|
|
||||||
|
- `dotnet build .\TrafagSalesExporter.csproj --verbosity minimal` erfolgreich
|
||||||
|
- `dotnet test .\TrafagSalesExporter.Tests\TrafagSalesExporter.Tests.csproj --verbosity minimal` erfolgreich
|
||||||
|
- `48/48` Tests gruen
|
||||||
|
- `ManualExcelImportServiceTests` pruefen die neuen Excel-Spalten
|
||||||
|
- `CentralSalesRecordServiceTests` pruefen Persistenz und Ruecklesen der neuen B1-Felder
|
||||||
|
|
||||||
|
## Nachtrag 2026-04-29 Clean-Code-/DI-Befund
|
||||||
|
|
||||||
|
Der aktuelle Code ist DI-orientiert und deutlich besser strukturiert als zu Beginn des Refactorings, aber noch nicht durchgehend ein Clean-Code-Ideal.
|
||||||
|
|
||||||
|
Positiv:
|
||||||
|
|
||||||
|
- Services werden weitgehend ueber Interfaces und DI verdrahtet
|
||||||
|
- `DataSourceAdapter` trennt die Quellsysteme
|
||||||
|
- Page-Services reduzieren direkte DB-Logik in mehreren Razor-Seiten
|
||||||
|
- `Scoped` fuer UI-nahe Services und `Singleton` fuer gemeinsame Infrastruktur/Orchestrierung ist bewusst gewaehlt
|
||||||
|
- Testabdeckung fuer zentrale Fachlogik ist vorhanden und waechst
|
||||||
|
|
||||||
|
Weiterhin offene Clean-Code-Risiken:
|
||||||
|
|
||||||
|
- `DatabaseInitializationService` ist weiterhin produktiver Reparatur-/Migrationspfad
|
||||||
|
- `Settings.razor` und `Standorte.razor` enthalten noch viel Workflow-/UI-Logik
|
||||||
|
- `ManagementCockpitService` und `ConfigTransferService` sind breit und sollten spaeter weiter aufgeteilt werden
|
||||||
|
- Retry-/Robustheitslayer fuer externe Systeme fehlt
|
||||||
|
- Secret-Store fehlt
|
||||||
|
- Auth-Rollenmodell ist aktuell pragmatisch, aber noch grob
|
||||||
|
|
||||||
|
Bewertung:
|
||||||
|
|
||||||
|
- Architektur: brauchbar bis gut
|
||||||
|
- DI: grundsaetzlich sauber
|
||||||
|
- Clean Code: mittel bis gut, mit klaren Altlasten
|
||||||
|
|
||||||
|
Dieser Befund wurde bewusst nur dokumentiert. Die strukturelle Bereinigung wird spaeter priorisiert.
|
||||||
|
|
||||||
|
## Nachtrag 2026-04-29 Authentifizierung / AD-Zugriffsschutz
|
||||||
|
|
||||||
|
Nach Rueckmeldung der IT wurde ein Zugriffsschutz fuer die Blazor-App eingebaut.
|
||||||
|
|
||||||
|
Vorher konnte jeder Benutzer mit Netzwerkzugriff auf die App-URL die Anwendung oeffnen. Das war kritisch, weil die App Verkaufsdaten, Standort-/Quellsystemkonfiguration, SharePoint-Konfiguration, Config Import/Export und Secrets bzw. Zugangsdatenfelder beruehrt.
|
||||||
|
|
||||||
|
Neuer Stand:
|
||||||
|
|
||||||
|
- die App ist grundsaetzlich authentifizierungspflichtig
|
||||||
|
- produktives Ziel ist Windows Authentication / Active Directory
|
||||||
|
- Berechtigungen laufen ueber AD-Gruppen
|
||||||
|
- es gibt keine eigene Benutzer-/Passwortverwaltung in der App
|
||||||
|
- es gibt keinen versteckten produktiven Backdoor
|
||||||
|
|
||||||
|
Neue Security-Dateien:
|
||||||
|
|
||||||
|
- `Security/SecurityOptions.cs`
|
||||||
|
- `Security/SecurityPolicies.cs`
|
||||||
|
- `Security/DevelopmentAuthenticationHandler.cs`
|
||||||
|
|
||||||
|
Geaenderte zentrale Dateien:
|
||||||
|
|
||||||
|
- `Program.cs`
|
||||||
|
- `Components/Routes.razor`
|
||||||
|
- `Components/_Imports.razor`
|
||||||
|
- `Components/Layout/NavMenu.razor`
|
||||||
|
- `Components/Layout/MainLayout.razor`
|
||||||
|
- `appsettings.json`
|
||||||
|
- `appsettings.Development.json`
|
||||||
|
|
||||||
|
Aktuelles Rollenmodell:
|
||||||
|
|
||||||
|
- `Security:AccessGroups` steuert Zugriff auf die App
|
||||||
|
- `Security:AdminGroups` steuert Admin-Berechtigung
|
||||||
|
- Default-Gruppen sind `TRAFAG\\TrafagSalesExporter-Users` und `TRAFAG\\TrafagSalesExporter-Admins`
|
||||||
|
- echte Gruppennamen muessen von der IT bestaetigt oder angepasst werden
|
||||||
|
|
||||||
|
Admin-geschuetzte Seiten:
|
||||||
|
|
||||||
|
- `Settings`
|
||||||
|
- `Standorte`
|
||||||
|
- `Transformations`
|
||||||
|
|
||||||
|
Dashboard, Management Cockpit und Logs bleiben fuer berechtigte angemeldete Benutzer sichtbar.
|
||||||
|
|
||||||
|
Development:
|
||||||
|
|
||||||
|
- `appsettings.Development.json` aktiviert bei `ASPNETCORE_ENVIRONMENT=Development` einen lokalen Development-Auth-Handler
|
||||||
|
- Default-User: `DEV\\TrafagDeveloper`
|
||||||
|
- `DevelopmentUserIsAdmin=true`, damit lokal weiter programmiert werden kann
|
||||||
|
- produktiv darf die App nicht mit `Development` laufen
|
||||||
|
|
||||||
|
IIS-/IT-Hinweise:
|
||||||
|
|
||||||
|
1. Windows Authentication aktivieren
|
||||||
|
2. Anonymous Authentication deaktivieren
|
||||||
|
3. `ASPNETCORE_ENVIRONMENT` produktiv nicht auf `Development` setzen
|
||||||
|
4. AD-Gruppen fuer Benutzer und Admins anlegen oder bestehende Gruppen eintragen
|
||||||
|
5. `Security:AccessGroups` und `Security:AdminGroups` in produktiver Konfiguration korrekt setzen
|
||||||
|
|
||||||
|
Verifikation:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dotnet build .\TrafagSalesExporter.csproj --verbosity minimal
|
||||||
|
dotnet test .\TrafagSalesExporter.Tests\TrafagSalesExporter.Tests.csproj --verbosity minimal
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis:
|
||||||
|
|
||||||
|
- Build erfolgreich
|
||||||
|
- Tests erfolgreich
|
||||||
|
- `48/48` Tests gruen
|
||||||
|
- Auth-Policy-Tests fuer AccessGroup, AdminGroup und Development-Admin vorhanden
|
||||||
|
- lokaler Development-Auth-Start geprueft: `http://localhost:55416` antwortet mit HTTP `200`
|
||||||
|
- bekannte MudBlazor-Analyzer-Warnungen zu `Dense` bleiben
|
||||||
|
|
||||||
## Nachtrag 2026-04-29 Management-Cockpit-Auswertung
|
## Nachtrag 2026-04-29 Management-Cockpit-Auswertung
|
||||||
|
|
||||||
Seit dem letzten dokumentierten Stand vom 2026-04-17 wurde das `Management Cockpit` weiter ausgebaut. Dieser Abschnitt rekonstruiert den aktuellen Stand aus dem Code, weil die Aenderungen nach einem PC-Absturz nicht direkt nachdokumentiert wurden.
|
Seit dem letzten dokumentierten Stand vom 2026-04-17 wurde das `Management Cockpit` weiter ausgebaut. Dieser Abschnitt rekonstruiert den aktuellen Stand aus dem Code, weil die Aenderungen nach einem PC-Absturz nicht direkt nachdokumentiert wurden.
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ Zielbild:
|
|||||||
## Technologie-Stack
|
## Technologie-Stack
|
||||||
|
|
||||||
- UI: Blazor Server + MudBlazor
|
- UI: Blazor Server + MudBlazor
|
||||||
|
- Authentifizierung: ASP.NET Core Authentication/Authorization, produktiv Windows Authentication / Active Directory
|
||||||
- Datenbank: SQLite (`trafag_exporter.db`)
|
- Datenbank: SQLite (`trafag_exporter.db`)
|
||||||
- Excel lesen/schreiben: ClosedXML
|
- Excel lesen/schreiben: ClosedXML
|
||||||
- SAP HANA Zugriff: `Sap.Data.Hana.Core.v2.1.dll`
|
- SAP HANA Zugriff: `Sap.Data.Hana.Core.v2.1.dll`
|
||||||
@@ -42,6 +43,14 @@ Wichtige Dateien:
|
|||||||
|
|
||||||
`Program.cs` registriert fast die komplette Architektur ueber DI und fuehrt beim Start `DatabaseInitializationService.InitializeAsync()` aus.
|
`Program.cs` registriert fast die komplette Architektur ueber DI und fuehrt beim Start `DatabaseInitializationService.InitializeAsync()` aus.
|
||||||
|
|
||||||
|
Zusaetzlich registriert `Program.cs` den Zugriffsschutz:
|
||||||
|
|
||||||
|
- `AddCascadingAuthenticationState`
|
||||||
|
- Windows Authentication fuer produktive Umgebungen
|
||||||
|
- Development-Authentication-Handler nur bei `ASPNETCORE_ENVIRONMENT=Development` und `Security:DevelopmentBypass=true`
|
||||||
|
- globale Fallback-Policy fuer authentifizierte/berechtigte User
|
||||||
|
- Policy `AdminOnly` fuer administrative Seiten
|
||||||
|
|
||||||
## Hauptseiten
|
## Hauptseiten
|
||||||
|
|
||||||
Navigation:
|
Navigation:
|
||||||
@@ -71,6 +80,13 @@ Kurzrollen:
|
|||||||
- `Settings`: SharePoint, Exportpfade, Quellsysteme, Wechselkurse, Config Import/Export
|
- `Settings`: SharePoint, Exportpfade, Quellsysteme, Wechselkurse, Config Import/Export
|
||||||
- `Logs`: technische Ereignisprotokolle
|
- `Logs`: technische Ereignisprotokolle
|
||||||
|
|
||||||
|
Security:
|
||||||
|
|
||||||
|
- alle Routen erfordern Authentifizierung
|
||||||
|
- `Settings`, `Standorte` und `Transformations` sind `AdminOnly`
|
||||||
|
- Admin-Navigation wird nur fuer Admins angezeigt
|
||||||
|
- eingeloggter Benutzer wird im App-Bar angezeigt
|
||||||
|
|
||||||
## Kernmodelle
|
## Kernmodelle
|
||||||
|
|
||||||
Wichtige Entity-Klassen:
|
Wichtige Entity-Klassen:
|
||||||
@@ -90,6 +106,18 @@ Wichtige Entity-Klassen:
|
|||||||
- [Models/AppEventLog.cs](C:/Users/koi/source/repos/Ai/TrafagSalesExporter/Models/AppEventLog.cs)
|
- [Models/AppEventLog.cs](C:/Users/koi/source/repos/Ai/TrafagSalesExporter/Models/AppEventLog.cs)
|
||||||
- [Models/CurrencyExchangeRate.cs](C:/Users/koi/source/repos/Ai/TrafagSalesExporter/Models/CurrencyExchangeRate.cs)
|
- [Models/CurrencyExchangeRate.cs](C:/Users/koi/source/repos/Ai/TrafagSalesExporter/Models/CurrencyExchangeRate.cs)
|
||||||
|
|
||||||
|
`SalesRecord` / `CentralSalesRecord` enthalten neben den positionsnahen Feldern auch B1-Belegwaehrungsfelder:
|
||||||
|
|
||||||
|
- `DocumentCurrency` aus `DocCur`
|
||||||
|
- `DocumentTotalForeignCurrency` aus `DocTotalFC`
|
||||||
|
- `DocumentTotalLocalCurrency` aus `DocTotal`
|
||||||
|
- `VatSumForeignCurrency` aus `VatSumFC`
|
||||||
|
- `VatSumLocalCurrency` aus `VatSum`
|
||||||
|
- `DocumentRate` aus `DocRate`
|
||||||
|
- `CompanyCurrency` aus `OADM.MainCurncy`
|
||||||
|
|
||||||
|
Wichtig: diese Dokumentwerte sind Belegkopfwerte und werden in der positionsbasierten Excel pro Position wiederholt. Fuer Belegkopfsummen muessen Auswertungen nach Beleg deduplizieren.
|
||||||
|
|
||||||
Wichtige Relationen:
|
Wichtige Relationen:
|
||||||
|
|
||||||
- `Site -> HanaServer` optional
|
- `Site -> HanaServer` optional
|
||||||
@@ -403,6 +431,49 @@ Bereits gehaertete Fehlerbilder:
|
|||||||
- Legacy-Credential-Spalten in `HanaServers`
|
- Legacy-Credential-Spalten in `HanaServers`
|
||||||
- verschobene Spalten im `Sites_old -> Sites`-Kopierpfad
|
- verschobene Spalten im `Sites_old -> Sites`-Kopierpfad
|
||||||
|
|
||||||
|
## Authentifizierung / Autorisierung
|
||||||
|
|
||||||
|
Dateien:
|
||||||
|
|
||||||
|
- [Security/SecurityOptions.cs](C:/Users/koi/source/repos/Ai/TrafagSalesExporter/Security/SecurityOptions.cs)
|
||||||
|
- [Security/SecurityPolicies.cs](C:/Users/koi/source/repos/Ai/TrafagSalesExporter/Security/SecurityPolicies.cs)
|
||||||
|
- [Security/DevelopmentAuthenticationHandler.cs](C:/Users/koi/source/repos/Ai/TrafagSalesExporter/Security/DevelopmentAuthenticationHandler.cs)
|
||||||
|
- [Components/Routes.razor](C:/Users/koi/source/repos/Ai/TrafagSalesExporter/Components/Routes.razor)
|
||||||
|
- [Components/Layout/NavMenu.razor](C:/Users/koi/source/repos/Ai/TrafagSalesExporter/Components/Layout/NavMenu.razor)
|
||||||
|
- [Components/Layout/MainLayout.razor](C:/Users/koi/source/repos/Ai/TrafagSalesExporter/Components/Layout/MainLayout.razor)
|
||||||
|
|
||||||
|
Produktives Ziel:
|
||||||
|
|
||||||
|
- Windows Authentication / Active Directory
|
||||||
|
- keine eigene Benutzerverwaltung
|
||||||
|
- Zugriff ueber AD-Gruppen
|
||||||
|
- Adminrechte ueber separate AD-Gruppe
|
||||||
|
|
||||||
|
Konfiguration in `appsettings.json`:
|
||||||
|
|
||||||
|
- `Security:AccessGroups`
|
||||||
|
- `Security:AdminGroups`
|
||||||
|
- `Security:DevelopmentBypass`
|
||||||
|
- `Security:DevelopmentUserIsAdmin`
|
||||||
|
- `Security:DevelopmentUserName`
|
||||||
|
|
||||||
|
Default-Gruppen:
|
||||||
|
|
||||||
|
- `TRAFAG\\TrafagSalesExporter-Users`
|
||||||
|
- `TRAFAG\\TrafagSalesExporter-Admins`
|
||||||
|
|
||||||
|
Development:
|
||||||
|
|
||||||
|
- `appsettings.Development.json` aktiviert einen lokalen Development-Auth-Handler
|
||||||
|
- dieser ist nur fuer lokale Entwicklung gedacht
|
||||||
|
- produktiv darf `ASPNETCORE_ENVIRONMENT` nicht `Development` sein
|
||||||
|
|
||||||
|
IIS-Betrieb:
|
||||||
|
|
||||||
|
- Windows Authentication aktivieren
|
||||||
|
- Anonymous Authentication deaktivieren
|
||||||
|
- AD-Gruppennamen in produktiver Konfiguration setzen
|
||||||
|
|
||||||
## Config Import / Export
|
## Config Import / Export
|
||||||
|
|
||||||
Dateien:
|
Dateien:
|
||||||
@@ -462,6 +533,19 @@ Aktuell vorhandene Schwerpunkte:
|
|||||||
- Mengen-Auswertung ohne Waehrungsumrechnung
|
- Mengen-Auswertung ohne Waehrungsumrechnung
|
||||||
- Zusatz-Summenfelder in Zeitreihen
|
- Zusatz-Summenfelder in Zeitreihen
|
||||||
|
|
||||||
|
`SecurityPolicyFactoryTests` decken inzwischen ab:
|
||||||
|
|
||||||
|
- App-Zugriff fuer User in `AccessGroups`
|
||||||
|
- Ablehnung fuer User ausserhalb der Access-Gruppen
|
||||||
|
- Development-Auth-Zugriff im lokalen Modus
|
||||||
|
- Admin-Zugriff fuer User in `AdminGroups`
|
||||||
|
- Ablehnung normaler User fuer `AdminOnly`
|
||||||
|
- Development-Admin-Claim
|
||||||
|
|
||||||
|
`CentralSalesRecordServiceTests` decken inzwischen ab:
|
||||||
|
|
||||||
|
- Persistenz und Ruecklesen der B1-Belegwaehrungsfelder in `CentralSalesRecords`
|
||||||
|
|
||||||
Wichtig:
|
Wichtig:
|
||||||
|
|
||||||
- es gibt aktuell keine echten UI-Komponententests mit `bUnit`
|
- es gibt aktuell keine echten UI-Komponententests mit `bUnit`
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ public class CentralSalesRecord
|
|||||||
public string PurchaseOrderNumber { get; set; } = string.Empty;
|
public string PurchaseOrderNumber { get; set; } = string.Empty;
|
||||||
public decimal SalesPriceValue { get; set; }
|
public decimal SalesPriceValue { get; set; }
|
||||||
public string SalesCurrency { get; set; } = string.Empty;
|
public string SalesCurrency { get; set; } = string.Empty;
|
||||||
|
public string DocumentCurrency { get; set; } = string.Empty;
|
||||||
|
public decimal DocumentTotalForeignCurrency { get; set; }
|
||||||
|
public decimal DocumentTotalLocalCurrency { get; set; }
|
||||||
|
public decimal VatSumForeignCurrency { get; set; }
|
||||||
|
public decimal VatSumLocalCurrency { get; set; }
|
||||||
|
public decimal DocumentRate { get; set; }
|
||||||
|
public string CompanyCurrency { get; set; } = string.Empty;
|
||||||
public string Incoterms2020 { get; set; } = string.Empty;
|
public string Incoterms2020 { get; set; } = string.Empty;
|
||||||
public string SalesResponsibleEmployee { get; set; } = string.Empty;
|
public string SalesResponsibleEmployee { get; set; } = string.Empty;
|
||||||
public DateTime? InvoiceDate { get; set; }
|
public DateTime? InvoiceDate { get; set; }
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ public class SalesRecord
|
|||||||
public string PurchaseOrderNumber { get; set; } = string.Empty;
|
public string PurchaseOrderNumber { get; set; } = string.Empty;
|
||||||
public decimal SalesPriceValue { get; set; }
|
public decimal SalesPriceValue { get; set; }
|
||||||
public string SalesCurrency { get; set; } = string.Empty;
|
public string SalesCurrency { get; set; } = string.Empty;
|
||||||
|
public string DocumentCurrency { get; set; } = string.Empty;
|
||||||
|
public decimal DocumentTotalForeignCurrency { get; set; }
|
||||||
|
public decimal DocumentTotalLocalCurrency { get; set; }
|
||||||
|
public decimal VatSumForeignCurrency { get; set; }
|
||||||
|
public decimal VatSumLocalCurrency { get; set; }
|
||||||
|
public decimal DocumentRate { get; set; }
|
||||||
|
public string CompanyCurrency { get; set; } = string.Empty;
|
||||||
public string Incoterms2020 { get; set; } = string.Empty;
|
public string Incoterms2020 { get; set; } = string.Empty;
|
||||||
public string SalesResponsibleEmployee { get; set; } = string.Empty;
|
public string SalesResponsibleEmployee { get; set; } = string.Empty;
|
||||||
public DateTime? InvoiceDate { get; set; }
|
public DateTime? InvoiceDate { get; set; }
|
||||||
|
|||||||
@@ -2,6 +2,183 @@
|
|||||||
|
|
||||||
Stand: 2026-04-15
|
Stand: 2026-04-15
|
||||||
|
|
||||||
|
## Nachtrag 2026-04-29 B1-Belegwaehrungsfelder
|
||||||
|
|
||||||
|
Der HANA/B1-Export zieht jetzt zusaetzliche Belegwaehrungsfelder:
|
||||||
|
|
||||||
|
- `DocCur`
|
||||||
|
- `DocTotalFC`
|
||||||
|
- `DocTotal`
|
||||||
|
- `VatSumFC`
|
||||||
|
- `VatSum`
|
||||||
|
- `DocRate`
|
||||||
|
- `OADM.MainCurncy`
|
||||||
|
|
||||||
|
Neue Zielfelder:
|
||||||
|
|
||||||
|
- `DocumentCurrency`
|
||||||
|
- `DocumentTotalForeignCurrency`
|
||||||
|
- `DocumentTotalLocalCurrency`
|
||||||
|
- `VatSumForeignCurrency`
|
||||||
|
- `VatSumLocalCurrency`
|
||||||
|
- `DocumentRate`
|
||||||
|
- `CompanyCurrency`
|
||||||
|
|
||||||
|
Zusaetzlich gilt jetzt:
|
||||||
|
|
||||||
|
- `StandardCostCurrency` kommt im HANA-Pfad aus `OADM.MainCurncy`
|
||||||
|
- `Sales_All_*.xlsx` enthaelt die neuen Spalten
|
||||||
|
- `CentralSalesRecords` enthaelt die neuen Spalten
|
||||||
|
- bestehende SQLite-DBs werden beim Start um die Spalten erweitert
|
||||||
|
- Manual-Excel-Import kann die neuen Spalten lesen
|
||||||
|
|
||||||
|
### Wichtig fuer Auswertungen
|
||||||
|
|
||||||
|
Die neuen `DocumentTotal*`- und `VatSum*`-Werte sind Belegkopfwerte und werden in der positionsbasierten Datei pro Position wiederholt.
|
||||||
|
|
||||||
|
Power BI:
|
||||||
|
|
||||||
|
- nicht positionsweise summieren
|
||||||
|
- zuerst nach Beleg deduplizieren, z. B. `TSC` + `DocumentType` + `Invoice Number`
|
||||||
|
- danach Belegkopfwerte summieren
|
||||||
|
|
||||||
|
Positionswerte wie `Sales Price/Value`, `Quantity` und `Standard cost` bleiben fuer positionsbasierte Summen geeignet.
|
||||||
|
|
||||||
|
### Verifikation
|
||||||
|
|
||||||
|
Geprueft:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dotnet build .\TrafagSalesExporter.csproj --verbosity minimal
|
||||||
|
dotnet test .\TrafagSalesExporter.Tests\TrafagSalesExporter.Tests.csproj --verbosity minimal
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis:
|
||||||
|
|
||||||
|
- Build erfolgreich
|
||||||
|
- Tests erfolgreich
|
||||||
|
- `48/48` Tests gruen
|
||||||
|
|
||||||
|
## Nachtrag 2026-04-29 Clean-Code-/DI-Befund
|
||||||
|
|
||||||
|
Aktueller Architektur- und DI-Zustand nach den letzten Umbauten:
|
||||||
|
|
||||||
|
Gesamturteil:
|
||||||
|
|
||||||
|
- die App ist deutlich besser strukturiert als zu Beginn
|
||||||
|
- die Grundarchitektur ist brauchbar bis gut und fuer pragmatischen produktiven Einsatz geeignet
|
||||||
|
- Dependency Injection wird grundsaetzlich sinnvoll genutzt
|
||||||
|
- Clean Code ist mittel bis gut, aber noch nicht durchgehend konsequent
|
||||||
|
|
||||||
|
Was positiv ist:
|
||||||
|
|
||||||
|
- Kernservices laufen weitgehend ueber Interfaces und DI
|
||||||
|
- `DataSourceAdapter`-Pattern trennt `HANA`, `SAP_GATEWAY` und `MANUAL_EXCEL`
|
||||||
|
- `SiteExportService` ist dadurch deutlich schlanker als frueher
|
||||||
|
- UI-nahe Page-Services wurden eingefuehrt
|
||||||
|
- viele Razor-Seiten sind nicht mehr direkt `DbContext`-lastig
|
||||||
|
- `Scoped` fuer Page-Services und `Singleton` fuer gemeinsame Infrastruktur/Orchestrierung ist bewusst gewaehlt
|
||||||
|
- Tests decken wichtige Fachlogik ab, u. a. Transformationen, ConfigTransfer, DatabaseInitialization und ManagementCockpit
|
||||||
|
|
||||||
|
Was noch nicht ideal ist:
|
||||||
|
|
||||||
|
- `DatabaseInitializationService` bleibt ein produktiver Reparatur-/Migrationsblock und ist kein sauberes versioniertes Migrationssystem
|
||||||
|
- `Settings.razor` und `Standorte.razor` enthalten weiterhin relativ viel UI-/Workflow-Logik
|
||||||
|
- `ManagementCockpitService`, `ConfigTransferService` und Teile der Initialisierung sind noch sehr breit
|
||||||
|
- konsolidierter Export hat historisch noch Semantikreste zwischen Live-Snapshot und `CentralSalesRecords`
|
||||||
|
- Secrets/Zugangsdaten sind noch nicht ideal geloest
|
||||||
|
- zentraler Retry-/Resilience-Layer fuer SAP/HANA/SharePoint fehlt
|
||||||
|
- Auth ist jetzt pragmatisch mit User/Admin geschnitten, aber noch nicht fein nach `Viewer`, `Exporter`, `Admin`, `Finance`
|
||||||
|
|
||||||
|
Sinnvolle spaetere Clean-Code-Schritte:
|
||||||
|
|
||||||
|
1. `ManagementCockpitService` in kleinere Query-, Aggregation- und Currency-Komponenten teilen
|
||||||
|
2. `Settings.razor` und `Standorte.razor` weiter Richtung Page-/Application-Services entlasten
|
||||||
|
3. `DatabaseInitializationService` langfristig durch versionierte Migrationen ersetzen
|
||||||
|
4. Auth-Policies fachlich feiner schneiden, z. B. `Viewer`, `Exporter`, `Admin`, `Finance`
|
||||||
|
5. Retry/Timeout/Failure-Handling fuer externe Systeme zentralisieren
|
||||||
|
6. Secret-Store-Konzept umsetzen
|
||||||
|
|
||||||
|
## Nachtrag 2026-04-29 Authentifizierung / AD
|
||||||
|
|
||||||
|
Die App wurde nach IT-Rueckmeldung gegen anonymen Zugriff abgesichert.
|
||||||
|
|
||||||
|
Neuer Stand:
|
||||||
|
|
||||||
|
- globale Authentifizierungspflicht
|
||||||
|
- produktiv vorgesehen: Windows Authentication / Active Directory
|
||||||
|
- Zugriff und Adminrechte ueber AD-Gruppen
|
||||||
|
- kein eigener App-Login
|
||||||
|
- kein versteckter produktiver Backdoor
|
||||||
|
- lokaler Development-Bypass nur bei `ASPNETCORE_ENVIRONMENT=Development`
|
||||||
|
|
||||||
|
Neue/angepasste Dateien:
|
||||||
|
|
||||||
|
- `Program.cs`
|
||||||
|
- `Security/SecurityOptions.cs`
|
||||||
|
- `Security/SecurityPolicies.cs`
|
||||||
|
- `Security/DevelopmentAuthenticationHandler.cs`
|
||||||
|
- `Components/Routes.razor`
|
||||||
|
- `Components/_Imports.razor`
|
||||||
|
- `Components/Layout/NavMenu.razor`
|
||||||
|
- `Components/Layout/MainLayout.razor`
|
||||||
|
- `Components/Pages/Settings.razor`
|
||||||
|
- `Components/Pages/Standorte.razor`
|
||||||
|
- `Components/Pages/Transformations.razor`
|
||||||
|
- `appsettings.json`
|
||||||
|
- `appsettings.Development.json`
|
||||||
|
|
||||||
|
Aktuelle Default-Gruppen:
|
||||||
|
|
||||||
|
- `TRAFAG\TrafagSalesExporter-Users`
|
||||||
|
- `TRAFAG\TrafagSalesExporter-Admins`
|
||||||
|
|
||||||
|
### Noch mit IT zu klaeren
|
||||||
|
|
||||||
|
1. Exakte AD-Domain-/Gruppennamen bestaetigen
|
||||||
|
2. AD-Gruppen anlegen oder bestehende Gruppen verwenden
|
||||||
|
3. IIS-Zielumgebung festlegen
|
||||||
|
4. Auf IIS Windows Authentication aktivieren
|
||||||
|
5. Auf IIS Anonymous Authentication deaktivieren
|
||||||
|
6. Sicherstellen, dass produktiv nicht `ASPNETCORE_ENVIRONMENT=Development` gesetzt ist
|
||||||
|
7. Test mit einem normalen User und einem Admin-User durchfuehren
|
||||||
|
|
||||||
|
### Fachliche Rollenentscheidung
|
||||||
|
|
||||||
|
Aktuell:
|
||||||
|
|
||||||
|
- Admin:
|
||||||
|
- `Settings`
|
||||||
|
- `Standorte`
|
||||||
|
- `Transformations`
|
||||||
|
- berechtigter User:
|
||||||
|
- Dashboard
|
||||||
|
- Management Cockpit
|
||||||
|
- Logs
|
||||||
|
|
||||||
|
Noch zu entscheiden:
|
||||||
|
|
||||||
|
- ob `Logs` ebenfalls Admin-only sein soll
|
||||||
|
- ob Export-Buttons im Dashboard nur fuer eine eigene Rolle `Exporter` sichtbar sein sollen
|
||||||
|
- ob Management Cockpit fuer alle berechtigten User oder nur fuer Management/Finance-Gruppen sichtbar sein soll
|
||||||
|
|
||||||
|
### Verifikation
|
||||||
|
|
||||||
|
Geprueft:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dotnet build .\TrafagSalesExporter.csproj --verbosity minimal
|
||||||
|
dotnet test .\TrafagSalesExporter.Tests\TrafagSalesExporter.Tests.csproj --verbosity minimal
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis:
|
||||||
|
|
||||||
|
- Build erfolgreich
|
||||||
|
- Tests erfolgreich
|
||||||
|
- `48/48` Tests gruen
|
||||||
|
- Auth-Policy-Tests fuer AccessGroup, AdminGroup und Development-Admin vorhanden
|
||||||
|
- lokaler Development-Auth-Start geprueft: `http://localhost:55416` antwortet mit HTTP `200`
|
||||||
|
|
||||||
## Nachtrag 2026-04-29 Management Cockpit
|
## Nachtrag 2026-04-29 Management Cockpit
|
||||||
|
|
||||||
Seit dem 2026-04-17 wurden im `Management Cockpit` weitere Auswertmoeglichkeiten umgesetzt und nachtraeglich aus dem aktuellen Code rekonstruiert.
|
Seit dem 2026-04-17 wurden im `Management Cockpit` weitere Auswertmoeglichkeiten umgesetzt und nachtraeglich aus dem aktuellen Code rekonstruiert.
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Server.IISIntegration;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MudBlazor.Services;
|
using MudBlazor.Services;
|
||||||
using TrafagSalesExporter.Data;
|
using TrafagSalesExporter.Data;
|
||||||
|
using TrafagSalesExporter.Security;
|
||||||
using TrafagSalesExporter.Services;
|
using TrafagSalesExporter.Services;
|
||||||
using TrafagSalesExporter.Services.DataSources;
|
using TrafagSalesExporter.Services.DataSources;
|
||||||
|
|
||||||
@@ -13,6 +17,29 @@ builder.Logging.AddFilter("Microsoft.EntityFrameworkCore.Database.Command", LogL
|
|||||||
|
|
||||||
builder.Services.AddRazorComponents()
|
builder.Services.AddRazorComponents()
|
||||||
.AddInteractiveServerComponents();
|
.AddInteractiveServerComponents();
|
||||||
|
builder.Services.AddCascadingAuthenticationState();
|
||||||
|
|
||||||
|
var securitySettings = builder.Configuration.GetSection(SecurityOptions.SectionName).Get<SecurityOptions>() ?? new SecurityOptions();
|
||||||
|
var useDevelopmentAuthentication = builder.Environment.IsDevelopment() && securitySettings.DevelopmentBypass;
|
||||||
|
|
||||||
|
if (useDevelopmentAuthentication)
|
||||||
|
{
|
||||||
|
builder.Services
|
||||||
|
.AddAuthentication(DevelopmentAuthenticationHandler.SchemeName)
|
||||||
|
.AddScheme<AuthenticationSchemeOptions, DevelopmentAuthenticationHandler>(
|
||||||
|
DevelopmentAuthenticationHandler.SchemeName,
|
||||||
|
options => { });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
builder.Services.AddAuthentication(IISDefaults.AuthenticationScheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Services.AddAuthorization(options =>
|
||||||
|
{
|
||||||
|
options.FallbackPolicy = SecurityPolicyFactory.BuildAccessPolicy(securitySettings, useDevelopmentAuthentication);
|
||||||
|
options.AddPolicy(SecurityPolicies.AdminOnly, SecurityPolicyFactory.BuildAdminPolicy(securitySettings, useDevelopmentAuthentication));
|
||||||
|
});
|
||||||
|
|
||||||
builder.Services.AddMudServices();
|
builder.Services.AddMudServices();
|
||||||
builder.Services.AddHttpClient(nameof(ExchangeRateImportService));
|
builder.Services.AddHttpClient(nameof(ExchangeRateImportService));
|
||||||
@@ -87,6 +114,8 @@ if (!app.Environment.IsDevelopment())
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
app.UseAntiforgery();
|
app.UseAntiforgery();
|
||||||
|
|
||||||
app.MapRazorComponents<TrafagSalesExporter.Components.App>()
|
app.MapRazorComponents<TrafagSalesExporter.Components.App>()
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Security;
|
||||||
|
|
||||||
|
public sealed class DevelopmentAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||||
|
{
|
||||||
|
public const string SchemeName = "Development";
|
||||||
|
public const string AdminClaimType = "TrafagSalesExporter.Admin";
|
||||||
|
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public DevelopmentAuthenticationHandler(
|
||||||
|
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||||
|
ILoggerFactory logger,
|
||||||
|
UrlEncoder encoder,
|
||||||
|
IConfiguration configuration)
|
||||||
|
: base(options, logger, encoder)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
|
{
|
||||||
|
var settings = _configuration.GetSection(SecurityOptions.SectionName).Get<SecurityOptions>() ?? new SecurityOptions();
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new(ClaimTypes.Name, settings.DevelopmentUserName),
|
||||||
|
new(ClaimTypes.NameIdentifier, settings.DevelopmentUserName)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (settings.DevelopmentUserIsAdmin)
|
||||||
|
claims.Add(new Claim(AdminClaimType, "true"));
|
||||||
|
|
||||||
|
var identity = new ClaimsIdentity(claims, SchemeName);
|
||||||
|
var principal = new ClaimsPrincipal(identity);
|
||||||
|
var ticket = new AuthenticationTicket(principal, SchemeName);
|
||||||
|
|
||||||
|
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace TrafagSalesExporter.Security;
|
||||||
|
|
||||||
|
public sealed class SecurityOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "Security";
|
||||||
|
|
||||||
|
public bool DevelopmentBypass { get; set; }
|
||||||
|
public bool DevelopmentUserIsAdmin { get; set; }
|
||||||
|
public string DevelopmentUserName { get; set; } = "DEV\\TrafagDeveloper";
|
||||||
|
public List<string> AccessGroups { get; set; } = [];
|
||||||
|
public List<string> AdminGroups { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace TrafagSalesExporter.Security;
|
||||||
|
|
||||||
|
public static class SecurityPolicies
|
||||||
|
{
|
||||||
|
public const string AdminOnly = nameof(AdminOnly);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Security;
|
||||||
|
|
||||||
|
public static class SecurityPolicyFactory
|
||||||
|
{
|
||||||
|
public static AuthorizationPolicy BuildAccessPolicy(SecurityOptions settings, bool useDevelopmentAuthentication)
|
||||||
|
{
|
||||||
|
var builder = new AuthorizationPolicyBuilder()
|
||||||
|
.RequireAuthenticatedUser();
|
||||||
|
|
||||||
|
if (!useDevelopmentAuthentication && settings.AccessGroups.Count > 0)
|
||||||
|
{
|
||||||
|
builder.RequireAssertion(context =>
|
||||||
|
settings.AccessGroups.Any(group => context.User.IsInRole(group)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AuthorizationPolicy BuildAdminPolicy(SecurityOptions settings, bool useDevelopmentAuthentication)
|
||||||
|
{
|
||||||
|
var builder = new AuthorizationPolicyBuilder()
|
||||||
|
.RequireAuthenticatedUser();
|
||||||
|
|
||||||
|
builder.RequireAssertion(context =>
|
||||||
|
useDevelopmentAuthentication && context.User.HasClaim(DevelopmentAuthenticationHandler.AdminClaimType, "true") ||
|
||||||
|
settings.AdminGroups.Any(group => context.User.IsInRole(group)));
|
||||||
|
|
||||||
|
return builder.Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,6 +82,13 @@ public class CentralSalesRecordService : ICentralSalesRecordService
|
|||||||
PurchaseOrderNumber = r.PurchaseOrderNumber,
|
PurchaseOrderNumber = r.PurchaseOrderNumber,
|
||||||
SalesPriceValue = r.SalesPriceValue,
|
SalesPriceValue = r.SalesPriceValue,
|
||||||
SalesCurrency = r.SalesCurrency,
|
SalesCurrency = r.SalesCurrency,
|
||||||
|
DocumentCurrency = r.DocumentCurrency,
|
||||||
|
DocumentTotalForeignCurrency = r.DocumentTotalForeignCurrency,
|
||||||
|
DocumentTotalLocalCurrency = r.DocumentTotalLocalCurrency,
|
||||||
|
VatSumForeignCurrency = r.VatSumForeignCurrency,
|
||||||
|
VatSumLocalCurrency = r.VatSumLocalCurrency,
|
||||||
|
DocumentRate = r.DocumentRate,
|
||||||
|
CompanyCurrency = r.CompanyCurrency,
|
||||||
Incoterms2020 = r.Incoterms2020,
|
Incoterms2020 = r.Incoterms2020,
|
||||||
SalesResponsibleEmployee = r.SalesResponsibleEmployee,
|
SalesResponsibleEmployee = r.SalesResponsibleEmployee,
|
||||||
InvoiceDate = r.InvoiceDate,
|
InvoiceDate = r.InvoiceDate,
|
||||||
@@ -158,14 +165,16 @@ public class CentralSalesRecordService : ICentralSalesRecordService
|
|||||||
Material, Name, ProductGroup, Quantity, SupplierNumber, SupplierName, SupplierCountry,
|
Material, Name, ProductGroup, Quantity, SupplierNumber, SupplierName, SupplierCountry,
|
||||||
CustomerNumber, CustomerName, CustomerCountry, CustomerIndustry, StandardCost,
|
CustomerNumber, CustomerName, CustomerCountry, CustomerIndustry, StandardCost,
|
||||||
StandardCostCurrency, PurchaseOrderNumber, SalesPriceValue, SalesCurrency, Incoterms2020,
|
StandardCostCurrency, PurchaseOrderNumber, SalesPriceValue, SalesCurrency, Incoterms2020,
|
||||||
SalesResponsibleEmployee, InvoiceDate, OrderDate, Land, DocumentType
|
DocumentCurrency, DocumentTotalForeignCurrency, DocumentTotalLocalCurrency, VatSumForeignCurrency,
|
||||||
|
VatSumLocalCurrency, DocumentRate, CompanyCurrency, SalesResponsibleEmployee, InvoiceDate, OrderDate, Land, DocumentType
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$storedAtUtc, $siteId, $sourceSystem, $extractionDate, $tsc, $invoiceNumber, $positionOnInvoice,
|
$storedAtUtc, $siteId, $sourceSystem, $extractionDate, $tsc, $invoiceNumber, $positionOnInvoice,
|
||||||
$material, $name, $productGroup, $quantity, $supplierNumber, $supplierName, $supplierCountry,
|
$material, $name, $productGroup, $quantity, $supplierNumber, $supplierName, $supplierCountry,
|
||||||
$customerNumber, $customerName, $customerCountry, $customerIndustry, $standardCost,
|
$customerNumber, $customerName, $customerCountry, $customerIndustry, $standardCost,
|
||||||
$standardCostCurrency, $purchaseOrderNumber, $salesPriceValue, $salesCurrency, $incoterms2020,
|
$standardCostCurrency, $purchaseOrderNumber, $salesPriceValue, $salesCurrency, $incoterms2020,
|
||||||
$salesResponsibleEmployee, $invoiceDate, $orderDate, $land, $documentType
|
$documentCurrency, $documentTotalForeignCurrency, $documentTotalLocalCurrency, $vatSumForeignCurrency,
|
||||||
|
$vatSumLocalCurrency, $documentRate, $companyCurrency, $salesResponsibleEmployee, $invoiceDate, $orderDate, $land, $documentType
|
||||||
);
|
);
|
||||||
""";
|
""";
|
||||||
|
|
||||||
@@ -192,6 +201,13 @@ public class CentralSalesRecordService : ICentralSalesRecordService
|
|||||||
command.Parameters.Add("$purchaseOrderNumber", SqliteType.Text);
|
command.Parameters.Add("$purchaseOrderNumber", SqliteType.Text);
|
||||||
command.Parameters.Add("$salesPriceValue", SqliteType.Real);
|
command.Parameters.Add("$salesPriceValue", SqliteType.Real);
|
||||||
command.Parameters.Add("$salesCurrency", SqliteType.Text);
|
command.Parameters.Add("$salesCurrency", SqliteType.Text);
|
||||||
|
command.Parameters.Add("$documentCurrency", SqliteType.Text);
|
||||||
|
command.Parameters.Add("$documentTotalForeignCurrency", SqliteType.Real);
|
||||||
|
command.Parameters.Add("$documentTotalLocalCurrency", SqliteType.Real);
|
||||||
|
command.Parameters.Add("$vatSumForeignCurrency", SqliteType.Real);
|
||||||
|
command.Parameters.Add("$vatSumLocalCurrency", SqliteType.Real);
|
||||||
|
command.Parameters.Add("$documentRate", SqliteType.Real);
|
||||||
|
command.Parameters.Add("$companyCurrency", SqliteType.Text);
|
||||||
command.Parameters.Add("$incoterms2020", SqliteType.Text);
|
command.Parameters.Add("$incoterms2020", SqliteType.Text);
|
||||||
command.Parameters.Add("$salesResponsibleEmployee", SqliteType.Text);
|
command.Parameters.Add("$salesResponsibleEmployee", SqliteType.Text);
|
||||||
command.Parameters.Add("$invoiceDate", SqliteType.Text);
|
command.Parameters.Add("$invoiceDate", SqliteType.Text);
|
||||||
@@ -227,6 +243,13 @@ public class CentralSalesRecordService : ICentralSalesRecordService
|
|||||||
command.Parameters["$purchaseOrderNumber"].Value = record.PurchaseOrderNumber ?? string.Empty;
|
command.Parameters["$purchaseOrderNumber"].Value = record.PurchaseOrderNumber ?? string.Empty;
|
||||||
command.Parameters["$salesPriceValue"].Value = record.SalesPriceValue;
|
command.Parameters["$salesPriceValue"].Value = record.SalesPriceValue;
|
||||||
command.Parameters["$salesCurrency"].Value = record.SalesCurrency ?? string.Empty;
|
command.Parameters["$salesCurrency"].Value = record.SalesCurrency ?? string.Empty;
|
||||||
|
command.Parameters["$documentCurrency"].Value = record.DocumentCurrency ?? string.Empty;
|
||||||
|
command.Parameters["$documentTotalForeignCurrency"].Value = record.DocumentTotalForeignCurrency;
|
||||||
|
command.Parameters["$documentTotalLocalCurrency"].Value = record.DocumentTotalLocalCurrency;
|
||||||
|
command.Parameters["$vatSumForeignCurrency"].Value = record.VatSumForeignCurrency;
|
||||||
|
command.Parameters["$vatSumLocalCurrency"].Value = record.VatSumLocalCurrency;
|
||||||
|
command.Parameters["$documentRate"].Value = record.DocumentRate;
|
||||||
|
command.Parameters["$companyCurrency"].Value = record.CompanyCurrency ?? string.Empty;
|
||||||
command.Parameters["$incoterms2020"].Value = record.Incoterms2020 ?? string.Empty;
|
command.Parameters["$incoterms2020"].Value = record.Incoterms2020 ?? string.Empty;
|
||||||
command.Parameters["$salesResponsibleEmployee"].Value = record.SalesResponsibleEmployee ?? string.Empty;
|
command.Parameters["$salesResponsibleEmployee"].Value = record.SalesResponsibleEmployee ?? string.Empty;
|
||||||
command.Parameters["$invoiceDate"].Value = record.InvoiceDate?.ToString("O") ?? (object)DBNull.Value;
|
command.Parameters["$invoiceDate"].Value = record.InvoiceDate?.ToString("O") ?? (object)DBNull.Value;
|
||||||
|
|||||||
@@ -332,6 +332,13 @@ public class ConfigTransferService : IConfigTransferService
|
|||||||
PurchaseOrderNumber = record.PurchaseOrderNumber,
|
PurchaseOrderNumber = record.PurchaseOrderNumber,
|
||||||
SalesPriceValue = record.SalesPriceValue,
|
SalesPriceValue = record.SalesPriceValue,
|
||||||
SalesCurrency = record.SalesCurrency,
|
SalesCurrency = record.SalesCurrency,
|
||||||
|
DocumentCurrency = record.DocumentCurrency,
|
||||||
|
DocumentTotalForeignCurrency = record.DocumentTotalForeignCurrency,
|
||||||
|
DocumentTotalLocalCurrency = record.DocumentTotalLocalCurrency,
|
||||||
|
VatSumForeignCurrency = record.VatSumForeignCurrency,
|
||||||
|
VatSumLocalCurrency = record.VatSumLocalCurrency,
|
||||||
|
DocumentRate = record.DocumentRate,
|
||||||
|
CompanyCurrency = record.CompanyCurrency,
|
||||||
Incoterms2020 = record.Incoterms2020,
|
Incoterms2020 = record.Incoterms2020,
|
||||||
SalesResponsibleEmployee = record.SalesResponsibleEmployee,
|
SalesResponsibleEmployee = record.SalesResponsibleEmployee,
|
||||||
InvoiceDate = record.InvoiceDate,
|
InvoiceDate = record.InvoiceDate,
|
||||||
|
|||||||
@@ -103,6 +103,13 @@ CREATE TABLE CentralSalesRecords (
|
|||||||
PurchaseOrderNumber TEXT NOT NULL,
|
PurchaseOrderNumber TEXT NOT NULL,
|
||||||
SalesPriceValue TEXT NOT NULL,
|
SalesPriceValue TEXT NOT NULL,
|
||||||
SalesCurrency TEXT NOT NULL,
|
SalesCurrency TEXT NOT NULL,
|
||||||
|
DocumentCurrency TEXT NOT NULL DEFAULT '',
|
||||||
|
DocumentTotalForeignCurrency TEXT NOT NULL DEFAULT '0',
|
||||||
|
DocumentTotalLocalCurrency TEXT NOT NULL DEFAULT '0',
|
||||||
|
VatSumForeignCurrency TEXT NOT NULL DEFAULT '0',
|
||||||
|
VatSumLocalCurrency TEXT NOT NULL DEFAULT '0',
|
||||||
|
DocumentRate TEXT NOT NULL DEFAULT '0',
|
||||||
|
CompanyCurrency TEXT NOT NULL DEFAULT '',
|
||||||
Incoterms2020 TEXT NOT NULL,
|
Incoterms2020 TEXT NOT NULL,
|
||||||
SalesResponsibleEmployee TEXT NOT NULL,
|
SalesResponsibleEmployee TEXT NOT NULL,
|
||||||
InvoiceDate TEXT NULL,
|
InvoiceDate TEXT NULL,
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ public class DatabaseSchemaMaintenanceService : IDatabaseSchemaMaintenanceServic
|
|||||||
EnsureSapJoinTable(db);
|
EnsureSapJoinTable(db);
|
||||||
EnsureSapFieldMappingTable(db);
|
EnsureSapFieldMappingTable(db);
|
||||||
EnsureCentralSalesRecordTable(db);
|
EnsureCentralSalesRecordTable(db);
|
||||||
|
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentCurrency", "TEXT NOT NULL DEFAULT ''");
|
||||||
|
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentTotalForeignCurrency", "TEXT NOT NULL DEFAULT '0'");
|
||||||
|
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentTotalLocalCurrency", "TEXT NOT NULL DEFAULT '0'");
|
||||||
|
AddColumnIfMissing(db, "CentralSalesRecords", "VatSumForeignCurrency", "TEXT NOT NULL DEFAULT '0'");
|
||||||
|
AddColumnIfMissing(db, "CentralSalesRecords", "VatSumLocalCurrency", "TEXT NOT NULL DEFAULT '0'");
|
||||||
|
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentRate", "TEXT NOT NULL DEFAULT '0'");
|
||||||
|
AddColumnIfMissing(db, "CentralSalesRecords", "CompanyCurrency", "TEXT NOT NULL DEFAULT ''");
|
||||||
EnsureAppEventLogTable(db);
|
EnsureAppEventLogTable(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ public class ExcelExportService : IExcelExportService
|
|||||||
"Purchase Order number",
|
"Purchase Order number",
|
||||||
"Sales Price/Value",
|
"Sales Price/Value",
|
||||||
"Sales Currency",
|
"Sales Currency",
|
||||||
|
"Document Currency",
|
||||||
|
"Document Total FC",
|
||||||
|
"Document Total LC",
|
||||||
|
"VAT Sum FC",
|
||||||
|
"VAT Sum LC",
|
||||||
|
"Document Rate",
|
||||||
|
"Company Currency",
|
||||||
"Incoterms 2020",
|
"Incoterms 2020",
|
||||||
"Sales responsible employee",
|
"Sales responsible employee",
|
||||||
"invoice date",
|
"invoice date",
|
||||||
@@ -97,12 +104,19 @@ public class ExcelExportService : IExcelExportService
|
|||||||
ws.Cell(row, 18).Value = record.PurchaseOrderNumber;
|
ws.Cell(row, 18).Value = record.PurchaseOrderNumber;
|
||||||
ws.Cell(row, 19).Value = record.SalesPriceValue;
|
ws.Cell(row, 19).Value = record.SalesPriceValue;
|
||||||
ws.Cell(row, 20).Value = record.SalesCurrency;
|
ws.Cell(row, 20).Value = record.SalesCurrency;
|
||||||
ws.Cell(row, 21).Value = record.Incoterms2020;
|
ws.Cell(row, 21).Value = record.DocumentCurrency;
|
||||||
ws.Cell(row, 22).Value = record.SalesResponsibleEmployee;
|
ws.Cell(row, 22).Value = record.DocumentTotalForeignCurrency;
|
||||||
ws.Cell(row, 23).Value = record.InvoiceDate?.ToString("dd.MM.yyyy") ?? string.Empty;
|
ws.Cell(row, 23).Value = record.DocumentTotalLocalCurrency;
|
||||||
ws.Cell(row, 24).Value = record.OrderDate?.ToString("dd.MM.yyyy") ?? string.Empty;
|
ws.Cell(row, 24).Value = record.VatSumForeignCurrency;
|
||||||
ws.Cell(row, 25).Value = record.Land;
|
ws.Cell(row, 25).Value = record.VatSumLocalCurrency;
|
||||||
ws.Cell(row, 26).Value = record.DocumentType;
|
ws.Cell(row, 26).Value = record.DocumentRate;
|
||||||
|
ws.Cell(row, 27).Value = record.CompanyCurrency;
|
||||||
|
ws.Cell(row, 28).Value = record.Incoterms2020;
|
||||||
|
ws.Cell(row, 29).Value = record.SalesResponsibleEmployee;
|
||||||
|
ws.Cell(row, 30).Value = record.InvoiceDate?.ToString("dd.MM.yyyy") ?? string.Empty;
|
||||||
|
ws.Cell(row, 31).Value = record.OrderDate?.ToString("dd.MM.yyyy") ?? string.Empty;
|
||||||
|
ws.Cell(row, 32).Value = record.Land;
|
||||||
|
ws.Cell(row, 33).Value = record.DocumentType;
|
||||||
row++;
|
row++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -177,6 +177,13 @@ public class HanaQueryService : IHanaQueryService
|
|||||||
PurchaseOrderNumber = reader["purchase_order_number"]?.ToString() ?? string.Empty,
|
PurchaseOrderNumber = reader["purchase_order_number"]?.ToString() ?? string.Empty,
|
||||||
SalesPriceValue = Convert.ToDecimal(reader["sales_value"]),
|
SalesPriceValue = Convert.ToDecimal(reader["sales_value"]),
|
||||||
SalesCurrency = reader["sales_currency"]?.ToString() ?? string.Empty,
|
SalesCurrency = reader["sales_currency"]?.ToString() ?? string.Empty,
|
||||||
|
DocumentCurrency = reader["document_currency"]?.ToString() ?? string.Empty,
|
||||||
|
DocumentTotalForeignCurrency = Convert.ToDecimal(reader["document_total_fc"]),
|
||||||
|
DocumentTotalLocalCurrency = Convert.ToDecimal(reader["document_total_lc"]),
|
||||||
|
VatSumForeignCurrency = Convert.ToDecimal(reader["vat_sum_fc"]),
|
||||||
|
VatSumLocalCurrency = Convert.ToDecimal(reader["vat_sum_lc"]),
|
||||||
|
DocumentRate = Convert.ToDecimal(reader["document_rate"]),
|
||||||
|
CompanyCurrency = reader["company_currency"]?.ToString() ?? string.Empty,
|
||||||
Incoterms2020 = reader["incoterms_2020"]?.ToString() ?? string.Empty,
|
Incoterms2020 = reader["incoterms_2020"]?.ToString() ?? string.Empty,
|
||||||
SalesResponsibleEmployee = reader["sales_responsible"]?.ToString() ?? string.Empty,
|
SalesResponsibleEmployee = reader["sales_responsible"]?.ToString() ?? string.Empty,
|
||||||
OrderDate = reader.IsDBNull(reader.GetOrdinal("order_date")) ? null : reader.GetDateTime(reader.GetOrdinal("order_date")),
|
OrderDate = reader.IsDBNull(reader.GetOrdinal("order_date")) ? null : reader.GetDateTime(reader.GetOrdinal("order_date")),
|
||||||
@@ -217,12 +224,19 @@ SELECT
|
|||||||
COALESCE(cust_adr.""Country"", '') AS customer_country,
|
COALESCE(cust_adr.""Country"", '') AS customer_country,
|
||||||
COALESCE(ind.""IndName"", '') AS customer_industry,
|
COALESCE(ind.""IndName"", '') AS customer_industry,
|
||||||
p.""StockPrice"" AS standard_cost,
|
p.""StockPrice"" AS standard_cost,
|
||||||
COALESCE(p.""Currency"", h.""DocCur"") AS standard_cost_currency,
|
COALESCE(adm.""MainCurncy"", '') AS standard_cost_currency,
|
||||||
CASE WHEN p.""BaseType"" = 22
|
CASE WHEN p.""BaseType"" = 22
|
||||||
THEN CAST(p.""BaseRef"" AS NVARCHAR(20))
|
THEN CAST(p.""BaseRef"" AS NVARCHAR(20))
|
||||||
ELSE '' END AS purchase_order_number,
|
ELSE '' END AS purchase_order_number,
|
||||||
p.""LineTotal"" AS sales_value,
|
p.""LineTotal"" AS sales_value,
|
||||||
COALESCE(p.""Currency"", h.""DocCur"") AS sales_currency,
|
COALESCE(p.""Currency"", h.""DocCur"") AS sales_currency,
|
||||||
|
COALESCE(h.""DocCur"", '') AS document_currency,
|
||||||
|
COALESCE(h.""DocTotalFC"", 0) AS document_total_fc,
|
||||||
|
COALESCE(h.""DocTotal"", 0) AS document_total_lc,
|
||||||
|
COALESCE(h.""VatSumFC"", 0) AS vat_sum_fc,
|
||||||
|
COALESCE(h.""VatSum"", 0) AS vat_sum_lc,
|
||||||
|
COALESCE(h.""DocRate"", 0) AS document_rate,
|
||||||
|
COALESCE(adm.""MainCurncy"", '') AS company_currency,
|
||||||
'' AS incoterms_2020,
|
'' AS incoterms_2020,
|
||||||
COALESCE(emp.""SlpName"", '') AS sales_responsible,
|
COALESCE(emp.""SlpName"", '') AS sales_responsible,
|
||||||
CASE WHEN p.""BaseType"" = 17
|
CASE WHEN p.""BaseType"" = 17
|
||||||
@@ -232,6 +246,7 @@ SELECT
|
|||||||
'INV' AS doc_type
|
'INV' AS doc_type
|
||||||
FROM {quotedSchema}.""OINV"" h
|
FROM {quotedSchema}.""OINV"" h
|
||||||
INNER JOIN {quotedSchema}.""INV1"" p ON h.""DocEntry"" = p.""DocEntry""
|
INNER JOIN {quotedSchema}.""INV1"" p ON h.""DocEntry"" = p.""DocEntry""
|
||||||
|
CROSS JOIN {quotedSchema}.""OADM"" adm
|
||||||
LEFT JOIN {quotedSchema}.""OITM"" itm ON p.""ItemCode"" = itm.""ItemCode""
|
LEFT JOIN {quotedSchema}.""OITM"" itm ON p.""ItemCode"" = itm.""ItemCode""
|
||||||
LEFT JOIN {quotedSchema}.""OITB"" grp ON itm.""ItmsGrpCod"" = grp.""ItmsGrpCod""
|
LEFT JOIN {quotedSchema}.""OITB"" grp ON itm.""ItmsGrpCod"" = grp.""ItmsGrpCod""
|
||||||
LEFT JOIN {quotedSchema}.""OCRD"" cust ON h.""CardCode"" = cust.""CardCode""
|
LEFT JOIN {quotedSchema}.""OCRD"" cust ON h.""CardCode"" = cust.""CardCode""
|
||||||
@@ -269,16 +284,24 @@ SELECT
|
|||||||
COALESCE(cust_adr.""Country"", '') AS customer_country,
|
COALESCE(cust_adr.""Country"", '') AS customer_country,
|
||||||
COALESCE(ind.""IndName"", '') AS customer_industry,
|
COALESCE(ind.""IndName"", '') AS customer_industry,
|
||||||
p.""StockPrice"" AS standard_cost,
|
p.""StockPrice"" AS standard_cost,
|
||||||
COALESCE(p.""Currency"", h.""DocCur"") AS standard_cost_currency,
|
COALESCE(adm.""MainCurncy"", '') AS standard_cost_currency,
|
||||||
'' AS purchase_order_number,
|
'' AS purchase_order_number,
|
||||||
p.""LineTotal"" * -1 AS sales_value,
|
p.""LineTotal"" * -1 AS sales_value,
|
||||||
COALESCE(p.""Currency"", h.""DocCur"") AS sales_currency,
|
COALESCE(p.""Currency"", h.""DocCur"") AS sales_currency,
|
||||||
|
COALESCE(h.""DocCur"", '') AS document_currency,
|
||||||
|
COALESCE(h.""DocTotalFC"", 0) * -1 AS document_total_fc,
|
||||||
|
COALESCE(h.""DocTotal"", 0) * -1 AS document_total_lc,
|
||||||
|
COALESCE(h.""VatSumFC"", 0) * -1 AS vat_sum_fc,
|
||||||
|
COALESCE(h.""VatSum"", 0) * -1 AS vat_sum_lc,
|
||||||
|
COALESCE(h.""DocRate"", 0) AS document_rate,
|
||||||
|
COALESCE(adm.""MainCurncy"", '') AS company_currency,
|
||||||
'' AS incoterms_2020,
|
'' AS incoterms_2020,
|
||||||
COALESCE(emp.""SlpName"", '') AS sales_responsible,
|
COALESCE(emp.""SlpName"", '') AS sales_responsible,
|
||||||
NULL AS order_date,
|
NULL AS order_date,
|
||||||
'CRN' AS doc_type
|
'CRN' AS doc_type
|
||||||
FROM {quotedSchema}.""ORIN"" h
|
FROM {quotedSchema}.""ORIN"" h
|
||||||
INNER JOIN {quotedSchema}.""RIN1"" p ON h.""DocEntry"" = p.""DocEntry""
|
INNER JOIN {quotedSchema}.""RIN1"" p ON h.""DocEntry"" = p.""DocEntry""
|
||||||
|
CROSS JOIN {quotedSchema}.""OADM"" adm
|
||||||
LEFT JOIN {quotedSchema}.""OITM"" itm ON p.""ItemCode"" = itm.""ItemCode""
|
LEFT JOIN {quotedSchema}.""OITM"" itm ON p.""ItemCode"" = itm.""ItemCode""
|
||||||
LEFT JOIN {quotedSchema}.""OITB"" grp ON itm.""ItmsGrpCod"" = grp.""ItmsGrpCod""
|
LEFT JOIN {quotedSchema}.""OITB"" grp ON itm.""ItmsGrpCod"" = grp.""ItmsGrpCod""
|
||||||
LEFT JOIN {quotedSchema}.""OCRD"" cust ON h.""CardCode"" = cust.""CardCode""
|
LEFT JOIN {quotedSchema}.""OCRD"" cust ON h.""CardCode"" = cust.""CardCode""
|
||||||
|
|||||||
@@ -28,6 +28,17 @@ public class ManualExcelImportService : IManualExcelImportService
|
|||||||
["purchaseordernumber"] = nameof(SalesRecord.PurchaseOrderNumber),
|
["purchaseordernumber"] = nameof(SalesRecord.PurchaseOrderNumber),
|
||||||
["salespricevalue"] = nameof(SalesRecord.SalesPriceValue),
|
["salespricevalue"] = nameof(SalesRecord.SalesPriceValue),
|
||||||
["salescurrency"] = nameof(SalesRecord.SalesCurrency),
|
["salescurrency"] = nameof(SalesRecord.SalesCurrency),
|
||||||
|
["documentcurrency"] = nameof(SalesRecord.DocumentCurrency),
|
||||||
|
["documenttotalfc"] = nameof(SalesRecord.DocumentTotalForeignCurrency),
|
||||||
|
["documenttotalforeigncurrency"] = nameof(SalesRecord.DocumentTotalForeignCurrency),
|
||||||
|
["documenttotallc"] = nameof(SalesRecord.DocumentTotalLocalCurrency),
|
||||||
|
["documenttotallocalcurrency"] = nameof(SalesRecord.DocumentTotalLocalCurrency),
|
||||||
|
["vatsumfc"] = nameof(SalesRecord.VatSumForeignCurrency),
|
||||||
|
["vatsumforeigncurrency"] = nameof(SalesRecord.VatSumForeignCurrency),
|
||||||
|
["vatsumlc"] = nameof(SalesRecord.VatSumLocalCurrency),
|
||||||
|
["vatsumlocalcurrency"] = nameof(SalesRecord.VatSumLocalCurrency),
|
||||||
|
["documentrate"] = nameof(SalesRecord.DocumentRate),
|
||||||
|
["companycurrency"] = nameof(SalesRecord.CompanyCurrency),
|
||||||
["incoterms2020"] = nameof(SalesRecord.Incoterms2020),
|
["incoterms2020"] = nameof(SalesRecord.Incoterms2020),
|
||||||
["salesresponsibleemployee"] = nameof(SalesRecord.SalesResponsibleEmployee),
|
["salesresponsibleemployee"] = nameof(SalesRecord.SalesResponsibleEmployee),
|
||||||
["invoicedate"] = nameof(SalesRecord.InvoiceDate),
|
["invoicedate"] = nameof(SalesRecord.InvoiceDate),
|
||||||
@@ -75,6 +86,13 @@ public class ManualExcelImportService : IManualExcelImportService
|
|||||||
PurchaseOrderNumber = ReadString(headerIndexes, row, nameof(SalesRecord.PurchaseOrderNumber)),
|
PurchaseOrderNumber = ReadString(headerIndexes, row, nameof(SalesRecord.PurchaseOrderNumber)),
|
||||||
SalesPriceValue = ReadDecimal(headerIndexes, row, nameof(SalesRecord.SalesPriceValue)),
|
SalesPriceValue = ReadDecimal(headerIndexes, row, nameof(SalesRecord.SalesPriceValue)),
|
||||||
SalesCurrency = ReadString(headerIndexes, row, nameof(SalesRecord.SalesCurrency)),
|
SalesCurrency = ReadString(headerIndexes, row, nameof(SalesRecord.SalesCurrency)),
|
||||||
|
DocumentCurrency = ReadString(headerIndexes, row, nameof(SalesRecord.DocumentCurrency)),
|
||||||
|
DocumentTotalForeignCurrency = ReadDecimal(headerIndexes, row, nameof(SalesRecord.DocumentTotalForeignCurrency)),
|
||||||
|
DocumentTotalLocalCurrency = ReadDecimal(headerIndexes, row, nameof(SalesRecord.DocumentTotalLocalCurrency)),
|
||||||
|
VatSumForeignCurrency = ReadDecimal(headerIndexes, row, nameof(SalesRecord.VatSumForeignCurrency)),
|
||||||
|
VatSumLocalCurrency = ReadDecimal(headerIndexes, row, nameof(SalesRecord.VatSumLocalCurrency)),
|
||||||
|
DocumentRate = ReadDecimal(headerIndexes, row, nameof(SalesRecord.DocumentRate)),
|
||||||
|
CompanyCurrency = ReadString(headerIndexes, row, nameof(SalesRecord.CompanyCurrency)),
|
||||||
Incoterms2020 = ReadString(headerIndexes, row, nameof(SalesRecord.Incoterms2020)),
|
Incoterms2020 = ReadString(headerIndexes, row, nameof(SalesRecord.Incoterms2020)),
|
||||||
SalesResponsibleEmployee = ReadString(headerIndexes, row, nameof(SalesRecord.SalesResponsibleEmployee)),
|
SalesResponsibleEmployee = ReadString(headerIndexes, row, nameof(SalesRecord.SalesResponsibleEmployee)),
|
||||||
InvoiceDate = ReadDate(headerIndexes, row, nameof(SalesRecord.InvoiceDate)),
|
InvoiceDate = ReadDate(headerIndexes, row, nameof(SalesRecord.InvoiceDate)),
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TrafagSalesExporter.Data;
|
||||||
|
using TrafagSalesExporter.Models;
|
||||||
|
using TrafagSalesExporter.Services;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Tests;
|
||||||
|
|
||||||
|
public class CentralSalesRecordServiceTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly SqliteConnection _connection;
|
||||||
|
private readonly TestDbContextFactory _dbFactory;
|
||||||
|
|
||||||
|
public CentralSalesRecordServiceTests()
|
||||||
|
{
|
||||||
|
_connection = new SqliteConnection("DataSource=:memory:");
|
||||||
|
_connection.Open();
|
||||||
|
|
||||||
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||||
|
.UseSqlite(_connection)
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
using var db = new AppDbContext(options);
|
||||||
|
db.Database.EnsureCreated();
|
||||||
|
|
||||||
|
_dbFactory = new TestDbContextFactory(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_connection.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReplaceForSiteAsync_Persists_B1_Document_Currency_Fields()
|
||||||
|
{
|
||||||
|
var site = new Site
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Schema = "SBODEMO",
|
||||||
|
TSC = "TRCH",
|
||||||
|
Land = "Schweiz",
|
||||||
|
SourceSystem = "BI1",
|
||||||
|
IsActive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
await using (var db = await _dbFactory.CreateDbContextAsync())
|
||||||
|
{
|
||||||
|
db.Sites.Add(site);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var service = new CentralSalesRecordService(_dbFactory, new NullAppEventLogService());
|
||||||
|
await service.ReplaceForSiteAsync(site, [
|
||||||
|
new SalesRecord
|
||||||
|
{
|
||||||
|
ExtractionDate = new DateTime(2026, 4, 29),
|
||||||
|
Tsc = "TRCH",
|
||||||
|
InvoiceNumber = "1001",
|
||||||
|
PositionOnInvoice = 1,
|
||||||
|
Material = "MAT",
|
||||||
|
Name = "Article",
|
||||||
|
ProductGroup = "PG",
|
||||||
|
Quantity = 2m,
|
||||||
|
StandardCost = 10m,
|
||||||
|
StandardCostCurrency = "CHF",
|
||||||
|
SalesPriceValue = 25m,
|
||||||
|
SalesCurrency = "EUR",
|
||||||
|
DocumentCurrency = "EUR",
|
||||||
|
DocumentTotalForeignCurrency = 100m,
|
||||||
|
DocumentTotalLocalCurrency = 95m,
|
||||||
|
VatSumForeignCurrency = 8m,
|
||||||
|
VatSumLocalCurrency = 7.6m,
|
||||||
|
DocumentRate = 0.95m,
|
||||||
|
CompanyCurrency = "CHF",
|
||||||
|
Land = "Schweiz",
|
||||||
|
DocumentType = "INV"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
var rows = await service.GetAllAsync();
|
||||||
|
|
||||||
|
var row = Assert.Single(rows);
|
||||||
|
Assert.Equal("EUR", row.DocumentCurrency);
|
||||||
|
Assert.Equal(100m, row.DocumentTotalForeignCurrency);
|
||||||
|
Assert.Equal(95m, row.DocumentTotalLocalCurrency);
|
||||||
|
Assert.Equal(8m, row.VatSumForeignCurrency);
|
||||||
|
Assert.Equal(7.6m, row.VatSumLocalCurrency);
|
||||||
|
Assert.Equal(0.95m, row.DocumentRate);
|
||||||
|
Assert.Equal("CHF", row.CompanyCurrency);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NullAppEventLogService : IAppEventLogService
|
||||||
|
{
|
||||||
|
public Task WriteAsync(string category, string message, string level = "Info", int? siteId = null, string? land = null, string? details = null)
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task WriteDebugAsync(string category, string message, int? siteId = null, string? land = null, string? details = null)
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestDbContextFactory : IDbContextFactory<AppDbContext>
|
||||||
|
{
|
||||||
|
private readonly DbContextOptions<AppDbContext> _options;
|
||||||
|
|
||||||
|
public TestDbContextFactory(DbContextOptions<AppDbContext> options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppDbContext CreateDbContext() => new(_options);
|
||||||
|
|
||||||
|
public Task<AppDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(new AppDbContext(_options));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,12 +38,19 @@ public class ManualExcelImportServiceTests
|
|||||||
ws.Cell(2, 18).Value = "PO-1";
|
ws.Cell(2, 18).Value = "PO-1";
|
||||||
ws.Cell(2, 19).Value = 21.40m;
|
ws.Cell(2, 19).Value = 21.40m;
|
||||||
ws.Cell(2, 20).Value = "EUR";
|
ws.Cell(2, 20).Value = "EUR";
|
||||||
ws.Cell(2, 21).Value = "DAP";
|
ws.Cell(2, 21).Value = "EUR";
|
||||||
ws.Cell(2, 22).Value = "Alice";
|
ws.Cell(2, 22).Value = 120.50m;
|
||||||
ws.Cell(2, 23).Value = "14.04.2026";
|
ws.Cell(2, 23).Value = 110.25m;
|
||||||
ws.Cell(2, 24).Value = "10.04.2026";
|
ws.Cell(2, 24).Value = 8.10m;
|
||||||
ws.Cell(2, 25).Value = "Deutschland";
|
ws.Cell(2, 25).Value = 7.45m;
|
||||||
ws.Cell(2, 26).Value = "Invoice";
|
ws.Cell(2, 26).Value = 1.0925m;
|
||||||
|
ws.Cell(2, 27).Value = "CHF";
|
||||||
|
ws.Cell(2, 28).Value = "DAP";
|
||||||
|
ws.Cell(2, 29).Value = "Alice";
|
||||||
|
ws.Cell(2, 30).Value = "14.04.2026";
|
||||||
|
ws.Cell(2, 31).Value = "10.04.2026";
|
||||||
|
ws.Cell(2, 32).Value = "Deutschland";
|
||||||
|
ws.Cell(2, 33).Value = "Invoice";
|
||||||
});
|
});
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -60,6 +67,13 @@ public class ManualExcelImportServiceTests
|
|||||||
Assert.Equal(2.5m, row.Quantity);
|
Assert.Equal(2.5m, row.Quantity);
|
||||||
Assert.Equal(10.25m, row.StandardCost);
|
Assert.Equal(10.25m, row.StandardCost);
|
||||||
Assert.Equal(21.40m, row.SalesPriceValue);
|
Assert.Equal(21.40m, row.SalesPriceValue);
|
||||||
|
Assert.Equal("EUR", row.DocumentCurrency);
|
||||||
|
Assert.Equal(120.50m, row.DocumentTotalForeignCurrency);
|
||||||
|
Assert.Equal(110.25m, row.DocumentTotalLocalCurrency);
|
||||||
|
Assert.Equal(8.10m, row.VatSumForeignCurrency);
|
||||||
|
Assert.Equal(7.45m, row.VatSumLocalCurrency);
|
||||||
|
Assert.Equal(1.0925m, row.DocumentRate);
|
||||||
|
Assert.Equal("CHF", row.CompanyCurrency);
|
||||||
Assert.Equal("Deutschland", row.Land);
|
Assert.Equal("Deutschland", row.Land);
|
||||||
Assert.Equal("Invoice", row.DocumentType);
|
Assert.Equal("Invoice", row.DocumentType);
|
||||||
Assert.Equal(new DateTime(2026, 4, 14), row.InvoiceDate);
|
Assert.Equal(new DateTime(2026, 4, 14), row.InvoiceDate);
|
||||||
@@ -205,6 +219,13 @@ public class ManualExcelImportServiceTests
|
|||||||
"Purchase Order number",
|
"Purchase Order number",
|
||||||
"Sales Price/Value",
|
"Sales Price/Value",
|
||||||
"Sales Currency",
|
"Sales Currency",
|
||||||
|
"Document Currency",
|
||||||
|
"Document Total FC",
|
||||||
|
"Document Total LC",
|
||||||
|
"VAT Sum FC",
|
||||||
|
"VAT Sum LC",
|
||||||
|
"Document Rate",
|
||||||
|
"Company Currency",
|
||||||
"Incoterms 2020",
|
"Incoterms 2020",
|
||||||
"Sales responsible employee",
|
"Sales responsible employee",
|
||||||
"invoice date",
|
"invoice date",
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using TrafagSalesExporter.Security;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Tests;
|
||||||
|
|
||||||
|
public class SecurityPolicyFactoryTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task AccessPolicy_Allows_User_In_Configured_Access_Group()
|
||||||
|
{
|
||||||
|
var policy = SecurityPolicyFactory.BuildAccessPolicy(new SecurityOptions
|
||||||
|
{
|
||||||
|
AccessGroups = ["TRAFAG\\TrafagSalesExporter-Users"]
|
||||||
|
}, useDevelopmentAuthentication: false);
|
||||||
|
|
||||||
|
var result = await AuthorizeAsync(policy, CreateUser(roles: ["TRAFAG\\TrafagSalesExporter-Users"]));
|
||||||
|
|
||||||
|
Assert.True(result.Succeeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AccessPolicy_Denies_User_Outside_Configured_Access_Group()
|
||||||
|
{
|
||||||
|
var policy = SecurityPolicyFactory.BuildAccessPolicy(new SecurityOptions
|
||||||
|
{
|
||||||
|
AccessGroups = ["TRAFAG\\TrafagSalesExporter-Users"]
|
||||||
|
}, useDevelopmentAuthentication: false);
|
||||||
|
|
||||||
|
var result = await AuthorizeAsync(policy, CreateUser(roles: ["TRAFAG\\OtherGroup"]));
|
||||||
|
|
||||||
|
Assert.False(result.Succeeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AccessPolicy_Allows_Authenticated_User_When_Development_Authentication_Is_Active()
|
||||||
|
{
|
||||||
|
var policy = SecurityPolicyFactory.BuildAccessPolicy(new SecurityOptions
|
||||||
|
{
|
||||||
|
AccessGroups = ["TRAFAG\\TrafagSalesExporter-Users"]
|
||||||
|
}, useDevelopmentAuthentication: true);
|
||||||
|
|
||||||
|
var result = await AuthorizeAsync(policy, CreateUser());
|
||||||
|
|
||||||
|
Assert.True(result.Succeeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AdminPolicy_Allows_User_In_Admin_Group()
|
||||||
|
{
|
||||||
|
var policy = SecurityPolicyFactory.BuildAdminPolicy(new SecurityOptions
|
||||||
|
{
|
||||||
|
AdminGroups = ["TRAFAG\\TrafagSalesExporter-Admins"]
|
||||||
|
}, useDevelopmentAuthentication: false);
|
||||||
|
|
||||||
|
var result = await AuthorizeAsync(policy, CreateUser(roles: ["TRAFAG\\TrafagSalesExporter-Admins"]));
|
||||||
|
|
||||||
|
Assert.True(result.Succeeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AdminPolicy_Denies_Normal_Access_User()
|
||||||
|
{
|
||||||
|
var policy = SecurityPolicyFactory.BuildAdminPolicy(new SecurityOptions
|
||||||
|
{
|
||||||
|
AdminGroups = ["TRAFAG\\TrafagSalesExporter-Admins"]
|
||||||
|
}, useDevelopmentAuthentication: false);
|
||||||
|
|
||||||
|
var result = await AuthorizeAsync(policy, CreateUser(roles: ["TRAFAG\\TrafagSalesExporter-Users"]));
|
||||||
|
|
||||||
|
Assert.False(result.Succeeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AdminPolicy_Allows_Development_Admin_Claim()
|
||||||
|
{
|
||||||
|
var policy = SecurityPolicyFactory.BuildAdminPolicy(new SecurityOptions(), useDevelopmentAuthentication: true);
|
||||||
|
|
||||||
|
var result = await AuthorizeAsync(policy, CreateUser(claims:
|
||||||
|
[
|
||||||
|
new Claim(DevelopmentAuthenticationHandler.AdminClaimType, "true")
|
||||||
|
]));
|
||||||
|
|
||||||
|
Assert.True(result.Succeeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AdminPolicy_Denies_Development_User_Without_Admin_Claim()
|
||||||
|
{
|
||||||
|
var policy = SecurityPolicyFactory.BuildAdminPolicy(new SecurityOptions(), useDevelopmentAuthentication: true);
|
||||||
|
|
||||||
|
var result = await AuthorizeAsync(policy, CreateUser());
|
||||||
|
|
||||||
|
Assert.False(result.Succeeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<AuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddLogging();
|
||||||
|
services.AddAuthorization();
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
var service = provider.GetRequiredService<IAuthorizationService>();
|
||||||
|
|
||||||
|
return await service.AuthorizeAsync(user, resource: null, policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ClaimsPrincipal CreateUser(IEnumerable<string>? roles = null, IEnumerable<Claim>? claims = null)
|
||||||
|
{
|
||||||
|
var allClaims = new List<Claim>
|
||||||
|
{
|
||||||
|
new(ClaimTypes.Name, "TRAFAG\\tester")
|
||||||
|
};
|
||||||
|
|
||||||
|
allClaims.AddRange((roles ?? []).Select(role => new Claim(ClaimTypes.Role, role)));
|
||||||
|
allClaims.AddRange(claims ?? []);
|
||||||
|
|
||||||
|
return new ClaimsPrincipal(new ClaimsIdentity(allClaims, "Test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Security": {
|
||||||
|
"DevelopmentBypass": true,
|
||||||
|
"DevelopmentUserIsAdmin": true,
|
||||||
|
"DevelopmentUserName": "DEV\\TrafagDeveloper",
|
||||||
|
"AccessGroups": [],
|
||||||
|
"AdminGroups": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,5 +4,17 @@
|
|||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Security": {
|
||||||
|
"DevelopmentBypass": false,
|
||||||
|
"DevelopmentUserIsAdmin": false,
|
||||||
|
"DevelopmentUserName": "DEV\\TrafagDeveloper",
|
||||||
|
"AccessGroups": [
|
||||||
|
"TRAFAG\\TrafagSalesExporter-Users",
|
||||||
|
"TRAFAG\\TrafagSalesExporter-Admins"
|
||||||
|
],
|
||||||
|
"AdminGroups": [
|
||||||
|
"TRAFAG\\TrafagSalesExporter-Admins"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user