Skip to content

Commit

Permalink
feat: ensure team accounts cannot see documents that are markedAdmin
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronchan32 committed Dec 7, 2024
1 parent 4a98159 commit 52ab6ca
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 88 deletions.
23 changes: 22 additions & 1 deletion backend/src/controllers/student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,22 @@ export const editStudent: RequestHandler = async (req, res, next) => {
}
};

export const getAllStudents: RequestHandler = async (_, res, next) => {
export const getAllStudents: RequestHandler = async (req, res, next) => {
try {
const students = await StudentModel.find().populate("enrollments");

// Even though this is a get request, we have verifyAuthToken middleware that sets the accountType in the request body
const { accountType } = req.body;

Check warning on line 120 in backend/src/controllers/student.ts

View workflow job for this annotation

GitHub Actions / Backend lint and style check

Unsafe assignment of an `any` value

// Ensure that documents that are marked admin are not returned to non-admin users
if (accountType !== "admin") {
students.forEach((student) => {
student.documents = student.documents.filter(
(doc) => !doc.markedAdmin,
) as typeof student.documents;
});
}

res.status(200).json(students);
} catch (error) {
next(error);
Expand All @@ -126,6 +138,8 @@ export const getStudent: RequestHandler = async (req, res, next) => {
try {
const errors = validationResult(req);

const { accountType } = req.body;

Check warning on line 141 in backend/src/controllers/student.ts

View workflow job for this annotation

GitHub Actions / Backend lint and style check

Unsafe assignment of an `any` value

validationErrorParser(errors);

const studentId = req.params.id;
Expand All @@ -135,6 +149,13 @@ export const getStudent: RequestHandler = async (req, res, next) => {
return res.status(404).json({ message: "Student not found" });
}

// Ensure that documents that are marked admin are not returned to non-admin users
if (accountType !== "admin") {
studentData.documents = studentData.documents.filter(
(doc) => !doc.markedAdmin,
) as typeof studentData.documents;
}

const enrollments = await EnrollmentModel.find({ studentId });

res.status(200).json({ ...studentData.toObject(), enrollments });
Expand Down
2 changes: 1 addition & 1 deletion backend/src/routes/student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const router = express.Router();

router.post("/create", StudentValidator.createStudent, StudentController.createStudent);
router.put("/edit/:id", ...StudentValidator.editStudent, StudentController.editStudent);
router.get("/all", StudentController.getAllStudents);
router.get("/all", [verifyAuthToken], StudentController.getAllStudents);
router.get("/:id", [verifyAuthToken], StudentController.getStudent);
router.delete("/:id", [verifyAuthToken], StudentController.deleteStudent);

Expand Down
5 changes: 3 additions & 2 deletions frontend/src/api/students.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ export async function editStudent(student: EditStudentRequest): Promise<APIResul
}
}

export async function getAllStudents(): Promise<APIResult<[Student]>> {
export async function getAllStudents(firebaseToken: string): Promise<APIResult<[Student]>> {
try {
const response = await GET("/student/all");
const headers = createAuthHeader(firebaseToken);
const response = await GET("/student/all", headers);
const json = (await response.json()) as [Student];
return { success: true, data: json };
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ export const useProgressNotes = () => {
}
};

const fetchStudentData = async (progressNotes: Record<string, ProgressNote>) => {
const fetchStudentData = async (progressNotes: Record<string, ProgressNote>, token: string) => {
try {
const result = await getAllStudents();
const result = await getAllStudents(token);
if (result.success) {
const studentDataWithNotes: StudentWithNotes[] = result.data.map((student) => ({
...student,
Expand Down Expand Up @@ -92,7 +92,7 @@ export const useProgressNotes = () => {

const progressNotes = await fetchProgressNotes(token);
if (progressNotes) {
await fetchStudentData(progressNotes);
await fetchStudentData(progressNotes, token);
}
})
.catch((error) => {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/StudentForm/StudentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ export default function StudentForm({
Student Information
</legend>

<StudentInfo data={data ?? null} documentData={documentData} />
<StudentInfo data={data ?? null} documentData={documentData} isAdmin={isAdmin} />
</fieldset>
</div>
<div className="grid w-full gap-10 lg:grid-cols-2">
Expand Down
79 changes: 44 additions & 35 deletions frontend/src/components/StudentForm/StudentInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type StudentInfoProps = {
setStudentDocuments: Dispatch<SetStateAction<Document[]>>;
setDidDeleteOrMark: Dispatch<SetStateAction<boolean>>;
};
isAdmin: boolean;
};

const SUPPORTED_FILETYPES = [
Expand All @@ -52,7 +53,7 @@ const SUPPORTED_FILETYPES = [
"image/webp",
];

export default function StudentInfo({ classname, data, documentData }: StudentInfoProps) {
export default function StudentInfo({ classname, data, documentData, isAdmin }: StudentInfoProps) {
const { register, setValue: setCalendarValue } = useFormContext<StudentFormData>();
const [modalOpen, setModalOpen] = useState(false);
const [documentError, setDocumentError] = useState("");
Expand Down Expand Up @@ -200,6 +201,8 @@ export default function StudentInfo({ classname, data, documentData }: StudentIn
);
};

console.log({ isAdmin });

return (
<div className={cn("grid flex-1 gap-x-8 gap-y-10 md:grid-cols-2", classname)}>
<div>
Expand Down Expand Up @@ -283,45 +286,51 @@ export default function StudentInfo({ classname, data, documentData }: StudentIn
onClick={() => {
window.open(document.link, "_blank");
}}
className="rounded-tl-md rounded-tr-md border-[1px] border-solid border-black bg-white px-10 py-4"
className={`${!isAdmin ? "rounded-bl-md rounded-br-md" : ""} rounded-tl-md rounded-tr-md border-[1px] border-solid border-black bg-white px-10 py-4`}
>
View File
</button>
)}

<ModalConfirmation
icon={<GreenQuestionIcon />}
triggerElement={
<button
className={`${!document.link && "rounded-tl-md rounded-tr-md border-t-[1px]"} border-[1px] border-b-0 border-t-0 border-solid border-black bg-white px-10 py-4 text-pia_dark_green`}
>
{document.markedAdmin ? "Unmark" : "Mark"} Admin
</button>
}
title={document.markedAdmin ? "Unmark admin?" : "Mark admin only?"}
description={`${document.markedAdmin ? "Everyone will be able to" : "Only admin will"} see these files`}
confirmText={document.markedAdmin ? "Unmark" : "Mark"}
kind="primary"
onConfirmClick={() => {
handleMarkAdmin(document);
setDidDeleteOrMark(true);
}}
/>
<ModalConfirmation
icon={<RedDeleteIcon />}
triggerElement={
<button className="rounded-bl-md rounded-br-md border-[1px] border-solid border-black bg-white px-10 py-4 text-destructive">
Delete File
</button>
}
title="Are you sure you want to delete?"
confirmText="Delete"
kind="destructive"
onConfirmClick={() => {
handleDeleteDocument(document);
setDidDeleteOrMark(true);
}}
/>
{isAdmin ? (
<>
<ModalConfirmation
icon={<GreenQuestionIcon />}
triggerElement={
<button
className={`${!document.link && "rounded-tl-md rounded-tr-md border-t-[1px]"} border-[1px] border-b-0 border-t-0 border-solid border-black bg-white px-10 py-4 text-pia_dark_green`}
>
{document.markedAdmin ? "Unmark" : "Mark"} Admin
</button>
}
title={document.markedAdmin ? "Unmark admin?" : "Mark admin only?"}
description={`${document.markedAdmin ? "Everyone will be able to" : "Only admin will"} see these files`}
confirmText={document.markedAdmin ? "Unmark" : "Mark"}
kind="primary"
onConfirmClick={() => {
handleMarkAdmin(document);
setDidDeleteOrMark(true);
}}
/>
<ModalConfirmation
icon={<RedDeleteIcon />}
triggerElement={
<button className="rounded-bl-md rounded-br-md border-[1px] border-solid border-black bg-white px-10 py-4 text-destructive">
Delete File
</button>
}
title="Are you sure you want to delete?"
confirmText="Delete"
kind="destructive"
onConfirmClick={() => {
handleDeleteDocument(document);
setDidDeleteOrMark(true);
}}
/>
</>
) : (
""
)}
</PopoverContent>
</Popover>
</Fragment>
Expand Down
51 changes: 32 additions & 19 deletions frontend/src/contexts/students.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React, { ReactNode, createContext, useEffect, useState } from "react";
import React, { ReactNode, createContext, useContext, useEffect, useState } from "react";

import { UserContext } from "./user";

import { Student, getAllStudents } from "@/api/students";
import { StudentMap } from "@/components/StudentsTable/types";
Expand All @@ -18,28 +20,39 @@ export const StudentsContext = createContext<StudentsContext>({
export const StudentsContextProvider = ({ children }: { children: ReactNode }) => {
const [allStudents, setAllStudents] = useState<StudentMap | undefined>(undefined);
const [isLoading, setIsLoading] = useState(true);
const { firebaseUser } = useContext(UserContext);

// Fetch Progress Notes and Students
useEffect(() => {
getAllStudents().then(
(result) => {
if (result.success) {
const StudentsObject = result.data.reduce(
(obj, student) => {
obj[student._id] = student;
return obj;
if (firebaseUser) {
firebaseUser
?.getIdToken()
.then((token) => {
getAllStudents(token).then(
(result) => {
if (result.success) {
const StudentsObject = result.data.reduce(
(obj, student) => {
obj[student._id] = student;
return obj;
},
{} as Record<string, Student>,
);
setAllStudents(StudentsObject);
setIsLoading(false);
}
},
(error) => {
console.log(error);
setIsLoading(false);
},
{} as Record<string, Student>,
);
setAllStudents(StudentsObject);
setIsLoading(false);
}
},
(error) => {
console.log(error);
setIsLoading(false);
},
);
}, []);
})
.catch((error) => {
console.error(error);
});
}
}, [firebaseUser]);

return (
<StudentsContext.Provider value={{ allStudents, setAllStudents, isLoading }}>
Expand Down
34 changes: 8 additions & 26 deletions frontend/src/pages/attendance.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useEffect, useRef, useState } from "react";
import { useContext, useEffect, useRef, useState } from "react";

import { Program, getAllPrograms } from "@/api/programs";
import { AbsenceSession, Session, getAbsenceSessions, getRecentSessions } from "@/api/sessions";
import { getAllStudents } from "@/api/students";
import { AttendanceCard } from "@/components/AttendanceCard";
import { AttendanceTable } from "@/components/AttendanceTable";
import { ProgramMap, StudentMap } from "@/components/StudentsTable/types";
import LoadingSpinner from "@/components/LoadingSpinner";
import { ProgramMap } from "@/components/StudentsTable/types";
import { StudentsContext } from "@/contexts/students";
import { useRedirectTo404IfNotAdmin, useRedirectToLoginIfNotSignedIn } from "@/hooks/redirect";

export type Sessions = [Session];
Expand All @@ -17,13 +18,14 @@ export default function AttendanceDashboard() {

const [allSessions, setAllSessions] = useState<Sessions>(); // map from program id to program
const [allPrograms, setAllPrograms] = useState<ProgramMap>({}); // map from program id to program
const [allStudents, setAllStudents] = useState<StudentMap>({});
const [allAbsenceSessions, setAllAbsenceSessions] = useState<AbsenceSessions>([]);
const [sessionsLoading, setSessionsLoading] = useState(true);
const [programsLoading, setProgramsLoading] = useState(true);
const [studentsLoading, setStudentsLoading] = useState(true);
const [absencsSessionsLoading, setAbsenceSessionsLoading] = useState(true);

const { allStudents } = useContext(StudentsContext);
const studentsLoading = allStudents === undefined;

const [remainingSessions, setRemainingSessions] = useState(0);
const [remainingAbsenceSessions, setRemainingAbsenceSessions] = useState(0);

Expand Down Expand Up @@ -87,26 +89,6 @@ export default function AttendanceDashboard() {
);
}, []);

useEffect(() => {
getAllStudents().then(
(result) => {
// console.log(result);
if (result.success) {
// Convert student array to object with keys as ids and values as corresponding student
const studentsObject = result.data.reduce((obj, student) => {
obj[student._id] = student;
return obj;
}, {} as StudentMap);
setAllStudents(studentsObject);
setStudentsLoading(false);
}
},
(error) => {
console.log(error);
},
);
}, []);

useEffect(() => {
getAbsenceSessions().then(
(result) => {
Expand All @@ -124,7 +106,7 @@ export default function AttendanceDashboard() {
}, []);

if (sessionsLoading || studentsLoading || programsLoading || absencsSessionsLoading)
return <p>Loading...</p>;
return <LoadingSpinner />;
else {
return (
<main>
Expand Down

0 comments on commit 52ab6ca

Please sign in to comment.