Gestion admin (contrat gelé)
Deux lanes à fichiers disjoints : admin-server (DB + API + realtime) et
admin-ui (panel client). Les messages protocole (AdminKick/Kicked) sont
déjà dans shared/src/protocol.ts — ne pas les modifier.
Rôles & ban
- Migration
server/migrations/003_admin.sql:ALTER TABLE accounts ADD COLUMN is_admin boolean NOT NULL DEFAULT false;ALTER TABLE accounts ADD COLUMN banned_at timestamptz; role = 'admin'siaccounts.is_adminou username ∈PLAZA_ADMINS(bootstrap env conservé). Recalculé à register/login/authenticate.- Ban :
logind'un compte banni →DbError 403 'account banned'(ajouter 403 aux statuts deDbError) ;authenticate→null(coupe les sessions actives) ; le join Colyseus avec token d'un banni est rejeté.
Db (ajouts à l'interface Db)
listAccounts(search: string, limit: number, offset: number): Promise<AdminAccount[]>;
setBanned(accountId: number, banned: boolean): Promise<void>;
setAdmin(accountId: number, isAdmin: boolean): Promise<void>;
grantPixels(characterId: number, delta: number): Promise<{ pixels: number }>; // solde jamais < 0
listAllRooms(): Promise<AdminRoom[]>;
createSystemRoom(slug: string, name: string, size: number): Promise<StoredRoom>; // slug [a-z0-9-]{3,24}, size 6..24
adminDeleteRoom(roomId: string): Promise<void>; // 'lobby' interdit (400)
interface AdminAccount {
id: number;
username: string;
isAdmin: boolean;
banned: boolean;
characters: { id: number; name: string; pixels: number }[];
}
interface AdminRoom {
id: string; // slug système ou 'r<id>'
name: string;
isSystem: boolean;
owner: string | null; // username du propriétaire
}
Les system rooms créées via createSystemRoom : owner_account_id NULL,
is_system true, définition = template plancher vide (réutiliser
templateDefinition), id ligne DB mais adressée par slug (nouvelle colonne
déjà existante slug — vérifier le schéma 001 et s'y conformer).
API /api/admin/*
Middleware : requireAuth + account.role === 'admin' sinon 403.
| Méthode | Route | Corps | Réponse |
|---|---|---|---|
| GET | /api/admin/players?search=&limit=50&offset=0 | — | { players: AdminAccount[] } |
| POST | /api/admin/players/:id/ban | { banned: boolean } | 204 |
| POST | /api/admin/players/:id/role | { admin: boolean } | 204 |
| POST | /api/admin/characters/:id/pixels | { delta: number } | { pixels } |
| GET | /api/admin/rooms | — | { rooms: AdminRoom[] } |
| POST | /api/admin/rooms | { slug, name, size } | StoredRoom |
| DELETE | /api/admin/rooms/:id | — | 204 |
Un admin ne peut ni se bannir ni se retirer son propre rôle (400).
Realtime (PlazaRoom)
MESSAGE.AdminKick{ sessionId }: émetteur admin sinon ignoré ; la cible reçoitMESSAGE.Kicked{ reason: 'kicked by admin' }puis force-leave.- Ban au join : token authentifié mais compte banni → reject.
Client (lane admin-ui)
client/src/ui/admin.ts+adminStyles.ts: panel 🛡️ (visible seulement sirole === 'admin'via/api/me), deux onglets :- Joueurs : recherche, liste (username, persos + pixels, badge banni), actions ban/déban, +/- pixels par perso, promouvoir/rétrograder, kick des joueurs présents dans la room courante.
- Mondes : liste système + joueurs (owner), créer un monde système (slug/nom/taille), supprimer une room, « Y aller » (join → éditer in-game).
client/src/network.ts: fetchs admin +sendKick(sessionId)+onKicked(toast + retour lobby).- Bouton HUD dans
hud.ts, même style que les panels existants (PanelDeck).