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:
root
2026-09-23 17:14:42 +08:00
parent b66530e7fd
commit 44dc1ac852
4 changed files with 140 additions and 10 deletions
+38 -6
View File
@@ -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