feat: target matrix + reset scores/environment buttons
- /api/targets (admin): matrix of all teams' domain:port targets - /api/reset/scores: wipe leaderboard (admin, confirm dialog) - /api/reset/environment: stop all teams, remove containers+receivers+ team dirs+systemd units, wipe scores, drop domains (admin, confirm) - portal targets now only domain+port (no ssh/labels) - UI: tab Target Matrix, header buttons Reset Skor / Reset Environment with confirm() alerts 'apakah anda yakin ingin mereset...'
This commit is contained in:
+38
-6
@@ -202,7 +202,7 @@ async def api_team_session(idx: int, req: Request):
|
||||
|
||||
@app.get("/api/team/{idx}/targets")
|
||||
async def api_team_targets(idx: int, req: Request):
|
||||
"""Team targets — requires team login + own host."""
|
||||
"""Team targets — requires team login + own host. Only domain + port."""
|
||||
td = orch.TEAMS_DIR / f"team{idx}"
|
||||
if not (td / "state.json").exists():
|
||||
raise HTTPException(404, "Team not found")
|
||||
@@ -223,12 +223,8 @@ async def api_team_targets(idx: int, req: Request):
|
||||
if not p:
|
||||
continue
|
||||
out.append({
|
||||
"team": st.get("label", f"Team {st.get('index')}"),
|
||||
"team_idx": st.get("index"),
|
||||
"domain": st.get("domain"),
|
||||
"challenge": name,
|
||||
"domain": st.get("domain") or (st.get("slug", f"team{st.get('index')}") + ".gemastik.imrnes.team"),
|
||||
"port": p["chall"],
|
||||
"ssh": p["ssh"],
|
||||
})
|
||||
return {"targets": out}
|
||||
|
||||
@@ -510,6 +506,19 @@ async def api_randomize(idx: int, req: Request):
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.post("/api/reset/scores")
|
||||
async def api_reset_scores(req: Request):
|
||||
"""Admin: wipe leaderboard (all solves)."""
|
||||
require_login(req)
|
||||
return orch.reset_scores()
|
||||
|
||||
@app.post("/api/reset/environment")
|
||||
async def api_reset_environment(req: Request):
|
||||
"""Admin: full environment reset (stop all, remove teams/containers/receivers)."""
|
||||
require_login(req)
|
||||
return orch.reset_environment()
|
||||
|
||||
|
||||
@app.post("/api/flag/submit")
|
||||
async def api_flag_submit(req: Request):
|
||||
"""Public endpoint: teams submit flags. No login needed."""
|
||||
@@ -549,6 +558,29 @@ async def api_public_teams():
|
||||
"""Public list of team names (for the submit dropdown)."""
|
||||
return {"teams": [{"index": t["index"], "label": t.get("label", f"Team {t['index']}")} for t in orch.list_teams()]}
|
||||
|
||||
@app.get("/api/targets")
|
||||
async def api_admin_targets(req: Request):
|
||||
"""Admin: matrix of every team's service domain:port (public targets)."""
|
||||
require_login(req)
|
||||
out = []
|
||||
for d in sorted(orch.TEAMS_DIR.glob("team*")):
|
||||
if not (d / "state.json").exists():
|
||||
continue
|
||||
st = json.loads((d / "state.json").read_text())
|
||||
dom = st.get("domain") or (st.get("slug", f"team{st.get('index')}") + ".gemastik.imrnes.team")
|
||||
for name, coff, soff in orch.CHALLENGES:
|
||||
p = st["ports"].get(name)
|
||||
if not p:
|
||||
continue
|
||||
out.append({
|
||||
"team": st.get("index"),
|
||||
"team_label": st.get("label", f"Team {st.get('index')}"),
|
||||
"challenge": name,
|
||||
"domain": dom,
|
||||
"port": p["chall"],
|
||||
})
|
||||
return {"targets": out}
|
||||
|
||||
|
||||
# ============ WebSocket SSH terminal (team portal) ============
|
||||
import paramiko
|
||||
|
||||
@@ -115,6 +115,8 @@
|
||||
<div class="actions">
|
||||
<span class="pill" id="clock">--:--:--</span>
|
||||
<button onclick="refresh()">🔄 Refresh</button>
|
||||
<button class="danger" onclick="resetScores()" style="background:#dc2626;border:none;border-radius:8px;padding:8px 14px;color:#fff;font-weight:600;cursor:pointer">🧹 Reset Skor</button>
|
||||
<button class="danger" onclick="resetEnv()" style="background:#7f1d1d;border:none;border-radius:8px;padding:8px 14px;color:#fff;font-weight:600;cursor:pointer">💥 Reset Environment</button>
|
||||
<button onclick="logout()">Keluar</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -125,6 +127,7 @@
|
||||
<button class="tab" data-view="teams" onclick="showView('teams'); loadTeams()">👥 Teams</button>
|
||||
<button class="tab" data-view="logs" onclick="showView('logs'); loadLogs()">📜 Logs</button>
|
||||
<button class="tab" data-view="creds" onclick="showView('creds'); loadCreds()">🔑 Creds</button>
|
||||
<button class="tab" data-view="targets" onclick="showView('targets'); loadTargetMatrix()">🎯 Target Matrix</button>
|
||||
</div>
|
||||
|
||||
<!-- Challenges view -->
|
||||
@@ -190,6 +193,17 @@
|
||||
<div class="grid" id="credsGrid"></div>
|
||||
</div>
|
||||
|
||||
<!-- Target Matrix view -->
|
||||
<div class="view" id="view-targets">
|
||||
<div class="card">
|
||||
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||
<b style="color:#5ad1ff">🎯 Target Matrix</b>
|
||||
<span style="font-size:11px;color:#718096">Domain:port semua service semua tim — buat dibagikan ke peserta sebagai daftar target</span>
|
||||
</div>
|
||||
<div id="targetMatrix" style="margin-top:14px;font-family:ui-monospace,monospace;font-size:12px;line-height:1.9;color:#dbe6f4;background:#0a101f;border:1px solid #1e3a5f;border-radius:10px;padding:14px;overflow-x:auto">Memuat…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-note">Receiver: https://gemastik.imrnes.team · Panel: https://panel.gemastik.imrnes.team · Auto-refresh tiap 15 detik</div>
|
||||
|
||||
<!-- modal flag -->
|
||||
@@ -519,6 +533,28 @@ async function stopAllTeams() {
|
||||
finally { hideLoading(); }
|
||||
}
|
||||
|
||||
async function resetScores() {
|
||||
if (!confirm('⚠️ Apakah anda yakin ingin mereset skor?\n\nSemua poin & solve di leaderboard akan DIHAPUS.\nTindakan ini tidak bisa dibatalkan.')) return;
|
||||
showLoading('Menghapus semua skor…');
|
||||
try {
|
||||
const d = await api('/api/reset/scores', {method:'POST', headers:{'Content-Type':'application/json'}, body: '{}'});
|
||||
toast('Skor direset: semua solve dihapus', false);
|
||||
} catch (e) { toast(e.message, true); }
|
||||
finally { hideLoading(); }
|
||||
}
|
||||
|
||||
async function resetEnv() {
|
||||
if (!confirm('⚠️ Apakah anda yakin ingin mereset environment?\n\nSEMUA team akan DISTOP:\n• Container challenge dihapus\n• Receiver team dihapus\n• Folder team dihapus (perlu create ulang)\n• Skor leaderboard direset\n\nTindakan ini TIDAK BISA dibatalkan.')) return;
|
||||
showLoading('Reset environment…\nMenghentikan semua team + menghapus container/receiver (beberapa menit)');
|
||||
try {
|
||||
const d = await api('/api/reset/environment', {method:'POST', headers:{'Content-Type':'application/json'}, body: '{}'});
|
||||
const n = (d.stopped || []).length;
|
||||
toast(`Environment direset: ${n} team dihentikan & dihapus`, false);
|
||||
setTimeout(() => location.reload(), 1200);
|
||||
} catch (e) { toast(e.message, true); }
|
||||
finally { hideLoading(); }
|
||||
}
|
||||
|
||||
async function randomizeTeam(idx) {
|
||||
if (!confirm(`Randomize SEMUA flag untuk Team ${idx}? Container akan di-recreate.`)) return;
|
||||
try {
|
||||
@@ -619,6 +655,27 @@ async function loadCreds() {
|
||||
} catch (e) { toast(e.message, true); }
|
||||
}
|
||||
|
||||
// ---------- Target Matrix ----------
|
||||
async function loadTargetMatrix() {
|
||||
const box = document.getElementById('targetMatrix');
|
||||
try {
|
||||
const d = await api('/api/targets');
|
||||
const targets = d.targets || [];
|
||||
if (!targets.length) { box.textContent = 'Belum ada team.'; return; }
|
||||
// group by team
|
||||
const rows = [];
|
||||
let curTeam = null;
|
||||
for (const t of targets) {
|
||||
if (t.team !== curTeam) {
|
||||
rows.push(`\n[${esc(t.team_label || ('Team ' + t.team))}]`);
|
||||
curTeam = t.team;
|
||||
}
|
||||
rows.push(` ${esc(t.domain)}:${t.port} (${esc(t.challenge)})`);
|
||||
}
|
||||
box.textContent = rows.join('\n');
|
||||
} catch (e) { box.textContent = 'Gagal: ' + e.message; }
|
||||
}
|
||||
|
||||
// ---------- misc ----------
|
||||
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
|
||||
async function logout() {
|
||||
|
||||
@@ -264,10 +264,7 @@ async function loadTargets() {
|
||||
if (!targets.length) { box.textContent = 'Belum ada tim musuh.'; return; }
|
||||
let html = '=== TARGET MUSUH ===\n\n';
|
||||
for (const t of targets) {
|
||||
html += `[${esc(t.team)}] — ${esc(t.challenge)}\n`;
|
||||
if (t.domain) html += ` domain : https://${esc(t.domain)}\n`;
|
||||
html += ` service: ${esc(window.location.hostname)}:${t.port}\n`;
|
||||
html += ` ssh : ${esc(window.location.hostname)}:${t.ssh}\n\n`;
|
||||
html += `${esc(t.domain)}:${t.port}\n`;
|
||||
}
|
||||
box.textContent = html;
|
||||
}
|
||||
|
||||
@@ -402,6 +402,50 @@ def submit_flag(team_idx: int, chall: str, flag: str, team_name: str = "") -> di
|
||||
lb_path.write_text(json.dumps(lb, indent=2))
|
||||
return {"success": True, "team": team_idx, "challenge": chall}
|
||||
|
||||
def reset_scores() -> dict:
|
||||
"""Wipe the leaderboard (all solves removed)."""
|
||||
lb_path = TEAMS_DIR / "leaderboard.json"
|
||||
lb_path.write_text(json.dumps({"solves": []}, indent=2))
|
||||
return {"ok": True, "cleared": True}
|
||||
|
||||
def reset_environment() -> dict:
|
||||
"""Full env reset: stop all teams, remove containers + receiver services,
|
||||
delete team dirs (fresh for re-create). Keeps panel + images."""
|
||||
results = []
|
||||
for d in sorted(TEAMS_DIR.glob("team*")):
|
||||
sf = d / "state.json"
|
||||
if not sf.exists():
|
||||
continue
|
||||
st = json.loads(sf.read_text())
|
||||
idx = st["index"]
|
||||
try:
|
||||
stop_team(idx) # compose down + stop receiver service + set status
|
||||
except Exception as e:
|
||||
results.append({"team": idx, "ok": False, "err": str(e)})
|
||||
continue
|
||||
# remove the per-team systemd receiver unit
|
||||
subprocess.run(["systemctl", "disable", f"gemastik-receiver-team{idx}.service"],
|
||||
check=False, capture_output=True)
|
||||
subprocess.run(["systemctl", "stop", f"gemastik-receiver-team{idx}.service"],
|
||||
check=False, capture_output=True)
|
||||
unit = Path("/etc/systemd/system") / f"gemastik-receiver-team{idx}.service"
|
||||
if unit.exists():
|
||||
unit.unlink()
|
||||
results.append({"team": idx, "ok": True})
|
||||
subprocess.run(["systemctl", "daemon-reload"], check=False)
|
||||
# remove team dirs entirely (fresh for re-create)
|
||||
for d in sorted(TEAMS_DIR.glob("team*")):
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
# wipe leaderboard too
|
||||
reset_scores()
|
||||
# drop team domains from Traefik (regenerate — no teams left)
|
||||
try:
|
||||
ensure_team_domains()
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": True, "stopped": results, "teams_dir": str(TEAMS_DIR)}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
cmd = sys.argv[1] if len(sys.argv) > 1 else "list"
|
||||
|
||||
Reference in New Issue
Block a user