Skip to content
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

# production
/build
/dist

# misc
.DS_Store
Expand Down
14 changes: 14 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ function App() {
element: <SubmittedContent />,
loader: loadAssignment,
},
{
path: "assignments/:id/viewsubmissions",
element: <ProtectedRoute element={<SubmittedContent />} />,
loader: loadAssignment,
},
{
path: "assignments/:id/submitcontent",
element: <ProtectedRoute element={<SubmittedContent />} />,
loader: loadAssignment,
},
{
path: "assignments/edit/:id/viewscores",
element: <ViewScores />,
Expand Down Expand Up @@ -315,6 +325,10 @@ function App() {
path: "assignments/:id/review",
element: <ReviewReportPage />,
},
{
path: "assignments/:id/assign-grades",
element: <ReviewReportPage />,
},
Comment on lines +328 to +331
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing ProtectedRoute wrapper creates a security gap.

The assign-grades route lacks both ProtectedRoute and a loader. Compare with the new routes at lines 112-121 which properly use ProtectedRoute. This inconsistency allows unauthenticated access to the ReviewReportPage component for grade assignment.

Similarly, the existing routes at lines 103-106 (viewsubmissions) and 108-111 (submitcontent) under the /edit/ path also lack ProtectedRoute, creating an inconsistent security posture.

🔒 Proposed fix to add protection
         {
           path: "assignments/:id/assign-grades",
-          element: <ReviewReportPage />,
+          element: <ProtectedRoute element={<ReviewReportPage />} leastPrivilegeRole={ROLE.TA} />,
+          loader: loadAssignment,
         },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
path: "assignments/:id/assign-grades",
element: <ReviewReportPage />,
},
{
path: "assignments/:id/assign-grades",
element: <ProtectedRoute element={<ReviewReportPage />} leastPrivilegeRole={ROLE.TA} />,
loader: loadAssignment,
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.tsx` around lines 328 - 331, Wrap the route for
"assignments/:id/assign-grades" with the existing ProtectedRoute component and
add the same loader used by the other protected routes so ReviewReportPage
requires authentication before rendering; similarly wrap the "viewsubmissions"
and "submitcontent" routes under the /edit/ path with ProtectedRoute and attach
the same authenticated loader function used by the nearby protected routes to
ensure consistent protection and data loading.

// Fixed the missing comma and added an opening curly brace
{
path: "courses",
Expand Down
168 changes: 143 additions & 25 deletions src/pages/Assignments/AssignmentEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,117 @@ interface TopicData {
updatedAt?: string;
}

interface ICalibrationSubmissionAsset {
url?: string;
display_name?: string;
name?: string;
}

interface ICalibrationSubmissionMember {
full_name?: string;
email?: string;
}

interface ICalibrationSubmissionResponse {
id?: number;
team_id?: number;
team_name?: string;
members?: ICalibrationSubmissionMember[];
links?: ICalibrationSubmissionAsset[];
files?: ICalibrationSubmissionAsset[];
}

interface ICalibrationSubmissionsApiResponse {
submissions?: ICalibrationSubmissionResponse[];
}

interface ICalibrationSubmissionItem {
label: string;
url?: string;
}

interface ICalibrationSubmissionRow {
id: number;
participant_name: string;
review_status: "not_started";
submitted_content: {
hyperlinks: ICalibrationSubmissionItem[];
files: ICalibrationSubmissionItem[];
};
}

const getCalibrationSubmissionLabel = (
submission: ICalibrationSubmissionResponse,
index: number
) => {
if (submission.team_name?.trim()) {
return submission.team_name;
}

const memberNames = Array.isArray(submission.members)
? submission.members
.map((member) => member.full_name?.trim() || member.email?.trim())
.filter((value): value is string => Boolean(value))
: [];

if (memberNames.length > 0) {
return memberNames.join(", ");
}

return `Submission ${index + 1}`;
};

const transformCalibrationAsset = (
asset: ICalibrationSubmissionAsset
): ICalibrationSubmissionItem | null => {
const url = asset.url?.trim();
const label = asset.display_name?.trim() || asset.name?.trim() || url;

if (!label) {
return null;
}

return {
label,
url: url || undefined,
};
};

const transformCalibrationSubmissions = (
response?: ICalibrationSubmissionsApiResponse | null
): ICalibrationSubmissionRow[] => {
const submissions = Array.isArray(response?.submissions) ? response.submissions : [];

return submissions
.map((submission, index) => {
const hyperlinks = Array.isArray(submission.links)
? submission.links
.map(transformCalibrationAsset)
.filter((item): item is ICalibrationSubmissionItem => item !== null)
: [];
const files = Array.isArray(submission.files)
? submission.files
.map(transformCalibrationAsset)
.filter((item): item is ICalibrationSubmissionItem => item !== null)
: [];

return {
id: submission.id ?? submission.team_id ?? index + 1,
participant_name: getCalibrationSubmissionLabel(submission, index),
review_status: "not_started" as const,
submitted_content: {
hyperlinks,
files,
},
};
})
.filter(
(submission) =>
submission.submitted_content.hyperlinks.length > 0 ||
submission.submitted_content.files.length > 0
);
};

const initialValues: IAssignmentFormValues = {
name: "",
directory_path: "",
Expand Down Expand Up @@ -538,30 +649,29 @@ const AssignmentEditor: React.FC<IEditor> = ({ mode }) => {

// Load calibration submissions on component mount
useEffect(() => {
// sendCalibrationSubmissionsRequest({
// url: `/calibration_submissions/get_instructor_calibration_submissions/${assignmentData.id}`,
// method: HttpMethod.GET,
// });
setCalibrationSubmissions([
{
id: 1,
participant_name: "Participant 1",
review_status: "not_started",
submitted_content: { hyperlinks: ["https://www.google.com"], files: ["file1.txt", "file2.pdf"] },
},
{
id: 2,
participant_name: "Participant 2",
review_status: "in_progress",
submitted_content: { hyperlinks: ["https://www.google.com"], files: ["file1.txt", "file2.pdf"] },
},
]);
}, []);
if (mode !== "update" || !assignmentData?.id) {
setCalibrationSubmissions([]);
return;
}

sendCalibrationSubmissionsRequest({
url: `/submitted_content/${assignmentData.id}/view_submissions`,
method: HttpMethod.GET,
});
}, [assignmentData?.id, mode, sendCalibrationSubmissionsRequest]);

// Handle calibration submissions response
useEffect(() => {
if (calibrationSubmissionsResponse && calibrationSubmissionsResponse.status >= 200 && calibrationSubmissionsResponse.status < 300) {
setCalibrationSubmissions(calibrationSubmissionsResponse.data || []);
if (
calibrationSubmissionsResponse &&
calibrationSubmissionsResponse.status >= 200 &&
calibrationSubmissionsResponse.status < 300
) {
setCalibrationSubmissions(
transformCalibrationSubmissions(
calibrationSubmissionsResponse.data as ICalibrationSubmissionsApiResponse
)
);
}
}, [calibrationSubmissionsResponse]);

Expand Down Expand Up @@ -1206,16 +1316,24 @@ const AssignmentEditor: React.FC<IEditor> = ({ mode }) => {
<div>Hyperlinks:</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
{
row.original.submitted_content.hyperlinks.map((item: any, index: number) => {
return <a style={{ color: '#986633', textDecoration: 'none' }} key={index} href={item}>{item}</a>;
row.original.submitted_content.hyperlinks.map((item: ICalibrationSubmissionItem, index: number) => {
return item.url ? (
<a style={{ color: '#986633', textDecoration: 'none' }} key={index} href={item.url}>{item.label}</a>
) : (
<span key={index}>{item.label}</span>
);
})
}
</div>
<div style={{ marginTop: '10px', display: 'flex', flexDirection: 'column' }}>Files:</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
{
row.original.submitted_content.files.map((item: any, index: number) => {
return <a style={{ color: '#986633', textDecoration: 'none' }} key={index} href={item}>{item}</a>;
row.original.submitted_content.files.map((item: ICalibrationSubmissionItem, index: number) => {
return item.url ? (
<a style={{ color: '#986633', textDecoration: 'none' }} key={index} href={item.url}>{item.label}</a>
) : (
<span key={index}>{item.label}</span>
);
})
}
</div>
Expand Down
Loading