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")
|
@app.get("/api/team/{idx}/targets")
|
||||||
async def api_team_targets(idx: int, req: Request):
|
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}"
|
td = orch.TEAMS_DIR / f"team{idx}"
|
||||||
if not (td / "state.json").exists():
|
if not (td / "state.json").exists():
|
||||||
raise HTTPException(404, "Team not found")
|
raise HTTPException(404, "Team not found")
|
||||||
@@ -223,12 +223,8 @@ async def api_team_targets(idx: int, req: Request):
|
|||||||
if not p:
|
if not p:
|
||||||
continue
|
continue
|
||||||
out.append({
|
out.append({
|
||||||
"team": st.get("label", f"Team {st.get('index')}"),
|
"domain": st.get("domain") or (st.get("slug", f"team{st.get('index')}") + ".gemastik.imrnes.team"),
|
||||||
"team_idx": st.get("index"),
|
|
||||||
"domain": st.get("domain"),
|
|
||||||
"challenge": name,
|
|
||||||
"port": p["chall"],
|
"port": p["chall"],
|
||||||
"ssh": p["ssh"],
|
|
||||||
})
|
})
|
||||||
return {"targets": out}
|
return {"targets": out}
|
||||||
|
|
||||||
@@ -510,6 +506,19 @@ async def api_randomize(idx: int, req: Request):
|
|||||||
raise HTTPException(500, str(e))
|
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")
|
@app.post("/api/flag/submit")
|
||||||
async def api_flag_submit(req: Request):
|
async def api_flag_submit(req: Request):
|
||||||
"""Public endpoint: teams submit flags. No login needed."""
|
"""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)."""
|
"""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()]}
|
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) ============
|
# ============ WebSocket SSH terminal (team portal) ============
|
||||||
import paramiko
|
import paramiko
|
||||||
|
|||||||
@@ -115,6 +115,8 @@
|
|||||||
<div class="actions">
|
<div class="actions">
|
||||||
<span class="pill" id="clock">--:--:--</span>
|
<span class="pill" id="clock">--:--:--</span>
|
||||||
<button onclick="refresh()">🔄 Refresh</button>
|
<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>
|
<button onclick="logout()">Keluar</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -125,6 +127,7 @@
|
|||||||
<button class="tab" data-view="teams" onclick="showView('teams'); loadTeams()">👥 Teams</button>
|
<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="logs" onclick="showView('logs'); loadLogs()">📜 Logs</button>
|
||||||
<button class="tab" data-view="creds" onclick="showView('creds'); loadCreds()">🔑 Creds</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>
|
</div>
|
||||||
|
|
||||||
<!-- Challenges view -->
|
<!-- Challenges view -->
|
||||||
@@ -190,6 +193,17 @@
|
|||||||
<div class="grid" id="credsGrid"></div>
|
<div class="grid" id="credsGrid"></div>
|
||||||
</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>
|
<div class="footer-note">Receiver: https://gemastik.imrnes.team · Panel: https://panel.gemastik.imrnes.team · Auto-refresh tiap 15 detik</div>
|
||||||
|
|
||||||
<!-- modal flag -->
|
<!-- modal flag -->
|
||||||
@@ -519,6 +533,28 @@ async function stopAllTeams() {
|
|||||||
finally { hideLoading(); }
|
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) {
|
async function randomizeTeam(idx) {
|
||||||
if (!confirm(`Randomize SEMUA flag untuk Team ${idx}? Container akan di-recreate.`)) return;
|
if (!confirm(`Randomize SEMUA flag untuk Team ${idx}? Container akan di-recreate.`)) return;
|
||||||
try {
|
try {
|
||||||
@@ -619,6 +655,27 @@ async function loadCreds() {
|
|||||||
} catch (e) { toast(e.message, true); }
|
} 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 ----------
|
// ---------- misc ----------
|
||||||
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
|
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
|
||||||
async function logout() {
|
async function logout() {
|
||||||
|
|||||||
@@ -264,10 +264,7 @@ async function loadTargets() {
|
|||||||
if (!targets.length) { box.textContent = 'Belum ada tim musuh.'; return; }
|
if (!targets.length) { box.textContent = 'Belum ada tim musuh.'; return; }
|
||||||
let html = '=== TARGET MUSUH ===\n\n';
|
let html = '=== TARGET MUSUH ===\n\n';
|
||||||
for (const t of targets) {
|
for (const t of targets) {
|
||||||
html += `[${esc(t.team)}] — ${esc(t.challenge)}\n`;
|
html += `${esc(t.domain)}:${t.port}\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`;
|
|
||||||
}
|
}
|
||||||
box.textContent = html;
|
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))
|
lb_path.write_text(json.dumps(lb, indent=2))
|
||||||
return {"success": True, "team": team_idx, "challenge": chall}
|
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__":
|
if __name__ == "__main__":
|
||||||
import sys
|
import sys
|
||||||
cmd = sys.argv[1] if len(sys.argv) > 1 else "list"
|
cmd = sys.argv[1] if len(sys.argv) > 1 else "list"
|
||||||
|
|||||||
Reference in New Issue
Block a user