Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ───────────────────────────────────────────────────────


Expand All @@ -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())
Expand Down
70 changes: 68 additions & 2 deletions ui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────────────────────
Expand All @@ -39,6 +40,7 @@ let state = {
cmdHistoryIndex: -1,
historyQuery: '',
historyFilter: 'all',
masterPassword: null,
historyEntries: [],
historySummary: {
total: 0,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
22 changes: 21 additions & 1 deletion ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ <h1>Dev<span class="accent">Shell</span></h1>
</div>
<div class="header-right">
<div class="header-right">
<button class="btn-icon" id="btn-master-lock" title="Global Master Lock" aria-label="Toggle Global Master Lock" style="margin-right: 4px; display: flex; align-items: center; justify-content: center;">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</button>
<button class="btn-icon" id="theme-toggle-btn" title="Toggle Light/Dark Mode" aria-label="Toggle UI Theme" style="margin-right: 4px; display: flex; align-items: center; justify-content: center;">
<svg id="theme-icon-moon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
<svg id="theme-icon-sun" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: none;"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m18.36 18.36 1.41 1.41"/><path d="M20 12h2"/><path d="M2 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
Expand Down Expand Up @@ -1057,7 +1060,24 @@ <h2>Workspace Profiles</h2>
</div>
</div>
</div>

<!-- Master Auth Modal -->
<div id="master-auth-modal" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h2>Master Authentication</h2>
</div>
<div class="modal-body">
<p>The application is locked. Please enter the master password to continue.</p>
<div class="form-group" style="margin-top: 15px;">
<label>Master Password</label>
<input type="password" id="master-auth-password" class="w-full">
</div>
<div style="margin-top: 20px; display: flex; justify-content: flex-end;">
<button id="master-auth-submit" class="btn">Unlock</button>
</div>
</div>
</div>
</div>
</body>

</html>
Loading