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:
@@ -61,14 +61,23 @@ def team_ports(idx: int) -> dict:
|
||||
def gen_password(n=16):
|
||||
return uuid.uuid4().hex[:n]
|
||||
|
||||
def slugify(s: str) -> str:
|
||||
"""'Tim Satu Beta!' -> 'tim-satu-beta'"""
|
||||
s = re.sub(r"[^a-zA-Z0-9\s-]", "", s.lower()).strip()
|
||||
s = re.sub(r"[\s_]+", "-", s)
|
||||
return s or "team"
|
||||
|
||||
def create_team(idx: int, label: str = None):
|
||||
"""Build a full team stack dir with unique ports/passwords."""
|
||||
ports = team_ports(idx)
|
||||
team_dir = TEAMS_DIR / f"team{idx}"
|
||||
label = label or f"Tim {idx}"
|
||||
slug = slugify(label)
|
||||
state = {
|
||||
"index": idx,
|
||||
"label": label,
|
||||
"slug": slug,
|
||||
"domain": f"{slug}.gemastik.imrnes.team",
|
||||
"ports": ports,
|
||||
"admin_user": f"admin_team{idx}",
|
||||
"admin_pass": gen_password(20),
|
||||
@@ -267,6 +276,57 @@ def team_logs(idx: int, service: str = None, tail: int = 100):
|
||||
data[s] = f"ERR: {e}"
|
||||
return data
|
||||
|
||||
def ensure_team_domains() -> str:
|
||||
"""Write Traefik dynamic config for each team domain -> panel (:18081).
|
||||
|
||||
Each team gets <slug>.gemastik.imrnes.team. The panel routes
|
||||
/team/<idx> to that team's portal page (public, no panitia login),
|
||||
and /api/team/<idx>/* serves team-scoped API. Writing this into the
|
||||
same dynamic dir Traefik watches means domains appear automatically.
|
||||
Returns a status string for logging.
|
||||
"""
|
||||
traefik_dir = Path("/data/coolify/proxy/dynamic")
|
||||
traefik_dir.mkdir(parents=True, exist_ok=True)
|
||||
teams = list_teams()
|
||||
out = []
|
||||
changed = False
|
||||
for t in teams:
|
||||
slug = t.get("slug") or slugify(t.get("label", ""))
|
||||
if not slug:
|
||||
continue
|
||||
# backfill slug/domain for teams created before this feature
|
||||
if not t.get("slug"):
|
||||
td = TEAMS_DIR / f"team{t['index']}"
|
||||
st = json.loads((td / "state.json").read_text())
|
||||
st["slug"] = slug
|
||||
st["domain"] = f"{slug}.gemastik.imrnes.team"
|
||||
(td / "state.json").write_text(json.dumps(st, indent=2))
|
||||
changed = True
|
||||
host = f"{slug}.gemastik.imrnes.team"
|
||||
out.append(f""" team-{slug}-http:
|
||||
rule: Host(`{host}`)
|
||||
entryPoints:
|
||||
- http
|
||||
service: gemastik-panel-service
|
||||
middlewares:
|
||||
- redirect-to-https
|
||||
team-{slug}-https:
|
||||
rule: Host(`{host}`)
|
||||
entryPoints:
|
||||
- https
|
||||
service: gemastik-panel-service
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
domains:
|
||||
- main: {host}
|
||||
""")
|
||||
if not out:
|
||||
return "no teams to route"
|
||||
# Keep the team domain block in its own file so the main gemastik.yaml stays untouched
|
||||
(traefik_dir / "gemastik-teams.yaml").write_text("http:\n routers:\n" + "".join(out))
|
||||
return f"wrote {len(out)} team domain(s) in gemastik-teams.yaml"
|
||||
|
||||
|
||||
def list_teams() -> list:
|
||||
out = []
|
||||
if not TEAMS_DIR.exists():
|
||||
|
||||
Reference in New Issue
Block a user