feat: team activity feed (attacks in/out) + activity tab + auto-refresh + leaderboard tab on team portal
This commit is contained in:
@@ -278,6 +278,42 @@ async def api_team_status(idx: int, req: Request):
|
|||||||
"ssh": st["ports"][name]["ssh"], "alive": ok})
|
"ssh": st["ports"][name]["ssh"], "alive": ok})
|
||||||
return {"results": results}
|
return {"results": results}
|
||||||
|
|
||||||
|
@app.get("/api/team/{idx}/activity")
|
||||||
|
async def api_team_activity(idx: int, req: Request):
|
||||||
|
"""Team-scoped activity feed: attack events where this team is attacker or
|
||||||
|
target (i.e. "ada serangan ke service kita" / "kita menyerang"), plus a
|
||||||
|
snapshot of own service health. Requires team login + own host."""
|
||||||
|
td = orch.TEAMS_DIR / f"team{idx}"
|
||||||
|
if not (td / "state.json").exists():
|
||||||
|
raise HTTPException(404, "Team not found")
|
||||||
|
st_self = json.loads((td / "state.json").read_text())
|
||||||
|
if not _check_team_host(req, st_self):
|
||||||
|
raise HTTPException(403, "Akses team lain tidak diizinkan")
|
||||||
|
if not _team_authorized(req, idx):
|
||||||
|
raise HTTPException(401, "Login portal team dulu")
|
||||||
|
|
||||||
|
# live label lookup (rename team = activity updates everywhere)
|
||||||
|
def label(i):
|
||||||
|
try:
|
||||||
|
s = json.loads((orch.TEAMS_DIR / f"team{i}" / "state.json").read_text())
|
||||||
|
return s.get("label") or f"Team {i}"
|
||||||
|
except Exception:
|
||||||
|
return f"Team {i}"
|
||||||
|
|
||||||
|
ap = orch.TEAMS_DIR / "attacks.json"
|
||||||
|
try:
|
||||||
|
events = json.loads(ap.read_text()).get("events", []) if ap.exists() else []
|
||||||
|
except Exception:
|
||||||
|
events = []
|
||||||
|
mine = []
|
||||||
|
for e in reversed(events): # newest first
|
||||||
|
if e.get("attacker") == idx or e.get("target") == idx:
|
||||||
|
e = dict(e)
|
||||||
|
e["attacker_label"] = label(e.get("attacker"))
|
||||||
|
e["target_label"] = label(e.get("target"))
|
||||||
|
mine.append(e)
|
||||||
|
return {"events": mine[:100], "team_label": st_self.get("label") or f"Team {idx}"}
|
||||||
|
|
||||||
@app.post("/api/login")
|
@app.post("/api/login")
|
||||||
async def api_login(req: Request):
|
async def api_login(req: Request):
|
||||||
data = await req.json()
|
data = await req.json()
|
||||||
|
|||||||
+124
-4
@@ -60,6 +60,29 @@
|
|||||||
.guide ol, .guide ul { padding-left:20px; }
|
.guide ol, .guide ul { padding-left:20px; }
|
||||||
.login-note { font-size:12px; color:var(--dim); margin-top:14px; text-align:center; }
|
.login-note { font-size:12px; color:var(--dim); margin-top:14px; text-align:center; }
|
||||||
.login-note a { color:var(--accent); }
|
.login-note a { color:var(--accent); }
|
||||||
|
/* activity feed */
|
||||||
|
.feed { display:flex; flex-direction:column; gap:10px; max-height:420px; overflow-y:auto; }
|
||||||
|
.feed-item { background:var(--panel2); border:1px solid var(--line); border-left:3px solid var(--line); border-radius:10px; padding:10px 14px; font-size:13px; line-height:1.5; }
|
||||||
|
.feed-item.in { border-left-color:var(--red); }
|
||||||
|
.feed-item.out { border-left-color:var(--green); }
|
||||||
|
.feed-item.ok { border-left-color:var(--green); }
|
||||||
|
.feed-item.fail { border-left-color:var(--red); }
|
||||||
|
.feed-time { color:var(--dim); font-size:11px; margin-left:8px; }
|
||||||
|
.feed-tag { font-size:11px; border-radius:5px; padding:1px 7px; margin-right:6px; font-weight:700; }
|
||||||
|
.tag-in { background:#3a1414; color:var(--red); }
|
||||||
|
.tag-out { background:#06352a; color:var(--green); }
|
||||||
|
code { background:var(--panel2); padding:2px 6px; border-radius:5px; font-size:12px; }
|
||||||
|
.lb-row { display:flex; align-items:center; gap:12px; padding:10px 12px; border-bottom:1px solid var(--line); font-size:14px; }
|
||||||
|
.lb-row.me { background:rgba(56,189,248,.08); border-left:3px solid var(--accent); }
|
||||||
|
.lb-rank { width:28px; font-weight:800; color:var(--dim); }
|
||||||
|
.lb-name { flex:1; font-weight:600; }
|
||||||
|
.lb-name small { color:var(--dim); font-weight:400; margin-left:6px; }
|
||||||
|
.lb-solves { background:var(--panel2); border:1px solid var(--line); border-radius:20px; padding:3px 12px; font-size:12px; color:var(--green); }
|
||||||
|
.lb-empty { color:var(--dim); font-size:13px; padding:20px; text-align:center; }
|
||||||
|
.svc-health { display:flex; flex-wrap:wrap; gap:8px; margin-top:12px; }
|
||||||
|
.svc-chip { border:1px solid var(--line); border-radius:8px; padding:4px 10px; font-size:12px; background:var(--panel2); }
|
||||||
|
.svc-chip.up { border-color:var(--green); color:var(--green); }
|
||||||
|
.svc-chip.down { border-color:var(--red); color:var(--red); }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -90,10 +113,12 @@
|
|||||||
<!-- PORTAL -->
|
<!-- PORTAL -->
|
||||||
<div id="portalScreen" style="display:none">
|
<div id="portalScreen" style="display:none">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="active" data-view="term" onclick="showView('term')">🖥️ Terminal SSH</button>
|
<button data-view="term" onclick="showView('term')">🖥️ Terminal SSH</button>
|
||||||
<button data-view="targets" onclick="showView('targets')">🎯 Target Musuh</button>
|
<button data-view="targets" onclick="showView('targets')">🎯 Target Musuh</button>
|
||||||
<button data-view="submit" onclick="showView('submit')">🚩 Submit Flag</button>
|
<button data-view="submit" onclick="showView('submit')">🚩 Submit Flag</button>
|
||||||
<button data-view="guide" onclick="showView('guide')">📖 Panduan SSH</button>
|
<button data-view="activity" onclick="showView('activity')">📡 Aktivitas</button>
|
||||||
|
<button data-view="leaderboard" onclick="showView('leaderboard')">🏆 Leaderboard</button>
|
||||||
|
<button data-view="guide" onclick="showView('guide')">📖 Panduan SSH</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="view active" id="view-term">
|
<div class="view active" id="view-term">
|
||||||
@@ -149,6 +174,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="view" id="view-activity">
|
||||||
|
<div class="card">
|
||||||
|
<h2>📡 Aktivitas & Serangan</h2>
|
||||||
|
<p style="font-size:13px;color:var(--dim);margin-bottom:12px">
|
||||||
|
🔴 <b>masuk</b> = ada yang menyerang service timmu · 🟢 <b>keluar</b> = kamu menyerang tim lain.
|
||||||
|
Update otomatis tiap 15 detik.
|
||||||
|
</p>
|
||||||
|
<div id="activityFeed" class="feed"><div class="lb-empty">Memuat aktivitas…</div></div>
|
||||||
|
<div class="svc-health" id="svcHealth"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="view" id="view-leaderboard">
|
||||||
|
<div class="card">
|
||||||
|
<h2>🏆 Leaderboard</h2>
|
||||||
|
<p style="font-size:13px;color:var(--dim);margin-bottom:12px">Papan skor flag yang berhasil dicuri. Kamu = baris biru.</p>
|
||||||
|
<div id="lbList"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="view" id="view-guide">
|
<div class="view" id="view-guide">
|
||||||
<div class="card guide">
|
<div class="card guide">
|
||||||
<h2>📖 Panduan SSH & Attack</h2>
|
<h2>📖 Panduan SSH & Attack</h2>
|
||||||
@@ -207,6 +252,8 @@ function showView(v) {
|
|||||||
document.querySelectorAll('.view').forEach(x => x.classList.toggle('active', x.id === 'view-' + v));
|
document.querySelectorAll('.view').forEach(x => x.classList.toggle('active', x.id === 'view-' + v));
|
||||||
if (v === 'term' && term) term.focus();
|
if (v === 'term' && term) term.focus();
|
||||||
if (v === 'targets') loadTargets();
|
if (v === 'targets') loadTargets();
|
||||||
|
if (v === 'activity') loadActivity();
|
||||||
|
if (v === 'leaderboard') loadLeaderboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- auth ----------
|
// ---------- auth ----------
|
||||||
@@ -340,6 +387,79 @@ async function loadTargetDropdown() {
|
|||||||
}
|
}
|
||||||
} catch (e) { /* non-fatal */ }
|
} catch (e) { /* non-fatal */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- activity feed (attacks involving this team) ----------
|
||||||
|
async function loadActivity() {
|
||||||
|
const box = document.getElementById('activityFeed');
|
||||||
|
const healthBox = document.getElementById('svcHealth');
|
||||||
|
try {
|
||||||
|
const d = await api(`/api/team/${TEAM_ID}/activity`);
|
||||||
|
const events = d.events || [];
|
||||||
|
if (!events.length) {
|
||||||
|
box.innerHTML = '<div class="lb-empty">Belum ada aktivitas serangan. Aman… untuk sekarang 😌</div>';
|
||||||
|
} else {
|
||||||
|
box.innerHTML = events.map(e => {
|
||||||
|
const inbound = e.target === TEAM_ID;
|
||||||
|
const cls = inbound ? 'in' : 'out';
|
||||||
|
const tag = inbound
|
||||||
|
? `<span class="feed-tag tag-in">⬇ MASUK</span>${esc(e.attacker_label)} menyerang`
|
||||||
|
: `<span class="feed-tag tag-out">⬆ KELUAR</span>kamu menyerang ${esc(e.target_label)}`;
|
||||||
|
const verdict = e.success
|
||||||
|
? '<span style="color:var(--green)">✅ flag benar</span>'
|
||||||
|
: '<span style="color:var(--red)">❌ flag salah</span>';
|
||||||
|
return `<div class="feed-item ${cls}">${tag} <code>${esc(e.challenge || '')}</code> ${verdict}` +
|
||||||
|
`<span class="feed-time">${esc(e.ts_human || '')}</span></div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
// own service health chips (all values escaped above via esc())
|
||||||
|
try {
|
||||||
|
const st = await api(`/api/team/${TEAM_ID}/status`);
|
||||||
|
if (st.results) {
|
||||||
|
healthBox.innerHTML = st.results.map(r =>
|
||||||
|
`<span class="svc-chip ${r.alive ? 'up' : 'down'}">${esc(r.name)} ${r.alive ? '● UP' : '○ DOWN'} :${r.port}</span>`
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
} catch (e) {
|
||||||
|
box.innerHTML = '<div class="lb-empty">Gagal memuat aktivitas.</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- leaderboard (public endpoint) ----------
|
||||||
|
async function loadLeaderboard() {
|
||||||
|
const box = document.getElementById('lbList');
|
||||||
|
try {
|
||||||
|
const d = await api('/api/leaderboard');
|
||||||
|
const teams = d.teams || [];
|
||||||
|
if (!teams.length) {
|
||||||
|
box.innerHTML = '<div class="lb-empty">Belum ada flag yang berhasil dicuri. Jadilah yang pertama! 🚀</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const medal = ['🥇','🥈','🥉'];
|
||||||
|
box.innerHTML = teams.map((t, i) => {
|
||||||
|
const isMe = t.team === TEAM_ID;
|
||||||
|
const rank = i < 3 ? medal[i] : `<span class="lb-rank" style="width:28px">${i+1}</span>`;
|
||||||
|
return `<div class="lb-row ${isMe ? 'me' : ''}">
|
||||||
|
<span class="lb-rank">${rank}</span>
|
||||||
|
<span class="lb-name">${esc(t.name)} ${isMe ? '<small>(kamu)</small>' : ''}</span>
|
||||||
|
<span class="lb-solves">⚑ ${t.solves} solve${t.solves > 1 ? 's' : ''}</span>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
const found = teams.some(t => t.team === TEAM_ID);
|
||||||
|
if (!found) {
|
||||||
|
box.innerHTML += `<div class="lb-row me"><span class="lb-rank">—</span><span class="lb-name">Tim kamu <small>(kamu)</small></span><span class="lb-solves">⚑ 0 solve</span></div>`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
box.innerHTML = '<div class="lb-empty">Gagal memuat leaderboard.</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// auto-refresh activity while its tab is open (every 15s)
|
||||||
|
setInterval(async () => {
|
||||||
|
const a = document.getElementById('view-activity');
|
||||||
|
if (a && a.classList.contains('active')) loadActivity();
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
loadTargetDropdown();
|
loadTargetDropdown();
|
||||||
|
|
||||||
checkSession();
|
checkSession();
|
||||||
|
|||||||
Reference in New Issue
Block a user