From 9cb84e709ff46537104e5f51402696eee1f593d1 Mon Sep 17 00:00:00 2001 From: wongcolin45 Date: Sat, 28 Mar 2026 10:33:30 -0400 Subject: [PATCH 01/25] Update database schema --- .../migration.sql | 22 +++++++++++++++++++ attendance-manager/prisma/schema.prisma | 6 +++++ 2 files changed, 28 insertions(+) create mode 100644 attendance-manager/prisma/migrations/20260328000000_add_role_type_and_voting_to_user/migration.sql diff --git a/attendance-manager/prisma/migrations/20260328000000_add_role_type_and_voting_to_user/migration.sql b/attendance-manager/prisma/migrations/20260328000000_add_role_type_and_voting_to_user/migration.sql new file mode 100644 index 00000000..607f3703 --- /dev/null +++ b/attendance-manager/prisma/migrations/20260328000000_add_role_type_and_voting_to_user/migration.sql @@ -0,0 +1,22 @@ +-- Add new RoleType enum values +ALTER TYPE "RoleType" ADD VALUE IF NOT EXISTS 'NONE'; +ALTER TYPE "RoleType" ADD VALUE IF NOT EXISTS 'SUPER_ADMIN'; +ALTER TYPE "RoleType" ADD VALUE IF NOT EXISTS 'ADMIN'; +ALTER TYPE "RoleType" ADD VALUE IF NOT EXISTS 'SENATOR'; + +-- Add roleType column with default NONE +ALTER TABLE "User" ADD COLUMN "roleType" "RoleType" NOT NULL DEFAULT 'NONE'; + +-- Add isVotingMember column with default false +ALTER TABLE "User" ADD COLUMN "isVotingMember" BOOLEAN NOT NULL DEFAULT false; + +-- Backfill roleType from existing Role table connection +UPDATE "User" u +SET "roleType" = r."roleType" +FROM "Role" r +WHERE u."roleId" = r."roleId"; + +-- Backfill isVotingMember: true for MEMBER role users +UPDATE "User" +SET "isVotingMember" = true +WHERE "roleType" = 'MEMBER'; diff --git a/attendance-manager/prisma/schema.prisma b/attendance-manager/prisma/schema.prisma index 8d241e80..bb939297 100644 --- a/attendance-manager/prisma/schema.prisma +++ b/attendance-manager/prisma/schema.prisma @@ -23,6 +23,8 @@ model User { firstName String lastName String roleId String + roleType RoleType. @default(None) + isVotingMember Boolean. @default(False); password String? // Optional - Supabase handles authentication attendance Attendance[] role Role @relation(fields: [roleId], references: [roleId]) @@ -91,6 +93,10 @@ model VotingRecord { } enum RoleType { + NONE + SUPER_ADMIN + ADMIN + SENATOR EBOARD MEMBER } From 0fc3e64b9b54b70eb16b19e54a3f749a108c857d Mon Sep 17 00:00:00 2001 From: wongcolin45 Date: Sat, 28 Mar 2026 11:10:26 -0400 Subject: [PATCH 02/25] Updated backend to user new RoleType fields --- attendance-manager/src/app/api/auth/signup/route.ts | 1 + attendance-manager/src/contexts/AuthContext.tsx | 2 +- attendance-manager/src/types/index.ts | 8 +++++--- attendance-manager/src/users/users.controller.ts | 8 -------- attendance-manager/src/users/users.service.ts | 13 ++++++------- attendance-manager/src/utils/auth_utils.ts | 10 +++++++--- 6 files changed, 20 insertions(+), 22 deletions(-) diff --git a/attendance-manager/src/app/api/auth/signup/route.ts b/attendance-manager/src/app/api/auth/signup/route.ts index 89ee4b78..dd3b90da 100644 --- a/attendance-manager/src/app/api/auth/signup/route.ts +++ b/attendance-manager/src/app/api/auth/signup/route.ts @@ -74,6 +74,7 @@ export async function POST(request: Request) { lastName, nuid, roleId: finalRoleId, + roleType: RoleType.MEMBER, password: undefined, // Optional - Supabase handles authentication }, include: { diff --git a/attendance-manager/src/contexts/AuthContext.tsx b/attendance-manager/src/contexts/AuthContext.tsx index 52c8625e..2b8b0251 100644 --- a/attendance-manager/src/contexts/AuthContext.tsx +++ b/attendance-manager/src/contexts/AuthContext.tsx @@ -72,7 +72,7 @@ export const AuthProvider: React.FC = ({ children }) => { id: userDetails.userId, email: userDetails.email, name: `${userDetails.firstName} ${userDetails.lastName}`, - role: userDetails.role.roleType, + role: userDetails.roleType, avatar: undefined }; diff --git a/attendance-manager/src/types/index.ts b/attendance-manager/src/types/index.ts index 30f04d4c..f39c127e 100644 --- a/attendance-manager/src/types/index.ts +++ b/attendance-manager/src/types/index.ts @@ -2,7 +2,7 @@ export interface User { id: string; email: string; name: string; - role: 'MEMBER' | 'EBOARD'; + role: 'NONE' | 'SUPER_ADMIN' | 'ADMIN' | 'SENATOR' | 'EBOARD' | 'MEMBER'; avatar?: string; } @@ -65,7 +65,8 @@ export interface UserData { email: string; firstName: string; lastName: string; - role: RoleData; + roleType: string; + isVotingMember: boolean; password: string; } @@ -89,7 +90,8 @@ export interface UserApiData { nuid: string; attendance: AttendanceApiData[]; attendanceId?: string; - role: RoleData; + roleType: string; + isVotingMember: boolean; } export interface AttendanceApiData { diff --git a/attendance-manager/src/users/users.controller.ts b/attendance-manager/src/users/users.controller.ts index 0baa4725..25b1fbff 100644 --- a/attendance-manager/src/users/users.controller.ts +++ b/attendance-manager/src/users/users.controller.ts @@ -49,14 +49,6 @@ export const UsersController = { return NextResponse.json(user); }, - async getRoleId(params: { role: RoleType }) { - const roleId = await UsersService.getRoleIdByRoleType(params.role); - if (!roleId) { - return NextResponse.json({ error: 'Role not found' }, { status: 404 }); - } - return roleId; - }, - async checkUserExists(params: { userEmail: string }) { const user = await UsersService.getUserByEmail(params.userEmail); if (!user) { diff --git a/attendance-manager/src/users/users.service.ts b/attendance-manager/src/users/users.service.ts index d1263ebe..fb101922 100644 --- a/attendance-manager/src/users/users.service.ts +++ b/attendance-manager/src/users/users.service.ts @@ -56,6 +56,8 @@ export const UsersService = { firstName: string; lastName: string; roleId: string; + roleType: RoleType; + isVotingMember: boolean; password?: string | null; }) { return prisma.user.create({ @@ -86,6 +88,8 @@ export const UsersService = { firstName: string; lastName: string; roleId: string; + roleType: RoleType; + isVotingMember: boolean; }> ) { return prisma.user.update({ @@ -121,17 +125,12 @@ export const UsersService = { }); }, - async getUsersByRole(roleId: string) { + async getUsersByRole(roleType: RoleType) { return prisma.user.findMany({ where: { deletedAt: null, - role: { - roleType: roleId as RoleType - } + roleType }, - include: { - role: true - } }); }, diff --git a/attendance-manager/src/utils/auth_utils.ts b/attendance-manager/src/utils/auth_utils.ts index cfefffdf..c13021bc 100644 --- a/attendance-manager/src/utils/auth_utils.ts +++ b/attendance-manager/src/utils/auth_utils.ts @@ -28,8 +28,12 @@ export const login = async ( let user_details: UserData = await res.json(); if ( !( - user_details.role.roleType === 'EBOARD' || - user_details.role.roleType === 'MEMBER' + user_details.roleType === 'NONE' || + user_details.roleType === 'SENATOR' || + user_details.roleType === 'SUPER_ADMIN' || + user_details.roleType === 'ADMIN' || + user_details.roleType === 'EBOARD' || + user_details.roleType === 'MEMBER' ) ) { alert('Incorrect Roles'); @@ -45,7 +49,7 @@ export const login = async ( id: user_details.userId, email: credentials.email, name: user_details.firstName + ' ' + user_details.lastName, - role: user_details.role.roleType, + role: user_details.roleType, avatar: undefined }; From f1fe2d39d98636042f09d6550cd56c12647ffcc7 Mon Sep 17 00:00:00 2001 From: wongcolin45 Date: Sat, 28 Mar 2026 11:28:02 -0400 Subject: [PATCH 03/25] Updated the frontend to use the new role fields --- .../src/components/attendance/AttendancePage.tsx | 4 ++-- attendance-manager/src/components/layout/Header.tsx | 6 ++++++ .../src/components/meetings/CreateMeetingModal.tsx | 4 ++-- attendance-manager/src/components/meetings/MeetingsPage.tsx | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/attendance-manager/src/components/attendance/AttendancePage.tsx b/attendance-manager/src/components/attendance/AttendancePage.tsx index 5a5ed735..8ac0a990 100644 --- a/attendance-manager/src/components/attendance/AttendancePage.tsx +++ b/attendance-manager/src/components/attendance/AttendancePage.tsx @@ -330,8 +330,8 @@ const AttendancePage: React.FC = () => { setShowEditAttendanceModal(true); }; - const eboardMembers = users.filter(m => m.role.roleType === 'EBOARD'); - const regularMembers = users.filter(m => m.role.roleType === 'MEMBER'); + const eboardMembers = users.filter(m => m.roleType === 'EBOARD'); + const regularMembers = users.filter(m => m.roleType === 'MEMBER'); return (
diff --git a/attendance-manager/src/components/layout/Header.tsx b/attendance-manager/src/components/layout/Header.tsx index 3bdf8ccb..36e28aed 100644 --- a/attendance-manager/src/components/layout/Header.tsx +++ b/attendance-manager/src/components/layout/Header.tsx @@ -13,6 +13,12 @@ const Header: React.FC = ({ onProfileClick, onLogoClick }) => { /* Maps enum value to appropriate plain text */ const rolePlainText = (role?: string) => { switch (role) { + case 'SUPER_ADMIN': + return 'Super admin' + case 'ADMIN': + return 'Admin' + case 'SENATOR': + return 'Senator'; case 'EBOARD': return 'E-Board'; case 'MEMBER': diff --git a/attendance-manager/src/components/meetings/CreateMeetingModal.tsx b/attendance-manager/src/components/meetings/CreateMeetingModal.tsx index 91b49baf..d8e5177b 100644 --- a/attendance-manager/src/components/meetings/CreateMeetingModal.tsx +++ b/attendance-manager/src/components/meetings/CreateMeetingModal.tsx @@ -244,12 +244,12 @@ const CreateMeetingModal: React.FC = ({

{member.email}

- {member.role.roleType === 'EBOARD' ? 'Eboard' : 'Member'} + {member.roleType === 'EBOARD' ? 'Eboard' : 'Member'}
diff --git a/attendance-manager/src/components/meetings/MeetingsPage.tsx b/attendance-manager/src/components/meetings/MeetingsPage.tsx index d942894a..67188b1f 100644 --- a/attendance-manager/src/components/meetings/MeetingsPage.tsx +++ b/attendance-manager/src/components/meetings/MeetingsPage.tsx @@ -181,7 +181,7 @@ const MeetingsPage: React.FC = () => { }, []); const nonEboardMembers = useMemo( - () => members.filter(member => member.role.roleType !== 'EBOARD'), + () => members.filter(member => member.roleType !== 'EBOARD'), [members] ); const nonEboardMemberIds = useMemo( From 5d08e919e9f45ec486006680bbedfc09bb79ce2a Mon Sep 17 00:00:00 2001 From: wongcolin45 Date: Sun, 29 Mar 2026 14:17:25 -0400 Subject: [PATCH 04/25] Update tests --- .../src/auth/__tests__/auth-flow.test.ts | 4 +-- .../src/users/__tests__/users.test.ts | 32 ++++++++++++------- .../src/utils/__tests__/api-auth.test.ts | 3 +- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/attendance-manager/src/auth/__tests__/auth-flow.test.ts b/attendance-manager/src/auth/__tests__/auth-flow.test.ts index a222154e..40e30902 100644 --- a/attendance-manager/src/auth/__tests__/auth-flow.test.ts +++ b/attendance-manager/src/auth/__tests__/auth-flow.test.ts @@ -104,7 +104,6 @@ describe('Auth Flow Integration Tests', () => { // Verify user was created in database const createdUser = await prisma.user.findUnique({ where: { supabaseAuthId }, - include: { role: true } }); expect(createdUser).toBeDefined(); expect(createdUser?.email).toBe(testUser.email); @@ -151,8 +150,7 @@ describe('Auth Flow Integration Tests', () => { expect(getUserResponse.status).toBe(200); expect(userProfile.supabaseAuthId).toBe(supabaseAuthId); expect(userProfile.email).toBe(testUser.email); - expect(userProfile.role).toBeDefined(); - expect(userProfile.role.roleType).toBe('MEMBER'); + expect(userProfile.roleType).toBe('MEMBER'); // Step 4: Session check (simulate middleware/AuthContext session check) mockGetSession.mockResolvedValueOnce({ diff --git a/attendance-manager/src/users/__tests__/users.test.ts b/attendance-manager/src/users/__tests__/users.test.ts index 07047f61..a198d1be 100644 --- a/attendance-manager/src/users/__tests__/users.test.ts +++ b/attendance-manager/src/users/__tests__/users.test.ts @@ -41,6 +41,8 @@ describe('UsersService', () => { firstName: 'John', lastName: 'Doe', roleId: testRoleId, + roleType: RoleType.MEMBER, + isVotingMember: true, password: null }); }); @@ -55,9 +57,9 @@ describe('UsersService', () => { }); it('should fetch users by role type', async () => { - const users = await UsersService.getUsersByRole('MEMBER'); + const users = await UsersService.getUsersByRole(RoleType.MEMBER); expect(users.length).toBeGreaterThan(0); - expect(users[0].role.roleType).toBe('MEMBER'); + expect(users[0].roleType).toBe(RoleType.MEMBER); }); it('should create a new user', async () => { @@ -68,7 +70,9 @@ describe('UsersService', () => { email: 'jdoe2@northeastern.edu', firstName: 'Jane', lastName: 'Doe', - roleId: testRoleId + roleId: testRoleId, + roleType: RoleType.MEMBER, + isVotingMember: true }); expect(newUser).toBeDefined(); @@ -95,7 +99,9 @@ describe('UsersService', () => { email: 'jdoe3@northeastern.edu', firstName: 'Jane', lastName: 'Doe', - roleId: testRoleId + roleId: testRoleId, + roleType: RoleType.MEMBER, + isVotingMember: true }); const updatedUser = await UsersService.updateUser(newUser.userId, { @@ -117,7 +123,9 @@ describe('UsersService', () => { email: 'jdoe4@northeastern.edu', firstName: 'Jane', lastName: 'Doe', - roleId: testRoleId + roleId: testRoleId, + roleType: RoleType.MEMBER, + isVotingMember: true }); await UsersService.deleteUser(newUser.userId); const deletedUser = await UsersService.getUserById(newUser.userId); @@ -144,7 +152,9 @@ describe('UsersController.validateNuid', () => { firstName: 'John', lastName: 'Doe', roleId: testRoleId, - password: null + password: null, + roleType: RoleType.MEMBER, + isVotingMember: true }); testUserId = user.userId; }); @@ -250,6 +260,8 @@ describe('GET /api/users/by-supabase-id/[supabaseAuthId]', () => { firstName: 'Route', lastName: 'User', roleId: routeTestRoleId, + roleType: RoleType.MEMBER, + isVotingMember: true, password: null } }); @@ -282,8 +294,7 @@ describe('GET /api/users/by-supabase-id/[supabaseAuthId]', () => { expect(data.firstName).toBe('Route'); expect(data.lastName).toBe('User'); expect(data.nuid).toBe('001234888'); - expect(data.role).toBeDefined(); - expect(data.role.roleType).toBe('MEMBER'); + expect(data.roleType).toBe('MEMBER'); }); it('should return 404 when user is not found', async () => { @@ -319,6 +330,7 @@ describe('GET /api/users/by-supabase-id/[supabaseAuthId]', () => { firstName: 'Eboard', lastName: 'Route', roleId: eboardRole.roleId, + roleType: RoleType.EBOARD, password: null } }); @@ -334,9 +346,7 @@ describe('GET /api/users/by-supabase-id/[supabaseAuthId]', () => { const data = await response.json(); expect(response.status).toBe(200); - expect(data.role).toBeDefined(); - expect(data.role.roleId).toBe(eboardRole.roleId); - expect(data.role.roleType).toBe('EBOARD'); + expect(data.roleType).toBe('EBOARD'); await prisma.user.delete({ where: { userId: eboardUser.userId } }); await prisma.role.delete({ where: { roleId: eboardRole.roleId } }); diff --git a/attendance-manager/src/utils/__tests__/api-auth.test.ts b/attendance-manager/src/utils/__tests__/api-auth.test.ts index e5004b4a..9002dbe9 100644 --- a/attendance-manager/src/utils/__tests__/api-auth.test.ts +++ b/attendance-manager/src/utils/__tests__/api-auth.test.ts @@ -78,8 +78,7 @@ describe('API Auth Utilities', () => { expect(user?.userId).toBe(testUserId); expect(user?.supabaseAuthId).toBe(testSupabaseAuthId); expect(user?.email).toBe('apitest@example.com'); - expect(user?.role).toBeDefined(); - expect(user?.role.roleType).toBe('MEMBER'); + expect(user?.roleType).toBe('MEMBER'); }); it('should return null when no session exists', async () => { From 831a5167d0204b8f391efd5207d8a483ed586ad1 Mon Sep 17 00:00:00 2001 From: wongcolin45 Date: Sun, 29 Mar 2026 14:30:03 -0400 Subject: [PATCH 05/25] Update schema.prisma --- attendance-manager/prisma/schema.prisma | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/attendance-manager/prisma/schema.prisma b/attendance-manager/prisma/schema.prisma index bb939297..15894323 100644 --- a/attendance-manager/prisma/schema.prisma +++ b/attendance-manager/prisma/schema.prisma @@ -23,8 +23,8 @@ model User { firstName String lastName String roleId String - roleType RoleType. @default(None) - isVotingMember Boolean. @default(False); + roleType RoleType @default(NONE) + isVotingMember Boolean @default(false) password String? // Optional - Supabase handles authentication attendance Attendance[] role Role @relation(fields: [roleId], references: [roleId]) From 6b0bf6e16fd67e09435c025fc30f2f5092d29ffa Mon Sep 17 00:00:00 2001 From: wongcolin45 Date: Sun, 29 Mar 2026 18:00:47 -0400 Subject: [PATCH 06/25] Moved permission logic to single file --- .../src/components/attendance/AttendancePage.tsx | 7 ++++--- attendance-manager/src/components/layout/Header.tsx | 4 ++-- attendance-manager/src/components/layout/Layout.tsx | 5 +++-- .../src/components/layout/Sidebar.tsx | 11 ++++++----- .../src/components/meetings/MeetingHistoryPanel.tsx | 7 ++++--- .../src/components/meetings/MeetingsPage.tsx | 5 +++-- .../src/components/profile/ProfilePage.tsx | 9 +++++---- .../src/components/voting/VotingAdminPanel.tsx | 3 ++- attendance-manager/src/utils/permissions.ts | 13 +++++++++++++ 9 files changed, 42 insertions(+), 22 deletions(-) create mode 100644 attendance-manager/src/utils/permissions.ts diff --git a/attendance-manager/src/components/attendance/AttendancePage.tsx b/attendance-manager/src/components/attendance/AttendancePage.tsx index 8ac0a990..9f1a2852 100644 --- a/attendance-manager/src/components/attendance/AttendancePage.tsx +++ b/attendance-manager/src/components/attendance/AttendancePage.tsx @@ -18,6 +18,7 @@ import { meetingAPI } from '@/utils/attendance_utils'; import AttendancePageRequestsModal from './AttendancePageRequestsModal'; import DeleteUserModal from './DeleteUserModal'; import { ClipboardList, NotebookPen } from 'lucide-react'; +import { checkCanManageAttendance } from '@/utils/permissions'; const AttendancePage: React.FC = () => { const { user } = useAuth(); @@ -66,7 +67,7 @@ const AttendancePage: React.FC = () => { const [declinedRequestIds, setDeclinedRequestIds] = useState([]); // Check if user is admin (EBOARD) - const isAdmin = user?.role === 'EBOARD'; + const canManageAttendance = checkCanManageAttendance(user?.role); useEffect(() => { const loadMeetings = async () => { try { @@ -345,7 +346,7 @@ const AttendancePage: React.FC = () => { Manage SGA members and track attendance history

- {isAdmin && ( + {canManageAttendance && (
- {isAdmin && ( + {canViewMemberStats && ( <>
Total Members diff --git a/attendance-manager/src/components/meetings/MeetingHistoryPanel.tsx b/attendance-manager/src/components/meetings/MeetingHistoryPanel.tsx index 8093e25c..4f24c3a8 100644 --- a/attendance-manager/src/components/meetings/MeetingHistoryPanel.tsx +++ b/attendance-manager/src/components/meetings/MeetingHistoryPanel.tsx @@ -1,5 +1,6 @@ import { MeetingApiData, MeetingType } from '@/types'; import { useAuth } from '@/contexts/AuthContext'; +import { checkCanEditMeetings } from '@/utils/permissions'; interface MeetingHistoryPanelProps { setActiveTab: (option: 'past' | 'upcoming') => void; @@ -29,7 +30,7 @@ const MeetingHistoryPanel: React.FC = ({ return type; }; const { user } = useAuth(); - const isEboard = user?.role === 'EBOARD'; + const canEditMeetings = checkCanEditMeetings(user?.role); return (
@@ -119,7 +120,7 @@ const MeetingHistoryPanel: React.FC = ({ # of Members - {isEboard && ( + {canEditMeetings && ( Actions @@ -165,7 +166,7 @@ const MeetingHistoryPanel: React.FC = ({
- {isEboard && ( + {canEditMeetings && (
{/* Create Meeting Button - Only for Admins */} - {isAdmin && ( + {canManageMeetings && (
-
{record.notes}
+
+ {record.notes} +
diff --git a/attendance-manager/src/components/meetings/MeetingHistoryPanel.tsx b/attendance-manager/src/components/meetings/MeetingHistoryPanel.tsx index c887b8ea..3962296f 100644 --- a/attendance-manager/src/components/meetings/MeetingHistoryPanel.tsx +++ b/attendance-manager/src/components/meetings/MeetingHistoryPanel.tsx @@ -137,7 +137,7 @@ const MeetingHistoryPanel: React.FC = ({ - {visibleMeetings.map((meeting) => ( + {visibleMeetings.map((meeting) => ( { }; // visibleMeetings are meetings post-type-filter - sorted by most recent date - const visibleMeetings = (typeFilter - ? filteredMeetings.filter(m => m.type === typeFilter) - : filteredMeetings + const visibleMeetings = ( + typeFilter + ? filteredMeetings.filter((m) => m.type === typeFilter) + : filteredMeetings ).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); // Determine banner color based on remaining absences diff --git a/attendance-manager/src/voting-record/__tests__/voting-record.test.ts b/attendance-manager/src/voting-record/__tests__/voting-record.test.ts index df75df64..73249b96 100644 --- a/attendance-manager/src/voting-record/__tests__/voting-record.test.ts +++ b/attendance-manager/src/voting-record/__tests__/voting-record.test.ts @@ -123,7 +123,7 @@ describe('VotingRecordService', () => { votingEventId: testVotingEventId, userId: testUserId, result: 'YES', - }) + }), ).rejects.toThrow('User has already voted for this event'); }); From d0456bd622a1f682acea0fbadb6d34b76bb207df Mon Sep 17 00:00:00 2001 From: Colin Wong <150976788+wongcolin45@users.noreply.github.com> Date: Tue, 7 Apr 2026 05:29:43 -0400 Subject: [PATCH 23/25] Lint + Update --- .../src/app/api/auth/signup/route.ts | 268 +++++++++--------- .../src/app/api/meeting/[id]/route.ts | 5 +- .../components/attendance/AttendancePage.tsx | 77 ++--- .../meetings/MeetingHistoryPanel.tsx | 2 +- .../src/components/meetings/MeetingsPage.tsx | 146 +++++----- .../src/contexts/AuthContext.tsx | 12 +- attendance-manager/src/users/users.service.ts | 38 +-- attendance-manager/src/utils/auth_utils.ts | 8 +- attendance-manager/src/utils/permissions.ts | 50 ++-- 9 files changed, 303 insertions(+), 303 deletions(-) diff --git a/attendance-manager/src/app/api/auth/signup/route.ts b/attendance-manager/src/app/api/auth/signup/route.ts index ab55daba..d867aaae 100644 --- a/attendance-manager/src/app/api/auth/signup/route.ts +++ b/attendance-manager/src/app/api/auth/signup/route.ts @@ -1,134 +1,134 @@ -import { NextResponse } from 'next/server'; -import { createServerSupabaseClient } from '@/lib/supabase-server'; -import { prisma } from '@/lib/prisma'; -import { UsersService } from '@/users/users.service'; -import { RoleType } from '@/generated/prisma'; - -export async function POST(request: Request) { - try { - const { email, password, firstName, lastName, nuid, roleId } = - await request.json(); - - // Validate required fields - if (!email || !password || !firstName || !lastName || !nuid) { - return NextResponse.json( - { error: 'Missing required fields' }, - { status: 400 }, - ); - } - - const supabase = await createServerSupabaseClient(); - - // Use the request origin so verification links go to the same host - const origin = new URL(request.url).origin; - const emailRedirectTo = `${origin}/login`; - - // Create user in Supabase Auth - const { data: authData, error: authError } = await supabase.auth.signUp({ - email, - password, - options: { emailRedirectTo }, - }); - - if (authError) { - return NextResponse.json({ error: authError.message }, { status: 400 }); - } - - if (!authData.user) { - return NextResponse.json( - { error: 'Failed to create user' }, - { status: 500 }, - ); - } - - // Get roleId - default to MEMBER if not provided - let finalRoleId = roleId; - if (!finalRoleId) { - finalRoleId = await UsersService.getRoleIdByRoleType(RoleType.MEMBER); - if (!finalRoleId) { - // Try to create the MEMBER role if it doesn't exist - try { - const newRole = await prisma.role.create({ - data: { roleType: RoleType.MEMBER }, - }); - finalRoleId = newRole.roleId; - } catch { - return NextResponse.json( - { - error: - 'Failed to get or create default role. Please contact an administrator.', - }, - { status: 500 }, - ); - } - } - } - - // Create user profile in Prisma with Supabase user ID - try { - const userProfile = await prisma.user.create({ - data: { - supabaseAuthId: authData.user.id, // Store Supabase auth ID - email, - firstName, - lastName, - nuid, - roleId: finalRoleId, - roleType: RoleType.MEMBER, - password: undefined, // Optional - Supabase handles authentication - }, - include: { - role: true, - }, - }); - - return NextResponse.json( - { - message: 'User created successfully', - user: userProfile, - }, - { status: 201 }, - ); - } catch (dbError: any) { - // Handle Prisma unique constraint violations - if (dbError?.code === 'P2002') { - const target = dbError?.meta?.target; - if (Array.isArray(target)) { - if (target.includes('email')) { - return NextResponse.json( - { error: 'An account with this email already exists' }, - { status: 400 }, - ); - } - if (target.includes('nuid')) { - return NextResponse.json( - { error: 'An account with this NUID already exists' }, - { status: 400 }, - ); - } - if (target.includes('supabaseAuthId')) { - return NextResponse.json( - { - error: 'An account with this authentication ID already exists', - }, - { status: 400 }, - ); - } - } - return NextResponse.json( - { error: 'A user with this information already exists' }, - { status: 400 }, - ); - } - // Re-throw to be caught by outer catch block - throw dbError; - } - } catch (error) { - return NextResponse.json( - { - error: error instanceof Error ? error.message : 'Failed to create user', - }, - { status: 500 }, - ); - } -} +import { NextResponse } from 'next/server'; +import { createServerSupabaseClient } from '@/lib/supabase-server'; +import { prisma } from '@/lib/prisma'; +import { UsersService } from '@/users/users.service'; +import { RoleType } from '@/generated/prisma'; + +export async function POST(request: Request) { + try { + const { email, password, firstName, lastName, nuid, roleId } = + await request.json(); + + // Validate required fields + if (!email || !password || !firstName || !lastName || !nuid) { + return NextResponse.json( + { error: 'Missing required fields' }, + { status: 400 }, + ); + } + + const supabase = await createServerSupabaseClient(); + + // Use the request origin so verification links go to the same host + const origin = new URL(request.url).origin; + const emailRedirectTo = `${origin}/login`; + + // Create user in Supabase Auth + const { data: authData, error: authError } = await supabase.auth.signUp({ + email, + password, + options: { emailRedirectTo }, + }); + + if (authError) { + return NextResponse.json({ error: authError.message }, { status: 400 }); + } + + if (!authData.user) { + return NextResponse.json( + { error: 'Failed to create user' }, + { status: 500 }, + ); + } + + // Get roleId - default to MEMBER if not provided + let finalRoleId = roleId; + if (!finalRoleId) { + finalRoleId = await UsersService.getRoleIdByRoleType(RoleType.MEMBER); + if (!finalRoleId) { + // Try to create the MEMBER role if it doesn't exist + try { + const newRole = await prisma.role.create({ + data: { roleType: RoleType.MEMBER }, + }); + finalRoleId = newRole.roleId; + } catch { + return NextResponse.json( + { + error: + 'Failed to get or create default role. Please contact an administrator.', + }, + { status: 500 }, + ); + } + } + } + + // Create user profile in Prisma with Supabase user ID + try { + const userProfile = await prisma.user.create({ + data: { + supabaseAuthId: authData.user.id, // Store Supabase auth ID + email, + firstName, + lastName, + nuid, + roleId: finalRoleId, + roleType: RoleType.MEMBER, + password: undefined, // Optional - Supabase handles authentication + }, + include: { + role: true, + }, + }); + + return NextResponse.json( + { + message: 'User created successfully', + user: userProfile, + }, + { status: 201 }, + ); + } catch (dbError: any) { + // Handle Prisma unique constraint violations + if (dbError?.code === 'P2002') { + const target = dbError?.meta?.target; + if (Array.isArray(target)) { + if (target.includes('email')) { + return NextResponse.json( + { error: 'An account with this email already exists' }, + { status: 400 }, + ); + } + if (target.includes('nuid')) { + return NextResponse.json( + { error: 'An account with this NUID already exists' }, + { status: 400 }, + ); + } + if (target.includes('supabaseAuthId')) { + return NextResponse.json( + { + error: 'An account with this authentication ID already exists', + }, + { status: 400 }, + ); + } + } + return NextResponse.json( + { error: 'A user with this information already exists' }, + { status: 400 }, + ); + } + // Re-throw to be caught by outer catch block + throw dbError; + } + } catch (error) { + return NextResponse.json( + { + error: error instanceof Error ? error.message : 'Failed to create user', + }, + { status: 500 }, + ); + } +} diff --git a/attendance-manager/src/app/api/meeting/[id]/route.ts b/attendance-manager/src/app/api/meeting/[id]/route.ts index 779910b9..d7fd96e6 100644 --- a/attendance-manager/src/app/api/meeting/[id]/route.ts +++ b/attendance-manager/src/app/api/meeting/[id]/route.ts @@ -1,7 +1,10 @@ import { NextResponse } from 'next/server'; import { MeetingController } from '@/meeting/meeting.controller'; import { requireAuth } from '@/utils/api-auth'; -import { checkCanEditMeetings, checkCanManageMeetings } from '@/utils/permissions'; +import { + checkCanEditMeetings, + checkCanManageMeetings, +} from '@/utils/permissions'; export async function GET( request: Request, diff --git a/attendance-manager/src/components/attendance/AttendancePage.tsx b/attendance-manager/src/components/attendance/AttendancePage.tsx index 77a77728..2f2cc19b 100644 --- a/attendance-manager/src/components/attendance/AttendancePage.tsx +++ b/attendance-manager/src/components/attendance/AttendancePage.tsx @@ -8,7 +8,7 @@ import { AttendanceApiData, AttendanceSchema, UserSchema, - RequestApiData + RequestApiData, } from '@/types'; import AttendanceMeetingEdit from './AttendanceMeetingEdit'; import AttendanceMeetingUserList from './AttendanceMeetingUserList'; @@ -41,7 +41,7 @@ const AttendancePage: React.FC = () => { // New state for attendance marking and editing const [showEditAttendanceModal, setShowEditAttendanceModal] = useState(false); const [selectedMeeting, setSelectedMeeting] = useState( - null + null, ); const [nuidInput, setNuidInput] = useState(''); const [attendanceUsers, setAttendanceUsers] = useState< @@ -61,16 +61,14 @@ const AttendancePage: React.FC = () => { const [attendanceCheckStep, setAttendanceCheckStep] = useState< 'select-meeting' | 'user-list' | 'check-in' >('select-meeting'); - const [ - selectedMeetingForCheck, - setSelectedMeetingForCheck - ] = useState(null); + const [selectedMeetingForCheck, setSelectedMeetingForCheck] = + useState(null); // New state for Requests viewing (admin archive) const [showRequestsModal, setShowRequestsModal] = useState(false); const [requests, setRequests] = useState([]); const [requestsView, setRequestsView] = useState<'active' | 'history'>( - 'active' + 'active', ); const [declinedRequestIds, setDeclinedRequestIds] = useState([]); @@ -106,12 +104,12 @@ const AttendancePage: React.FC = () => { try { const response = await fetch( - `/api/attendance/meeting/${selectedMeetingForCheck.meetingId}` + `/api/attendance/meeting/${selectedMeetingForCheck.meetingId}`, ); const allAttendance = await response.json(); setAttendanceRecord((prev: Record) => ({ ...prev, - [selectedMeetingForCheck.meetingId]: allAttendance + [selectedMeetingForCheck.meetingId]: allAttendance, })); } catch { /* 'Error loading user profile:', error */ @@ -134,8 +132,9 @@ const AttendancePage: React.FC = () => { const attendances = await meetingAPI.getAttendances(meeting.meetingId); const totalMembers = attendances.length; - const attendedMembers = attendances.filter(a => a.status === 'Present') - .length; + const attendedMembers = attendances.filter( + (a) => a.status === 'Present', + ).length; const percentage = totalMembers === 0 ? 0 @@ -145,11 +144,11 @@ const AttendancePage: React.FC = () => { ...meeting, totalMembers, attendedMembers, - percentage + percentage, }); setAttendanceRecord((prev: Record) => ({ ...prev, - [meeting.meetingId]: attendances + [meeting.meetingId]: attendances, })); } setMeetingsWithAttendance(updatedMeetings); @@ -197,7 +196,9 @@ const AttendancePage: React.FC = () => { try { // Find user by NUID - const userToMark = attendanceUsers.find(u => u.nuid === nuidInput.trim()); + const userToMark = attendanceUsers.find( + (u) => u.nuid === nuidInput.trim(), + ); if (!userToMark) { alert('NUID not found. Please check and try again.'); return; @@ -205,14 +206,14 @@ const AttendancePage: React.FC = () => { // Check if already marked as present const attendanceForMeeting = userToMark.attendance.find( (attendance: AttendanceType) => - attendance.meetingId === selectedMeetingForCheck.meetingId + attendance.meetingId === selectedMeetingForCheck.meetingId, ); if ( attendanceForMeeting?.status === 'PRESENT' || attendanceForMeeting?.status === 'Present' ) { alert( - `${userToMark.firstName} ${userToMark.lastName} is already marked as present!` + `${userToMark.firstName} ${userToMark.lastName} is already marked as present!`, ); setNuidInput(''); return; @@ -224,10 +225,10 @@ const AttendancePage: React.FC = () => { body: JSON.stringify({ userId: userToMark.userId, meetingId: selectedMeetingForCheck.meetingId, - status: 'PRESENT' - }) + status: 'PRESENT', + }), }); - const updatedUsers = attendanceUsers.map(u => { + const updatedUsers = attendanceUsers.map((u) => { if (u.nuid === nuidInput.trim()) { return { ...u, status: 'PRESENT' }; // update just this user } @@ -241,7 +242,7 @@ const AttendancePage: React.FC = () => { } alert( - `✓ ${userToMark.firstName} ${userToMark.lastName} marked as present!` + `✓ ${userToMark.firstName} ${userToMark.lastName} marked as present!`, ); setNuidInput(''); @@ -264,7 +265,7 @@ const AttendancePage: React.FC = () => { // Function to soft delete a member const handleDeleteMember = async (userId: string) => { await fetch(`/api/users/${userId}`, { method: 'DELETE' }); - setUsers(prev => prev.filter(u => u.userId !== userId)); + setUsers((prev) => prev.filter((u) => u.userId !== userId)); }; // Function to toggle attendance status in edit modal @@ -272,7 +273,7 @@ const AttendancePage: React.FC = () => { attendanceId: string, currentStatus: string, userId: string, - meetingId: string + meetingId: string, ) => { try { const newStatus = @@ -282,7 +283,7 @@ const AttendancePage: React.FC = () => { const response = await fetch(`/api/attendance/${attendanceId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ status: newStatus }) + body: JSON.stringify({ status: newStatus }), }); if (!response.ok) { @@ -291,34 +292,34 @@ const AttendancePage: React.FC = () => { // Reload attendance data if (selectedMeeting) { - setAttendanceUsers(prev => - prev.map(u => + setAttendanceUsers((prev) => + prev.map((u) => u.userId === userId ? { ...u, attendance: u.attendance.map((a: AttendanceType) => a.attendanceId === attendanceId ? { ...a, status: newStatus } - : a - ) + : a, + ), } - : u - ) + : u, + ), ); // TODO (jwuchen): either find a better way to do this or don't trigger this till edit attendance componenet is closed - setMeetings(prevMeetings => - prevMeetings.map(meeting => + setMeetings((prevMeetings) => + prevMeetings.map((meeting) => meeting.meetingId === meetingId ? { ...meeting, - attendance: meeting.attendance.map(a => + attendance: meeting.attendance.map((a) => a.attendanceId === attendanceId ? { ...a, status: newStatus } - : a - ) + : a, + ), } - : meeting - ) + : meeting, + ), ); } } catch { @@ -333,8 +334,8 @@ const AttendancePage: React.FC = () => { setShowEditAttendanceModal(true); }; - const eboardMembers = users.filter(m => m.roleType === 'EBOARD'); - const regularMembers = users.filter(m => m.roleType === 'MEMBER'); + const eboardMembers = users.filter((m) => m.roleType === 'EBOARD'); + const regularMembers = users.filter((m) => m.roleType === 'MEMBER'); return (
diff --git a/attendance-manager/src/components/meetings/MeetingHistoryPanel.tsx b/attendance-manager/src/components/meetings/MeetingHistoryPanel.tsx index 3962296f..6d45b39f 100644 --- a/attendance-manager/src/components/meetings/MeetingHistoryPanel.tsx +++ b/attendance-manager/src/components/meetings/MeetingHistoryPanel.tsx @@ -170,7 +170,7 @@ const MeetingHistoryPanel: React.FC = ({
- {isEboard && ( + {canEditMeetings && (