WerksverkaufScanner: -Login/Logout Prozedur geändert (Logout) Inventurconfig löschen

-InventurScan erst möglich bei vorhandener Config
This commit is contained in:
Christopher Meinhold 2025-12-01 13:07:49 +01:00
parent 4d811702f0
commit 748fa505fe
3 changed files with 248 additions and 89 deletions

View File

@ -41,10 +41,30 @@ public class AuthController : ControllerBase
}
[Authorize]
[HttpPost("logout")]
public async Task<IActionResult> Logout()
[HttpPost("/auth/logout")]
public IActionResult Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return LocalRedirect("/login");
// Serverseitig abmelden
HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
// Antwort mit JS, das localStorage löscht und weiterleitet
var html = """
<!DOCTYPE html>
<html lang="de">
<head><meta charset="utf-8"><title>Logout</title></head>
<body>
<script>
try {
localStorage.removeItem("scannerpilot.filialId");
localStorage.removeItem("scannerpilot.inventurId");
localStorage.removeItem("scannerpilot.user");
} catch(e) { console.warn("localStorage cleanup failed", e); }
window.location.href = '/login';
</script>
</body>
</html>
""";
return Content(html, "text/html");
}
}

View File

@ -9,58 +9,69 @@
<h3>Inventur Scannen / Eingeben</h3>
<!-- 1) Barcode ODER Artikelnummer -->
<input @ref="barcodeRef" id="barcode"
class="form-control form-control-lg mb-2"
placeholder="Barcode scannen oder Artikelnummer eingeben"
@bind="scanText" @bind:event="oninput"
@onkeydown="OnScanKey" />
@if (varianten is { Count: > 1 })
@if (needsSetup)
{
<div class="mb-3">
<div class="mb-1">Varianten gefunden bitte auswählen:</div>
<div class="d-flex flex-wrap gap-2">
@foreach (var art in varianten)
{
var isActive = ReferenceEquals(gefunden, art);
<button type="button"
class="btn btn-lg"
style="@GetButtonStyle(art, isActive)"
@onclick="@(() => VarianteAuswaehlen(art))">
@GetVarianteLabel(art)
</button>
}
<div class="alert alert-warning d-flex align-items-center" role="alert">
<div>
Es fehlen lokale Einstellungen (Filial-ID oder Inventur-ID). Bitte zuerst den QR-Code
auf der Seite <a href="/inventur/vorbereiten" class="alert-link">Inventur vorbereiten</a> scannen.
</div>
</div>
}
@if (gefunden is not null)
else
{
<div class="alert alert-info py-2 mb-2">
<strong>@gefunden.Bezeichnung</strong>
<span class="text-muted">
· @gefunden.ArtikelNummer
· Variante @gefunden.Variante
@if (!string.IsNullOrWhiteSpace(gefunden.Farbe))
{
<text> · @gefunden.Farbe</text>
}
@if (!string.IsNullOrWhiteSpace(gefunden.Text))
{
<text> · @gefunden.Text</text>
}
</span>
</div>
<!-- 2) Menge -->
<input @ref="mengeRef" id="menge"
<!-- 1) Barcode ODER Artikelnummer -->
<input @ref="barcodeRef" id="barcode"
class="form-control form-control-lg mb-2"
placeholder="Menge (natürliche Zahl) …"
@bind="mengeText" @bind:event="oninput"
@onkeydown="OnMengeKey"
inputmode="numeric" pattern="[0-9]*" />
placeholder="Barcode scannen oder Artikelnummer eingeben"
@bind="scanText" @bind:event="oninput"
@onkeydown="OnScanKey" />
@if (varianten is { Count: > 1 })
{
<div class="mb-3">
<div class="mb-1">Varianten gefunden bitte auswählen:</div>
<div class="d-flex flex-wrap gap-2">
@foreach (var art in varianten)
{
var isActive = ReferenceEquals(gefunden, art);
<button type="button"
class="btn btn-lg"
style="@GetButtonStyle(art, isActive)"
@onclick="@(() => VarianteAuswaehlen(art))">
@GetVarianteLabel(art)
</button>
}
</div>
</div>
}
@if (gefunden is not null)
{
<div class="alert alert-info py-2 mb-2">
<strong>@gefunden.Bezeichnung</strong>
<span class="text-muted">
· @gefunden.ArtikelNummer
· Variante @gefunden.Variante
@if (!string.IsNullOrWhiteSpace(gefunden.Farbe))
{
<text> · @gefunden.Farbe</text>
}
@if (!string.IsNullOrWhiteSpace(gefunden.Text))
{
<text> · @gefunden.Text</text>
}
</span>
</div>
<!-- 2) Menge -->
<input @ref="mengeRef" id="menge"
class="form-control form-control-lg mb-2"
placeholder="Menge (natürliche Zahl) …"
@bind="mengeText" @bind:event="oninput"
@onkeydown="OnMengeKey"
inputmode="numeric" pattern="[0-9]*" />
}
}
@if (!string.IsNullOrEmpty(status))
@ -69,6 +80,12 @@
}
@code {
// --- Konfig aus localStorage ---
private bool needsSetup; // true = FilialId/InventurId fehlen/ungültig
private string? filialId; // bereinigt (ohne Anführungszeichen)
private int? inventurId;
private string? currentUser;
// Eingaben/Status
private string? scanText;
private string? mengeText;
@ -88,24 +105,62 @@
await Cache.RefreshAsync();
}
// JS-Interop nur after first render (sonst Prerender-Fehler)
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (focusBarcode)
if (firstRender)
{
focusBarcode = false;
await barcodeRef.FocusAsync();
await LoadConfigAsync();
// Erst fokussieren, wenn Setup ok
if (!needsSetup && focusBarcode)
{
focusBarcode = false;
await barcodeRef.FocusAsync();
}
StateHasChanged(); // UI nach Konfig-Check aktualisieren
return;
}
if (focusMenge)
if (!needsSetup && focusMenge)
{
focusMenge = false;
await mengeRef.FocusAsync();
}
}
private async Task LoadConfigAsync()
{
try
{
var f = await JS.InvokeAsync<string?>("localStorage.getItem", "scannerpilot.filialId");
var i = await JS.InvokeAsync<string?>("localStorage.getItem", "scannerpilot.inventurId");
var u = await JS.InvokeAsync<string?>("localStorage.getItem", "scannerpilot.user");
filialId = f?.Trim('"');
currentUser = u?.Trim('"');
if (!string.IsNullOrWhiteSpace(i) && int.TryParse(i.Trim('"'), out var inv))
inventurId = inv;
else
inventurId = null;
// needsSetup = true, wenn irgendwas fehlt/ungültig
needsSetup = string.IsNullOrWhiteSpace(filialId) || inventurId is null;
}
catch
{
needsSetup = true;
filialId = null;
inventurId = null;
currentUser = null;
}
}
private async Task OnScanKey(KeyboardEventArgs e)
{
if (e.Key == "Enter")
if (e.Key == "Enter" && !needsSetup)
await SucheArtikelAsync();
}
@ -126,13 +181,11 @@
return;
}
// sicherstellen, dass Daten im Cache sind
if (Cache.GetArtikel().Count == 0)
await Cache.RefreshAsync();
var alle = Cache.GetArtikel();
// alle Varianten zu Barcode ODER Artikelnummer holen
var matches = alle
.Where(a =>
string.Equals(a.Barcode, input, StringComparison.OrdinalIgnoreCase) ||
@ -148,7 +201,6 @@
}
else if (matches.Count == 1)
{
// ⬇️ Nur eine Variante → automatisch wählen und Menge fokussieren
gefunden = matches[0];
varianten = null;
status = $"„{gefunden.Bezeichnung}“ Variante {GetVarianteLabel(gefunden)} ausgewählt. Bitte Menge eingeben.";
@ -156,7 +208,6 @@
}
else
{
// Mehrere Varianten → Auswahl anzeigen
varianten = matches;
status = $"{matches.Count} Varianten gefunden bitte auswählen.";
focusMenge = false;
@ -184,6 +235,12 @@
status = null;
ok = false;
if (needsSetup)
{
status = "Bitte zuerst die Inventur vorbereiten (QR-Code scannen).";
return;
}
if (gefunden is null)
{
status = "Bitte zuerst Barcode scannen und eine Variante auswählen.";
@ -203,34 +260,28 @@
try
{
// FilialId & InventurId aus localStorage holen
var filialIdStr = await JS.InvokeAsync<string?>("localStorage.getItem", "scannerpilot.filialId");
var inventurIdStr = await JS.InvokeAsync<string?>("localStorage.getItem", "scannerpilot.inventurId");
var user = await JS.InvokeAsync<string>("localStorage.getItem", "scannerpilot.user");
// Sicherheitsnetz noch einmal kurz prüfen (kann durch Tabwechsel gelöscht sein)
if (filialId is null || inventurId is null)
await LoadConfigAsync();
if (string.IsNullOrWhiteSpace(filialIdStr))
if (string.IsNullOrWhiteSpace(filialId))
throw new InvalidOperationException("Filial-ID fehlt.");
if (string.IsNullOrWhiteSpace(inventurIdStr)
|| !int.TryParse(inventurIdStr, out var inventurId))
if (inventurId is null)
throw new InvalidOperationException("Inventur-ID fehlt oder ungültig.");
if (string.IsNullOrWhiteSpace(user))
throw new InvalidOperationException("Benutzername fehlt.");
filialIdStr = filialIdStr.Trim('"');
var rows = await Inventur.SaveAsync(
filialIdStr,
inventurId,
filialId,
inventurId.Value,
gefunden.ArtikelId,
gefunden.ArtikelVariantenId,
gefunden.Variante,
menge,
user
currentUser ?? string.Empty // optional Benutzer mitschicken
);
ok = rows > 0;
status = ok
? $"Gespeichert: {gefunden.ArtikelNummer} Variante {gefunden.Variante} ({GetVarianteLabel(gefunden)}) · Menge {menge} (Filiale {filialIdStr})."
? $"Gespeichert: {gefunden.ArtikelNummer} Variante {gefunden.Variante} ({GetVarianteLabel(gefunden)}) · Menge {menge} (Filiale {filialId})."
: "Nichts gespeichert.";
mengeText = null;
@ -284,7 +335,7 @@
case "gelb": return "#ffc107";
case "orange": return "#fd7e14";
case "lila": return "#6f42c1";
default: return "#e0e0e0"; // neutrale Grau-Farbe
default: return "#e0e0e0"; // neutral
}
}

View File

@ -1,14 +1,92 @@
//using Microsoft.AspNetCore.Authentication.Cookies;
//using Microsoft.AspNetCore.HttpOverrides;
//using Microsoft.EntityFrameworkCore;
//using WerksverkaufScanner.Data;
//using WerksverkaufScanner.Services;
//var builder = WebApplication.CreateBuilder(args);
//// Optional: explizit Port/Bind-Adresse setzen
//builder.WebHost.UseUrls("https://0.0.0.0:3300");
////builder.WebHost.UseUrls("http://0.0.0.0:3300");
//// 1) ConnectionString prüfen
//var cs = builder.Configuration.GetConnectionString("Default");
//if (string.IsNullOrWhiteSpace(cs))
// throw new InvalidOperationException("ConnectionStrings:Default fehlt oder ist leer.");
//// 2) Framework-Services
//builder.Services.AddRazorPages();
//builder.Services.AddServerSideBlazor();
//builder.Services.AddControllers(); // für AuthController (Login/Logout)
//builder.Services.AddHttpClient(); // für Logout/Calls aus Layout/Login
//builder.Services.AddHttpContextAccessor(); // falls Services HttpContext brauchen
//// 3) Auth/Authorization (Cookie)
//builder.Services
// .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
// .AddCookie(o =>
// {
// o.LoginPath = "/login"; // ohne Auth -> Redirect hierher
// o.AccessDeniedPath = "/login";
// o.ReturnUrlParameter = "returnUrl";
// o.SlidingExpiration = true;
// o.ExpireTimeSpan = TimeSpan.FromDays(7);
// // Cookie-Härtung
// o.Cookie.Name = "Werksverkauf.Auth";
// o.Cookie.HttpOnly = true;
// o.Cookie.SecurePolicy = CookieSecurePolicy.None; // nur über HTTPS eigentlich .Always
// o.Cookie.SameSite = SameSiteMode.Lax; // für normale Navigations-Requests
// });
//builder.Services.AddAuthorization();
//// 4) App-Services & Datenzugriff
//builder.Services.AddDbContextFactory<ScannerDb>(opt => opt.UseSqlServer(cs));
//builder.Services.AddSingleton<StammdatenCache>();
//builder.Services.AddScoped<InventurService>();
//builder.Services.AddScoped<AuthService>(); // prüft KassenLogin in DB
//// (Optional) Forwarded Headers, wenn hinter Proxy/LoadBalancer (NGINX/IIS/K8s)
//builder.Services.Configure<ForwardedHeadersOptions>(opt =>
//{
// opt.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
// // opt.KnownProxies.Add(IPAddress.Parse("127.0.0.1")); // bei Bedarf setzen
//});
//var app = builder.Build();
//// 5) Middleware-Pipeline
//app.UseForwardedHeaders(); // vor allem anderen, wenn Proxy im Spiel
//app.UseStaticFiles();
//app.UseRouting();
//app.UseAuthentication(); // erst Auth…
//app.UseAuthorization(); // …dann Authorization
//// 6) Endpoints
//app.MapControllers(); // /auth/login, /auth/logout, /auth/me
//app.MapBlazorHub();
//app.MapFallbackToPage("/_Host");
//app.Run();
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.EntityFrameworkCore;
using WerksverkaufScanner.Data;
using WerksverkaufScanner.Services;
// using System.Net; // nur nötig, wenn du KnownProxies/KnownNetworks setzt
var builder = WebApplication.CreateBuilder(args);
// Optional: explizit Port/Bind-Adresse setzen
builder.WebHost.UseUrls("https://0.0.0.0:3300");
//builder.WebHost.UseUrls("http://0.0.0.0:3300");
// --- WICHTIG FÜR IIS: KEIN eigenes HTTPS/UseUrls hier setzen! ---
// Kestrel lauscht hinter IIS nicht selbst auf :443. TLS terminiert im IIS.
Entfernt: builder.WebHost.UseUrls("https://0.0.0.0:3300");
// Optional (schadet nicht, macht die Absicht klar):
builder.WebHost.UseIIS();
// 1) ConnectionString prüfen
var cs = builder.Configuration.GetConnectionString("Default");
@ -27,17 +105,18 @@ builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o =>
{
o.LoginPath = "/login"; // ohne Auth -> Redirect hierher
o.LoginPath = "/login";
o.AccessDeniedPath = "/login";
o.ReturnUrlParameter = "returnUrl";
o.SlidingExpiration = true;
o.ExpireTimeSpan = TimeSpan.FromDays(7);
o.ExpireTimeSpan = TimeSpan.FromHours(15); //somit AutoLogoff nach 15 Stunden
// Cookie-Härtung
// Cookie-Härtung:
o.Cookie.Name = "Werksverkauf.Auth";
o.Cookie.HttpOnly = true;
o.Cookie.SecurePolicy = CookieSecurePolicy.None; // nur über HTTPS eigentlich .Always
o.Cookie.SameSite = SameSiteMode.Lax; // für normale Navigations-Requests
// Hinter IIS wird über HTTPS ausgeliefert -> Always ist korrekt
o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
o.Cookie.SameSite = SameSiteMode.Lax;
});
builder.Services.AddAuthorization();
@ -45,29 +124,38 @@ builder.Services.AddAuthorization();
builder.Services.AddDbContextFactory<ScannerDb>(opt => opt.UseSqlServer(cs));
builder.Services.AddSingleton<StammdatenCache>();
builder.Services.AddScoped<InventurService>();
builder.Services.AddScoped<AuthService>(); // prüft KassenLogin in DB
builder.Services.AddScoped<AuthService>();
// (Optional) Forwarded Headers, wenn hinter Proxy/LoadBalancer (NGINX/IIS/K8s)
// (Optional) Forwarded Headers sinnvoll hinter IIS/Proxy
builder.Services.Configure<ForwardedHeadersOptions>(opt =>
{
opt.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
// opt.KnownProxies.Add(IPAddress.Parse("127.0.0.1")); // bei Bedarf setzen
// Falls du strikte Proxy-Liste willst:
// opt.KnownProxies.Add(IPAddress.Parse("10.250.1.30"));
});
var app = builder.Build();
// 5) Middleware-Pipeline
app.UseForwardedHeaders(); // vor allem anderen, wenn Proxy im Spiel
app.UseHttpsRedirection(); // ok hinter IIS, nutzt X-Forwarded-Proto
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication(); // erst Auth…
app.UseAuthorization(); // …dann Authorization
app.UseAuthentication();
app.UseAuthorization();
// 6) Endpoints
app.MapControllers(); // /auth/login, /auth/logout, /auth/me
app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
// --- Für lokalen Start (ohne IIS) z. B. auf HTTP-Port 3300 testen ---
// if (!app.Environment.IsProduction())
// {
// app.Urls.Add("http://localhost:3300");
// }
app.Run();