feat: team-specific subdomains + portal tim
- create_team now takes label -> slug -> <slug>.gemastik.imrnes.team - ensure_team_domains() writes Traefik dynamic config (gemastik-teams.yaml) - team portal at /team/<idx> (public, shows chall ports, SSH, submit form) - /api/team/<idx>/info + /api/team/<idx>/status (server-side receiver auth) - UI: name inputs per team (set count -> labels), domain link in card - delete team endpoint drops its domain
This commit is contained in:
+58
-1
@@ -95,6 +95,60 @@ async def submit_page(req: Request):
|
||||
"""Public flag submission page for teams (no login)."""
|
||||
return HTMLResponse((BASE_DIR / "static" / "submit.html").read_text())
|
||||
|
||||
|
||||
# ============ Per-team portal (public via <slug>.gemastik.imrnes.team) ============
|
||||
|
||||
@app.get("/team/{idx}", response_class=HTMLResponse)
|
||||
async def team_portal(idx: int, req: Request):
|
||||
"""Public portal page for a team (served at <slug>.gemastik.imrnes.team/team/<idx>)."""
|
||||
html = (BASE_DIR / "static" / "team.html").read_text()
|
||||
html = html.replace('name="team-id" content="0"', f'name="team-id" content="{idx}"')
|
||||
return HTMLResponse(html)
|
||||
|
||||
|
||||
@app.get("/api/team/{idx}/info")
|
||||
async def api_team_info(idx: int):
|
||||
"""Public: basic team info for the portal (no secrets besides per-team SSH creds)."""
|
||||
td = orch.TEAMS_DIR / f"team{idx}"
|
||||
if not (td / "state.json").exists():
|
||||
raise HTTPException(404, "Team not found")
|
||||
st = json.loads((td / "state.json").read_text())
|
||||
# expose only what a team needs: label, domain, ports, ssh creds, status
|
||||
return {"team": {
|
||||
"index": st.get("index"),
|
||||
"label": st.get("label"),
|
||||
"slug": st.get("slug"),
|
||||
"domain": st.get("domain"),
|
||||
"status": st.get("status"),
|
||||
"ports": st.get("ports"),
|
||||
"ssh_user": st.get("ssh_user"),
|
||||
"ssh_pass": st.get("ssh_pass"),
|
||||
}}
|
||||
|
||||
|
||||
@app.get("/api/team/{idx}/status")
|
||||
async def api_team_status(idx: int):
|
||||
"""Public: SLA status for a team's challenges (no auth — read-only health)."""
|
||||
td = orch.TEAMS_DIR / f"team{idx}"
|
||||
if not (td / "state.json").exists():
|
||||
raise HTTPException(404, "Team not found")
|
||||
st = json.loads ((td / "state.json").read_text())
|
||||
recv_port = st["ports"]["receiver"]
|
||||
# use team's receiver admin creds (server-side only; never sent to browser)
|
||||
au, ap = st.get("admin_user", ""), st.get("admin_pass", "")
|
||||
results = []
|
||||
for name, coff, soff in orch.CHALLENGES:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=8) as client:
|
||||
resp = await client.get(f"http://127.0.0.1:{recv_port}/check/{name}",
|
||||
auth=(au, ap))
|
||||
ok = bool(resp.json().get("success")) if resp.status_code == 200 else False
|
||||
except Exception:
|
||||
ok = False
|
||||
results.append({"name": name, "port": st["ports"][name]["chall"],
|
||||
"ssh": st["ports"][name]["ssh"], "alive": ok})
|
||||
return {"results": results}
|
||||
|
||||
@app.post("/api/login")
|
||||
async def api_login(req: Request):
|
||||
data = await req.json()
|
||||
@@ -214,12 +268,15 @@ async def api_teams_set(req: Request):
|
||||
n = int(data.get("count", 0))
|
||||
if n < 0 or n > 50:
|
||||
raise HTTPException(400, "Team count must be 0-50")
|
||||
labels = data.get("labels") or {} # { "1": "Tim Satu", ... }
|
||||
created = []
|
||||
for i in range(1, n + 1):
|
||||
td = orch.TEAMS_DIR / f"team{i}"
|
||||
if not td.exists():
|
||||
st = orch.create_team(i, data.get("label_prefix", "Tim"))
|
||||
label = labels.get(str(i)) or labels.get(i) or f"Tim {i}"
|
||||
st = orch.create_team(i, label)
|
||||
created.append(st["index"])
|
||||
orch.ensure_team_domains()
|
||||
return {"created": created, "total": len(orch.list_teams())}
|
||||
|
||||
@app.post("/api/teams/start")
|
||||
|
||||
Reference in New Issue
Block a user