diff --git a/WerksverkaufScanner/WerksverkaufScanner/Controllers/AuthController.cs b/WerksverkaufScanner/WerksverkaufScanner/Controllers/AuthController.cs index d60e6c2..5a44bc7 100644 --- a/WerksverkaufScanner/WerksverkaufScanner/Controllers/AuthController.cs +++ b/WerksverkaufScanner/WerksverkaufScanner/Controllers/AuthController.cs @@ -41,10 +41,30 @@ public class AuthController : ControllerBase } [Authorize] - [HttpPost("logout")] - public async Task 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 = """ + + + Logout + + + + + """; + + return Content(html, "text/html"); } } diff --git a/WerksverkaufScanner/WerksverkaufScanner/Pages/Inventur/InventurScan.razor b/WerksverkaufScanner/WerksverkaufScanner/Pages/Inventur/InventurScan.razor index 062b753..506cec4 100644 --- a/WerksverkaufScanner/WerksverkaufScanner/Pages/Inventur/InventurScan.razor +++ b/WerksverkaufScanner/WerksverkaufScanner/Pages/Inventur/InventurScan.razor @@ -9,58 +9,69 @@

Inventur – Scannen / Eingeben

- - - -@if (varianten is { Count: > 1 }) +@if (needsSetup) { -
-
Varianten gefunden – bitte auswählen:
- -
- @foreach (var art in varianten) - { - var isActive = ReferenceEquals(gefunden, art); - - } + } - -@if (gefunden is not null) +else { -
- @gefunden.Bezeichnung - - · @gefunden.ArtikelNummer - · Variante @gefunden.Variante - @if (!string.IsNullOrWhiteSpace(gefunden.Farbe)) - { - · @gefunden.Farbe - } - @if (!string.IsNullOrWhiteSpace(gefunden.Text)) - { - · @gefunden.Text - } - -
- - - + + placeholder="Barcode scannen oder Artikelnummer eingeben" + @bind="scanText" @bind:event="oninput" + @onkeydown="OnScanKey" /> + + @if (varianten is { Count: > 1 }) + { +
+
Varianten gefunden – bitte auswählen:
+
+ @foreach (var art in varianten) + { + var isActive = ReferenceEquals(gefunden, art); + + } +
+
+ } + + @if (gefunden is not null) + { +
+ @gefunden.Bezeichnung + + · @gefunden.ArtikelNummer + · Variante @gefunden.Variante + @if (!string.IsNullOrWhiteSpace(gefunden.Farbe)) + { + · @gefunden.Farbe + } + @if (!string.IsNullOrWhiteSpace(gefunden.Text)) + { + · @gefunden.Text + } + +
+ + + + } } @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("localStorage.getItem", "scannerpilot.filialId"); + var i = await JS.InvokeAsync("localStorage.getItem", "scannerpilot.inventurId"); + var u = await JS.InvokeAsync("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("localStorage.getItem", "scannerpilot.filialId"); - var inventurIdStr = await JS.InvokeAsync("localStorage.getItem", "scannerpilot.inventurId"); - var user = await JS.InvokeAsync("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 } } diff --git a/WerksverkaufScanner/WerksverkaufScanner/Program.cs b/WerksverkaufScanner/WerksverkaufScanner/Program.cs index 2e3b106..ecb3627 100644 --- a/WerksverkaufScanner/WerksverkaufScanner/Program.cs +++ b/WerksverkaufScanner/WerksverkaufScanner/Program.cs @@ -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 prfen +//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(); // fr AuthController (Login/Logout) +//builder.Services.AddHttpClient(); // fr 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-Hrtung +// o.Cookie.Name = "Werksverkauf.Auth"; +// o.Cookie.HttpOnly = true; +// o.Cookie.SecurePolicy = CookieSecurePolicy.None; // nur ber HTTPS eigentlich .Always +// o.Cookie.SameSite = SameSiteMode.Lax; // fr normale Navigations-Requests +// }); +//builder.Services.AddAuthorization(); + +//// 4) App-Services & Datenzugriff +//builder.Services.AddDbContextFactory(opt => opt.UseSqlServer(cs)); +//builder.Services.AddSingleton(); +//builder.Services.AddScoped(); +//builder.Services.AddScoped(); // prft KassenLogin in DB + +//// (Optional) Forwarded Headers, wenn hinter Proxy/LoadBalancer (NGINX/IIS/K8s) +//builder.Services.Configure(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 ntig, 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 FR 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 prfen 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-Hrtung + // Cookie-Hrtung: o.Cookie.Name = "Werksverkauf.Auth"; o.Cookie.HttpOnly = true; - o.Cookie.SecurePolicy = CookieSecurePolicy.None; // nur ber HTTPS eigentlich .Always - o.Cookie.SameSite = SameSiteMode.Lax; // fr 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(opt => opt.UseSqlServer(cs)); builder.Services.AddSingleton(); builder.Services.AddScoped(); -builder.Services.AddScoped(); // prft KassenLogin in DB +builder.Services.AddScoped(); -// (Optional) Forwarded Headers, wenn hinter Proxy/LoadBalancer (NGINX/IIS/K8s) +// (Optional) Forwarded Headers sinnvoll hinter IIS/Proxy builder.Services.Configure(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"); +// --- Fr lokalen Start (ohne IIS) z. B. auf HTTP-Port 3300 testen --- +// if (!app.Environment.IsProduction()) +// { +// app.Urls.Add("http://localhost:3300"); +// } + app.Run(); +