Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(demo-game): make report's page available for players #78

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/demo-game/src/components/DecisionsDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ function DecisionsDisplayCompact({ segmentDecisions }: DecisionDisplayProps) {
<TableBody>
{segmentDecisions.map((e) => {
return (
<TableRow key={e.segment.id}>
<TableRow key={e.segment?.id}>
<TableCell className="flex text-nowrap">
P{e.period.index + 1} S{e.segment.index + 1}
P{e.period?.index + 1} S{e.segment?.index + 1}
</TableCell>
<TableCell>
<OnOffIcon on={e.decisions.bank} />
Expand Down
5 changes: 5 additions & 0 deletions apps/demo-game/src/graphql/generated/nexus-typegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ export interface NexusGenFieldTypes {
}
Query: { // field return type
game: NexusGenRootTypes['Game'] | null; // Game
gameWithoutFacts: NexusGenRootTypes['Game'] | null; // Game
games: NexusGenRootTypes['Game'][] | null; // [Game!]
learningElement: NexusGenRootTypes['LearningElementState'] | null; // LearningElementState
learningElements: NexusGenRootTypes['LearningElement'][] | null; // [LearningElement!]
Expand Down Expand Up @@ -565,6 +566,7 @@ export interface NexusGenFieldTypeNames {
}
Query: { // field return type name
game: 'Game'
gameWithoutFacts: 'Game'
games: 'Game'
learningElement: 'LearningElementState'
learningElements: 'LearningElement'
Expand Down Expand Up @@ -646,6 +648,9 @@ export interface NexusGenArgTypes {
game: { // args
id?: number | null; // Int
}
gameWithoutFacts: { // args
id?: number | null; // Int
}
learningElement: { // args
id: string; // ID!
}
Expand Down
15 changes: 15 additions & 0 deletions apps/demo-game/src/graphql/generated/ops.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions apps/demo-game/src/graphql/generated/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ type PlayerState {

type Query {
game(id: Int): Game
gameWithoutFacts(id: Int): Game
games: [Game!]
learningElement(id: ID!): LearningElementState
learningElements: [LearningElement!]
Expand Down
25 changes: 25 additions & 0 deletions apps/demo-game/src/graphql/generated/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2838,6 +2838,31 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "gameWithoutFacts",
"description": null,
"args": [
{
"name": "id",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "OBJECT",
"name": "Game",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "games",
"description": null,
Expand Down
4 changes: 2 additions & 2 deletions apps/demo-game/src/pages/admin/games/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ function ManageGame() {
const [isSegmentModalOpen, setIsSegmentModalOpen] = useState(false)

const { data, error, loading } = useQuery(GameDocument, {
variables: { id: Number(router.query.id) },
variables: { id: Number(router.query.id), includeFacts: true },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coming from a security perspective, this is not "secure" as the variable can just be changed and the request sent with true to receive all data (even though probably no student ever would do that for a game :D). to make it secure, we would need two requests and authorization such that one request (including facts) is only allowed for the game master, and one request (excluding facts) is accessible to the student. the authorization would work based on the player role.

even if it is not really critical here, I think it would be worth it to do it the right way so that we have 1) a good example for how it should be done and 2) so that you have seen how it works for all of the other game elements where it is more critical to authorize

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yes, I see what you mean - good point :D!

I took a look and modified it to the best of my knowledge -> I basically check with the useSession hook who is authenticated and based on that I load the corresponding graphql doc.
Changing the variable (in the new case) isAdmin, should not be possible, right?
Please have a look and let me know if it's aligning with your description

pollInterval: 15000,
skip: !router.query.id,
})
Expand Down Expand Up @@ -788,7 +788,7 @@ function ManageGame() {
</div>
<div className="mt-2 flex flex-row gap-2">
{getButton()}
<Link target="_blank" href={`/admin/reports/${game?.id}`}>
<Link target="_blank" href={`/reports/${game?.id}`}>
<Button>Report</Button>
</Link>
</div>
Expand Down
12 changes: 7 additions & 5 deletions apps/demo-game/src/pages/play/cockpit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ function GameLayout({ children }: { children: React.ReactNode }) {
role: data.self.role,
}

const tabs = [
{
name: 'Reports',
href: `/reports/${data?.result?.currentGame?.id}`,
},
]

const sidebar = (
<div id="sidebar" className="flex flex-col justify-between">
<Card
Expand Down Expand Up @@ -218,11 +225,6 @@ function GameLayout({ children }: { children: React.ReactNode }) {
)
}

const tabs = [
{ name: 'Welcome', href: '/play/welcome' },
{ name: 'Cockpit', href: '/play/cockpit' },
]

const colors = [
'hsl(var(--chart-1))',
'hsl(var(--chart-2))',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useQuery } from '@apollo/client'
import {
Game,
GameDocument,
GameWithoutFactsDocument,
SpecificResultsDocument,
} from 'src/graphql/generated/ops'

Expand Down Expand Up @@ -48,6 +49,7 @@ import {
YAxis,
} from 'recharts'

import { useSession } from 'next-auth/react'
import { composeChartData } from '~/lib/analysis'
import { NUM_MONTHS } from '~/lib/constants'

Expand All @@ -71,11 +73,20 @@ function ReportGame() {

const [currPeriod, setCurrPeriod] = useState<number>(0)

const { data, error, loading } = useQuery(GameDocument, {
variables: { id: Number(router.query.id) },
// pollInterval: 15000,
skip: !router.query.id,
})
const { data: sessionData, status: sessionStatus } = useSession()
const isAuthenticated = sessionStatus === 'authenticated'
const isAdmin = sessionData?.user?.role === 'ADMIN'

const skipQuery = !router.query.id || !isAuthenticated

const { data, error, loading } = useQuery(
isAdmin ? GameDocument : GameWithoutFactsDocument,
{
variables: { id: Number(router.query.id) },
// pollInterval: 15000,
skip: skipQuery,
}
)

const {
data: segmentEndResults,
Expand Down Expand Up @@ -258,8 +269,6 @@ function ReportGame() {

const previousPeriodResults = periodEndResults.specificResults

console.log('previousPeriodResults', previousPeriodResults)

const riskReturnPerPeriod = previousPeriodResults.reduce((acc, result) => {
if (!acc[result.period.index]) {
acc[result.period.index] = {
Expand Down Expand Up @@ -308,7 +317,8 @@ function ReportGame() {
segmentEndResultsLoading ||
periodEndResultsLoading ||
!memoizedDataPeriod ||
!memoizedData
!memoizedData ||
skipQuery
) {
return <div>loading...</div>
}
Expand Down
63 changes: 63 additions & 0 deletions packages/platform/public/ops/QGameWithoutFacts.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#import "./FGameData.graphql"
#import "./FPlayerData.graphql"

query GameWithoutFacts($id: Int) {
gameWithoutFacts(id: $id) {
...GameData
activePeriod {
id
activeSegmentIx
activeSegment {
id
countdownExpiresAt
countdownDurationMs
}
segmentCount
segments {
id
}
}
periods {
id

index
activeSegmentIx

segments {
id

index

countdownExpiresAt
countdownDurationMs

learningElements {
id
title
}

storyElements {
id
title
}
}
segmentCount
}
players {
id

isReady

role

number
name

facts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Security concern: Remove facts field from player data

The query still includes the facts field in player data, which could potentially expose future game data. This contradicts the PR's goal of preventing access to future data.

Remove this line to ensure no facts are exposed to non-admin users:

-      facts


experience
experienceToNext
token
}
}
}
10 changes: 10 additions & 0 deletions packages/platform/src/types/Query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ export function generateBaseQueries() {
},
})

t.field('gameWithoutFacts', {
type: Game,
args: {
id: intArg(),
},
async resolve(_, args, ctx) {
return GameService.getGame(args, ctx)
},
})
Comment on lines +30 to +38
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Security concern: Implement facts filtering in resolver

The gameWithoutFacts field uses the same resolver as game, which means it doesn't actually filter out facts at the data access level. This could potentially expose sensitive data if the GraphQL schema's field masking fails.

Consider implementing a separate resolver that explicitly filters out facts:

 t.field('gameWithoutFacts', {
   type: Game,
   args: {
     id: intArg(),
   },
   async resolve(_, args, ctx) {
-    return GameService.getGame(args, ctx)
+    const game = await GameService.getGame(args, ctx)
+    return {
+      ...game,
+      periods: game.periods.map(period => ({
+        ...period,
+        segments: period.segments.map(segment => ({
+          ...segment,
+          facts: undefined
+        }))
+      }))
+    }
   },
 })

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rschlaefli that's an interesting suggestion ... Maybe we only need one graphql doc?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


t.field('result', {
type: PlayerState,
async resolve(_, args, ctx) {
Expand Down
7 changes: 5 additions & 2 deletions packages/ui/src/components/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
NavigationMenuList,
navigationMenuTriggerStyle,
} from '@uzh-bf/design-system/dist/future'
import { Button } from '@uzh-bf/design-system'

interface Props {
tabs: { name: string; href: string }[]
Expand All @@ -23,7 +24,7 @@ function NavBar({
playerLevel,
playerColor,
playerImgPathAvatar,
playerHref = '/play/cockpit',
playerHref = '/play/welcome',
}: Props) {
return (
<>
Expand All @@ -48,7 +49,9 @@ function NavBar({
<div className="text-orange-700">Level {playerLevel}</div>
</div>
<div className="w-10">
<Logo imgPathAvatar={playerImgPathAvatar} color={playerColor} />
<Button className={{ root: 'p-0 hover:' }}>
<Logo imgPathAvatar={playerImgPathAvatar} color={playerColor} />
</Button>
</div>
</div>
</Link>
Expand Down