Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d3b12bc
Live Server Setup
Himir-Desai Feb 11, 2026
6d5a473
Pair programming stuff
OrigamiStarz Feb 11, 2026
a6501cd
Add styling for Staff Add Modal
OrigamiStarz Feb 12, 2026
b6dfe07
Add dropdowns for add staff modal
OrigamiStarz Feb 13, 2026
f0d401c
Connected create user form with api
Himir-Desai Feb 13, 2026
cff71ba
Added reset password email functionality
Himir-Desai Feb 20, 2026
da8da10
Merge and minor changes to frontend
OrigamiStarz Feb 21, 2026
d9e0e9c
Add login, forgot password, and activate pages
OrigamiStarz Feb 21, 2026
ac86f40
Added error styling and user data fetching
Himir-Desai Feb 24, 2026
dcb806a
Finished Login Page style and Reset Password functionality
Himir-Desai Feb 25, 2026
a796f33
Updated staff form style
Himir-Desai Feb 25, 2026
199054f
Add auth state logic for logging in
OrigamiStarz Feb 26, 2026
9f0f2dc
Add logout
OrigamiStarz Feb 26, 2026
b84c5da
Create a script that creates an admin for testing
OrigamiStarz Feb 26, 2026
b69b713
Merge branch 'main' into frontend-account-activation
Himir-Desai Feb 27, 2026
3e01286
Updated styling of staff form
Himir-Desai Mar 1, 2026
e71ec41
lint fix
OrigamiStarz Mar 2, 2026
4b34c3f
Fix build issues
OrigamiStarz Mar 3, 2026
09edb61
Fix password icon bug
OrigamiStarz Mar 7, 2026
7cad1b7
Delete page component
OrigamiStarz Mar 7, 2026
d47cc4d
Fix warning about ActivatePageContent
OrigamiStarz Mar 7, 2026
4c23745
added phoneNumber and assignedSection functionality to user form
Himir-Desai Mar 7, 2026
1b4be91
updating sections on user form submit
Himir-Desai Mar 7, 2026
74c62bc
added error message for invalid credentials on login
Himir-Desai Mar 10, 2026
ca29558
fixed forgot password email verification
Himir-Desai Mar 11, 2026
d00e477
Merge with main
OrigamiStarz Mar 12, 2026
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
33 changes: 33 additions & 0 deletions backend/src/controllers/attendance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { validationResult } from "express-validator";
import { Types } from "mongoose";

import { AttendanceModel } from "../models/attendance";
import { Section } from "../models/sections";
import { SessionModel } from "../models/session";

import type { RequestHandler } from "express";

Expand Down Expand Up @@ -54,6 +56,37 @@ export const updateAttendanceById: RequestHandler = async (req, res, next) => {
}
};

const getStudentsInSection = async (sectionId: Types.ObjectId): Promise<Types.ObjectId[]> => {
const section = await Section.findById(sectionId).select("enrolledStudents");

// Can't be null because of validation checks prior to calling this function
return section!.enrolledStudents;
};

export const ensureAttendanceForSession = async (sessionId: string) => {
const existing = await AttendanceModel.find({ session: sessionId });
if (existing.length > 0) return existing;

const session = await SessionModel.findById(sessionId);
if (!session) throw new Error("Session not found");

const sessionDate = new Date(session.sessionDate);
const today = new Date();
sessionDate.setHours(0, 0, 0, 0);
today.setHours(0, 0, 0, 0);
// Don't create attendance for future sessions
if (sessionDate > today) return [];

const students = await getStudentsInSection(session.section);
await Promise.all(
students.map(async (studentId) =>
AttendanceModel.create({ session: session._id, student: studentId, status: "PRESENT" }),
),
);

return AttendanceModel.find({ session: sessionId });
};

