fix: guide link IDOR, full team-api IDOR hardening, loading overlay

- guide link now server-side replaced to /team/<idx>/guide (no /team/0 403)
- _check_team_host() applied to ALL team endpoints (login, info, targets,
  status, guide, portal, ssh-ws): host must match team domain; panel/gemastik
  host only with admin session. Cross-domain session reuse -> 403.
- host check BEFORE auth on info/targets (no team-existence oracle)
- loading overlay (spinner + text) on start/stop all-team/set; JS util
  showLoading/hideLoading
This commit is contained in:
root
2026-09-23 17:02:52 +08:00
parent 01ffc05a11
commit 72382c43d0
4 changed files with 93 additions and 19 deletions
+2 -2
View File
@@ -96,13 +96,13 @@
<div class="card">
<h2>📞 Butuh Bantuan?</h2>
<p>Hubungi panitia untuk: reset password SSH, restart container, atau lapor service down. Portal ini untuk peserta — panel admin terpisah.</p>
<p style="margin-top:8px"><a href="/team/0" id="portalLink">← Kembali ke portal tim</a></p>
<p style="margin-top:8px"><a href="/team/0" data-portal-link>← Kembali ke portal tim</a></p>
</div>
</div>
<script>
const TEAM_ID = parseInt(document.querySelector('meta[name="team-id"]').content || '0', 10);
document.getElementById('portalLink').href = `/team/${TEAM_ID}`;
document.querySelectorAll('[data-portal-link]').forEach(a => a.href = `/team/${TEAM_ID}`);
</script>
</body>
</html>
+36 -1
View File
@@ -83,6 +83,15 @@
}
.toast.show { opacity:1; transform:translateY(0); }
.toast.err { border-color:#f87171; color:#fca5a5; }
/* loading overlay */
#loadingOverlay {
display:none; position:fixed; inset:0; background:rgba(4,8,14,0.82); z-index:2000;
align-items:center; justify-content:center; flex-direction:column; gap:18px;
}
#loadingOverlay.show { display:flex; }
.loader { width:44px; height:44px; border:4px solid #2a3444; border-top-color:#38bdf8; border-radius:50%; animation:spin 0.9s linear infinite; }
@keyframes spin { to { transform:rotate(360deg); } }
#loadingText { color:#dbe6f4; font-size:14px; text-align:center; padding:0 24px; line-height:1.6; }
.history-box {
background:#0a101f; border:1px solid #1e3a5f; border-radius:10px; padding:14px;
font-size:11px; color:#7dd3fc; max-height:300px; overflow-y:auto; white-space:pre-wrap;
@@ -219,6 +228,12 @@
<div class="toast" id="toast"></div>
<!-- loading overlay -->
<div id="loadingOverlay">
<div class="loader"></div>
<div id="loadingText">Memproses…</div>
</div>
<script>
let CURRENT = null;
let REFRESH_MS = 15000;
@@ -237,6 +252,16 @@ function toast(msg, err=false) {
setTimeout(() => t.className = 'toast', 3000);
}
function showLoading(html) {
const ov = document.getElementById('loadingOverlay');
document.getElementById('loadingText').innerHTML = html;
ov.classList.add('show');
return ov;
}
function hideLoading() {
document.getElementById('loadingOverlay').classList.remove('show');
}
function esc(s) {
return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
@@ -448,10 +473,12 @@ async function setTeams() {
if (inp && inp.value.trim()) labels[i] = inp.value.trim();
}
try {
showLoading(`Membuat ${n} team…<br>Generate compose, flags, domain, Traefik config`);
const d = await api('/api/teams/set', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({count: n, labels})});
toast(`Team dibuat: ${d.created.join(', ') || 'tidak ada yang baru'} · total ${d.total}`, false);
loadTeams();
} catch (e) { toast(e.message, true); }
finally { hideLoading(); }
}
function teamCountChanged() {
@@ -468,20 +495,28 @@ function teamCountChanged() {
}
async function startTeam(idx) {
try { await api('/api/teams/start', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({index: idx})}); toast(`Team ${idx} start (build bisa makan waktu)`, false); setTimeout(loadTeams, 3000); }
const ov = showLoading(`Menyiapkan Team ${idx}…<br>Membangun & start container (bisa butuh 1-3 menit)`);
try { await api('/api/teams/start', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({index: idx})}); toast(`Team ${idx} start`, false); }
catch (e) { toast(e.message, true); }
finally { hideLoading(); setTimeout(loadTeams, 2500); }
}
async function stopTeam(idx) {
const ov = showLoading(`Menghentikan Team ${idx}…`);
try { await api('/api/teams/stop', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({index: idx})}); toast(`Team ${idx} stop`, false); loadTeams(); }
catch (e) { toast(e.message, true); }
finally { hideLoading(); }
}
async function startAllTeams() {
const ov = showLoading('Start SEMUA team…<br>Ini bisa butuh beberapa menit (build + boot + set password)');
try { const d = await api('/api/teams/start', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({})}); toast(`Start semua: ${d.results.filter(r=>r.ok).length}/${d.results.length} ok`, false); setTimeout(loadTeams, 4000); }
catch (e) { toast(e.message, true); }
finally { hideLoading(); }
}
async function stopAllTeams() {
const ov = showLoading('Menghentikan SEMUA team…');
try { const d = await api('/api/teams/stop', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({})}); toast(`Stop semua: ${d.results.filter(r=>r.ok).length}/${d.results.length} ok`, false); loadTeams(); }
catch (e) { toast(e.message, true); }
finally { hideLoading(); }
}
async function randomizeTeam(idx) {
+4 -1
View File
@@ -83,7 +83,7 @@
<input type="password" id="loginPass" placeholder="password SSH tim" autocomplete="off">
<button onclick="doLogin()" style="width:100%;margin-top:16px">Masuk Portal</button>
<div class="login-err" id="loginErr"></div>
<div class="login-note">Belum punya password? Hubungi panitia.<br>Tutorial SSH: <a href="/team/0/guide" id="guideLink">baca panduan</a></div>
<div class="login-note">Belum punya password? Hubungi panitia.<br>Tutorial SSH: <a href="/team/0/guide" data-guide-link>baca panduan</a></div>
</div>
</div>
@@ -193,6 +193,9 @@
const TEAM_ID = parseInt(document.querySelector('meta[name="team-id"]').content || '0', 10);
let term = null, ws = null;
// fix all guide links immediately (no auth needed) — /team/0 would 403
document.querySelectorAll('[data-guide-link]').forEach(a => a.href = `/team/${TEAM_ID}/guide`);
async function api(url, opts) { const r = await fetch(url, opts); return r.json(); }
function esc(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
function showView(v) {