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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
SPP UserandClaude Opus 5.5 committed 2026-10-25 17:53:39 +01:00
1 parent 470b12f232
commit 4152f9c0a6
15 files changed
+248 -59

No files matched your search

+3 -1
View File
@@ -6,7 +6,7 @@ export class ApiError extends Error {
}
}
export async function api(path, { method = 'GET', body, token } = {}) {
export async function api(path, { method = 'GET', body, token, playerToken } = {}) {
let res;
try {
res = await fetch(`/api${path}`, {
@@ -14,6 +14,8 @@ export async function api(path, { method = 'GET', body, token } = {}) {
headers: {
...(body ? { 'content-type': 'application/json' } : {}),
...(token ? { authorization: `Bearer ${token}` } : {}),
// Zweiter Zugang: Spieler-Token des Organisators, wenn er selbst mitspielt
...(playerToken ? { 'x-player-token': playerToken } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
+23 -6
View File
@@ -1,34 +1,51 @@
// Zugangsdaten pro Match im Browser. Nach Neuladen oder Verbindungsabbruch
// meldet sich der Client damit automatisch wieder an.
//
// Aufbau: { token, kind: 'organizer' | 'player' | 'spectator', playerToken? }
// playerToken gibt es nur beim Organisator, wenn er selbst einem Team beigetreten ist.
const KEY = (matchId) => `cs2mv:match:${matchId}`;
const memory = new Map();
export function getAccess(matchId) {
try {
return JSON.parse(localStorage.getItem(KEY(matchId))) ?? null;
return JSON.parse(localStorage.getItem(KEY(matchId))) ?? memory.get(matchId) ?? null;
} catch {
return null;
return memory.get(matchId) ?? null;
}
}
export function setAccess(matchId, access) {
memory.set(matchId, access);
try {
localStorage.setItem(KEY(matchId), JSON.stringify(access));
} catch {
/* privater Modus – Zugang gilt dann nur für diese Sitzung */
}
memory.set(matchId, access);
}
export function clearAccess(matchId) {
memory.delete(matchId);
try {
localStorage.removeItem(KEY(matchId));
} catch {
/* ignorieren */
}
memory.delete(matchId);
}
const memory = new Map();
export function getToken(matchId) {
return (getAccess(matchId) ?? memory.get(matchId))?.token ?? null;
return getAccess(matchId)?.token ?? null;
}
export function getPlayerToken(matchId) {
return getAccess(matchId)?.playerToken ?? null;
}
/** Spieler-Token neben dem Organisator-Zugang speichern bzw. entfernen (null). */
export function setPlayerToken(matchId, playerToken) {
const access = getAccess(matchId);
if (!access) return;
const next = { ...access };
if (playerToken) next.playerToken = playerToken;
else delete next.playerToken;
setAccess(matchId, next);
}
+6 -6
View File
@@ -9,17 +9,17 @@ export function getSocket() {
}
/** Abonniert den Live-Zustand eines Matches. Meldet sich nach jedem Reconnect neu an. */
export function useMatch(matchId, token) {
export function useMatch(matchId, token, playerToken = null) {
const [state, setState] = useState(null);
const [error, setError] = useState(null);
// null = Verbindung wird erstmals aufgebaut (kein Hinweis), false = Verbindung abgerissen
const [connected, setConnected] = useState(null);
const tokenRef = useRef(token);
tokenRef.current = token;
const tokenRef = useRef({ token, playerToken });
tokenRef.current = { token, playerToken };
useEffect(() => {
const socket = getSocket();
const subscribe = () => socket.emit('match:subscribe', { matchId, token: tokenRef.current });
const subscribe = () => socket.emit('match:subscribe', { matchId, ...tokenRef.current });
const onState = (s) => {
if (s.id !== matchId) return;
setState(s);
@@ -40,7 +40,7 @@ export function useMatch(matchId, token) {
// Erster Stand per REST, falls der Socket noch verbindet; neuere Socket-Stände haben Vorrang.
let active = true;
api(`/matches/${matchId}`, { token })
api(`/matches/${matchId}`, { token, playerToken })
.then((s) => active && setState((prev) => (prev && prev.updatedAt >= s.updatedAt ? prev : s)))
.catch((e) => active && setError({ error: e.message, code: e.code }));
@@ -51,7 +51,7 @@ export function useMatch(matchId, token) {
socket.off('connect', onConnect);
socket.off('disconnect', onDisconnect);
};
}, [matchId, token]);
}, [matchId, token, playerToken]);
return { state, error, connected };
}