feat: admin controls
This commit is contained in:
@@ -40,9 +40,11 @@ interface ConnectionContextValue {
|
||||
username: string;
|
||||
status: ConnectionStatus;
|
||||
isInMatch: boolean;
|
||||
isAdmin: boolean;
|
||||
reconnectAttempts: number;
|
||||
shouldRedirectToConnect: boolean;
|
||||
becomePlayer: (username: string) => void;
|
||||
authenticateAdmin: (password: string) => boolean;
|
||||
connect: (options: ConnectOptions) => void;
|
||||
disconnect: () => void;
|
||||
send: (message: string) => boolean;
|
||||
@@ -68,6 +70,7 @@ export function ConnectionProvider({
|
||||
const [username, setUsername] = useState("");
|
||||
const [status, setStatus] = useState<ConnectionStatus>("idle");
|
||||
const [isInMatch, setIsInMatch] = useState(false);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [reconnectAttempts, setReconnectAttempts] = useState(0);
|
||||
const [shouldRedirectToConnect, setShouldRedirectToConnect] = useState(false);
|
||||
|
||||
@@ -169,25 +172,33 @@ export function ConnectionProvider({
|
||||
setRole("observer");
|
||||
setShouldRedirectToConnect(false);
|
||||
setStatus("connected");
|
||||
setIsAdmin(false);
|
||||
}
|
||||
|
||||
if (parsed.type === "CONNECT_ACK") {
|
||||
setRole("player");
|
||||
setIsAdmin(false);
|
||||
}
|
||||
|
||||
if (parsed.type === "RECONNECT_ACK") {
|
||||
clearReconnectState();
|
||||
setShouldRedirectToConnect(false);
|
||||
setStatus("connected");
|
||||
setIsAdmin(false);
|
||||
}
|
||||
|
||||
if (parsed.type === "DISCONNECT_ACK") {
|
||||
setRole("observer");
|
||||
setUsername("");
|
||||
setIsAdmin(false);
|
||||
isInMatchRef.current = false;
|
||||
setIsInMatch(false);
|
||||
}
|
||||
|
||||
if (parsed.type === "ADMIN_AUTH_ACK") {
|
||||
setIsAdmin(true);
|
||||
}
|
||||
|
||||
if (parsed.type === "GAME_START") {
|
||||
isInMatchRef.current = true;
|
||||
setIsInMatch(true);
|
||||
@@ -286,6 +297,7 @@ export function ConnectionProvider({
|
||||
clearReconnectState();
|
||||
isInMatchRef.current = false;
|
||||
setIsInMatch(false);
|
||||
setIsAdmin(false);
|
||||
setStatus("connecting");
|
||||
|
||||
openSocket(false);
|
||||
@@ -293,6 +305,15 @@ export function ConnectionProvider({
|
||||
[clearReconnectState, openSocket],
|
||||
);
|
||||
|
||||
const send = useCallback((message: string) => {
|
||||
if (wsRef.current?.readyState !== WebSocket.OPEN) return false;
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log("Sending: " + message);
|
||||
}
|
||||
wsRef.current.send(message);
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const becomePlayer = useCallback(
|
||||
(username: string) => {
|
||||
const resolvedUsername = (username ?? "").trim();
|
||||
@@ -300,9 +321,19 @@ export function ConnectionProvider({
|
||||
setUsername(resolvedUsername);
|
||||
isInMatchRef.current = false;
|
||||
setIsInMatch(false);
|
||||
setIsAdmin(false);
|
||||
send(cmd.connect(resolvedUsername));
|
||||
},
|
||||
[clearReconnectState, openSocket],
|
||||
[send],
|
||||
);
|
||||
|
||||
const authenticateAdmin = useCallback(
|
||||
(password: string) => {
|
||||
const trimmed = password.trim();
|
||||
if (!trimmed) return false;
|
||||
return send(cmd.adminAuth(trimmed));
|
||||
},
|
||||
[send],
|
||||
);
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
@@ -315,19 +346,11 @@ export function ConnectionProvider({
|
||||
setStatus("idle");
|
||||
setUsername("");
|
||||
setIsInMatch(false);
|
||||
setIsAdmin(false);
|
||||
isInMatchRef.current = false;
|
||||
setShouldRedirectToConnect(false);
|
||||
}, [clearReconnectState, safeCloseSocket]);
|
||||
|
||||
const send = useCallback((message: string) => {
|
||||
if (wsRef.current?.readyState !== WebSocket.OPEN) return false;
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log("Sending: " + message);
|
||||
}
|
||||
wsRef.current.send(message);
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const subscribe = useCallback((listener: MessageListener) => {
|
||||
listenersRef.current.add(listener);
|
||||
return () => {
|
||||
@@ -354,9 +377,11 @@ export function ConnectionProvider({
|
||||
username,
|
||||
status,
|
||||
isInMatch,
|
||||
isAdmin,
|
||||
reconnectAttempts,
|
||||
shouldRedirectToConnect,
|
||||
becomePlayer,
|
||||
authenticateAdmin,
|
||||
connect,
|
||||
disconnect,
|
||||
send,
|
||||
@@ -369,8 +394,11 @@ export function ConnectionProvider({
|
||||
username,
|
||||
status,
|
||||
isInMatch,
|
||||
isAdmin,
|
||||
reconnectAttempts,
|
||||
shouldRedirectToConnect,
|
||||
becomePlayer,
|
||||
authenticateAdmin,
|
||||
connect,
|
||||
disconnect,
|
||||
send,
|
||||
|
||||
@@ -22,6 +22,11 @@ export interface MoveEntry {
|
||||
column: number;
|
||||
}
|
||||
|
||||
export interface ReservationEntry {
|
||||
player1: string;
|
||||
player2: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_WS_URL =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "ws://localhost:8080"
|
||||
@@ -69,6 +74,9 @@ export type ParsedMessage =
|
||||
| { type: "TOURNAMENT_WINNER"; username: string }
|
||||
| { type: "TOURNAMENT_END" }
|
||||
| { type: "ADMIN_AUTH_ACK" }
|
||||
| { type: "RESERVATION_ADD"; player1: string; player2: string }
|
||||
| { type: "RESERVATION_DELETE"; player1: string; player2: string }
|
||||
| { type: "RESERVATION_LIST"; reservations: ReservationEntry[] }
|
||||
| { type: "GET_DATA"; key: string; value: string }
|
||||
| { type: "SET_DATA_ACK"; key: string }
|
||||
| { type: "ERROR"; message: string }
|
||||
@@ -257,6 +265,36 @@ export function parseMessage(raw: string): ParsedMessage {
|
||||
if (parts[2] === "ACK") return { type: "SET_DATA_ACK", key: parts[1] };
|
||||
break;
|
||||
|
||||
case "RESERVATION": {
|
||||
const payload = parts[2] ?? "";
|
||||
|
||||
if (parts[1] === "ADD" || parts[1] === "DELETE") {
|
||||
const [player1, player2] = payload.split(",");
|
||||
if (player1 && player2) {
|
||||
return {
|
||||
type: parts[1] === "ADD" ? "RESERVATION_ADD" : "RESERVATION_DELETE",
|
||||
player1,
|
||||
player2,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (parts[1] === "LIST") {
|
||||
const reservations =
|
||||
payload.length === 0
|
||||
? []
|
||||
: payload
|
||||
.split("|")
|
||||
.map((entry) => {
|
||||
const [player1, player2] = entry.split(",");
|
||||
return player1 && player2 ? { player1, player2 } : null;
|
||||
})
|
||||
.filter((entry): entry is ReservationEntry => entry !== null);
|
||||
return { type: "RESERVATION_LIST", reservations };
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "ERROR":
|
||||
return { type: "ERROR", message: raw };
|
||||
}
|
||||
@@ -283,14 +321,7 @@ export const cmd = {
|
||||
adminKick: (username: string) => `ADMIN:KICK:${username}`,
|
||||
tournamentStart: (type = "RoundRobin") => `TOURNAMENT:START:${type}`,
|
||||
tournamentCancel: () => "TOURNAMENT:CANCEL",
|
||||
getData: (
|
||||
key:
|
||||
| "TOURNAMENT_STATUS"
|
||||
| "TOURNAMENT_DATA"
|
||||
| "MOVE_WAIT"
|
||||
| "DEMO_MODE"
|
||||
| "MAX_TIMEOUT",
|
||||
) => `GET:${key}`,
|
||||
getData: (key: string) => `GET:${key}`,
|
||||
setData: (key: string, value: string) => `SET:${key}:${value}`,
|
||||
reservationAdd: (p1: string, p2: string) => `RESERVATION:ADD:${p1},${p2}`,
|
||||
reservationDelete: (p1: string, p2: string) =>
|
||||
|
||||
Reference in New Issue
Block a user