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:
+24
-4
@@ -132,12 +132,13 @@
|
||||
<div class="view" id="view-teams">
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<div class="team-head">
|
||||
<div><b style="color:#5ad1ff">Jumlah Team</b> <span style="font-size:11px;color:#718096">(auto-create node per team)</span></div>
|
||||
<div><b style="color:#5ad1ff">Jumlah Team</b> <span style="font-size:11px;color:#718096">(auto-create node + domain per team)</span></div>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<input type="number" id="teamCount" min="0" max="50" value="0" style="width:90px">
|
||||
<input type="number" id="teamCount" min="0" max="50" value="0" style="width:90px" oninput="teamCountChanged()">
|
||||
<button class="primary" onclick="setTeams()">Set & Buat</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="teamNameInputs" style="margin-top:10px;display:flex;flex-direction:column;gap:6px"></div>
|
||||
<div style="margin-top:10px;display:flex;gap:8px;flex-wrap:wrap">
|
||||
<button class="primary" onclick="startAllTeams()">▶ Start CTF (semua)</button>
|
||||
<button class="danger" onclick="stopAllTeams()">⏹ Stop CTF (semua)</button>
|
||||
@@ -414,6 +415,7 @@ async function loadTeams() {
|
||||
let html = '';
|
||||
for (const t of d.teams) {
|
||||
const alive = t.status === 'running';
|
||||
const dom = t.domain || '';
|
||||
html += `<div class="card ${alive ? '' : 'dead'}">
|
||||
<div class="team-head">
|
||||
<span class="ch-name">${esc(t.label || ('Team ' + t.index))}</span>
|
||||
@@ -423,7 +425,7 @@ async function loadTeams() {
|
||||
<b>Receiver</b><span>${t.ports.receiver}</span>
|
||||
<b>Admin</b><span>${esc(t.admin_user)}</span>
|
||||
<b>SSH user</b><span>ctfuser</span>
|
||||
<b>Status</b><span>${esc(t.status)}</span>
|
||||
<b>Status</b><span>${esc(t.status)}</span>${dom ? `<b>Domain</b><span><a href="https://${esc(dom)}/team/${t.index}" target="_blank" style="color:#5ad1ff">${esc(dom)} →</a></span>` : ''}
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="primary" onclick="startTeam(${t.index})">▶ Start</button>
|
||||
@@ -440,13 +442,31 @@ async function loadTeams() {
|
||||
|
||||
async function setTeams() {
|
||||
const n = parseInt(document.getElementById('teamCount').value || '0', 10);
|
||||
const labels = {};
|
||||
for (let i = 1; i <= n; i++) {
|
||||
const inp = document.getElementById('teamNameInputs')?.querySelector(`input[data-idx="${i}"]`);
|
||||
if (inp && inp.value.trim()) labels[i] = inp.value.trim();
|
||||
}
|
||||
try {
|
||||
const d = await api('/api/teams/set', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({count: n})});
|
||||
const d = await api('/api/teams/set', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({count: n, labels})});
|
||||
toast(`Team dibuat: ${d.created.join(', ') || 'tidak ada yang baru'} · total ${d.total}`, false);
|
||||
loadTeams();
|
||||
} catch (e) { toast(e.message, true); }
|
||||
}
|
||||
|
||||
function teamCountChanged() {
|
||||
const n = parseInt(document.getElementById('teamCount').value || '0', 10);
|
||||
const box = document.getElementById('teamNameInputs');
|
||||
let html = '';
|
||||
for (let i = 1; i <= n; i++) {
|
||||
html += `<div style="display:flex;align-items:center;gap:8px">
|
||||
<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">
|
||||
</div>`;
|
||||
}
|
||||
box.innerHTML = html;
|
||||
}
|
||||
|
||||
async function startTeam(idx) {
|
||||
try { await api('/api/teams/start', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({index: idx})}); toast(`Team ${idx} start (build bisa makan waktu)`, false); setTimeout(loadTeams, 3000); }
|
||||
catch (e) { toast(e.message, true); }
|
||||
|
||||
Reference in New Issue
Block a user