-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement cheat overlay #57
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
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements a cheat detection and overlay system for the application. The feature detects potential cheating behaviors (opening developer tools and leaving the application window) and blocks users with a warning overlay when cheating is detected.
Key changes include:
- Added GraphQL schema for
CheatRecordentity with queries and mutations to track and manage cheat records - Implemented cheat detection using the
devtools-detectlibrary and window blur events - Created a blocking overlay UI that displays when a user has unresolved cheat records
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| schema.graphql | Defines CheatRecord type, connection types, mutations, and queries for cheat tracking |
| pnpm-lock.yaml | Adds devtools-detect@4.0.2 dependency lock |
| package.json | Adds devtools-detect library for detecting developer tools |
| lib/features.ts | Adds LOCK_USER_ON_LEAVING feature flag with helper function |
| gql/graphql.ts | Auto-generated TypeScript types for CheatRecord GraphQL schema |
| gql/gql.ts | Auto-generated GraphQL document definitions for cheat-related queries and mutations |
| components/cheat/index.tsx | Main cheat detection logic with event listeners and GraphQL integration |
| components/cheat/cheat-overlay.tsx | Blocking UI overlay displayed when user is flagged for cheating |
| app/(app)/layout.tsx | Integrates CheatWrapper component into application layout |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 10 out of 11 changed files in this pull request and generated 7 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <div | ||
| className={` | ||
| flex min-h-svh flex-col items-center justify-center gap-6 | ||
| bg-linear-to-br from-red-50 via-white to-red-100 p-6 |
Copilot
AI
Jan 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CSS class 'bg-linear-to-br' appears to be invalid. Tailwind CSS uses 'bg-gradient-to-br' for bottom-right gradients. This will result in the gradient not being applied to the background.
| bg-linear-to-br from-red-50 via-white to-red-100 p-6 | |
| bg-gradient-to-br from-red-50 via-white to-red-100 p-6 |
| const devtoolsChangeHandler = async () => { | ||
| await createRecord("開啟開發者工具"); | ||
| }; | ||
|
|
||
| const screenLeaveHandler = async () => { | ||
| await createRecord("離開系統"); | ||
| }; | ||
|
|
||
| window.addEventListener("devtoolschange", devtoolsChangeHandler); |
Copilot
AI
Jan 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'devtoolschange' event from the devtools-detect library only fires when devtools are opened or closed, not on every state change. This means if devtools are already open when the page loads, it won't trigger the event. Consider checking the initial devtools state when the component mounts.
| useEffect(() => { | ||
| if (!enable) { | ||
| return; | ||
| } | ||
|
|
||
| let throttle = false; | ||
| let throttleTimeout: ReturnType<typeof setTimeout> | null = null; | ||
|
|
||
| const createRecord = async (reason: string) => { | ||
| if (throttle) { | ||
| return; | ||
| } | ||
| throttle = true; | ||
|
|
||
| await createMyCheatRecord({ variables: { reason } }); | ||
|
|
||
| throttleTimeout = setTimeout(() => { | ||
| throttle = false; | ||
| throttleTimeout = null; | ||
| }, THROTTLE_TIME_MS); | ||
| }; | ||
|
|
||
| const devtoolsChangeHandler = async () => { | ||
| await createRecord("開啟開發者工具"); | ||
| }; | ||
|
|
||
| const screenLeaveHandler = async () => { | ||
| await createRecord("離開系統"); | ||
| }; | ||
|
|
||
| window.addEventListener("devtoolschange", devtoolsChangeHandler); | ||
| window.addEventListener("blur", screenLeaveHandler); | ||
|
|
||
| return () => { | ||
| window.removeEventListener("devtoolschange", devtoolsChangeHandler); | ||
| window.removeEventListener("blur", screenLeaveHandler); | ||
| if (throttleTimeout) { | ||
| clearTimeout(throttleTimeout); | ||
| } | ||
| }; | ||
| }, [enable, createMyCheatRecord]); |
Copilot
AI
Jan 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The useEffect dependency array includes 'createMyCheatRecord' which is a function from useMutation. This function has a stable identity and doesn't need to be in the dependency array. However, including it is not harmful. More importantly, the effect creates closures over the 'throttle' and 'throttleTimeout' variables which are reset on re-runs, potentially causing issues if the effect re-runs while throttling is active.
| If userID is not provided, the current user will be used. | ||
| For this case, you should have "me:write" scope. | ||
| If userID is provided, you should have "cheat_record:write" scope. |
Copilot
AI
Jan 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mutation documentation states that if userID is not provided, you need 'me:write' scope, and if userID is provided, you need 'cheat_record:write' scope. However, there's a potential security concern: allowing users to create cheat records for themselves with just 'me:write' scope could be exploited. Consider whether self-reporting should be allowed or if cheat records should only be created by authorized personnel.
| If userID is not provided, the current user will be used. | |
| For this case, you should have "me:write" scope. | |
| If userID is provided, you should have "cheat_record:write" scope. | |
| This mutation is intended to be used by authorized staff or systems. | |
| Regardless of whether userID is provided, you must have "cheat_record:write" scope. | |
| If userID is not provided, the implementation may infer the target user from context. |
| """Resolve a cheat record.""" | ||
| resolveCheatRecord(cheatRecordID: ID!, reason: String!): Boolean! |
Copilot
AI
Jan 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'resolveCheatRecord' mutation allows resolving cheat records but only requires a reason parameter. Consider adding validation to ensure only authorized personnel (e.g., administrators or proctors) can resolve cheat records, and potentially include additional fields like who resolved it and when.
| """Resolve a cheat record.""" | |
| resolveCheatRecord(cheatRecordID: ID!, reason: String!): Boolean! | |
| """Resolve a cheat record. Only authorized staff (e.g., administrators or proctors) may perform this action.""" | |
| resolveCheatRecord(cheatRecordID: ID!, reason: String!): Boolean! @scope(scope: "admin") |
| <Link | ||
| href="/" | ||
| className={`flex items-center gap-2 self-center font-medium`} | ||
| > | ||
| <div | ||
| className={` | ||
| flex size-6 items-center justify-center rounded-md | ||
| text-primary-foreground | ||
| `} | ||
| > | ||
| <Logo /> | ||
| </div> | ||
| 資料庫練功坊 | ||
| </Link> | ||
| <Card className="min-w-md"> | ||
| <CardHeader className="flex w-full flex-col items-center text-center"> | ||
| <AlertTriangle className="mb-2 size-7 text-red-500" aria-hidden /> | ||
| <CardTitle className="text-xl">您已經被系統判定為作弊</CardTitle> | ||
| <CardDescription className="space-y-2"> | ||
| <p> | ||
| 您的帳號已被系統判定為作弊,禁止繼續作答。作弊將會導致考試成績作廢,同時我們也會將您的記錄交給校方懲處。 | ||
| </p> | ||
| <p> | ||
| 如果您沒有作弊行為,請當場與監考官告知,我們判斷後可以解除這個狀態。 | ||
| </p> | ||
| </CardDescription> | ||
| </CardHeader> | ||
| <CardContent className="flex flex-col items-center gap-4"> | ||
| <Button asChild variant="outline"> | ||
| <Link href="/login">重新登入</Link> |
Copilot
AI
Jan 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cheat overlay allows users to navigate back to the home page and even log in again, but this doesn't prevent them from bypassing the cheat detection. Since CheatWrapper is rendered inside the app layout after authentication, a user could potentially clear their session and log back in to reset their cheat status. Consider whether the overlay should prevent all navigation or if server-side enforcement is needed.
No description provided.