fix: editTeam endpoint + leaderboard live name resolution + domains in team set + apt-insecure.conf in all service dirs
This commit is contained in:
+37
-4
@@ -391,23 +391,46 @@ async def api_teams(req: Request):
|
|||||||
|
|
||||||
@app.post("/api/teams/set")
|
@app.post("/api/teams/set")
|
||||||
async def api_teams_set(req: Request):
|
async def api_teams_set(req: Request):
|
||||||
"""Set/create N teams (idempotent: creates missing, keeps existing)."""
|
"""Set/create N teams (idempotent: creates missing, keeps existing).
|
||||||
|
|
||||||
|
body: {count, labels: {"1": "Tim Satu"}, domains: {"1": "tim-satu"}}"""
|
||||||
require_login(req)
|
require_login(req)
|
||||||
data = await req.json()
|
data = await req.json()
|
||||||
n = int(data.get("count", 0))
|
n = int(data.get("count", 0))
|
||||||
if n < 0 or n > 50:
|
if n < 0 or n > 50:
|
||||||
raise HTTPException(400, "Team count must be 0-50")
|
raise HTTPException(400, "Team count must be 0-50")
|
||||||
labels = data.get("labels") or {} # { "1": "Tim Satu", ... }
|
labels = data.get("labels") or {} # { "1": "Tim Satu", ... }
|
||||||
|
domains = data.get("domains") or {} # { "1": "mycustom", ... }
|
||||||
created = []
|
created = []
|
||||||
for i in range(1, n + 1):
|
for i in range(1, n + 1):
|
||||||
td = orch.TEAMS_DIR / f"team{i}"
|
td = orch.TEAMS_DIR / f"team{i}"
|
||||||
if not td.exists():
|
if not td.exists():
|
||||||
label = labels.get(str(i)) or labels.get(i) or f"Tim {i}"
|
label = labels.get(str(i)) or labels.get(i) or f"Tim {i}"
|
||||||
st = orch.create_team(i, label)
|
dom = domains.get(str(i)) or domains.get(i) or None
|
||||||
|
st = orch.create_team(i, label, domain=dom)
|
||||||
created.append(st["index"])
|
created.append(st["index"])
|
||||||
|
else:
|
||||||
|
# team exists: apply any label/domain overrides
|
||||||
|
label = labels.get(str(i)) or labels.get(i)
|
||||||
|
dom = domains.get(str(i)) or domains.get(i)
|
||||||
|
if label or dom:
|
||||||
|
orch.update_team(i, label=label, domain=dom)
|
||||||
orch.ensure_team_domains()
|
orch.ensure_team_domains()
|
||||||
return {"created": created, "total": len(orch.list_teams())}
|
return {"created": created, "total": len(orch.list_teams())}
|
||||||
|
|
||||||
|
@app.put("/api/teams/{idx}")
|
||||||
|
async def api_team_update(idx: int, req: Request):
|
||||||
|
"""Update a team's custom name and/or domain (applies live)."""
|
||||||
|
require_login(req)
|
||||||
|
if not (orch.TEAMS_DIR / f"team{idx}" / "state.json").exists():
|
||||||
|
raise HTTPException(404, "Team not found")
|
||||||
|
data = await req.json()
|
||||||
|
try:
|
||||||
|
st = orch.update_team(idx, label=data.get("label"), domain=data.get("domain"))
|
||||||
|
return {"ok": True, "team": st}
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
raise HTTPException(404, str(e))
|
||||||
|
|
||||||
@app.post("/api/teams/start")
|
@app.post("/api/teams/start")
|
||||||
async def api_teams_start(req: Request):
|
async def api_teams_start(req: Request):
|
||||||
require_login(req)
|
require_login(req)
|
||||||
@@ -553,16 +576,26 @@ async def api_attacks(req: Request):
|
|||||||
|
|
||||||
@app.get("/api/leaderboard")
|
@app.get("/api/leaderboard")
|
||||||
async def api_leaderboard(req: Request):
|
async def api_leaderboard(req: Request):
|
||||||
"""Leaderboard of solves so far."""
|
"""Leaderboard of solves so far. Team names resolved LIVE from state.json
|
||||||
|
so renaming a team updates leaderboard + topology everywhere."""
|
||||||
lb_path = orch.TEAMS_DIR / "leaderboard.json"
|
lb_path = orch.TEAMS_DIR / "leaderboard.json"
|
||||||
if lb_path.exists():
|
if lb_path.exists():
|
||||||
lb = json.loads(lb_path.read_text())
|
lb = json.loads(lb_path.read_text())
|
||||||
else:
|
else:
|
||||||
lb = {"solves": []}
|
lb = {"solves": []}
|
||||||
|
# live name lookup: state.json label fallback to snapshot
|
||||||
|
def team_name(idx):
|
||||||
|
try:
|
||||||
|
st = json.loads((orch.TEAMS_DIR / f"team{idx}" / "state.json").read_text())
|
||||||
|
return st.get("label") or f"Team {idx}"
|
||||||
|
except Exception:
|
||||||
|
return f"Team {idx}"
|
||||||
# aggregate per team
|
# aggregate per team
|
||||||
teams = {}
|
teams = {}
|
||||||
for e in lb["solves"]:
|
for e in lb["solves"]:
|
||||||
t = teams.setdefault(e["team"], {"team": e["team"], "name": e["team_name"], "solves": 0, "challs": []})
|
name = team_name(e["team"])
|
||||||
|
t = teams.setdefault(e["team"], {"team": e["team"], "name": name, "solves": 0, "challs": []})
|
||||||
|
t["name"] = name
|
||||||
t["solves"] += 1
|
t["solves"] += 1
|
||||||
t["challs"].append(e["challenge"])
|
t["challs"].append(e["challenge"])
|
||||||
return {"solves": lb["solves"], "teams": sorted(teams.values(), key=lambda x: -x["solves"])}
|
return {"solves": lb["solves"], "teams": sorted(teams.values(), key=lambda x: -x["solves"])}
|
||||||
|
|||||||
+113
-29
@@ -143,8 +143,14 @@
|
|||||||
|
|
||||||
<!-- Topology view -->
|
<!-- Topology view -->
|
||||||
<div class="view" id="view-topo">
|
<div class="view" id="view-topo">
|
||||||
|
<div class="topo-toolbar" style="display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap">
|
||||||
|
<button onclick="topoZoom(1.25)">🔍 + Zoom In</button>
|
||||||
|
<button onclick="topoZoom(0.8)">🔎 Zoom Out</button>
|
||||||
|
<button onclick="topoReset()">⟳ Reset</button>
|
||||||
|
<span id="topoZoomLabel" style="font-size:12px;color:var(--dim);min-width:40px">100%</span>
|
||||||
|
</div>
|
||||||
<div class="topo-wrap"><svg id="topoSvg" class="topo-svg" height="520"></svg></div>
|
<div class="topo-wrap"><svg id="topoSvg" class="topo-svg" height="520"></svg></div>
|
||||||
<div class="topo-hint">💡 Geser node untuk mengatur layout • ⚔️ garis merah = serangan (panah attacker → target) • garis <b>putus-putus</b> = serangan baru (60 detik terakhir)</div>
|
<div class="topo-hint">💡 <b>Geser node</b> untuk atur layout • <b>scroll / ctrl+scroll</b> untuk zoom in-out • <b>drag area kosong</b> untuk geser canvas • ⚔️ garis merah = serangan (panah attacker → target) • garis <b>putus-putus</b> = serangan baru (60 detik terakhir)</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Teams view -->
|
<!-- Teams view -->
|
||||||
@@ -482,44 +488,99 @@ function renderTopo(nodes, edges) {
|
|||||||
${status ? `<circle cx="${p.x + r - 8}" cy="${p.y - r + 8}" r="5" fill="#34d399"/>` : ''}
|
${status ? `<circle cx="${p.x + r - 8}" cy="${p.y - r + 8}" r="5" fill="#34d399"/>` : ''}
|
||||||
</g>`;
|
</g>`;
|
||||||
});
|
});
|
||||||
svg.innerHTML = `<defs><marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#2a4a6f"/></marker></defs>` + html;
|
svg.innerHTML = `<defs><marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#2a4a6f"/></marker></defs><g data-root>${html}</g>`;
|
||||||
enableTopoDrag(svg, pos);
|
enableTopoDrag(svg, pos);
|
||||||
|
applyTopoView();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- draggable topology ----
|
// ---- draggable topology + zoom/pan ----
|
||||||
|
let topoScale = 1;
|
||||||
|
let topoPanX = 0, topoPanY = 0;
|
||||||
function enableTopoDrag(svg, pos) {
|
function enableTopoDrag(svg, pos) {
|
||||||
|
const gMain = svg.querySelector('g[data-root]') || svg;
|
||||||
let dragEl = null, dx = 0, dy = 0;
|
let dragEl = null, dx = 0, dy = 0;
|
||||||
const onMove = (ev) => {
|
let panning = false, px = 0, py = 0;
|
||||||
if (!dragEl) return;
|
|
||||||
|
const toLocal = (ev) => {
|
||||||
const ctm = svg.getScreenCTM();
|
const ctm = svg.getScreenCTM();
|
||||||
if (!ctm) return;
|
if (!ctm) return {x: 0, y: 0};
|
||||||
const pt = svg.createSVGPoint();
|
const pt = svg.createSVGPoint();
|
||||||
pt.x = ev.clientX; pt.y = ev.clientY;
|
pt.x = ev.clientX; pt.y = ev.clientY;
|
||||||
const p = pt.matrixTransform(ctm.inverse());
|
return pt.matrixTransform(ctm.inverse());
|
||||||
dragEl.setAttribute('transform', `translate(${p.x - dx}, ${p.y - dy})`);
|
|
||||||
};
|
};
|
||||||
const onUp = () => { dragEl = null; svg.style.cursor = ''; };
|
|
||||||
|
const onMove = (ev) => {
|
||||||
|
if (dragEl) {
|
||||||
|
const p = toLocal(ev);
|
||||||
|
dragEl.setAttribute('transform', `translate(${p.x - dx}, ${p.y - dy})`);
|
||||||
|
} else if (panning) {
|
||||||
|
topoPanX += ev.clientX - px;
|
||||||
|
topoPanY += ev.clientY - py;
|
||||||
|
px = ev.clientX; py = ev.clientY;
|
||||||
|
applyTopoView();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onUp = () => { dragEl = null; panning = false; svg.style.cursor = ''; };
|
||||||
|
|
||||||
svg.addEventListener('mousedown', (ev) => {
|
svg.addEventListener('mousedown', (ev) => {
|
||||||
const g = ev.target.closest('g[data-node]');
|
const g = ev.target.closest('g[data-node]');
|
||||||
if (!g) return;
|
if (g) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const ctm = svg.getScreenCTM();
|
const p = toLocal(ev);
|
||||||
const pt = svg.createSVGPoint();
|
const c = g.querySelector('circle');
|
||||||
pt.x = ev.clientX; pt.y = ev.clientY;
|
dx = p.x - parseFloat(c.getAttribute('cx'));
|
||||||
const p = pt.matrixTransform(ctm.inverse());
|
dy = p.y - parseFloat(c.getAttribute('cy'));
|
||||||
const c = g.querySelector('circle');
|
dragEl = g;
|
||||||
const cx = parseFloat(c.getAttribute('cx'));
|
svg.style.cursor = 'grabbing';
|
||||||
const cy = parseFloat(c.getAttribute('cy'));
|
g.setPointerCapture && g.setPointerCapture(ev.pointerId);
|
||||||
dx = p.x - cx; dy = p.y - cy;
|
} else {
|
||||||
dragEl = g;
|
// empty area -> pan the canvas
|
||||||
svg.style.cursor = 'grabbing';
|
panning = true;
|
||||||
g.setPointerCapture && g.setPointerCapture(ev.pointerId);
|
px = ev.clientX; py = ev.clientY;
|
||||||
|
svg.style.cursor = 'move';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
svg.addEventListener('pointermove', onMove);
|
svg.addEventListener('pointermove', onMove);
|
||||||
svg.addEventListener('pointerup', onUp);
|
svg.addEventListener('pointerup', onUp);
|
||||||
svg.addEventListener('pointercancel', onUp);
|
svg.addEventListener('pointercancel', onUp);
|
||||||
|
|
||||||
|
// wheel zoom (ctrl+wheel or plain wheel on empty area)
|
||||||
|
svg.addEventListener('wheel', (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
const factor = ev.deltaY < 0 ? 1.12 : 1 / 1.12;
|
||||||
|
const ns = Math.min(3, Math.max(0.3, topoScale * factor));
|
||||||
|
// zoom around cursor
|
||||||
|
const rect = svg.getBoundingClientRect();
|
||||||
|
const mx = ev.clientX - rect.left, my = ev.clientY - rect.top;
|
||||||
|
const W = svg.clientWidth, H = svg.clientHeight;
|
||||||
|
topoPanX = mx - (mx - topoPanX) * (ns / topoScale);
|
||||||
|
topoPanY = my - (my - topoPanY) * (ns / topoScale);
|
||||||
|
topoScale = ns;
|
||||||
|
applyTopoView();
|
||||||
|
}, {passive: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyTopoView() {
|
||||||
|
const svg = document.getElementById('topoSvg');
|
||||||
|
const g = svg.querySelector('g[data-root]');
|
||||||
|
if (!g) return;
|
||||||
|
g.setAttribute('transform', `translate(${topoPanX}, ${topoPanY}) scale(${topoScale})`);
|
||||||
|
// update hint + zoom % label
|
||||||
|
const zl = document.getElementById('topoZoomLabel');
|
||||||
|
if (zl) zl.textContent = Math.round(topoScale * 100) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
function topoZoom(factor) {
|
||||||
|
const svg = document.getElementById('topoSvg');
|
||||||
|
const rect = svg.getBoundingClientRect();
|
||||||
|
const ns = Math.min(3, Math.max(0.3, topoScale * factor));
|
||||||
|
topoPanX = rect.width / 2 - (rect.width / 2 - topoPanX) * (ns / topoScale);
|
||||||
|
topoPanY = rect.height / 2 - (rect.height / 2 - topoPanY) * (ns / topoScale);
|
||||||
|
topoScale = ns;
|
||||||
|
applyTopoView();
|
||||||
|
}
|
||||||
|
function topoReset() { topoScale = 1; topoPanX = 0; topoPanY = 0; applyTopoView(); }
|
||||||
|
|
||||||
// ---------- Teams ----------
|
// ---------- Teams ----------
|
||||||
async function loadTeams() {
|
async function loadTeams() {
|
||||||
try {
|
try {
|
||||||
@@ -547,6 +608,7 @@ async function loadTeams() {
|
|||||||
<button class="primary" onclick="startTeam(${t.index})">▶ Start</button>
|
<button class="primary" onclick="startTeam(${t.index})">▶ Start</button>
|
||||||
<button class="danger" onclick="stopTeam(${t.index})">⏹ Stop</button>
|
<button class="danger" onclick="stopTeam(${t.index})">⏹ Stop</button>
|
||||||
<button onclick="viewTeamCred(${t.index})">🔑 SSH & Pass</button>
|
<button onclick="viewTeamCred(${t.index})">🔑 SSH & Pass</button>
|
||||||
|
<button onclick="editTeam(${t.index})">✏️ Edit Nama/Domain</button>
|
||||||
<button onclick="openTeamLogs(${t.index})">📜 Logs</button>
|
<button onclick="openTeamLogs(${t.index})">📜 Logs</button>
|
||||||
<button onclick="randomizeTeam(${t.index})">🎲 Randomize Flag</button>
|
<button onclick="randomizeTeam(${t.index})">🎲 Randomize Flag</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -558,14 +620,16 @@ async function loadTeams() {
|
|||||||
|
|
||||||
async function setTeams() {
|
async function setTeams() {
|
||||||
const n = parseInt(document.getElementById('teamCount').value || '0', 10);
|
const n = parseInt(document.getElementById('teamCount').value || '0', 10);
|
||||||
const labels = {};
|
const labels = {}, domains = {};
|
||||||
for (let i = 1; i <= n; i++) {
|
for (let i = 1; i <= n; i++) {
|
||||||
const inp = document.getElementById('teamNameInputs')?.querySelector(`input[data-idx="${i}"]`);
|
const inpN = document.getElementById('teamNameInputs')?.querySelector(`input[data-idx="${i}"][data-field="name"]`);
|
||||||
if (inp && inp.value.trim()) labels[i] = inp.value.trim();
|
if (inpN && inpN.value.trim()) labels[i] = inpN.value.trim();
|
||||||
|
const inpD = document.getElementById('teamNameInputs')?.querySelector(`input[data-idx="${i}"][data-field="domain"]`);
|
||||||
|
if (inpD && inpD.value.trim()) domains[i] = inpD.value.trim();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
showLoading(`Membuat ${n} team…<br>Generate compose, flags, domain, Traefik config`);
|
showLoading(`Membuat ${n} team…<br>Generate compose, flags, domain, Traefik config`);
|
||||||
const d = await api('/api/teams/set', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({count: n, labels})});
|
const d = await api('/api/teams/set', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({count: n, labels, domains})});
|
||||||
toast(`Team dibuat: ${d.created.join(', ') || 'tidak ada yang baru'} · total ${d.total}`, false);
|
toast(`Team dibuat: ${d.created.join(', ') || 'tidak ada yang baru'} · total ${d.total}`, false);
|
||||||
loadTeams();
|
loadTeams();
|
||||||
} catch (e) { toast(e.message, true); }
|
} catch (e) { toast(e.message, true); }
|
||||||
@@ -575,16 +639,36 @@ async function setTeams() {
|
|||||||
function teamCountChanged() {
|
function teamCountChanged() {
|
||||||
const n = parseInt(document.getElementById('teamCount').value || '0', 10);
|
const n = parseInt(document.getElementById('teamCount').value || '0', 10);
|
||||||
const box = document.getElementById('teamNameInputs');
|
const box = document.getElementById('teamNameInputs');
|
||||||
let html = '';
|
let html = '<div style="font-size:11px;color:#718096;margin-bottom:4px">Nama team & domain custom (opsional — kosongkan untuk auto dari nama)</div>';
|
||||||
for (let i = 1; i <= n; i++) {
|
for (let i = 1; i <= n; i++) {
|
||||||
html += `<div style="display:flex;align-items:center;gap:8px">
|
html += `<div style="display:flex;align-items:center;gap:6px">
|
||||||
<span style="color:#718096;font-size:12px;width:60px">Team ${i}</span>
|
<span style="color:#718096;font-size:12px;width:60px">Team ${i}</span>
|
||||||
<input data-idx="${i}" placeholder="Nama team (jd domain: nama.gemastik.imrnes.team)" style="flex:1">
|
<input data-idx="${i}" data-field="name" placeholder="Nama team (contoh: Cyber Warriors)" style="flex:1;min-width:120px">
|
||||||
|
<input data-idx="${i}" data-field="domain" placeholder="domain custom (contoh: team-cyber)" style="flex:1;min-width:120px">
|
||||||
|
<span style="color:#3b4a63;font-size:11px">.gemastik.imrnes.team</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
box.innerHTML = html;
|
box.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function editTeam(idx) {
|
||||||
|
// fetch current team info
|
||||||
|
let t = null;
|
||||||
|
try { t = (await api(`/api/team/${idx}/creds`)).team; } catch (e) { toast('Gagal ambil data team', true); return; }
|
||||||
|
const newLabel = prompt(`✏️ Nama team ${idx} (sekarang: ${t.label || ('Team ' + idx)}):`, t.label || '');
|
||||||
|
if (newLabel === null) return;
|
||||||
|
const newDomain = prompt(`🌐 Domain custom team ${idx} (sekarang: ${t.domain || ''} — ketik subdomain saja, contoh: team-cyber):`, (t.domain || '').split('.')[0]);
|
||||||
|
if (newDomain === null) return;
|
||||||
|
try {
|
||||||
|
showLoading('Update nama/domain team…');
|
||||||
|
const d = await api(`/api/teams/${idx}`, {method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({label: newLabel.trim(), domain: newDomain.trim()})});
|
||||||
|
toast(`Team ${idx} diupdate: ${d.team.label} @ ${d.team.domain}`, false);
|
||||||
|
loadTeams();
|
||||||
|
if (document.getElementById('view-topo').classList.contains('active')) loadTopo();
|
||||||
|
} catch (e) { toast(e.message, true); }
|
||||||
|
finally { hideLoading(); }
|
||||||
|
}
|
||||||
|
|
||||||
async function startTeam(idx) {
|
async function startTeam(idx) {
|
||||||
const ov = showLoading(`Menyiapkan Team ${idx}…<br>Membangun & start container (bisa butuh 1-3 menit)`);
|
const ov = showLoading(`Menyiapkan Team ${idx}…<br>Membangun & start container (bisa butuh 1-3 menit)`);
|
||||||
try { await api('/api/teams/start', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({index: idx})}); toast(`Team ${idx} start`, false); }
|
try { await api('/api/teams/start', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({index: idx})}); toast(`Team ${idx} start`, false); }
|
||||||
|
|||||||
+39
-3
@@ -68,17 +68,29 @@ def slugify(s: str) -> str:
|
|||||||
s = re.sub(r"[\s_]+", "-", s)
|
s = re.sub(r"[\s_]+", "-", s)
|
||||||
return s or "team"
|
return s or "team"
|
||||||
|
|
||||||
def create_team(idx: int, label: str = None):
|
def create_team(idx: int, label: str = None, domain: str = None):
|
||||||
"""Build a full team stack dir with unique ports/passwords."""
|
"""Build a full team stack dir with unique ports/passwords.
|
||||||
|
|
||||||
|
label -> team display name (leaderboard/topology)
|
||||||
|
domain -> custom subdomain host, e.g. "cyber-warriors" or
|
||||||
|
"cyber-warriors.gemastik.imrnes.team" (full host accepted).
|
||||||
|
Defaults to slugify(label).gemastik.imrnes.team.
|
||||||
|
"""
|
||||||
ports = team_ports(idx)
|
ports = team_ports(idx)
|
||||||
team_dir = TEAMS_DIR / f"team{idx}"
|
team_dir = TEAMS_DIR / f"team{idx}"
|
||||||
label = label or f"Tim {idx}"
|
label = label or f"Tim {idx}"
|
||||||
slug = slugify(label)
|
slug = slugify(label)
|
||||||
|
# normalize custom domain -> host under *.gemastik.imrnes.team
|
||||||
|
host = (domain or f"{slug}.gemastik.imrnes.team").strip().lower()
|
||||||
|
host = host.replace("https://", "").replace("http://", "").rstrip("/")
|
||||||
|
if not host.endswith(".gemastik.imrnes.team"):
|
||||||
|
host = f"{host}.gemastik.imrnes.team"
|
||||||
|
slug = host.split(".")[0]
|
||||||
state = {
|
state = {
|
||||||
"index": idx,
|
"index": idx,
|
||||||
"label": label,
|
"label": label,
|
||||||
"slug": slug,
|
"slug": slug,
|
||||||
"domain": f"{slug}.gemastik.imrnes.team",
|
"domain": host,
|
||||||
"ports": ports,
|
"ports": ports,
|
||||||
"admin_user": f"admin_team{idx}",
|
"admin_user": f"admin_team{idx}",
|
||||||
"admin_pass": gen_password(20),
|
"admin_pass": gen_password(20),
|
||||||
@@ -185,6 +197,30 @@ def create_team(idx: int, label: str = None):
|
|||||||
(team_dir / "state.json").write_text(json.dumps(state, indent=2))
|
(team_dir / "state.json").write_text(json.dumps(state, indent=2))
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
def update_team(idx: int, label: str = None, domain: str = None) -> dict:
|
||||||
|
"""Update a team's display name and/or custom domain (live re-route).
|
||||||
|
|
||||||
|
- label updates state.json['label'] (leaderboard/topology use this).
|
||||||
|
- domain updates slug + domain and rewrites the Traefik team route.
|
||||||
|
Returns updated state.
|
||||||
|
"""
|
||||||
|
team_dir = TEAMS_DIR / f"team{idx}"
|
||||||
|
if not (team_dir / "state.json").exists():
|
||||||
|
raise FileNotFoundError(f"Team {idx} not created")
|
||||||
|
st = json.loads((team_dir / "state.json").read_text())
|
||||||
|
if label:
|
||||||
|
st["label"] = str(label).strip()[:48] or st["label"]
|
||||||
|
if domain:
|
||||||
|
host = domain.strip().lower().replace("https://", "").replace("http://", "").rstrip("/")
|
||||||
|
if not host.endswith(".gemastik.imrnes.team"):
|
||||||
|
host = f"{host}.gemastik.imrnes.team"
|
||||||
|
st["domain"] = host
|
||||||
|
st["slug"] = host.split(".")[0]
|
||||||
|
(team_dir / "state.json").write_text(json.dumps(st, indent=2))
|
||||||
|
# re-route domain in Traefik immediately
|
||||||
|
ensure_team_domains()
|
||||||
|
return st
|
||||||
|
|
||||||
def start_team(idx: int):
|
def start_team(idx: int):
|
||||||
team_dir = TEAMS_DIR / f"team{idx}"
|
team_dir = TEAMS_DIR / f"team{idx}"
|
||||||
if not (team_dir / "state.json").exists():
|
if not (team_dir / "state.json").exists():
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Allow apt to work on hosts whose clock is past GPG key expiry (2026+)
|
||||||
|
Acquire::AllowInsecureRepositories "true";
|
||||||
|
Acquire::AllowDowngradeToInsecureRepositories "true";
|
||||||
|
Apt::Get::AllowUnauthenticated "true";
|
||||||
|
Apt::Get::force-yes "true";
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Allow apt to work on hosts whose clock is past GPG key expiry (2026+)
|
||||||
|
Acquire::AllowInsecureRepositories "true";
|
||||||
|
Acquire::AllowDowngradeToInsecureRepositories "true";
|
||||||
|
Apt::Get::AllowUnauthenticated "true";
|
||||||
|
Apt::Get::force-yes "true";
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Allow apt to work on hosts whose clock is past GPG key expiry (2026+)
|
||||||
|
Acquire::AllowInsecureRepositories "true";
|
||||||
|
Acquire::AllowDowngradeToInsecureRepositories "true";
|
||||||
|
Apt::Get::AllowUnauthenticated "true";
|
||||||
|
Apt::Get::force-yes "true";
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Allow apt to work on hosts whose clock is past GPG key expiry (2026+)
|
||||||
|
Acquire::AllowInsecureRepositories "true";
|
||||||
|
Acquire::AllowDowngradeToInsecureRepositories "true";
|
||||||
|
Apt::Get::AllowUnauthenticated "true";
|
||||||
|
Apt::Get::force-yes "true";
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Allow apt to work on hosts whose clock is past GPG key expiry (2026+)
|
||||||
|
Acquire::AllowInsecureRepositories "true";
|
||||||
|
Acquire::AllowDowngradeToInsecureRepositories "true";
|
||||||
|
Apt::Get::AllowUnauthenticated "true";
|
||||||
|
Apt::Get::force-yes "true";
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Allow apt to work on hosts whose clock is past GPG key expiry (2026+)
|
||||||
|
Acquire::AllowInsecureRepositories "true";
|
||||||
|
Acquire::AllowDowngradeToInsecureRepositories "true";
|
||||||
|
Apt::Get::AllowUnauthenticated "true";
|
||||||
|
Apt::Get::force-yes "true";
|
||||||
Reference in New Issue
Block a user