diff --git a/app.py b/app.py index db1c926..e03669a 100644 --- a/app.py +++ b/app.py @@ -2392,6 +2392,14 @@ def is_valid_local(url): if any(b in user_agent for b in ['Mozilla', 'Chrome', 'Safari', 'Edge']): abort(403) + # 3. Master Auth Validation + if request.path.startswith("/api/") and request.path != "/api/master/status": + locks = load_locks() + if "__master__" in locks: + master_pass = request.headers.get("X-Master-Password", "") + if not check_lock("__master__", master_pass): + return jsonify({"error": "Master locked", "master_locked": True}), 401 + # ─── Routes ─────────────────────────────────────────────────────── @@ -2400,6 +2408,13 @@ def index(): return send_from_directory("ui", "index.html") +@app.route("/api/master/status") +def master_status(): + locks = load_locks() + is_locked = "__master__" in locks + return jsonify({"locked": is_locked}) + + @app.route("/api/scripts") def list_scripts(): return jsonify(get_all_scripts()) diff --git a/ui/app.js b/ui/app.js index a4b7f5a..a27ddfc 100644 --- a/ui/app.js +++ b/ui/app.js @@ -25,6 +25,7 @@ const API = { reliability_trends: '/api/reliability/trends', reliability_recommendations: '/api/reliability/recommendations', reliability_diagnostics: '/api/reliability/diagnostics', + master_status: '/api/master/status', }; // ─── State ──────────────────────────────────────────────── @@ -39,6 +40,7 @@ let state = { cmdHistoryIndex: -1, historyQuery: '', historyFilter: 'all', + masterPassword: null, historyEntries: [], historySummary: { total: 0, @@ -137,6 +139,70 @@ function getCategoryIcon(name) { return ICONS[name.toLowerCase()] || ICONS.default; } +// ─── Global Fetch Wrapper ────────────────────────────────────────── +const originalFetch = window.fetch; +window.fetch = async function(...args) { + let [resource, config] = args; + config = config || {}; + + if (typeof resource === 'string' && resource.startsWith('/api/') && state.masterPassword) { + config.headers = config.headers || {}; + config.headers['X-Master-Password'] = state.masterPassword; + } + + let res = await originalFetch(resource, config); + + if (res.status === 401) { + try { + const clone = res.clone(); + const data = await clone.json(); + if (data.master_locked) { + return new Promise((resolve, reject) => { + const modal = document.getElementById('master-auth-modal'); + const input = document.getElementById('master-auth-password'); + const submit = document.getElementById('master-auth-submit'); + + if (!modal) return resolve(res); + + modal.classList.add('active'); + input.value = ''; + input.focus(); + + const cleanup = () => { + submit.removeEventListener('click', onSubmit); + input.removeEventListener('keydown', onKey); + }; + + const onSubmit = async () => { + const pwd = input.value; + if (!pwd) return; + + state.masterPassword = pwd; + modal.classList.remove('active'); + cleanup(); + + try { + const retryRes = await window.fetch(resource, config); + resolve(retryRes); + } catch (e) { + reject(e); + } + }; + + const onKey = (e) => { if (e.key === 'Enter') onSubmit(); }; + + submit.addEventListener('click', onSubmit); + input.addEventListener('keydown', onKey); + }); + } + } catch (e) { + console.error(e); + } + } + + return res; +}; + // Register global lifecycle cleanup listeners exactly once if (!window.__devshell_lifecycle_registered) { window.__devshell_lifecycle_registered = true; @@ -3660,6 +3726,7 @@ function bindEvents() { // Lock Features const btnLock = document.getElementById('btn-lock'); + const btnMasterLock = document.getElementById('btn-master-lock'); const lockOverlay = document.getElementById('lock-modal-overlay'); function openLockModal(targetPath, isLocked) { @@ -3689,8 +3756,7 @@ function bindEvents() { if (btnLock && lockOverlay) { btnLock.addEventListener('click', () => { if (!state.activeScript) return; - - // Check if it's already locked from state + let isLocked = false; for (let cat in state.scripts) { let sc = state.scripts[cat].find(s => s.relative_path === state.activeScript); diff --git a/ui/index.html b/ui/index.html index 66e5a01..c7e4156 100644 --- a/ui/index.html +++ b/ui/index.html @@ -46,6 +46,9 @@

DevShell

+
- + + \ No newline at end of file