security: public view with masked IPs, unlock button for full view

This commit is contained in:
mango
2026-02-22 22:22:57 +08:00
parent 670695066d
commit 4519d4042d
2 changed files with 16 additions and 16 deletions

View File

@@ -92,15 +92,15 @@ td{padding:8px 10px;border-bottom:1px solid rgba(26,39,64,.5)}
</style> </style>
</head> </head>
<body> <body>
<div class="login-mask" id="loginMask"> <div class="login-mask" id="loginMask" style="display:none">
<div class="login-box"><h2>🐾 Mission Control</h2><p>输入访问令牌</p> <div class="login-box"><h2>🔓 解锁完整视图</h2><p>输入令牌查看 IP 等敏感信息</p>
<input id="tokenInput" type="password" placeholder="Token" onkeydown="if(event.key==='Enter')doLogin()"> <input id="tokenInput" type="password" placeholder="Token" onkeydown="if(event.key==='Enter')doLogin()">
<button onclick="doLogin()">登录</button> <button onclick="doLogin()">解锁</button>
<div class="login-err" id="loginErr">令牌无效</div></div></div> <div class="login-err" id="loginErr">令牌无效</div></div></div>
<div class="wrap" id="mainWrap" style="display:none"> <div class="wrap" id="mainWrap">
<div class="hdr"> <div class="hdr">
<div class="hdr-row"><h1>🐾 OpenClaw Mission Control</h1> <div class="hdr-row"><h1>🐾 OpenClaw Mission Control</h1>
<div class="hdr-links"><a href="/admin.html">⚙️ Admin</a><span id="themeBtn" onclick="toggleTheme()">🌙</span></div></div> <div class="hdr-links"><a href="/admin.html">⚙️ Admin</a><span id="lockBtn" onclick="$('#loginMask').style.display='flex'" style="cursor:pointer">🔒</span><span id="themeBtn" onclick="toggleTheme()">🌙</span></div></div>
<div class="sub" id="subtitle">Loading...</div> <div class="sub" id="subtitle">Loading...</div>
</div> </div>
<div class="stats" id="stats"></div> <div class="stats" id="stats"></div>
@@ -126,11 +126,11 @@ const authHdr=()=>({headers:{'Authorization':'Bearer '+TOKEN}});
function doLogin(){ function doLogin(){
const t=$('#tokenInput').value.trim();if(!t)return; const t=$('#tokenInput').value.trim();if(!t)return;
fetch('/api/dashboard',{headers:{'Authorization':'Bearer '+t}}).then(r=>{ fetch('/api/dashboard',{headers:{'Authorization':'Bearer '+t}}).then(r=>r.json()).then(d=>{
if(r.status===401){$('#loginErr').style.display='block';return} if(!d.authed){$('#loginErr').style.display='block';return}
TOKEN=t;localStorage.setItem('oc-token',t); TOKEN=t;localStorage.setItem('oc-token',t);
$('#loginMask').style.display='none';$('#mainWrap').style.display=''; $('#loginMask').style.display='none';$('#lockBtn').textContent='🔓';
r.json().then(d=>{DATA=d;render();});connectWS();setInterval(load,60000); DATA=d;render();
}); });
} }
@@ -314,6 +314,7 @@ function toggleTheme(){
localStorage.setItem('theme',light?'dark':'light'); localStorage.setItem('theme',light?'dark':'light');
} }
(function(){const t=localStorage.getItem('theme');if(t==='light'){document.documentElement.setAttribute('data-theme','light');document.getElementById('themeBtn').textContent='☀️';}})(); (function(){const t=localStorage.getItem('theme');if(t==='light'){document.documentElement.setAttribute('data-theme','light');document.getElementById('themeBtn').textContent='☀️';}})();
if(TOKEN){fetch('/api/dashboard',authHdr()).then(r=>{if(r.status===401){$('#loginMask').style.display='';return}$('#loginMask').style.display='none';$('#mainWrap').style.display='';r.json().then(d=>{DATA=d;render();});connectWS();setInterval(load,60000);}).catch(()=>{$('#loginMask').style.display='';});} if(TOKEN){$('#lockBtn').textContent='🔓';}
load();connectWS();setInterval(load,60000);
</script> </script>
</body></html> </body></html>

View File

@@ -119,20 +119,19 @@ const server = http.createServer((req, res) => {
// API routes // API routes
(async () => { (async () => {
try { try {
// GET /api/dashboard - requires auth // GET /api/dashboard - public (masked) or authed (full)
if (url.pathname === '/api/dashboard' && method === 'GET') { if (url.pathname === '/api/dashboard' && method === 'GET') {
if (!auth()) return; const authed = (req.headers.authorization||'').replace('Bearer ','') === AUTH_TOKEN;
const now = Math.floor(Date.now()/1000); const now = Math.floor(Date.now()/1000);
const today = now - (now % 86400); const today = now - (now % 86400);
const nodes = getNodes.all(); const nodes = getNodes.all().map(n => authed ? n : {...n, host: '***'});
const stats = getStats.get(today); const stats = getStats.get(today);
const reqs = getReqs.all(500); const reqs = getReqs.all(500);
return json(200, { nodes, stats, requests: reqs, token: undefined }); return json(200, { nodes, stats, requests: reqs, authed });
} }
// GET /api/requests - requires auth // GET /api/requests - public (masked) or authed (full)
if (url.pathname === '/api/requests' && method === 'GET') { if (url.pathname === '/api/requests' && method === 'GET') {
if (!auth()) return;
const page = parseInt(url.searchParams.get('page') || '1'); const page = parseInt(url.searchParams.get('page') || '1');
const size = Math.min(parseInt(url.searchParams.get('size') || '50'), 200); const size = Math.min(parseInt(url.searchParams.get('size') || '50'), 200);
const offset = (page - 1) * size; const offset = (page - 1) * size;