Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | 5x 5x 5x 24x 24x 24x 24x 4x 8x | import React from "react";
import { Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Stack, Tooltip, Typography } from "@mui/material";
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
import useAssessmentLockdown from "@/utils/hooks/useAssessmentLockdown";
/** Human-readable label per violation `type` — see useAssessmentLockdown.js for the
* full set the client reports, and the backend's KNOWN_LOCKDOWN_VIOLATION_TYPES this
* must stay in sync with. "unknown" is the backend's own catch-all bucket, not a
* client-emitted type — kept here defensively in case it ever comes back that way. */
const VIOLATION_TYPE_LABELS = {
fullscreen_exit: "Fullscreen exits",
tab_hidden: "Tab switches",
window_blur: "Window focus lost",
clipboard_blocked: "Copy/paste attempts",
blocked_shortcut: "Blocked shortcuts",
unknown: "Other",
};
/** Build-time kill switch for the whole feature — see docs/implementation/browser-lockdown.md.
* Defaults to off (lockdown stays on); set VITE_DISABLE_BROWSER_LOCKDOWN=true in an env file
* to turn the feature off everywhere the guard is mounted (e.g. while debugging the
* take-assignment page without fighting fullscreen/copy-paste blocking). Distinct from the
* per-instance `enabled` prop, which callers don't currently use. */
const BROWSER_LOCKDOWN_FEATURE_ENABLED = import.meta.env.VITE_DISABLE_BROWSER_LOCKDOWN !== "true";
/**
* Client-side exam-integrity deterrents for a quiz/assessment-taking page:
* fullscreen enforcement, tab-switch/blur detection, copy-paste/
* dev-tools-shortcut blocking, and a beforeunload warning. Right-click is not
* blocked. See docs/implementation/browser-lockdown.md — this is "soft" enforcement
* only, not a true lockdown browser.
*
* Shared by the student's real take-assignment page and the teacher's
* "View As Student" preview, so both show identical deterrents.
*
* @param {boolean} [enabled=true] - ANDed with VITE_DISABLE_BROWSER_LOCKDOWN (see
* BROWSER_LOCKDOWN_FEATURE_ENABLED above) — `enabled={false}` here or the env var set to
* "true" either one disables the guard.
* @param {() => void} [onCancel] - Shows a "Cancel" action on the fullscreen
* prompt when provided, letting the caller route the student away instead of
* forcing them into fullscreen (e.g. reusing the page's own "Confirm Back" flow).
* @param {string} [assignmentId] - Real attempt id; enables backend violation
* reporting and the auto-submit threshold (see useAssessmentLockdown.js). Omit
* for a preview with no real attempt behind it, e.g. "View As Student".
* @param {(payload: {violationCount: number, maxViolations: number}) => void} [onThresholdExceeded]
* - Fires once when the backend reports the violation threshold exceeded; the
* caller is responsible for actually submitting the assignment and locking the
* student out (this component has no access to the answers payload).
*/
const LockdownGuard = ({ enabled = true, onCancel, assignmentId, onThresholdExceeded }) => {
const { needsFullscreenPrompt, requestFullscreen, violationBreakdown } = useAssessmentLockdown({
enabled: enabled && BROWSER_LOCKDOWN_FEATURE_ENABLED,
assignmentId,
onThresholdExceeded,
});
// Backend-confirmed only (see useAssessmentLockdown.js) — empty, and thus
// invisible, for the teacher's "View As Student" preview, which never reports
// violations at all since it has no real assignmentId.
const violationEntries = Object.entries(violationBreakdown).filter(([, count]) => count > 0);
const totalViolations = violationEntries.reduce((sum, [, count]) => sum + count, 0);
return (
<>
<Dialog open={needsFullscreenPrompt} disableEscapeKeyDown maxWidth="sm" fullWidth>
<DialogTitle>Fullscreen Required</DialogTitle>
<DialogContent dividers>
<Typography>
This assessment must be taken in fullscreen mode. Click below to continue — leaving
fullscreen, switching tabs, and copy/paste are disabled while the assessment is in progress.
</Typography>
</DialogContent>
<DialogActions sx={{ display: "flex", justifyContent: "flex-end", gap: 1 }}>
{onCancel && (
<Button variant="outlined" color="inherit" onClick={onCancel}>
Cancel
</Button>
)}
<Button variant="contained" color="primary" sx={{ width: "220px" }} onClick={requestFullscreen}>
Begin in Fullscreen
</Button>
</DialogActions>
</Dialog>
{totalViolations > 0 && (
<Tooltip
arrow
placement="left"
title={
<Stack spacing={0.5} sx={{ py: 0.5, minWidth: 160 }}>
{violationEntries.map(([type, count]) => (
<Typography
key={type}
variant="caption"
sx={{ display: "flex", justifyContent: "space-between", gap: 2 }}
>
<span>{VIOLATION_TYPE_LABELS[type] || type}</span>
<strong>{count}</strong>
</Typography>
))}
</Stack>
}
>
<Chip
icon={<WarningAmberIcon fontSize="small" />}
color="warning"
variant="filled"
label={`${totalViolations} violation${totalViolations === 1 ? "" : "s"}`}
sx={{
position: "fixed",
bottom: 16,
right: 16,
zIndex: (theme) => theme.zIndex.snackbar,
cursor: "default",
boxShadow: 3,
}}
/>
</Tooltip>
)}
</>
);
};
export default LockdownGuard;
|