JuByteNexus – API-Vertrag v1 (verbindlich)
API-First: ACP und UCP sind nur Clients dieser API. Basis-URL: http://localhost:8080/api/v1.
Konventionen
- JSON, UTF-8. Fehlerobjekt immer:
{ "code": "NX-1042", "message": "...", "docsUrl": "https://docs.jubyte.net/errors/NX-1042" } - Auth per
Authorization: Bearer <accessToken>(JWT, 15 min). Refresh-Token-Rotation. - Pagination:
?cursor=<opaque>&limit=50→ Antwort{ "items": [...], "nextCursor": "..." | null } - Permissions == API-Scopes (z. B.
bansystem.ban.create).
Auth
| Methode | Pfad | Body | Antwort |
|---|---|---|---|
| POST | /auth/login | {email, password, totp?} | {accessToken, refreshToken, user} oder 401 NX-1001; bei aktivem 2FA ohne totp: 401 NX-1002 |
| POST | /auth/refresh | {refreshToken} | {accessToken, refreshToken} (Rotation, Reuse-Detection → 401 NX-1003) |
| POST | /auth/logout | {refreshToken} | 204 |
| GET | /auth/me | – | {id, email, displayName, mcName, mcId, rankLabel, rankColor, roles: [..], permissions: [..], totpEnabled} – mcName/mcId = verknüpfter MC-Account (genau einer pro E-Mail), rankLabel/rankColor = Team-Rang fürs Account-Chip |
| POST | /auth/totp/setup | – | {secret, otpauthUrl} (ACP zeigt otpauthUrl als QR-Code) |
| POST | /auth/totp/confirm | {code} | 204 |
| POST | /auth/webauthn/login/options | {email} | Public; {ceremonyId, publicKey:{challenge, rpId, allowCredentials, …}} für navigator.credentials.get() |
| POST | /auth/webauthn/login | {ceremonyId, credentialId, authenticatorData, clientDataJSON, signature, userHandle?} | Public; verifiziert die Assertion (webauthn4j) → {accessToken, refreshToken, user} |
| POST | /auth/webauthn/register/options | – | Auth; {ceremonyId, publicKey:{challenge, rp, user, pubKeyCredParams, …}} für navigator.credentials.create() |
| POST | /auth/webauthn/register | {ceremonyId, attestationObject, clientDataJSON, transports?, label?} | Auth; speichert den Passkey → 204 |
| GET/DELETE | /auth/webauthn/credentials[/{id}] | – | Auth; eigene Passkeys auflisten/entfernen |
Passkeys (WebAuthn): Login passwortlos per Passkey; alle Binärfelder base64url-codiert. Challenges liegen kurz (5 min) serverseitig in-memory, geschlüsselt per ceremonyId. RP-Id = nexus.webauthn.rp-id oder der Origin-Host.
User-Objekt: {id, email, displayName, roles: [{id,name}], permissions: ["bansystem.view", ...], totpEnabled}
Players (PlayerManagement)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | `/players?query=<name | uuid>&cursor=&limit=` |
| GET | /players/{id} | Spielerakte: {id, name, game, gameId, online, currentServer, firstSeen, lastSeen, playtimeSeconds, sessions: [{serverId, serverName, joinedAt, leftAt}], notes: [{id, author, text, createdAt}]} |
| GET | /players/overview | Quick-Stats: {total, online, newLast7d}; Permission core.player.view |
| POST | /players/{id}/kick | {reason} → 202; Permission core.player.kick; auditiert |
| POST | /players/{id}/message | {message} → 202 (Ingame-Nachricht via Bridge); Permission core.player.message; auditiert |
| POST | /players/{id}/notes | {text} → Note; Permission playermanagement.notes.write |
| GET | /players/{id}/ips | IP-Historie: {items: [{ip, firstSeen, lastSeen, joinCount}]}; Permission playermanagement.ip.view; jeder Zugriff wird auditiert (DSGVO) |
| GET | /players/{id}/alts | Alt-Account-Erkennung über geteilte IPs: {items: [{playerId, name, gameId, online, firstSeen, lastSeen, sharedIps, sharedJoins, confidence}]}; Permission playermanagement.ip.view; auditiert |
| GET | /players/ranks | Vault-Rang-Katalog {items: [{name, color}]} – color automatisch aus LuckPerms/Permission-System (Gruppen-Prefix); Permission core.player.view |
| POST | /players/{id}/rank | {rank, exclusive?} → setzt den Ingame-Rang via Vault-Bridge (command.setrank {gameId, rank, exclusive}). exclusive=false (Default) fügt die Gruppe hinzu (vorhandene Ränge bleiben); exclusive=true ersetzt sie (für Team-Rollen). Permission playermanagement.rank.set; auditiert |
Vault-Rang-Bridge: Der Ingame-Primärrang (Vault-Gruppe) wird beim player.join als Feld rank hochgemeldet und im Player gespeichert; das Plugin meldet seinen Gruppen-Katalog per vault.groups. Setzt das ACP einen Rang, sendet der Core command.setrank {gameId, rank} an den Server, wo die Vault-Bridge die Gruppe setzt. Spieler-DTO enthält rank.
Die Bridge sendet beim player.join optional das Feld ip; der Core speichert es pro Session (player_sessions.player_ip) und aggregiert es in player_ip_history. Der DSGVO-Export (/ucp/me/data-export) enthält die IP-Historie des eigenen Accounts.
Servers (Bridges)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /servers | `{items: [{id, name, type: "PAPER" |
| POST | /servers/tokens | {name} → {token} Einmal-Token für Bridge-Setup; Permission core.server.manage |
| POST | /servers/{id}/broadcast | {message} → 202 |
Servers (Gruppen)
| POST | /servers/{id}/group | {group} – Server einer Gruppe zuordnen (leer = entfernen); Permission core.server.manage. Config-Scopes lösen über die Gruppe auf |
Modules
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /modules | `{items: [{id, name, version, description, enabled, health: "OK" |
| POST | /modules/{id}/enable / /disable | 204; Premium ohne Entitlement → 403 NX-2001 LICENSE_REQUIRED |
| GET | /modules/{id}/config?scope= | Schema-getriebene Konfiguration an einem Scope (16.2): {scope, items: [{key, type, defaultValue, description, value, origin, changed}]} – origin zeigt die Herkunft des wirksamen Werts (default/global/group:<g>/server:<s>) |
| PUT | /modules/{id}/config?scope= | {values: {key: value}} – Scope global (Default), group:<name> oder server:<name>; leerer Wert an Gruppe/Server entfernt den Override (Vererbung); validiert, Audit-Diff, Event config.changed (Hot-Reload). Auflösung: Server → Gruppe des Servers → Global → Default |
Roles & Team
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /permissions | Alle registrierten Permissions, gruppiert: {items: [{key, module, description}]} |
| GET/POST | /roles | {id, name, permissions: [..], system, teamRole, requireTotp, ingameRank, color, colorOverride}; POST/PUT {name, permissions, ingameRank?, color?, teamRole?, requireTotp?}. Systemrollen sind editierbar (Name gesperrt, Rechte/Rang/Farbe/Flags änderbar; nur Löschen blockiert). color = ACP-Override, sonst Auto-Farbe der Vault-Gruppe. teamRole steuert, wer als Staff zählt; requireTotp erzwingt 2FA-Setup beim nächsten ACP-Login |
| PUT/DELETE | /roles/{id} | Update/Löschen (System-Rollen editierbar, aber nicht löschbar → 409 NX-1102); ein Account verlinkt genau einen MC-Account (1:1) |
| GET | /team | ACP-Accounts: {id, email, displayName, roles, roleIds, totpEnabled, totpRequired, lastLoginAt, identities: [{playerId, name, gameId, online, rank}]} – inkl. verknüpfter Spiel-Identitäten mit Ingame-Rang |
| POST | /team/invite | {email, roleIds} → Einladung (Dev: Account mit Temp-Passwort in Antwort) |
| PUT | /team/{id}/roles | {roleIds} → ersetzt die ACP-Rollen eines Mitglieds (Team-Rollen exklusiv); Permission core.team.manage; auditiert |
Audit-Log
| GET | /audit?actor=&action=&module=&from=&to=&cursor= | {items: [{id, at, actorEmail, action, module, targetType, targetId, oldValue, newValue, ip, chainHash}]} |
License
| GET | /license | {plan, status, expiresAt, gracePeriod, limits: {servers, players, apiRph}, features: [..], modules: [..]} |
| POST | /license/activate | {key} |
Modul: BanSystem (/modules/bansystem/...)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /modules/bansystem/bans?playerId=&active=&type=&cursor= | {items: [Ban]} |
| POST | /modules/bansystem/bans | `{playerId, type: "BAN" |
| DELETE | /modules/bansystem/bans/{id} | Revoke ({reason} im Body); Permission bansystem.ban.revoke |
| GET/POST | /modules/bansystem/templates | Strafvorlagen {id, name, type, reason, durationSeconds}; POST braucht bansystem.templates.manage |
| DELETE | /modules/bansystem/templates/{id} | Vorlage löschen; Permission bansystem.templates.manage |
Ban-Objekt: {id, playerId, playerName, type, reason, createdAt, expiresAt, active, revokedAt, revokedBy, revokeReason, actorEmail}
Ingame-Ban-Modul (Bridge, nicht im Core): /ban, /tempban, /mute, /tempmute, /kick, /warn, /unban, /unmute senden punishment.request/punishment.revoke über die Bridge; der Core legt die Strafe via PunishmentCreator-SPI an. Neue Strafen werden über ban.created an alle Bridges verteilt und bei BAN/TEMPBAN/KICK sofort gegen Online-Spieler durchgesetzt (Kick), nicht erst beim nächsten Login. /punish <Spieler> <Vorlage> wendet eine Strafvorlage per Name an (Core löst Typ/Grund/Dauer auf). Ingame-/report <Spieler> <VorlagenName> übernimmt Kategorie + Grund der Report-Vorlage.
Modul: ReportSystem (/modules/reports/...)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /modules/reports?status=&playerId=&limit= | {items: [Report], openCount}; Permission reports.view |
| POST | /modules/reports/{id}/claim | Claim-Workflow; teleportiert die Ingame-Identität des Bearbeiters zum Ziel (sofern beide online); Permission reports.manage |
| POST | /modules/reports/{id}/resolve | {resolution, dismiss?} → RESOLVED/DISMISSED |
| GET/POST | /modules/reports/templates | Report-Vorlagen {id, name, category, reason, suggestedPunishment?, suggestedDurationSeconds?}; POST braucht reports.templates.manage |
| DELETE | /modules/reports/templates/{id} | Vorlage löschen; Permission reports.templates.manage |
Report-Objekt: {id, reporterPlayerId?, reporterName, targetPlayerId?, targetName, category, reason, serverName, status: "OPEN"|"CLAIMED"|"RESOLVED"|"DISMISSED", claimedBy?, resolution?, chatLog?, createdAt, resolvedAt?}. Ingame-Quellen: /report <Spieler> <Grund> (Kategorie GENERAL) und /reportchat <Spieler> [Grund] (Kategorie CHAT) – Bridge-Event player.report mit optionalem Feld category. Nur CHAT-Reports bekommen einen chatLog-Snapshot der letzten Chat-Zeilen des Ziels (via ChatLogProvider-SPI), der im Report-Detail direkt angezeigt wird. Eine Report-Vorlage mit Kategorie CHAT erzeugt ebenfalls CHAT-Reports. Spam-Schutz: max. offene Reports pro Melder (Config spam.max-open-per-reporter).
Modul: TicketSystem (/modules/tickets/...)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /modules/tickets?status=&limit= | Staff-Inbox {items: [Ticket], openCount}; Permission tickets.view |
| GET | /modules/tickets/{id} | Ticket inkl. messages (auch interne Notizen) |
| POST | /modules/tickets/{id}/reply | {body, internal?}; Permission tickets.manage |
| POST | /modules/tickets/{id}/assign | {assignee} |
| POST | /modules/tickets/{id}/close | Schließen |
| GET/POST | /modules/tickets/my | Self-Service (UCP): eigene Tickets / `{subject, category: "UNBAN" |
| GET | /modules/tickets/my/{id} | Eigenes Ticket inkl. Verlauf (ohne interne Notizen) |
| POST | /modules/tickets/my/{id}/reply / /close / /rating | Antworten, schließen, bewerten ({rating: 1-5}, nur nach Schließung) |
Entbannungsanträge (ban.appeal.created) haben ihren eigenen Appeals-Posteingang und erzeugen daher standardmäßig kein Ticket. Wer möchte, kann pro Antrag ein UNBAN-Ticket spiegeln lassen (Opt-in via Config appeals.create-tickets, Soft-Dependency über den Event-Bus).
Modul: ChatGuard (/modules/chatguard/...)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /modules/chatguard/messages?playerId=&q=&flagged=&limit= | Durchsuchbares Chat-Archiv; Permission chatguard.view |
| GET/POST | /modules/chatguard/rules | Filterregeln `{name, type: "WORD" |
| DELETE | /modules/chatguard/rules/{id} | Regel löschen |
WORD-Regeln matchen leetspeak-normalisiert; Caps-/Wiederholungs-Spam wird engine-seitig erkannt. WARN/MUTE eskalieren über die PunishmentCreator-SPI ins BanSystem (ohne BanSystem: Degradierung zu FLAG). Aufbewahrung per Retention-Job (Config retention-days, DSGVO).
Modul: Statistics (/modules/stats/...)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /modules/stats/overview | Netzwerk-Analytics: {trackedPlayers, totalJoins, totalPlaytimeSeconds, dailyJoins: {date: n}, hourlyJoins: {"00".."23": n}}; Permission stats.view |
| GET | `/modules/stats/leaderboard?metric=playtime | joins` |
| GET | /modules/stats/me | Persönliche Stats über alle verknüpften Identitäten (UCP) |
Modul: Maintenance (/modules/maintenance/...)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /modules/maintenance/state | {active, message}; Permission maintenance.view |
| GET/POST | /modules/maintenance/whitelist | Whitelist-Einträge {playerName, durationSeconds?} (zeitlich begrenzt möglich); Schreiben: maintenance.manage |
| DELETE | /modules/maintenance/whitelist/{id} | Eintrag entfernen |
Der Wartungs-Schalter selbst ist Modul-Config (PUT /modules/maintenance/config, Keys maintenance.enabled/maintenance.message) – ein Mechanismus für alles (Audit, Hot-Reload). Beim Join werden Nicht-Whitelist-Spieler über die PlayerActions-SPI gekickt; Event maintenance.blocked.
Modul: Friends (/modules/friends/my/..., Self-Service)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /modules/friends/my | {friends, incoming, outgoing, blocked} – Freund-Objekte {id, displayName, email, status, online, since}; online = irgendeine verknüpfte Spiel-Identität ist online (netzwerkweit) |
| POST | /modules/friends/my/requests | {email} → PENDING-Anfrage; geblockte Anfragende erhalten dieselbe Antwort wie „Account nicht gefunden“ |
| POST | /modules/friends/my/requests/{id}/accept / /decline | Anfrage beantworten (nur Adressat) |
| DELETE | /modules/friends/my/{id} | Freundschaft/Anfrage entfernen |
| POST | /modules/friends/my/block | {email} → blockt (entfernt bestehende Relation, verhindert neue Anfragen) |
Events: friend.requested, friend.accepted, friend.removed. Party (gemeinsamer Serverwechsel) folgt mit der Proxy-Move-Capability der Bridges.
Modul: AntiCheat-Hub (/modules/anticheat-hub/..., Pro)
Premium-Modul: ohne Lizenz-Entitlement anticheat-hub ist die Aktivierung gesperrt (403 NX-2001).
| Methode | Pfad | Beschreibung |
|---|---|---|
| POST | /modules/anticheat-hub/violations | Adapter-API für Anticheats: `{gameId? |
| GET | /modules/anticheat-hub/violations?playerId=&limit= | Violations (Spielerakte/Übersicht); Permission anticheat.view |
Korrelation: Severity-Summe im Fenster (Config correlation.window-hours); ab escalation.threshold automatischer Tempban über die PunishmentCreator-SPI, einmal pro Fenster (Marker-Eintrag ESCALATION). Events: anticheat.violation, anticheat.escalated.
Modul: Vote & Rewards (/modules/votes/...)
| Methode | Pfad | Beschreibung |
|---|---|---|
| POST | /modules/votes/hook/{siteId} | Vote-Callback (öffentlich, Auth per Header X-Vote-Token der Seite): {playerName} → {streak, totalVotes} |
| GET | /modules/votes/sites | UCP: aktive Vote-Links + eigene Streak/Gesamt über verknüpfte Identitäten |
| GET/POST | /modules/votes/admin/sites | ACP: Seiten verwalten; POST {name, url} erzeugt Token + Hook-Pfad; Permission votes.manage |
| DELETE | /modules/votes/admin/sites/{id} | Seite entfernen |
| GET | /modules/votes/admin/top | Top-Voter (Gesamt, Streak, zuletzt); Permission votes.view |
Streak = aufeinanderfolgende UTC-Tage mit Vote. Jeder Vote publiziert vote.received (für Reward-Ketten/Webhooks), dankt dem Spieler ingame und broadcastet optional (Config).
Modul: Shop (/modules/shop/..., Pro)
Premium-Modul (Entitlement shop). Bestellungen sind eine Zustandsmaschine PENDING_PAYMENT → PAID → DELIVERED (oder CANCELLED); PAID-Bestellungen bilden die Command-Queue mit Zustellgarantie: Auslieferung wird alle 30 s wiederholt, bis der Server des Spielers den Konsolen-Befehl annimmt (Capability EXECUTE, Bridge-Frame command.execute).
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /modules/shop/catalog | Storefront (UCP): aktive Produkte {id, name, description, priceCents, currency, category} |
| POST | /modules/shop/orders | Checkout: {productId, playerId} – playerId muss eine verknüpfte Identität des Käufers sein |
| GET | /modules/shop/orders/my | Eigene Bestellhistorie |
| GET/POST | /modules/shop/admin/products | ACP: Produkte (POST mit {name, description?, priceCents, currency?, category, deliveryCommand?} – {player} wird ersetzt); Permission shop.manage |
| DELETE | /modules/shop/admin/products/{id} | Produkt entfernen |
| GET | /modules/shop/admin/orders | Alle Bestellungen; Permission shop.view |
| POST | /modules/shop/admin/orders/{id}/mark-paid | Manuelle Zahlungsbestätigung – PSP-Webhooks (Stripe/PayPal) nutzen denselben Übergang |
| POST | /modules/shop/admin/orders/{id}/cancel | Stornieren (nicht nach Auslieferung) |
Zahlungen: Sandbox-Provider (Config payments.sandbox, Default an) bestätigt sofort. Events: shop.order.created/paid/delivered/cancelled. Belege sind Produkt-Snapshots (Preisänderungen wirken nie rückwirkend).
Nachrichten-Editor (/messages)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /messages | Katalog aller spielersichtbaren Texte: `{items: [{key, description, defaultContent, content (Override |
| PUT | /messages/{key} | {content} – Override setzen (auditiert mit Diff) |
| DELETE | /messages/{key} | Override entfernen (zurück zum Standard) |
Permission core.messages.manage. Jede Änderung publiziert messages.changed; der Core konvertiert serverseitig nach Legacy-§ (inkl. §x-Hex; Gradients werden echt gerendert: pro Zeichen interpolierte §x-Hex-Codes, Multi-Stop unterstützt, {platzhalter} bleiben als Token erhalten) und pusht das Bundle als messages.sync an alle verbundenen Bridges – Hot-Reload ohne Server-Neustart. Beim Bridge-Handshake wird das Bundle initial gesendet; die Bridge fällt vor dem ersten Sync auf eingebaute Defaults zurück.
Observability
| GET | /actuator/health | Liveness (öffentlich) |
| GET | /actuator/prometheus | Prometheus-Metriken (öffentlich; hinter Reverse-Proxy ggf. abschirmen) |
Webhooks (/webhooks)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET/POST | /webhooks | `{name, url, secret?, eventPattern (z. B. "ban.*"), format: "GENERIC" |
| PUT/DELETE | /webhooks/{id} | Ändern/Löschen (Hot-Reload, kein Neustart) |
| POST | /webhooks/{id}/test | Sendet ein Test-Event, Antwort enthält delivered |
Zustellung asynchron mit Retry/Backoff; GENERIC ist HMAC-signiert (X-Nexus-Signature: sha256=<hex> über den Body), DISCORD liefert Embeds. embedTemplate (nur DISCORD) ist das JSON eines Embed-Builders (z. B. embed.dan.onl) — ein einzelnes Embed-Objekt oder ein komplettes Payload mit embeds; die Platzhalter {type}, {label}, {at}, {data} und {data.<feld>} werden pro Event ersetzt. eventTitles ist eine Map Event-Typ → Anzeigetitel (z. B. {"ban.created": "Neuer Bann"}): sie bestimmt den Titel des Standard-Embeds und den {label}-Platzhalter. Ohne Template wird das eingebaute Standard-Embed gesendet.
Setup (First-Run-Wizard, öffentlich bis zur Ersteinrichtung)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /setup/status | {needsSetup} – true solange kein Staff-Account existiert |
| POST | /setup | {email, password (≥10), displayName, networkName?} → Owner-Account; danach dauerhaft 400 |
UCP
| Methode | Pfad | Beschreibung |
|---|---|---|
| POST | /ucp/register | {email, password, displayName} |
| POST | /ucp/link | {code} – Einmal-Code aus /nexus link ingame |
| GET | /ucp/me/punishments | Eigene Strafen (über verknüpfte Identitäten) |
| POST | /ucp/me/punishments/{id}/appeal | {message} → Entbannungsantrag |
| GET | /ucp/me/data-export | DSGVO-Export als JSON |
Realtime API (WebSocket)
ws://localhost:8080/api/v1/ws?token=<accessToken>
Nachrichten (Server→Client): { "type": "<event>", "data": {...}, "at": "<iso>" }
Events: player.join, player.quit, ban.created, ban.revoked, server.online, server.offline.
Client→Server: { "type": "subscribe", "events": ["player.*", "ban.*"] } (Default: alles, was die Permissions erlauben).
Standard-Fehlercodes
| Code | Bedeutung |
|---|---|
| NX-1001 | Ungültige Zugangsdaten |
| NX-1002 | 2FA-Code erforderlich |
| NX-1003 | Refresh-Token ungültig/wiederverwendet |
| NX-1042 | Validierungsfehler |
| NX-1101 | Keine Berechtigung |
| NX-1102 | System-Rolle nicht veränderbar |
| NX-1090 | Rate-Limit überschritten (429, Retry-After-Header; Login/Setup/Registrierung pro Client-IP) |
| NX-2001 | Lizenz/Entitlement erforderlich |
| NX-3001 | Bridge-Token ungültig |
Dev-Seed (Profil dev)
- Admin:
[email protected]/admin1234(Rolle Owner, alle Permissions, 2FA aus) - Beispiel-Spieler + Beispiel-Bans werden geseedet.
- CORS erlaubt:
http://localhost:5173,http://localhost:5174.