WerksverkaufScanner: -Login/Logout Prozedur geändert (Logout) Inventurconfig löschen
-InventurScan erst möglich bei vorhandener Config
This commit is contained in:
parent
4d811702f0
commit
748fa505fe
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user