/* 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 — Security features (loads right after DMSCore.jsx.txt). // • K2BSecurityHost — idle auto-lock + manual lock overlay (mounted by app.jsx) // • jbLockNow() — lock this station immediately (also Ctrl+Shift+L) // • jbMaskPII / JbSecret — PII masking with an audited 20-second reveal // • jbPasswordPolicy — shared password rules (Settings → Users (Server)) // • SecurityPanel — Settings → Security tab (admin only) // // Audit actions logged here: "Lock", "Unlock", "Failed Unlock", "Sign Off" // (from the lock screen), "PII Revealed". // localStorage: jb_sec_idle_min (idle minutes, 0 = off). sessionStorage: // jb_sec_locked ("1" while this tab is locked — survives a reload). const JB_SEC_IDLE_KEY = "jb_sec_idle_min"; const JB_SEC_LOCKED_KEY = "jb_sec_locked"; const JB_SEC_IDLE_DEFAULT = 15; const JB_SEC_MAX_FAILS = 5; const JB_SEC_COOLDOWN_MS = 60 * 1000; const JB_SEC_REVEAL_MS = 20 * 1000; const jbSecIdleMinutes = () => { try { const raw = localStorage.getItem(JB_SEC_IDLE_KEY); if (raw === null || raw === "") return JB_SEC_IDLE_DEFAULT; const n = Number(raw); return Number.isFinite(n) && n >= 0 ? n : JB_SEC_IDLE_DEFAULT; } catch (e) { return JB_SEC_IDLE_DEFAULT; } }; const jbSecSetIdleMinutes = (min) => { try { localStorage.setItem(JB_SEC_IDLE_KEY, String(Math.max(0, Number(min) || 0))); } catch (e) {} try { window.dispatchEvent(new CustomEvent("jb-sec-idle")); } catch (e) {} }; const jbSecDMS = () => (typeof window !== "undefined" && window.DMS) ? window.DMS : null; const jbSecUser = () => { const D = jbSecDMS(); try { return (D && D.userId) ? D.userId() : ""; } catch (e) { return ""; } }; const jbSecAudit = (action, detail) => { const D = jbSecDMS(); try { if (D && D.logAudit) D.logAudit(action, detail || ""); } catch (e) {} }; /* ── Lock this station now (also bound to Ctrl+Shift+L by the host) ── */ const jbLockNow = () => { if (!jbSecUser()) { window.k2bToast && window.k2bToast("Nobody is signed in on this station.", "info"); return false; } if (!window.__jbSecHostMounted) { window.k2bToast && window.k2bToast("Lock screen is not mounted on this page.", "warn"); return false; } try { window.dispatchEvent(new CustomEvent("jb-sec-lock", { detail: { reason: "manual" } })); } catch (e) {} return true; }; /* ── Sign off exactly the way the app shell does (app.jsx Sign Off button): presence cleared → DMS.signOut() → per-session working keys cleared → reload (the token is gone, so the reload lands on SignOn). ── */ const jbSecSignOff = async () => { const D = jbSecDMS(); jbSecAudit("Sign Off", jbSecUser() + " · from lock screen"); try { sessionStorage.removeItem(JB_SEC_LOCKED_KEY); } catch (e) {} // A pop-out window only closes itself — the main window keeps its sign-in // and its deal pointers (same rule as the pop-out bar's Sign Off). const isPopout = /[?&]popout=/.test(String(window.location.search || "")); if (isPopout) { try { window.close(); } catch (e) {} return; } if (D) { try { if (D.clearPresence) D.clearPresence(); } catch (e) {} if (typeof D.signOut === "function") { try { await D.signOut(); } catch (e) {} } } try { if (typeof jbClearSessionWork === "function") jbClearSessionWork(); } catch (e) {} try { window.location.reload(); } catch (e) {} }; /* ═══════════════════════ K2BSecurityHost ═══════════════════════ */ const K2BSecurityHost = ({ go }) => { const [locked, setLocked] = React.useState(() => { try { return sessionStorage.getItem(JB_SEC_LOCKED_KEY) === "1" && !!jbSecUser(); } catch (e) { return false; } }); const [idleMin, setIdleMin] = React.useState(jbSecIdleMinutes); const lastActive = React.useRef(Date.now()); const hiddenAt = React.useRef(0); const lockedRef = React.useRef(locked); const overlayRef = React.useRef(null); lockedRef.current = locked; const lock = React.useCallback((reason) => { if (lockedRef.current) return; if (!jbSecUser()) return; lockedRef.current = true; setLocked(true); try { sessionStorage.setItem(JB_SEC_LOCKED_KEY, "1"); } catch (e) {} try { if (document.activeElement && document.activeElement.blur) document.activeElement.blur(); } catch (e) {} jbSecAudit("Lock", jbSecUser() + " · " + (reason || "manual")); }, []); const unlock = React.useCallback(() => { lockedRef.current = false; lastActive.current = Date.now(); setLocked(false); try { sessionStorage.removeItem(JB_SEC_LOCKED_KEY); } catch (e) {} }, []); // Activity tracking (throttled to one write / 2s), idle timer, visibility, // manual lock event, Ctrl+Shift+L, and the keyboard fence while locked. React.useEffect(() => { window.__jbSecHostMounted = true; let lastMark = 0; const mark = () => { const now = Date.now(); if (now - lastMark > 2000) { lastMark = now; lastActive.current = now; } }; const EV = ["mousemove", "keydown", "mousedown", "touchstart", "scroll"]; EV.forEach(ev => window.addEventListener(ev, mark, { passive: true, capture: true })); const onIdlePref = () => setIdleMin(jbSecIdleMinutes()); window.addEventListener("jb-sec-idle", onIdlePref); const onLockEvt = (e) => lock((e && e.detail && e.detail.reason) || "manual"); window.addEventListener("jb-sec-lock", onLockEvt); const onVis = () => { if (document.visibilityState === "hidden") { hiddenAt.current = Date.now(); return; } const m = jbSecIdleMinutes(); if (m > 0 && hiddenAt.current && Date.now() - hiddenAt.current > m * 60000) lock("away " + Math.round((Date.now() - hiddenAt.current) / 60000) + " min"); hiddenAt.current = 0; }; document.addEventListener("visibilitychange", onVis); // Capture-phase key fence: Ctrl+Shift+L locks; while locked, F-keys and // any key outside the overlay never reach the app's shortcut handlers. const onKeyCapture = (e) => { if ((e.ctrlKey || e.metaKey) && e.shiftKey && String(e.key || "").toLowerCase() === "l") { if (jbSecUser() && !lockedRef.current) { e.preventDefault(); e.stopPropagation(); lock("Ctrl+Shift+L"); } return; } if (!lockedRef.current) return; if (/^F\d{1,2}$/.test(String(e.key || ""))) { e.preventDefault(); e.stopPropagation(); return; } const ov = overlayRef.current; if (!ov || !(e.target instanceof Node) || !ov.contains(e.target)) { e.stopPropagation(); } }; window.addEventListener("keydown", onKeyCapture, true); const tick = setInterval(() => { if (lockedRef.current) return; const m = jbSecIdleMinutes(); if (m <= 0 || !jbSecUser()) return; if (Date.now() - lastActive.current > m * 60000) lock("idle " + m + " min"); }, 5000); return () => { window.__jbSecHostMounted = false; EV.forEach(ev => window.removeEventListener(ev, mark, { capture: true })); window.removeEventListener("jb-sec-idle", onIdlePref); window.removeEventListener("jb-sec-lock", onLockEvt); document.removeEventListener("visibilitychange", onVis); window.removeEventListener("keydown", onKeyCapture, true); clearInterval(tick); }; }, [lock]); if (!locked) return null; return ; }; /* ── The lock overlay itself ── */ const JbLockScreen = ({ overlayRef, onUnlocked, idleMin }) => { const D = jbSecDMS(); const user = jbSecUser(); const ti = (D && D.tokenInfo) ? (() => { try { return D.tokenInfo(); } catch (e) { return null; } })() : null; const name = (ti && ti.name) ? String(ti.name) : ""; const offline = !!(D && D.offlineByChoice && D.offlineByChoice()); const noServer = !D || typeof D.login !== "function"; const canVerify = !offline && !noServer; const [pw, setPw] = React.useState(""); const [err, setErr] = React.useState(""); const [busy, setBusy] = React.useState(false); const [fails, setFails] = React.useState(0); const [shake, setShake] = React.useState(false); const [coolUntil, setCoolUntil] = React.useState(0); const [now, setNow] = React.useState(Date.now()); const [caps, setCaps] = React.useState(false); const inputRef = React.useRef(null); React.useEffect(() => { if (!coolUntil) return; const t = setInterval(() => setNow(Date.now()), 500); return () => clearInterval(t); }, [coolUntil]); const coolLeft = coolUntil > now ? Math.ceil((coolUntil - now) / 1000) : 0; React.useEffect(() => { if (coolUntil && !coolLeft) { setCoolUntil(0); setFails(0); setErr(""); setTimeout(() => inputRef.current && inputRef.current.focus(), 0); } }, [coolLeft, coolUntil]); const fail = (msg) => { const n = fails + 1; setFails(n); setPw(""); setShake(true); setTimeout(() => setShake(false), 450); jbSecAudit("Failed Unlock", user + " · attempt " + n); if (n >= JB_SEC_MAX_FAILS) { setCoolUntil(Date.now() + JB_SEC_COOLDOWN_MS); setErr("Too many failed attempts — wait 60 seconds."); } else setErr(msg || "Wrong password"); setTimeout(() => inputRef.current && inputRef.current.focus(), 0); }; const tryUnlock = async () => { if (busy || coolLeft || !canVerify) return; if (!pw) { setErr("Enter your password."); return; } setBusy(true); setErr(""); try { const r = await D.login(user, pw); if (r && r.ok && r.mfaRequired) { // Unlock re-checks the password only; the MFA step was already // satisfied when this session signed on. jbSecAudit("Unlock", user + " · password verified (MFA session)"); onUnlocked(); return; } if (r && r.ok) { jbSecAudit("Unlock", user); onUnlocked(); return; } if (r && r.serverReached) { fail("Wrong password"); return; } setErr("Server unreachable — your password cannot be verified right now. Try again in a moment or sign off."); setPw(""); } catch (e) { setErr(e.message || "Unlock failed."); } finally { setBusy(false); } }; const onKey = (e) => { try { setCaps(!!(e.getModifierState && e.getModifierState("CapsLock"))); } catch (x) {} if (e.key === "Enter") { e.preventDefault(); tryUnlock(); } }; const card = { width: 400, background: "var(--jb-surface, #FFFFFF)", borderRadius: "var(--jb-r-lg, 10px)", boxShadow: "var(--jb-shadow-lg, 0 24px 80px rgba(0,0,0,0.55))", overflow: "hidden", fontFamily: "var(--jb-font, var(--font-ui))", color: "var(--jb-text, #0F1A33)", animation: shake ? "jbSecShake 0.4s" : "none", }; const btn = (primary) => ({ flex: 1, padding: "10px 14px", cursor: "pointer", fontFamily: "var(--jb-font, var(--font-ui))", fontSize: 13, fontWeight: 700, borderRadius: "var(--jb-r-sm, 4px)", border: primary ? "1px solid var(--jb-navy, #0E1430)" : "1px solid var(--jb-border-strong, #BCC4D0)", background: primary ? "var(--jb-navy, #0E1430)" : "var(--jb-surface, #FFF)", color: primary ? "var(--jb-yellow, #FFD24A)" : "var(--jb-text, #0F1A33)", }); return (
e.stopPropagation()} onKeyUp={(e) => e.stopPropagation()} onContextMenu={(e) => e.preventDefault()} style={{ position: "fixed", inset: 0, zIndex: 99990, background: "linear-gradient(160deg, var(--jb-navy-deep, #070B1F) 0%, var(--jb-navy, #0E1430) 45%, var(--jb-blue, #1B66A8) 100%)", display: "flex", alignItems: "center", justifyContent: "center", padding: 24, }}>
🔒 Locked — {name ? `${name} (${user})` : user}
Dealer Management System · Station Locked
{canVerify ? ( <>
{idleMin > 0 ? `This station locks after ${idleMin} minutes without activity. ` : ""}Enter your password to continue where you left off.
Password
{ setPw(e.target.value); if (err && !coolLeft) setErr(""); }} onKeyDown={onKey} onKeyUp={onKey} placeholder="••••••••••" style={{ width: "100%", padding: "10px 12px", boxSizing: "border-box", fontSize: 15, border: "1px solid " + (err ? "var(--jb-red, #C03546)" : "var(--jb-border-strong, #BCC4D0)"), borderRadius: "var(--jb-r-sm, 4px)", outline: 0, fontFamily: "var(--jb-font, var(--font-ui))", background: coolLeft ? "var(--jb-surface-2, #F9FAFC)" : "#FFF", color: "var(--jb-text, #0F1A33)" }} /> {caps &&
⇪ Caps Lock is on
} {err && (
⚠ {coolLeft ? `Too many failed attempts — try again in ${coolLeft}s.` : err}
)} {!coolLeft && fails > 0 && fails < JB_SEC_MAX_FAILS && (
{JB_SEC_MAX_FAILS - fails} attempt{JB_SEC_MAX_FAILS - fails === 1 ? "" : "s"} left before a 60-second cooldown.
)}
) : ( <>
{offline ? "This is an offline session — there is no server to verify a password against, so the station can only be signed off." : "The DMS server is not available on this page, so a password cannot be verified — sign off to continue."}
)}
Ctrl+Shift+L locks any time · every lock and unlock is written to the audit trail
); }; /* ═══════════════════════ PII masking ═══════════════════════ */ const jbMaskPII = (value, kind) => { const s = String(value == null ? "" : value).trim(); if (!s) return ""; const clean = s.replace(/[^A-Za-z0-9]/g, ""); const last4 = clean.slice(-4) || s.slice(-4); const k = String(kind || "").toLowerCase(); if (k === "ssn") return "•••-••-" + last4; if (k === "dl") return "••••" + last4; return "••••" + last4; // "acct" / default }; const JbSecret = ({ value, kind, label, style }) => { const [shown, setShown] = React.useState(false); const timer = React.useRef(null); const s = String(value == null ? "" : value); React.useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []); const reveal = (e) => { if (e) { e.preventDefault(); e.stopPropagation(); } if (shown) { setShown(false); if (timer.current) clearTimeout(timer.current); return; } setShown(true); jbSecAudit("PII Revealed", String(kind || "acct") + " · " + (label || "")); if (timer.current) clearTimeout(timer.current); timer.current = setTimeout(() => setShown(false), JB_SEC_REVEAL_MS); }; if (!s.trim()) return —; return ( {shown ? s : jbMaskPII(s, kind)} ); }; /* ═══════════════════════ Password policy ═══════════════════════ */ const JB_SEC_COMMON_PW = [ "password", "password1", "password123", "passw0rd", "123456", "1234567", "12345678", "123456789", "1234567890", "qwerty", "qwerty123", "qwertyuiop", "abc123", "abcd1234", "welcome", "welcome1", "welcome123", "letmein", "letmein1", "iloveyou", "monkey", "dragon", "football", "baseball", "sunshine", "princess", "master", "shadow", "superman", "trustno1", "111111", "000000", "654321", "696969", "changeme", "temp1234", "chevrolet", "chevy", "silverado", "dealer", "dealership", "admin", "admin123", "administrator", "manager", "finance", "sales", "service", "parts", "k2b", "dms", "gmail", "spring2026", "summer2026", "winter2026", "fall2026", ]; const jbPasswordPolicy = (pw, username) => { const s = String(pw || ""); const u = String(username || "").trim().toLowerCase(); const problems = []; if (s.length < 10) problems.push("At least 10 characters"); if (!/[A-Z]/.test(s)) problems.push("An uppercase letter"); if (!/[a-z]/.test(s)) problems.push("A lowercase letter"); if (!/[0-9]/.test(s)) problems.push("A digit"); const low = s.toLowerCase(); if (u && u.length >= 2 && low.includes(u)) problems.push("Must not contain the employee # / user name"); const stripped = low.replace(/[^a-z0-9]/g, ""); // RC35 (brd): + every word of the names of the stores this login sees (no store name is written in this file) const storeWords = (() => { try { const st = window.K2BStore; const names = st ? st.list().map(x => x.dba).concat([st.branding().dba, st.branding().legalName]) : []; return names.join(" ").toLowerCase().split(/[^a-z0-9]+/).filter(w => w.length >= 4); } catch (e) { return []; } })(); if (JB_SEC_COMMON_PW.concat(storeWords).some(c => stripped === c || low === c)) problems.push("Too common — pick something less guessable"); let score = 0; if (s.length >= 10) score++; if (/[A-Z]/.test(s) && /[a-z]/.test(s)) score++; if (/[0-9]/.test(s)) score++; if (/[^A-Za-z0-9]/.test(s) || s.length >= 14) score++; if (problems.length) score = Math.min(score, problems.length >= 3 ? 1 : 2); if (!s) score = 0; return { ok: problems.length === 0, score: Math.max(0, Math.min(4, score)), problems }; }; /* Live strength meter + problem list (used by Settings → Users (Server)). */ const JbPasswordMeter = ({ pw, username, confirm, showConfirm }) => { const r = jbPasswordPolicy(pw, username); const colors = ["var(--jb-border, #DDE2EA)", "var(--jb-red, #C03546)", "var(--jb-amber, #B26A00)", "var(--jb-blue, #1B66A8)", "var(--jb-green, #16A34A)"]; const words = ["", "Weak", "Fair", "Good", "Strong"]; const mismatch = showConfirm && confirm !== undefined && confirm !== "" && confirm !== pw; if (!pw) return null; return (
{[1, 2, 3, 4].map(i => ( ))} {words[r.score]}
{r.problems.length > 0 && ( )} {r.ok && !mismatch &&
✔ Meets the password policy
} {mismatch &&
Passwords do not match
}
); }; /* ═══════════════════════ Settings → Security ═══════════════════════ */ const JB_SEC_EVENT_RE = /^(Sign On|Sign Off|Lock|Unlock|PII Revealed|Post Override|Password|Failed Sign-On|Failed Unlock|Encryption Enabled|MFA)/i; const JbSecHead = ({ title, sub }) => (
{title}
{sub}
); const JbSecGroup = ({ label, children }) => (
{label}
{children}
); const JbSecCheck = ({ ok, warn, label, detail }) => (
{ok ? "✔" : warn ? "!" : "✖"}
{label}
{detail &&
{detail}
}
); const SecurityPanel = ({ go }) => { const D = jbSecDMS(); const isAdmin = !!(D && D.isAdmin && D.isAdmin()); const [idle, setIdle] = React.useState(jbSecIdleMinutes); const [tickN, setTick] = React.useState(0); React.useEffect(() => { const refresh = () => setTick(n => n + 1); const onIdle = () => setIdle(jbSecIdleMinutes()); window.addEventListener("k2b-conn", refresh); window.addEventListener("jb-sec-idle", onIdle); const off = (D && D.subscribe) ? D.subscribe(refresh) : null; const t = setInterval(refresh, 15000); return () => { window.removeEventListener("k2b-conn", refresh); window.removeEventListener("jb-sec-idle", onIdle); if (off) off(); clearInterval(t); }; }, []); if (!isAdmin) { return (
Security settings are available to the dealership Admin only.
); } const user = jbSecUser(); const role = (D && D.userRole) ? D.userRole() : ""; const ti = (D && D.tokenInfo) ? (() => { try { return D.tokenInfo(); } catch (e) { return null; } })() : null; const cs = (D && D.connState) ? D.connState() : "offline"; const offline = !!(D && D.offlineByChoice && D.offlineByChoice()); const station = (() => { try { const s = D && D.session ? D.session().station : ""; const a = window.jbLoadAuth ? window.jbLoadAuth() : {}; return a.station ? `${a.station} (${s})` : s; } catch (e) { return ""; } })(); const https = (typeof location !== "undefined") && location.protocol === "https:"; const localhost = (typeof location !== "undefined") && /^(localhost|127\.0\.0\.1|\[::1\])$/.test(location.hostname); // (blocker 4) the setting + what is actually on disk + whether the vault is open, from the one cache path const cst = (D && D.cacheStatus) ? (() => { try { return D.cacheStatus(); } catch (e) { return null; } })() : null; const encrypted = cst ? (cst.encryption === "on" && cst.stored !== "plaintext") : !!(D && D.isEncryptedAtRest && (() => { try { return D.isEncryptedAtRest(); } catch (e) { return false; } })()); const encDetail = !encrypted ? (cst && cst.encryption === "on" ? "Encryption is on, but this PC still holds a plaintext copy — it is re-encrypted at the next save." : "Turn on encryption at rest in Settings → Data & Backup to protect local data on this PC.") : (cst && cst.vault === "locked" ? "AES-256-GCM vault — LOCKED: changes are not saved on this PC until it is unlocked (reload and enter the passphrase)." : "AES-256-GCM vault — local data needs the dealership passphrase to open."); const audit = (D && D.audit) ? D.audit() : []; const events = audit.filter(e => JB_SEC_EVENT_RE.test(String(e.action || ""))).slice(0, 40); const connLabel = { online: "Online — server session", offline: offline ? "Offline by choice — local session" : "Offline — not connected", "auth-expired": "Session expired", connecting: "Connecting…" }[cs] || cs; const connColor = cs === "online" ? "var(--jb-green, #16A34A)" : cs === "connecting" ? "var(--jb-text-dim)" : "var(--jb-amber, #B26A00)"; const expText = ti ? (ti.expired ? "EXPIRED" : (ti.exp ? new Date(ti.exp * 1000).toLocaleString() : "n/a")) : "— (no server token)"; const cell = { padding: "5px 10px", fontSize: 12.5, verticalAlign: "top" }; const lab = { ...cell, color: "var(--jb-text-dim, #58637A)", fontWeight: 600, width: 130, whiteSpace: "nowrap" }; return (
Shortcut: Ctrl+Shift+L · setting is per workstation
User{user || "—"}{ti && ti.name ? ` · ${ti.name}` : ""}
Role{role || (no role — least privilege)}
Station{station || "—"}
Connection● {connLabel}
Token expires{expText}
0} warn={idle === 0} label={idle > 0 ? `Auto-lock on (${idle} min)` : "Auto-lock off"} detail="An unattended station locks itself and needs the user's password to resume." /> {!events.length &&
No security events recorded on this station yet.
} {events.length > 0 && (
{events.map(e => { const bad = /Failed|Override/i.test(e.action || ""); return ( ); })}
WhenAction UserDetail
{e.ts ? new Date(e.ts).toLocaleString() : ""} {e.action} {e.user || "—"} {e.detail}
)} {go && }
  • HTTPS — every request to the DMS server is encrypted in transit.
  • Hashed passwords — the server stores password hashes only; no workstation ever holds a password.
  • Encrypted PII fields — SSNs, license numbers and account numbers are encrypted on the server and masked on screen (👁 reveals for 20 seconds and is audited).
  • Role fences — Sales sees CRM and Customers only; deal posting, funding and Settings are limited to managers, F&I, Accounting and Admin.
  • Audit trail — sign-ons, sign-offs, locks, reveals, posting overrides and password changes are logged with user, role and time.
  • Auto-lock — an idle station locks itself; resuming needs the signed-in user's password.
); }; Object.assign(window, { K2BSecurityHost, jbLockNow, jbMaskPII, JbSecret, jbPasswordPolicy, JbPasswordMeter, SecurityPanel, jbSecIdleMinutes, jbSecSetIdleMinutes });