- 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
129 lines
6.2 KiB
HTML
129 lines
6.2 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="id">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Portal Team — Gemastik A/D</title>
|
|
<style>
|
|
:root { --bg:#0b0e14; --panel:#151a23; --panel2:#1d2430; --line:#2a3444; --txt:#e6edf3; --dim:#8b98a9; --accent:#38bdf8; --green:#34d399; --red:#f87171; }
|
|
* { margin:0; padding:0; box-sizing:border-box; }
|
|
body { background:var(--bg); color:var(--txt); font-family:'Segoe UI',system-ui,sans-serif; min-height:100vh; }
|
|
.wrap { max-width:900px; margin:0 auto; padding:24px 16px 60px; }
|
|
header { display:flex; align-items:center; gap:12px; margin-bottom:24px; flex-wrap:wrap; }
|
|
.logo { width:42px; height:42px; border-radius:10px; background:linear-gradient(135deg,#38bdf8,#6366f1); display:flex; align-items:center; justify-content:center; font-weight:800; font-size:20px; color:#fff; }
|
|
h1 { font-size:22px; }
|
|
.sub { color:var(--dim); font-size:13px; }
|
|
.badge { background:#06352a; border:1px solid var(--green); color:var(--green); border-radius:20px; padding:3px 12px; font-size:12px; font-weight:600; }
|
|
.card { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:20px; margin-bottom:16px; }
|
|
.card h2 { font-size:15px; margin-bottom:12px; color:var(--accent); }
|
|
.kv { display:grid; grid-template-columns:150px 1fr; gap:6px 12px; font-size:13px; margin-bottom:4px; }
|
|
.kv b { color:var(--dim); font-weight:500; }
|
|
.kv span { font-family:ui-monospace,monospace; }
|
|
table { width:100%; border-collapse:collapse; font-size:13px; }
|
|
th, td { text-align:left; padding:8px 6px; border-bottom:1px solid var(--line); }
|
|
th { color:var(--dim); font-weight:500; }
|
|
.dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px; }
|
|
.dot.up { background:var(--green); } .dot.down { background:var(--red); }
|
|
label { display:block; font-size:12px; color:var(--dim); margin:12px 0 5px; }
|
|
select, input { width:100%; padding:10px 12px; border-radius:8px; border:1px solid var(--line); background:var(--panel2); color:var(--txt); font-size:14px; }
|
|
button { margin-top:16px; width:100%; padding:12px; border:none; border-radius:8px; background:linear-gradient(135deg,#38bdf8,#6366f1); color:#fff; font-weight:600; font-size:15px; cursor:pointer; }
|
|
button:hover { filter:brightness(1.1); }
|
|
#result { margin-top:14px; padding:12px; border-radius:8px; display:none; font-size:14px; }
|
|
#result.ok { display:block; background:#06352a; border:1px solid var(--green); color:var(--green); }
|
|
#result.err { display:block; background:#3a1414; border:1px solid var(--red); color:var(--red); }
|
|
.sshbox { background:var(--panel2); border:1px solid var(--line); border-radius:8px; padding:10px 12px; font-family:ui-monospace,monospace; font-size:12px; overflow-x:auto; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<header>
|
|
<div class="logo">G</div>
|
|
<div style="flex:1">
|
|
<h1 id="teamTitle">Portal Tim</h1>
|
|
<div class="sub" id="teamDomain"></div>
|
|
</div>
|
|
<span class="badge" id="teamStatus">…</span>
|
|
</header>
|
|
|
|
<div class="card">
|
|
<h2>🌐 Akses Server Tim Kamu</h2>
|
|
<div id="accessBox" class="sshbox">Memuat…</div>
|
|
<div style="margin-top:12px;font-size:12px;color:var(--dim)">
|
|
Tiap challenge punya port sendiri. SSH login: <b>ctfuser</b> + password dari panel panitia.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>📊 Status Challenge</h2>
|
|
<table>
|
|
<thead><tr><th>Challenge</th><th>Port</th><th>SSH</th><th>Status</th></tr></thead>
|
|
<tbody id="statusBody"></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>🚩 Submit Flag</h2>
|
|
<label>Challenge</label>
|
|
<select id="challenge">
|
|
<option value="blogpost">blogpost (web)</option>
|
|
<option value="carbeat">carbeat (pwn)</option>
|
|
<option value="cdn">cdn (web)</option>
|
|
<option value="phew">phew (crypto)</option>
|
|
<option value="sheesh">sheesh (crypto)</option>
|
|
<option value="warmup">warmup</option>
|
|
</select>
|
|
<label>Flag</label>
|
|
<input id="flag" placeholder="GEMASTIK18{...}" autocomplete="off">
|
|
<button onclick="submitFlag()">Submit Flag</button>
|
|
<div id="result"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const TEAM_ID = parseInt(document.querySelector('meta[name="team-id"]').content || '0', 10);
|
|
|
|
async function api(url, opts) { const r = await fetch(url, opts); return r.json(); }
|
|
function esc(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
|
|
|
async function load() {
|
|
try {
|
|
const t = await api(`/api/team/${TEAM_ID}/info`);
|
|
document.getElementById('teamTitle').textContent = t.team.label || ('Team ' + TEAM_ID);
|
|
document.getElementById('teamDomain').textContent = t.team.domain || '';
|
|
document.getElementById('teamStatus').textContent = t.team.status === 'running' ? '● RUNNING' : '○ STOPPED';
|
|
const access = [];
|
|
for (const [name, p] of Object.entries(t.team.ports)) {
|
|
if (name === 'receiver' || name === 'panel') continue;
|
|
access.push(`${name}: ${window.location.hostname}:${p.chall} (ssh :${p.ssh})`);
|
|
}
|
|
document.getElementById('accessBox').textContent = access.join('\n');
|
|
// status table
|
|
const st = await api(`/api/team/${TEAM_ID}/status`);
|
|
document.getElementById('statusBody').innerHTML = (st.results || []).map(c => `
|
|
<tr><td>${esc(c.name)}</td><td>${c.port}</td><td>${c.ssh}</td>
|
|
<td><span class="dot ${c.alive ? 'up' : 'down'}"></span>${c.alive ? 'UP' : 'DOWN'}</td></tr>`).join('');
|
|
} catch (e) {
|
|
document.getElementById('accessBox').textContent = 'Gagal memuat: ' + e.message;
|
|
}
|
|
}
|
|
|
|
async function submitFlag() {
|
|
const res = document.getElementById('result');
|
|
res.className = '';
|
|
const d = await api('/api/flag/submit', {
|
|
method: 'POST', headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
team: TEAM_ID,
|
|
challenge: document.getElementById('challenge').value,
|
|
flag: document.getElementById('flag').value.trim(),
|
|
}),
|
|
});
|
|
if (d.success) { res.className = 'ok'; res.innerHTML = '✅ Flag benar!'; }
|
|
else { res.className = 'err'; res.innerHTML = '❌ ' + esc(d.error || 'Gagal'); }
|
|
}
|
|
|
|
load();
|
|
setInterval(load, 15000);
|
|
</script>
|
|
</body>
|
|
</html> |