Skip to content

Commit

Permalink
Merge pull request #302 from dataforgoodfr/fix/D4G-295-prevent-player…
Browse files Browse the repository at this point in the history
…-access-to-game-console

Fix/d4g 295 prevent player access to game console
  • Loading branch information
Baboo7 authored Jul 7, 2023
2 parents 5684b06 + 7649635 commit 6d12fb8
Show file tree
Hide file tree
Showing 16 changed files with 173 additions and 85 deletions.
4 changes: 2 additions & 2 deletions packages/client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AuthenticatedApp } from "./AuthenticatedApp";
import { AuthenticatedApp } from "./modules/router/components/AuthenticatedApp";
import { UnauthenticatedApp } from "./modules/router/components/UnauthenticatedApp";
import { useAuth } from "./modules/auth/authProvider";
import { UnauthenticatedApp } from "./UnauthenticatedApp";
import { hotjar } from "react-hotjar";
import { useEffect } from "react";
import { ErrorBoundary } from "./modules/error-handling/ErrorBoundary";
Expand Down
3 changes: 3 additions & 0 deletions packages/client/src/modules/auth/authProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface IAuthContext {
type UserPermissions = {
canAccessAdminList: boolean;
canAccessAdminPanel: boolean;
canAccessGameConsole: boolean;
canAccessTeacherList: boolean;
canEditUserRole: boolean;
};
Expand All @@ -32,6 +33,7 @@ const defaultContext: IAuthContext = {
permissions: {
canAccessAdminList: false,
canAccessAdminPanel: false,
canAccessGameConsole: false,
canAccessTeacherList: false,
canEditUserRole: false,
},
Expand Down Expand Up @@ -102,6 +104,7 @@ function AuthProvider({ children }: { children: React.ReactNode }) {
() => ({
canAccessAdminList: hasRole([RoleNames.ADMIN], user),
canAccessAdminPanel: hasRole([RoleNames.ADMIN, RoleNames.TEACHER], user),
canAccessGameConsole: hasRole([RoleNames.ADMIN, RoleNames.TEACHER], user),
canAccessTeacherList: hasRole([RoleNames.ADMIN], user),
canEditUserRole: hasRole([RoleNames.ADMIN], user),
}),
Expand Down
39 changes: 23 additions & 16 deletions packages/client/src/modules/play/context/playContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ function PlayProvider({ children }: { children: React.ReactNode }) {
} = usePlayStore();
const { socket } = useGameSocket({
gameId,
isUserPlayer: user?.role.name === "player",
token,
setConsumptionActions,
setGame,
Expand All @@ -144,18 +145,17 @@ function PlayProvider({ children }: { children: React.ReactNode }) {
[setPlayers]
);

const updateGame = useCallback(
(update: Partial<IGame>) => {
socket?.emit("game:update", { update });
},
[socket]
);

if (!isInitialised || !game || !socket) {
return <CircularProgress color="secondary" sx={{ margin: "auto" }} />;
}

const updateGame = (update: Partial<IGame>) => {
setGame((previous) => {
if (previous === null) return null;
return { ...previous, ...update };
});
socket.emit("game:update", { update });
};

const updatePlayerActions = (
playerActions: {
isPerformed: boolean;
Expand Down Expand Up @@ -369,6 +369,7 @@ function useCurrentStep(): GameStep | null {

function useGameSocket({
gameId,
isUserPlayer,
token,
setConsumptionActions,
setGame,
Expand All @@ -378,6 +379,7 @@ function useGameSocket({
setTeams,
}: {
gameId: number;
isUserPlayer: boolean;
token: string | null;
setConsumptionActions: React.Dispatch<React.SetStateAction<Action[]>>;
setGame: React.Dispatch<React.SetStateAction<IGame | null>>;
Expand Down Expand Up @@ -417,16 +419,20 @@ function useGameSocket({
if (previous === null) {
return null;
}
if (previous.status !== "finished" && update.status === "finished") {
navigate("/play");
}

if (
update.lastFinishedStep &&
update.lastFinishedStep !== previous.lastFinishedStep
) {
navigate(`/play/games/${previous.id}/persona/stats`);
if (isUserPlayer) {
if (previous.status !== "finished" && update.status === "finished") {
navigate("/play");
}

if (
update.lastFinishedStep &&
update.lastFinishedStep !== previous.lastFinishedStep
) {
navigate(`/play/games/${previous.id}/persona/stats`);
}
}

return { ...previous, ...update };
});
});
Expand Down Expand Up @@ -482,6 +488,7 @@ function useGameSocket({
};
}, [
gameId,
isUserPlayer,
token,
/**
* Don't include `navigate` in dependencies since it changes on every route change,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Navigate, Route, Routes } from "react-router-dom";
import { useAuth } from "./modules/auth/authProvider";
import { useAuth } from "../../../auth/authProvider";
import {
Admins,
Players,
Expand All @@ -8,22 +8,24 @@ import {
Layout as AdministrationLayout,
Settings,
Teachers,
} from "./modules/administration";
} from "../../../administration";
import {
GameConsoleView,
MyGames,
PlayerPersona,
PlayLayout,
PlayerActionsPage,
Stats,
} from "./modules/play";
} from "../../../play";
import {
PersonalizationLayout,
PersonalizationChoice,
} from "./modules/play/Personalization";
import { PersonalizationForm } from "./modules/play/Personalization/PersonalizationForm";
import { PersonalizationPredefinedPersona } from "./modules/play/Personalization/PersonalizationPredefinedPersona";
import { FormVerification } from "./modules/administration/Games/Game/FormVerification";
} from "../../../play/Personalization";
import { PersonalizationForm } from "../../../play/Personalization/PersonalizationForm";
import { PersonalizationPredefinedPersona } from "../../../play/Personalization/PersonalizationPredefinedPersona";
import { FormVerification } from "../../../administration/Games/Game/FormVerification";
import { RouteGuard } from "../RouteGuard";
import { gameConsoleGuard } from "../../guards/gameConsoleGuard";

export { AuthenticatedApp };

Expand Down Expand Up @@ -63,7 +65,13 @@ function AuthenticatedApp() {
path="games/:id/persona/actions"
element={<PlayerActionsPage />}
/>
<Route path="games/:id/console" element={<GameConsoleView />} />
<Route
path="games/:id/console"
element={<RouteGuard guard={gameConsoleGuard} />}
>
<Route path="" element={<GameConsoleView />} />
<Route path="*" element={<GameConsoleView />} />
</Route>
<Route path="" element={<Navigate to="my-games" />} />
<Route path="*" element={<Navigate to="my-games" />} />
</Route>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AuthenticatedApp } from "./AuthenticatedApp";
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../../../auth/authProvider";
import { Guard } from "../../types/guard";

export { RouteGuard };

function RouteGuard({ guard }: { guard: Guard }) {
const { permissions } = useAuth();

if (!guard(permissions)) {
return <Navigate to="/" />;
}

return <Outlet />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { RouteGuard } from "./RouteGuard";
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Routes, Route, Navigate } from "react-router-dom";
import OgreHeader from "./modules/common/components/OgreHeader";
import MagicLink from "./modules/magic-link";
import Signup from "./modules/signup";
import { theme } from "./utils/theme";
import SignIn from "./modules/sign-in/SignIn";
import OgreHeader from "../../../common/components/OgreHeader";
import MagicLink from "../../../magic-link";
import Signup from "../../../signup";
import { theme } from "../../../../utils/theme";
import SignIn from "../../../sign-in/SignIn";

export { UnauthenticatedApp };

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { UnauthenticatedApp } from "./UnauthenticatedApp";
6 changes: 6 additions & 0 deletions packages/client/src/modules/router/guards/gameConsoleGuard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Guard } from "../types/guard";

export { gameConsoleGuard };

const gameConsoleGuard: Guard = (permissions) =>
permissions.canAccessGameConsole;
5 changes: 5 additions & 0 deletions packages/client/src/modules/router/types/guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { UserPermissions } from "../../auth/authProvider";

export type { Guard };

type Guard = (permissions: UserPermissions) => boolean;
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ function handleJoinGame(io: Server, socket: Socket) {
const game = await gameServices.findOne(gameId);
invariant(game, `Could not find game ${gameId}`);

const userRole = await roleServices.findOne(user.roleId);
invariant(userRole, `User ${user.id} has no roles`);

socket.data.gameId = gameId;
socket.data.user = user;
socket.data.role = userRole;

let gameInitData: GameInitEmitted;
if (await hasPlayerAccess(user, game)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,62 +4,75 @@ import { Server, Socket } from "../types";
import { rooms } from "../constants";
import { getSocketData, hasFinishedStep, wrapHandler } from "../utils";
import { gameServices, playerServices } from "../services";
import { buildPermissions } from "../router/permissions";

export { handleUpdateGame };

function handleUpdateGame(io: Server, socket: Socket) {
socket.on(
"game:update",
wrapHandler(async (args: unknown) => {
const schema = z.object({
update: z.object({
step: z.number().optional(),
lastFinishedStep: z.number().optional(),
status: z.enum(["ready", "draft", "playing", "finished"]).optional(),
}),
});
const { update } = schema.parse(args);
wrapHandler(
async () => {
const { gameId, user, role } = getSocketData(socket);
const permissions = buildPermissions(user, role);
invariant(
permissions.canUpdateGame,
`User ${user.id} does not have permissions to update game ${gameId}`
);
},
async (args: unknown) => {
const schema = z.object({
update: z.object({
step: z.number().optional(),
lastFinishedStep: z.number().optional(),
status: z
.enum(["ready", "draft", "playing", "finished"])
.optional(),
}),
});
const { update } = schema.parse(args);

const { gameId } = getSocketData(socket);
const game = await gameServices.findOne(gameId);
invariant(game, `Could not find game with id ${gameId}`);
const { gameId } = getSocketData(socket);
const game = await gameServices.findOne(gameId);
invariant(game, `Could not find game with id ${gameId}`);

const gameUpdated = await gameServices.queries.update({
where: { id: gameId },
data: update,
include: {
players: {
select: {
userId: true,
const gameUpdated = await gameServices.queries.update({
where: { id: gameId },
data: update,
include: {
players: {
select: {
userId: true,
},
},
},
},
});
});

io.to(rooms.game(gameId)).emit("game:update", { update });
io.to(rooms.game(gameId)).emit("game:update", { update });

const hasGameFinishedStep = hasFinishedStep(gameUpdated);
const hasPreviousGameFinishedStep = hasFinishedStep(game);
const hasGameFinishedStep = hasFinishedStep(gameUpdated);
const hasPreviousGameFinishedStep = hasFinishedStep(game);

if (hasPreviousGameFinishedStep !== hasGameFinishedStep) {
await playerServices.updateMany(gameId, {
hasFinishedStep: hasGameFinishedStep,
});
}
if (hasPreviousGameFinishedStep !== hasGameFinishedStep) {
await playerServices.updateMany(gameId, {
hasFinishedStep: hasGameFinishedStep,
});
}

const playerUpdates = gameUpdated.players.map((p) => ({
userId: p.userId,
hasFinishedStep: hasGameFinishedStep,
}));
const playerUpdates = gameUpdated.players.map((p) => ({
userId: p.userId,
hasFinishedStep: hasGameFinishedStep,
}));

playerUpdates.forEach((playerUpdate) => {
io.to(rooms.user(gameId, playerUpdate.userId)).emit("player:update", {
updates: [playerUpdate],
playerUpdates.forEach((playerUpdate) => {
io.to(rooms.user(gameId, playerUpdate.userId)).emit("player:update", {
updates: [playerUpdate],
});
});
});
io.to(rooms.teachers(gameId)).emit("player:update", {
updates: playerUpdates,
});
})
io.to(rooms.teachers(gameId)).emit("player:update", {
updates: playerUpdates,
});
}
)
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Role, User } from "@prisma/client";

export { buildPermissions };

function buildPermissions(user: User, role: Role) {
return {
canUpdateGame: ["admin", "teacher"].includes(role.name),
};
}
2 changes: 2 additions & 0 deletions packages/server/src/modules/websocket/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Players,
ProductionAction,
Profile,
Role,
Team,
TeamActions,
} from "@prisma/client";
Expand Down Expand Up @@ -65,6 +66,7 @@ type ServerSideEvents = any;
type SocketData = {
gameId: number;
user: User;
role: Role;
};

type Server = ServerLib<ListenEvents, EmitEvents, ServerSideEvents, SocketData>;
Expand Down
Loading

0 comments on commit 6d12fb8

Please sign in to comment.