feat: ATM10-Statistikportal

Statistikportal für den All-the-Mods-10-Server (Minecraft 1.21.1, NeoForge 21.1):

- mod/: serverseitige NeoForge-Mod atmstats – Ereignisse, Live-Zähler, Servergesundheit,
  FTB-Dateien; Outbox auf der Festplatte, signierter gzip-Versand per HTTPS mit Backoff
- backend/: Fastify + PostgreSQL – idempotenter Ingest, Projektionen, Parser für SNBT,
  FTB Quests/Teams, world/stats, world/advancements und latest.log; Ranglisten, Snapshots,
  Retention, öffentliche API, SSE-Live-Updates, Admin mit argon2/CSRF, SFTP-Worker
- web/: Next.js-Dashboard – Übersicht, Ranglisten, Spielerprofile, ATM10, Server,
  Datenquellen, Verwaltung
- shared/: Protokoll v1 und API-Typen; tools/: Simulator, Log-Anonymisierer, RCON-Client
- deploy/: Docker Compose hinter Traefik, Backup; docs/: Architektur, Datenquellen, Betrieb

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lucashahmannandClaude Opus 5.5 committed 2026-10-13 09:33:25 +02:00
1 parent 56d2078e6a
commit e66f89f49e
214 files changed
+27077

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
import { createHash, createHmac } from 'node:crypto';
import { gzipSync } from 'node:zlib';
export const TEST_SECRET = 'test-hmac-secret-0123456789abcdef0123';
export function signature(secret: string, ts: number, body: Buffer): string {
const bodyHash = createHash('sha256').update(body).digest('hex');
return createHmac('sha256', secret).update(`${ts}.${bodyHash}`).digest('hex');
}
export interface SignOpts {
token: string;
secret?: string;
ts?: number;
gzip?: boolean;
serverId?: string;
}
/** Builds headers + payload exactly as the mod sends them: signature covers the transmitted (gzipped) bytes. */
export function signedRequest(body: unknown, opts: SignOpts): { headers: Record<string, string>; payload: Buffer } {
const raw = Buffer.from(typeof body === 'string' ? body : JSON.stringify(body));
const gzip = opts.gzip ?? true;
const payload = gzip ? gzipSync(raw) : raw;
const ts = opts.ts ?? Date.now();
const headers: Record<string, string> = {
authorization: `Bearer ${opts.token}`,
'content-type': 'application/json',
'x-atm-server': opts.serverId ?? 'atm10',
'x-atm-timestamp': String(ts),
'x-atm-signature': signature(opts.secret ?? TEST_SECRET, ts, payload),
};
if (gzip) headers['content-encoding'] = 'gzip';
return { headers, payload };
}