export const getAttendanceBySessionId: RequestHandler = async (req, res, next) => {
try {
const errors = validationResult(req);
Expand Down
51 changes: 21 additions & 30 deletions backend/src/controllers/session.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import { validationResult } from "express-validator";

import { AttendanceModel } from "../models/attendance";
import { Section } from "../models/sections";
import { SessionModel } from "../models/session";

import type { RequestHandler } from "express";
import type { Types } from "mongoose";

const getStudentsInSection = async (sectionId: string): Promise<Types.ObjectId[]> => {
const section = await Section.findById(sectionId).select("enrolledStudents");
import { ensureAttendanceForSession } from "./attendance";

// Can't be null because of validation checks prior to calling this function
return section!.enrolledStudents;
};
import type { RequestHandler } from "express";

type CreateSessionBody = {
section: string;
Expand All @@ -30,22 +23,6 @@ export const createSession: RequestHandler = async (req, res, next) => {
sessionDate,
});

// create Attendance records for all students enrolled in Section

// Get all student Ids in the section (enrolledStudents list)
const students = await getStudentsInSection(section);

// Create attendance records for all students in session
await Promise.all(
students.map(async (studentId) =>
AttendanceModel.create({
session: session._id,
student: studentId,
status: "PRESENT",
}),
),
);

return res.status(201).json(session);
} catch (error) {
next(error);
Expand Down Expand Up @@ -87,14 +64,13 @@ export const getSession: RequestHandler = async (req, res, next) => {
return res.status(404).json({ error: "Session not found" });
}

// Check what we are searching for in the Attendance collection
const query = { session: id };
// Run the query
const attendanceRecords = await AttendanceModel.find(query).populate("student");
// This creates records if missing, checks the date, returns them
const attendanceRecords = await ensureAttendanceForSession(id);
const populated = await AttendanceModel.populate(attendanceRecords, { path: "student" });

const response = {
...session.toObject(),
attendees: attendanceRecords,
attendees: populated,
};

res.status(200).json(response);
Expand All @@ -103,6 +79,21 @@ export const getSession: RequestHandler = async (req, res, next) => {
}
};

// Returns all Sessions under a specific Section ID
export const getSessionsBySectionId: RequestHandler = async (req, res, next) => {
const { sectionId } = req.params;

try {
const sessions = await SessionModel.find({ section: sectionId });

// No population of attendance records needed

res.status(200).json(sessions);
} catch (error) {
next(error);
}
};

export const getAllSessions: RequestHandler = async (req, res, next) => {
try {
const sessions = await SessionModel.find().populate("section");
Expand Down
10 changes: 9 additions & 1 deletion backend/src/controllers/user.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { validationResult } from "express-validator";
import { FirebaseAuthError } from "firebase-admin/auth";
import { Types } from "mongoose";

import UserModel from "../models/user";
import { firebaseAdminAuth } from "../util/firebase";
Expand All @@ -13,13 +14,16 @@ type CreateUserBody = {
lastName: string;
personalEmail: string;
meemliEmail: string;
phoneNumber: string;
admin: boolean;
assignedSections?: string[];
};

export const createUser: RequestHandler = async (req, res, next) => {
const errors = validationResult(req);

const { firstName, lastName, personalEmail, meemliEmail, admin } = req.body as CreateUserBody;
const { firstName, lastName, personalEmail, meemliEmail, phoneNumber, admin, assignedSections } =
req.body as CreateUserBody;

try {
validationErrorParser(errors);
Expand All @@ -32,13 +36,17 @@ export const createUser: RequestHandler = async (req, res, next) => {
return userRecord;
});

const assignedSectionsIds = assignedSections?.map((section) => new Types.ObjectId(section));

const user = await UserModel.create({
_id: userFirebase.uid,
firstName,
lastName,
personalEmail,
meemliEmail,
phoneNumber,
admin,
assignedSections: assignedSectionsIds,
});

res.status(201).json(user);
Expand Down
3 changes: 1 addition & 2 deletions backend/src/models/sections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ const sectionSchema = new mongoose.Schema(
required: true, // Must be associated with a program
},
teachers: {
type: [mongoose.Schema.Types.ObjectId],
ref: "User", // Reference to the User model
type: [String],
required: true, // Must contain at least one teacher
default: [], // Default to an empty array if no teachers are added
},
Expand Down
3 changes: 3 additions & 0 deletions backend/src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ const userSchema = new Schema({
lastName: { type: String, required: true },
personalEmail: { type: String, required: true },
meemliEmail: { type: String, required: true },
phoneNumber: { type: String, required: true },
admin: { type: Boolean, required: true },
archived: { type: Boolean, required: true, default: false },
assignedSections: [{ type: Schema.Types.ObjectId, ref: "Section" }],
});

type User = InferSchemaType<typeof userSchema>;
Expand Down
1 change: 1 addition & 0 deletions backend/src/routes/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as SessionValidator from "../validators/session";
const router = express.Router();

router.get("/", SessionController.getAllSessions);
router.get("/section/:sectionId", SessionController.getSessionsBySectionId);
router.get("/:id", SessionController.getSession);

router.post("/", SessionValidator.createSession, SessionController.createSession);
Expand Down
31 changes: 31 additions & 0 deletions backend/src/scripts/createAdmin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// To run:
// cd backend
// npx ts-node src/scripts/createAdmin.ts
import mongoose from "mongoose";

import UserModel from "../models/user";
import { firebaseAdminAuth } from "../util/firebase";

async function createAdmin() {
await mongoose.connect(process.env.MONGO_URI!);

const firebaseUser = await firebaseAdminAuth.createUser({
email: "<test@gmail.com>",
password: "password",
});

const user = await UserModel.create({
_id: firebaseUser.uid,
firstName: "Admin",
lastName: "User",
personalEmail: "<test@gmail.com>",
meemliEmail: "<test@gmail.com>",
phoneNumber: "1234567890",
admin: true,
});

console.info("Admin created:", user);
process.exit();
}

void createAdmin();
2 changes: 1 addition & 1 deletion backend/src/validators/sections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const validateProgram = body("program")

export const validateTeachers: ValidationChain[] = [
body("teachers").isArray().withMessage("Teachers must be an array"),
body("teachers.*").isMongoId().withMessage("Each teacher must be a valid MongoDB ObjectID"),
body("teachers.*").isString().withMessage("Each teacher must be a string").bail(),
];

export const validateEnrolledStudents: ValidationChain[] = [
Expand Down
31 changes: 31 additions & 0 deletions backend/src/validators/user.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { body } from "express-validator";
import { Types } from "mongoose";

import type { ValidationChain } from "express-validator";

Expand Down Expand Up @@ -44,6 +45,18 @@ const makeMeemliEmailValidator = (): ValidationChain => {
.withMessage("meemliEmail must be a valid email");
};

const makePhoneNumberValidator = (): ValidationChain => {
return body("phoneNumber")
.exists()
.withMessage("phoneNumber is required")
.bail()
.isString()
.withMessage("phoneNumber must be a string")
.bail()
.matches(/^\d{10}$/)
.withMessage("phoneNumber must be a valid 10-digit number");
};

const makeAdminValidator = (): ValidationChain => {
return body("admin")
.exists()
Expand All @@ -53,18 +66,36 @@ const makeAdminValidator = (): ValidationChain => {
.withMessage("admin must be a boolean");
};

const makeAssignedSectionsValidator = (): ValidationChain => {
return body("assignedSections")
.exists()
.withMessage("assignedSections is required")
.bail()
.isArray()
.withMessage("assignedSections must be an array")
.bail()
.custom((value: string[]) => {
return value.every((section) => Types.ObjectId.isValid(new Types.ObjectId(section)));
})
.withMessage("assignedSections must contain valid ObjectId strings");
};

export const validateCreateUser = [
makeFirstNameValidator(),
makeLastNameValidator(),
makePersonalEmailValidator(),
makeMeemliEmailValidator(),
makePhoneNumberValidator(),
makeAdminValidator(),
makeAssignedSectionsValidator(),
];

export const validateEditUser = [
makeFirstNameValidator().optional(),
makeLastNameValidator().optional(),
makePersonalEmailValidator().optional(),
makeMeemliEmailValidator().optional(),
makePhoneNumberValidator().optional(),
makeAdminValidator().optional(),
makeAssignedSectionsValidator().optional(),
];
Loading
Loading