/* eslint-disable */ /* © 2026 K2B NETWORK SOLUTIONS LLC. All rights reserved. Proprietary and confidential. Unauthorized copying, distribution, or use of this file, via any medium, is strictly prohibited. Original work product. */ // JBREWS DMS — Sign-On screen. // Sign-on is validated by the SERVER (DMS.login / DMS.loginMfa). Nothing on // this workstation holds a password or an MFA code. If the server cannot be // reached the user may choose "Work Offline Anyway", which opens a RESTRICTED // local session (role "Offline": no admin, no deal creation, nothing shared). const JB_AUTH_KEY = "jbrews-auth-config-v1"; // Workstation-only preferences (no credentials): the Dealer Id pre-filled on // this screen and an optional workstation name shown in presence. // RC35 (brd): "dealer" is no longer asked - the server picks the store; the stored value only names an RC34 local copy (moved once) const JB_DEFAULT_AUTH = { dealer: "", station: "" }; window.jbLoadAuth = function() { try { const raw = localStorage.getItem(JB_AUTH_KEY); if (raw) { const p = JSON.parse(raw) || {}; // Only the workstation preferences are honoured; any credential fields // written by an older build are ignored and scrubbed on the next save. return { ...JB_DEFAULT_AUTH, dealer: p.dealer || JB_DEFAULT_AUTH.dealer, station: p.station || "" }; } } catch (e) {} return { ...JB_DEFAULT_AUTH }; }; window.jbSaveAuth = function(a) { const safe = { dealer: (a && a.dealer) || JB_DEFAULT_AUTH.dealer, station: (a && a.station) || "" }; try { localStorage.setItem(JB_AUTH_KEY, JSON.stringify(safe)); } catch (e) {} }; // Scrub any credential fields an older build left in this key. try { const raw = localStorage.getItem(JB_AUTH_KEY); if (raw && /"(password|mfaCode|user)"/.test(raw)) window.jbSaveAuth(window.jbLoadAuth()); } catch (e) {} // Station-level brute-force brake: after JB_SIGNON_MAX_FAILS refused sign-ins // this workstation waits 60 s (the server has its own limits; this one just // slows a keyboard). Nothing about the user or password is stored. const JB_SIGNON_COOL_KEY = "jb_signon_cooldown_until"; const JB_SIGNON_FAILS_KEY = "jb_signon_fails"; const JB_SIGNON_MAX_FAILS = 5; const JB_SIGNON_COOL_MS = 60 * 1000; const jbSignOnCoolUntil = () => { try { return Number(localStorage.getItem(JB_SIGNON_COOL_KEY)) || 0; } catch (e) { return 0; } }; const jbSignOnFails = () => { try { return Number(localStorage.getItem(JB_SIGNON_FAILS_KEY)) || 0; } catch (e) { return 0; } }; const jbSignOnAudit = (action, detail) => { try { if (window.DMS && window.DMS.logAudit) window.DMS.logAudit(action, detail || ""); } catch (e) {} }; const SignOn = ({ onLogin }) => { const [auth] = React.useState(() => window.jbLoadAuth()); const [step, setStep] = React.useState("creds"); // "creds" | "mfa" const dealer = auth.dealer; // RC35 (brd): kept as stored (never shown, never changed) const storeNow = () => (window.K2BStore && (window.K2BStore.id() || window.K2BStore.pageId())) || ""; // RC35 (brd): before sign-in the card shows the LAST store used on this PC (its cached branding) - never a list of stores - else K2B const SB = window.K2BStore && window.K2BStore.known() ? window.K2BStore.branding() : null; const soPrimary = SB ? SB.colors.primary : "#0E1430", soAccent = SB ? SB.colors.accent : "#FFD24A"; const soLight = (() => { const h = String(soPrimary).replace("#", ""); const c = [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) || 0).map((v) => Math.round(v + (255 - v) * 0.09)); return "#" + c.map((v) => v.toString(16).padStart(2, "0")).join(""); })(); const soLogo = SB ? (window.K2BStore.logoUrl("large") || window.K2BStore.monogramUrl(200)) : "../../assets/k2b-logo-mark.svg"; const [user, setUser] = React.useState(""); const [pass, setPass] = React.useState(""); const [mfa, setMfa] = React.useState(""); const [mfaToken, setMfaToken] = React.useState(null); // RC22 (own): the owner's sign-in codes — which ways this login has, the chosen one, the text-message state, set-up at sign-in const [mfaInfo, setMfaInfo] = React.useState(null); // { methods, phone, enrollTotp, message, smsReady } const [mfaMethod, setMfaMethod] = React.useState("totp"); const [smsNote, setSmsNote] = React.useState(""); const [resendAt, setResendAt] = React.useState(0); const [enroll, setEnroll] = React.useState(null); // { qrPng, secret } — the authenticator being set up at sign-in const [showPw, setShowPw] = React.useState(false); const [err, setErr] = React.useState(""); const [busy, setBusy] = React.useState(false); const [modal, setModal] = React.useState(null); const [caps, setCaps] = React.useState(false); // Cooldown after repeated refused sign-ins (per station, localStorage). const [coolUntil, setCoolUntil] = React.useState(jbSignOnCoolUntil); const [now, setNow] = React.useState(Date.now()); React.useEffect(() => { if (!coolUntil) return; const t = setInterval(() => setNow(Date.now()), 500); return () => clearInterval(t); }, [coolUntil]); React.useEffect(() => { if (!resendAt) return; const t = setInterval(() => setNow(Date.now()), 500); return () => clearInterval(t); }, [resendAt]); // RC22 (own): the "Send again in Ns" countdown const coolLeft = coolUntil > now ? Math.ceil((coolUntil - now) / 1000) : 0; React.useEffect(() => { if (coolUntil && !coolLeft) { setCoolUntil(0); setErr(""); try { localStorage.removeItem(JB_SIGNON_COOL_KEY); localStorage.removeItem(JB_SIGNON_FAILS_KEY); } catch (e) {} } }, [coolLeft, coolUntil]); const noteFailure = (who, why) => { jbSignOnAudit("Failed Sign-On", String(who || "").trim() + (why ? " · " + why : "")); const n = jbSignOnFails() + 1; try { localStorage.setItem(JB_SIGNON_FAILS_KEY, String(n)); } catch (e) {} if (n >= JB_SIGNON_MAX_FAILS) { const until = Date.now() + JB_SIGNON_COOL_MS; try { localStorage.setItem(JB_SIGNON_COOL_KEY, String(until)); } catch (e) {} setCoolUntil(until); } }; const noteSuccess = (who, how) => { try { localStorage.removeItem(JB_SIGNON_FAILS_KEY); localStorage.removeItem(JB_SIGNON_COOL_KEY); } catch (e) {} setCoolUntil(0); jbSignOnAudit("Sign On", String(who || "").trim() + (how ? " · " + how : "")); }; const capsKey = (e) => { try { setCaps(!!(e.getModifierState && e.getModifierState("CapsLock"))); } catch (x) {} }; // Server-unreachable state: { detail } → show the explicit yellow choice // (Work Offline Anyway / Retry). There is NO automatic silent fallback. const [offlineOffer, setOfflineOffer] = React.useState(null); // Session-expired notice (stored token was already dead at page load). const [connNote, setConnNote] = React.useState(() => (window.DMS && window.DMS.connState && window.DMS.connState() === "auth-expired") ? "Your session expired — please sign in again." : ""); React.useEffect(() => { const onConn = () => { const cs = window.DMS && window.DMS.connState ? window.DMS.connState() : ""; setConnNote(cs === "auth-expired" ? "Your session expired — please sign in again." : ""); }; window.addEventListener("k2b-conn", onConn); return () => window.removeEventListener("k2b-conn", onConn); }, []); const submitCreds = async () => { if (busy) return; if (jbSignOnCoolUntil() > Date.now()) { setCoolUntil(jbSignOnCoolUntil()); setErr("Too many failed sign-ins on this station — wait for the countdown."); return; } setErr(""); setBusy(true); setOfflineOffer(null); try { // ALWAYS try the server first. if (window.DMS && typeof window.DMS.login === "function") { const r = await window.DMS.login(user, pass); if (r && r.ok && r.mfaRequired) { setMfaToken(r.mfaToken); setStep("mfa"); setMfa(""); setSmsNote(""); setEnroll(null); const ms = Array.isArray(r.methods) ? r.methods : ["totp"]; setMfaInfo({ methods: ms, phone: r.phone || null, enrollTotp: !!r.enrollTotp, message: r.message || "", smsReady: r.smsReady !== false }); const first = ms.indexOf("sms") !== -1 ? "sms" : ms.indexOf("totp") !== -1 ? "totp" : (ms[0] || "totp"); setMfaMethod(r.enrollTotp ? "totp" : first); if (r.enrollTotp && window.DMS && window.DMS.mfaEnrollAtSignIn) { const e = await window.DMS.mfaEnrollAtSignIn(r.mfaToken); if (e && e.ok) setEnroll({ qrPng: e.qrPng, secret: e.secret }); else setErr((e && e.msg) || "Could not start the authenticator set-up."); } else if (first === "sms" && !r.enrollTotp) { await sendSms(r.mfaToken); } else if (r.message) { setErr(r.message); } return; } if (r && r.ok) { setConnNote(""); if (window.DMS && typeof window.DMS.setOfflineByChoice === "function") { try { window.DMS.setOfflineByChoice(false); } catch (e) {} } noteSuccess(r.user || user, "server"); onLogin({ dealer: storeNow(), user: r.user || user, role: r.role || "" }); return; } if (r && r.serverReached) { // The server ANSWERED and said no. Show its exact words on screen. // Never quietly fall back to local sign-on — that is how stations // end up working on trapped, unshared data. noteFailure(user, "refused by server"); setErr(r.msg || "Sign-on refused by the server. Check your credentials."); return; } // Server genuinely unreachable (network error / no server) → offer an // EXPLICIT choice instead of silently going local. setOfflineOffer({ detail: (r && r.msg) || "no response" }); return; } // DMS core not loaded at all — same explicit choice. setOfflineOffer({ detail: "DMS core not loaded on this page" }); } catch (e) { setErr(e.message || "Sign-on failed."); } finally { setBusy(false); } }; // Explicit offline session — ONLY reachable from the yellow "Server // unreachable" notice via the [Work Offline Anyway] button. // Audit C-02c / C-03b: this NEVER grants Admin and validates no password. // The typed User Name becomes a restricted local session (role "Offline"): // work on this PC only, no deal creation, nothing shared. The core is told // the session is offline-by-choice so a stale token can't quietly reconnect // and push as a previous identity. const workOffline = () => { setErr(""); setOfflineOffer(null); const id = String(user || "").trim(); if (!id) { setErr("Enter your User Name to work offline on this workstation."); return; } if (window.DMS && typeof window.DMS.setOfflineByChoice === "function") { try { window.DMS.setOfflineByChoice(true); } catch (e) {} } onLogin({ dealer: storeNow(), user: id, role: "Offline", offline: true }); jbSignOnAudit("Sign On", id + " · offline session (no password verified)"); }; // RC22 (own): a sign-in code by text message (Twilio Verify through the server; the code never touches this station's storage) const sendSms = async (tok) => { const t = tok || mfaToken; if (!t || !window.DMS || !window.DMS.mfaSendSms) return; setErr(""); setSmsNote("Sending a code…"); const r = await window.DMS.mfaSendSms(t); if (r && r.ok) { setSmsNote("Code sent to " + (r.sentTo || "your phone") + ". It can take a minute."); setResendAt(Date.now() + 1000 * (r.resendInSec || 30)); } else { setSmsNote(""); setErr((r && r.msg) || "The code could not be sent."); if (r && r.retryInSec) setResendAt(Date.now() + 1000 * r.retryInSec); } }; const submitMfa = async () => { if (busy) return; setErr(""); setBusy(true); try { if (enroll && mfaToken && window.DMS && window.DMS.mfaConfirmAtSignIn) { const r = await window.DMS.mfaConfirmAtSignIn(mfaToken, mfa.trim()); if (r && r.ok) { noteSuccess(r.user || user, "server + new authenticator"); onLogin({ dealer: storeNow(), user: r.user || user, role: r.role || "" }); return; } setErr((r && r.msg) || "That code did not match. Try the current one."); return; } if (mfaToken && window.DMS && typeof window.DMS.loginMfa === "function") { const r = await window.DMS.loginMfa(mfaToken, mfa.trim(), mfaInfo ? mfaMethod : undefined); if (r && r.ok) { noteSuccess(r.user || user, "server + MFA"); onLogin({ dealer: storeNow(), user: r.user || user, role: r.role || "" }); return; } if (r && r.serverReached) noteFailure(user, "MFA code rejected"); setErr((r && r.msg) || "Incorrect verification code. Try again."); return; } // No server MFA token → there is nothing to verify against locally. setErr("Verification requires the server. Go back and sign in again."); } catch (e) { setErr(e.message || "MFA verification failed."); } finally { setBusy(false); } }; const onKey = (e) => { if (modal) return; if (e.key === "Enter") { e.preventDefault(); step === "creds" ? submitCreds() : submitMfa(); } else if (e.key === "F1") { e.preventDefault(); setModal("help"); } else if (e.key === "F9") { e.preventDefault(); step === "creds" ? submitCreds() : submitMfa(); } else if (e.key === "Escape") { if (step === "mfa") setStep("creds"); } }; React.useEffect(() => { window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }); return (