Organisator kann im selben Browser selbst mitspielen
Ein Aufrufer darf Organisator- und Spieler-Zugang kombinieren (Authorization + X-Player-Token). Berechtigungen prüfen jetzt einzelne Merkmale statt einer Rangstufe. In der Lobby gibt es „Selbst mitspielen?“, Teamlinks funktionieren auch im Organisator-Browser. Online-Status wird direkt nach dem Beitritt aktualisiert.
This commit is contained in:
1 parent
3a74e16bb8
commit
8be8b3d879
15 files changed
+248
-59
No files matched your search
+45
-20
@@ -22,7 +22,6 @@ export const OPEN_STATUSES = ['lobby', 'veto'];
|
||||
export const RUNNING_STATUSES = ['server_setup', 'live'];
|
||||
export const CLOSED_STATUSES = ['finished', 'cancelled'];
|
||||
|
||||
const ROLE_RANK = { public: 0, spectator: 1, player: 2, captain: 3, organizer: 4 };
|
||||
|
||||
function cleanName(value, field, max = 32) {
|
||||
const name = String(value ?? '').replace(/\s+/g, ' ').trim();
|
||||
@@ -91,26 +90,49 @@ export class MatchService extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/** Ermittelt die Rolle eines Tokens für ein Match. Unbekannte Tokens ergeben "public". */
|
||||
resolveViewer(matchId, token) {
|
||||
/**
|
||||
* Ermittelt, wer der Aufrufer in einem Match ist. Ein Browser darf bis zu zwei Zugänge mitschicken
|
||||
* (z. B. Organisator-Token und Spieler-Token), damit der Organisator selbst mitspielen kann.
|
||||
* Unbekannte Tokens werden ignoriert; ohne gültigen Zugang ergibt sich "public".
|
||||
*/
|
||||
resolveViewer(matchId, tokens) {
|
||||
const match = this.getMatch(matchId);
|
||||
const viewer = { matchId, role: 'public', playerId: null, team: null };
|
||||
if (!token) return viewer;
|
||||
if (safeEqual(hashToken(token), match.organizer_token_hash)) return { ...viewer, role: 'organizer' };
|
||||
const player = this.db
|
||||
.prepare('SELECT * FROM players WHERE match_id = ? AND token_hash = ? AND active = 1')
|
||||
.get(matchId, hashToken(token));
|
||||
if (player) {
|
||||
return { ...viewer, role: player.is_captain ? 'captain' : 'player', playerId: player.id, team: player.team };
|
||||
const viewer = {
|
||||
matchId, role: 'public', isOrganizer: false, isSpectator: false, isCaptain: false, playerId: null, team: null,
|
||||
};
|
||||
const list = (Array.isArray(tokens) ? tokens : [tokens]).filter((t) => typeof t === 'string' && t).slice(0, 2);
|
||||
for (const token of list) {
|
||||
if (safeEqual(hashToken(token), match.organizer_token_hash)) {
|
||||
viewer.isOrganizer = true;
|
||||
continue;
|
||||
}
|
||||
const player = this.db
|
||||
.prepare('SELECT * FROM players WHERE match_id = ? AND token_hash = ? AND active = 1')
|
||||
.get(matchId, hashToken(token));
|
||||
if (player) {
|
||||
// Höchstens ein Spieler pro Aufrufer – ein zweiter Spieler-Token wird ignoriert.
|
||||
if (!viewer.playerId) Object.assign(viewer, { playerId: player.id, team: player.team, isCaptain: !!player.is_captain });
|
||||
continue;
|
||||
}
|
||||
if (safeEqual(normalizeCode(token), match.code_spectator)) viewer.isSpectator = true;
|
||||
}
|
||||
if (safeEqual(normalizeCode(token), match.code_spectator)) return { ...viewer, role: 'spectator' };
|
||||
viewer.role = viewer.isOrganizer ? 'organizer'
|
||||
: viewer.isCaptain ? 'captain'
|
||||
: viewer.playerId ? 'player'
|
||||
: viewer.isSpectator ? 'spectator' : 'public';
|
||||
return viewer;
|
||||
}
|
||||
|
||||
require(viewer, minimum) {
|
||||
if (ROLE_RANK[viewer.role] < ROLE_RANK[minimum]) {
|
||||
require(viewer, need) {
|
||||
const ok = {
|
||||
organizer: viewer.isOrganizer,
|
||||
captain: viewer.isCaptain,
|
||||
player: !!viewer.playerId,
|
||||
spectator: viewer.role !== 'public',
|
||||
}[need];
|
||||
if (!ok) {
|
||||
const labels = { organizer: 'den Organisator', captain: 'Team-Captains', player: 'Spieler', spectator: 'Teilnehmer' };
|
||||
throw new HttpError(403, `Diese Aktion ist nur für ${labels[minimum]} erlaubt`, 'forbidden');
|
||||
throw new HttpError(403, `Diese Aktion ist nur für ${labels[need]} erlaubt`, 'forbidden');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +281,7 @@ export class MatchService extends EventEmitter {
|
||||
.prepare('SELECT * FROM players WHERE id = ? AND match_id = ? AND active = 1')
|
||||
.get(playerId, viewer.matchId);
|
||||
if (!target) throw new HttpError(404, 'Spieler nicht gefunden', 'not_found');
|
||||
const allowed = viewer.role === 'organizer' || (viewer.role === 'captain' && viewer.team === target.team);
|
||||
const allowed = viewer.isOrganizer || (viewer.isCaptain && viewer.team === target.team);
|
||||
if (!allowed) throw new HttpError(403, 'Nur der Organisator oder der Captain des Teams kann den Captain ändern', 'forbidden');
|
||||
this.db.prepare('UPDATE players SET is_captain = 0 WHERE match_id = ? AND team = ?').run(viewer.matchId, target.team);
|
||||
this.db.prepare('UPDATE players SET is_captain = 1 WHERE id = ?').run(target.id);
|
||||
@@ -529,7 +551,10 @@ export class MatchService extends EventEmitter {
|
||||
updatedAt: match.updated_at,
|
||||
vetoStart: match.veto_start,
|
||||
currentMapNumber: match.current_map_number,
|
||||
viewer: { role: viewer.role, playerId: viewer.playerId, team: viewer.team },
|
||||
viewer: {
|
||||
role: viewer.role, playerId: viewer.playerId, team: viewer.team,
|
||||
isOrganizer: viewer.isOrganizer, isCaptain: viewer.isCaptain, isSpectator: viewer.isSpectator,
|
||||
},
|
||||
canStartVeto: match.status === 'lobby' && readiness.A.ready && readiness.B.ready,
|
||||
requireFullTeams: this.config.requireFullTeams,
|
||||
maps: pool.maps,
|
||||
@@ -561,7 +586,7 @@ export class MatchService extends EventEmitter {
|
||||
.all(matchId),
|
||||
};
|
||||
|
||||
if (viewer.role === 'organizer') {
|
||||
if (viewer.isOrganizer) {
|
||||
const base = this.config.publicUrl;
|
||||
snapshot.organizer = {
|
||||
codes: { A: formatCode(match.code_a), B: formatCode(match.code_b), spectator: formatCode(match.code_spectator) },
|
||||
@@ -591,8 +616,8 @@ export class MatchService extends EventEmitter {
|
||||
const configured = row.status === 'configured';
|
||||
if (!configured || viewer.role === 'public') return view;
|
||||
|
||||
const isTeam = ['player', 'captain', 'organizer'].includes(viewer.role);
|
||||
const isSpectatorView = ['spectator', 'organizer'].includes(viewer.role);
|
||||
const isTeam = viewer.isOrganizer || !!viewer.playerId;
|
||||
const isSpectatorView = viewer.isOrganizer || viewer.isSpectator;
|
||||
if (isTeam) {
|
||||
view.connect = {
|
||||
host: row.host,
|
||||
|
||||
Reference in new issue
Block a user