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 && (
|
{canEditMeetings && (
+
handleEditMeeting(meeting)}
className='px-3 py-1 bg-[#C8102E] text-white text-sm rounded-lg hover:bg-[#A8102E] transition-colors'
From 07bd0d5e94dc8371ffcad80eeed62f51e0f3c32b Mon Sep 17 00:00:00 2001
From: wongcolin45
Date: Sun, 29 Mar 2026 18:29:03 -0400
Subject: [PATCH 10/25] Change CI to do migration first
---
.github/workflows/ci.yml | 6 +++++-
attendance-manager/src/utils/__tests__/api-auth.test.ts | 1 +
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index faade0ec..ebfa34cc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,7 +30,11 @@ jobs:
- name: Generate Prisma Client
working-directory: attendance-manager
run: npx prisma generate
-
+
+ - name: Run Database Migrations
+ working-directory: attendance-manager
+ run: npx prisma migrate deploy
+
- name: Lint
working-directory: attendance-manager
run: npm run lint
diff --git a/attendance-manager/src/utils/__tests__/api-auth.test.ts b/attendance-manager/src/utils/__tests__/api-auth.test.ts
index 9002dbe9..47719f7d 100644
--- a/attendance-manager/src/utils/__tests__/api-auth.test.ts
+++ b/attendance-manager/src/utils/__tests__/api-auth.test.ts
@@ -34,6 +34,7 @@ describe('API Auth Utilities', () => {
firstName: 'API',
lastName: 'Test',
roleId: testRoleId,
+ roleType: 'MEMBER',
password: null,
},
});
From bdbd06e9516066cb65ff53629136cea812130938 Mon Sep 17 00:00:00 2001
From: wongcolin45
Date: Sun, 29 Mar 2026 18:35:30 -0400
Subject: [PATCH 11/25] Update ci.yml
---
.github/workflows/ci.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ebfa34cc..61acc4cb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,6 +13,7 @@ jobs:
runs-on: ubuntu-latest
env:
DATABASE_URL: ${{ secrets.DATABASE_URL_TEST }}
+ DIRECT_URL: ${{ secrets.DATABASE_URL_TEST }}
NODE_ENV: test
steps:
- name: Checkout repository
From 33fcfb890228fa1c668c4848c6e9051eb7f42442 Mon Sep 17 00:00:00 2001
From: wongcolin45
Date: Mon, 30 Mar 2026 06:51:13 -0400
Subject: [PATCH 12/25] Remove None Role Type
---
.../migration.sql | 1 -
.../20260328000001_add_role_type_column/migration.sql | 4 ++--
attendance-manager/prisma/schema.prisma | 3 +--
attendance-manager/src/types/index.ts | 2 +-
attendance-manager/src/utils/auth_utils.ts | 1 -
5 files changed, 4 insertions(+), 7 deletions(-)
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
index f276c9fc..cc57e825 100644
--- 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
@@ -1,5 +1,4 @@
-- Add new RoleType enum values only
-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';
diff --git a/attendance-manager/prisma/migrations/20260328000001_add_role_type_column/migration.sql b/attendance-manager/prisma/migrations/20260328000001_add_role_type_column/migration.sql
index fe26af1c..ecc9e71f 100644
--- a/attendance-manager/prisma/migrations/20260328000001_add_role_type_column/migration.sql
+++ b/attendance-manager/prisma/migrations/20260328000001_add_role_type_column/migration.sql
@@ -1,5 +1,5 @@
--- Add roleType column with default NONE
-ALTER TABLE "User" ADD COLUMN "roleType" "RoleType" NOT NULL DEFAULT 'NONE';
+-- Add roleType column with default MEMBER
+ALTER TABLE "User" ADD COLUMN "roleType" "RoleType" NOT NULL DEFAULT 'MEMBER';
-- Add isVotingMember column with default false
ALTER TABLE "User" ADD COLUMN "isVotingMember" BOOLEAN NOT NULL DEFAULT false;
diff --git a/attendance-manager/prisma/schema.prisma b/attendance-manager/prisma/schema.prisma
index b49aef77..ec7423b8 100644
--- a/attendance-manager/prisma/schema.prisma
+++ b/attendance-manager/prisma/schema.prisma
@@ -23,7 +23,7 @@ model User {
firstName String
lastName String
roleId String
- roleType RoleType @default(NONE)
+ roleType RoleType @default(MEMBER)
isVotingMember Boolean @default(false)
password String? // Optional - Supabase handles authentication
attendance Attendance[]
@@ -97,7 +97,6 @@ model VotingRecord {
}
enum RoleType {
- NONE
SUPER_ADMIN
ADMIN
SENATOR
diff --git a/attendance-manager/src/types/index.ts b/attendance-manager/src/types/index.ts
index bbf4a00b..9108ae0f 100644
--- a/attendance-manager/src/types/index.ts
+++ b/attendance-manager/src/types/index.ts
@@ -4,7 +4,7 @@ export interface User {
id: string;
email: string;
name: string;
- role: 'NONE' | 'SUPER_ADMIN' | 'ADMIN' | 'SENATOR' | 'EBOARD' | 'MEMBER';
+ role: 'SUPER_ADMIN' | 'ADMIN' | 'SENATOR' | 'EBOARD' | 'MEMBER';
avatar?: string;
}
diff --git a/attendance-manager/src/utils/auth_utils.ts b/attendance-manager/src/utils/auth_utils.ts
index c13021bc..5aa4b7e7 100644
--- a/attendance-manager/src/utils/auth_utils.ts
+++ b/attendance-manager/src/utils/auth_utils.ts
@@ -28,7 +28,6 @@ export const login = async (
let user_details: UserData = await res.json();
if (
!(
- user_details.roleType === 'NONE' ||
user_details.roleType === 'SENATOR' ||
user_details.roleType === 'SUPER_ADMIN' ||
user_details.roleType === 'ADMIN' ||
From ff9c87fc42eb65a56d91def6454131ee1d2e1d57 Mon Sep 17 00:00:00 2001
From: wongcolin45
Date: Mon, 30 Mar 2026 06:56:39 -0400
Subject: [PATCH 13/25] Fix
---
attendance-manager/src/app/api/auth/signup/route.ts | 2 +-
.../src/components/meetings/MeetingHistoryPanel.tsx | 1 -
attendance-manager/src/utils/auth_utils.ts | 1 -
3 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/attendance-manager/src/app/api/auth/signup/route.ts b/attendance-manager/src/app/api/auth/signup/route.ts
index 5517ac68..ab55daba 100644
--- a/attendance-manager/src/app/api/auth/signup/route.ts
+++ b/attendance-manager/src/app/api/auth/signup/route.ts
@@ -74,7 +74,7 @@ export async function POST(request: Request) {
lastName,
nuid,
roleId: finalRoleId,
- roleType: RoleType.MEMBER
+ roleType: RoleType.MEMBER,
password: undefined, // Optional - Supabase handles authentication
},
include: {
diff --git a/attendance-manager/src/components/meetings/MeetingHistoryPanel.tsx b/attendance-manager/src/components/meetings/MeetingHistoryPanel.tsx
index 3b250fd4..dffa34b8 100644
--- a/attendance-manager/src/components/meetings/MeetingHistoryPanel.tsx
+++ b/attendance-manager/src/components/meetings/MeetingHistoryPanel.tsx
@@ -3,7 +3,6 @@ import { MeetingApiData, MeetingType } from '@/types';
import { useAuth } from '@/contexts/AuthContext';
import { checkCanEditMeetings } from '@/utils/permissions';
import { Calendar } from 'lucide-react';
-import React from 'react';
interface MeetingHistoryPanelProps {
setActiveTab: (option: 'past' | 'upcoming') => void;
diff --git a/attendance-manager/src/utils/auth_utils.ts b/attendance-manager/src/utils/auth_utils.ts
index 3f843707..e549d360 100644
--- a/attendance-manager/src/utils/auth_utils.ts
+++ b/attendance-manager/src/utils/auth_utils.ts
@@ -24,7 +24,6 @@ export const login = async (
let userDetails: UserData = await res.json();
if (
!(
- userDetails.roleType === 'NONE' ||
userDetails.roleType === 'SENATOR' ||
userDetails.roleType === 'SUPER_ADMIN' ||
userDetails.roleType === 'ADMIN' ||
From d929d688e8209e293ca7b4f44dc70367632cc570 Mon Sep 17 00:00:00 2001
From: wongcolin45
Date: Mon, 30 Mar 2026 07:05:36 -0400
Subject: [PATCH 14/25] Prettier
---
.../components/attendance/AttendancePage.tsx | 73 +++++----
.../src/components/layout/Header.tsx | 34 ++---
.../src/components/meetings/MeetingsPage.tsx | 142 +++++++++---------
.../src/contexts/AuthContext.tsx | 10 +-
attendance-manager/src/users/users.service.ts | 34 ++---
attendance-manager/src/utils/auth_utils.ts | 10 +-
attendance-manager/src/utils/permissions.ts | 2 +-
7 files changed, 154 insertions(+), 151 deletions(-)
diff --git a/attendance-manager/src/components/attendance/AttendancePage.tsx b/attendance-manager/src/components/attendance/AttendancePage.tsx
index d2fb2608..77a77728 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,14 +61,16 @@ 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([]);
@@ -104,12 +106,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 */
@@ -132,9 +134,8 @@ 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
@@ -144,11 +145,11 @@ const AttendancePage: React.FC = () => {
...meeting,
totalMembers,
attendedMembers,
- percentage,
+ percentage
});
setAttendanceRecord((prev: Record) => ({
...prev,
- [meeting.meetingId]: attendances,
+ [meeting.meetingId]: attendances
}));
}
setMeetingsWithAttendance(updatedMeetings);
@@ -196,9 +197,7 @@ 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;
@@ -206,14 +205,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;
@@ -225,10 +224,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
}
@@ -242,7 +241,7 @@ const AttendancePage: React.FC = () => {
}
alert(
- `✓ ${userToMark.firstName} ${userToMark.lastName} marked as present!`,
+ `✓ ${userToMark.firstName} ${userToMark.lastName} marked as present!`
);
setNuidInput('');
@@ -265,7 +264,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
@@ -273,7 +272,7 @@ const AttendancePage: React.FC = () => {
attendanceId: string,
currentStatus: string,
userId: string,
- meetingId: string,
+ meetingId: string
) => {
try {
const newStatus =
@@ -283,7 +282,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) {
@@ -292,34 +291,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 {
diff --git a/attendance-manager/src/components/layout/Header.tsx b/attendance-manager/src/components/layout/Header.tsx
index 0ef1e9d9..ad5a4a5d 100644
--- a/attendance-manager/src/components/layout/Header.tsx
+++ b/attendance-manager/src/components/layout/Header.tsx
@@ -11,23 +11,23 @@ interface HeaderProps {
const Header: React.FC = ({ onProfileClick, onLogoClick }) => {
const { user } = useAuth();
-/* 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':
- return 'Member';
- default:
- return role;
- }
-};
+ /* 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':
+ return 'Member';
+ default:
+ return role;
+ }
+ };
return (
diff --git a/attendance-manager/src/components/meetings/MeetingsPage.tsx b/attendance-manager/src/components/meetings/MeetingsPage.tsx
index b15ac190..b2396868 100644
--- a/attendance-manager/src/components/meetings/MeetingsPage.tsx
+++ b/attendance-manager/src/components/meetings/MeetingsPage.tsx
@@ -5,7 +5,7 @@ import {
RequestApiData,
RemainingAbsences,
MeetingType,
- RequestForm,
+ RequestForm
} from '../../types';
import { z } from 'zod';
import { useAuth } from '../../contexts/AuthContext';
@@ -39,7 +39,8 @@ const normalizeDate = (dateStr: string) => {
const parsed = new Date(dateStr);
if (!Number.isNaN(parsed.getTime())) {
- return `${parsed.getMonth() + 1}/${parsed.getDate()}/${parsed.getFullYear()}`;
+ return `${parsed.getMonth() +
+ 1}/${parsed.getDate()}/${parsed.getFullYear()}`;
}
const today = new Date();
@@ -53,7 +54,7 @@ const MeetingsPage: React.FC = () => {
const [showEditMeetingModal, setShowEditMeetingModal] = useState(false);
const [showDeleteMeetingModal, setShowDeleteMeetingModal] = useState(false);
const [editingMeeting, setEditingMeeting] = useState(
- null,
+ null
);
const [newMeeting, setNewMeeting] = useState({
name: '',
@@ -62,7 +63,7 @@ const MeetingsPage: React.FC = () => {
endTime: '',
notes: '',
type: 'REGULAR' as MeetingType, // defaults to REGULAR
- selectedAttendees: [] as string[],
+ selectedAttendees: [] as string[]
});
const [editMeeting, setEditMeeting] = useState({
name: '',
@@ -70,10 +71,10 @@ const MeetingsPage: React.FC = () => {
startTime: '',
endTime: '',
notes: '',
- type: 'REGULAR' as 'FULL_BODY' | 'REGULAR',
+ type: 'REGULAR' as 'FULL_BODY' | 'REGULAR'
});
const [deleteMeeting, setDeleteMeeting] = useState(
- null,
+ null
);
const [meetings, setMeetings] = useState([]);
const [showCreateRequestModal, setShowCreateRequestModal] = useState(false);
@@ -84,9 +85,9 @@ const MeetingsPage: React.FC = () => {
requestTypes: {
leavingEarly: false,
comingLate: false,
- goingOnline: false,
+ goingOnline: false
},
- explanation: '',
+ explanation: ''
});
const [typeFilter, setTypeFilter] = useState(null);
const [selectedUserIds, setSelectedUserIds] = useState([]);
@@ -94,13 +95,15 @@ const MeetingsPage: React.FC = () => {
// Check if user is admin (EBOARD)
const canManageMeetings = checkCanManageMeetings(user?.role);
const isMember = user?.role === 'MEMBER';
- const [remainingAbsences, setRemainingAbsences] =
- useState(null);
+ const [
+ remainingAbsences,
+ setRemainingAbsences
+ ] = useState(null);
const fetchMeetings = () => {
fetch('/api/meeting')
- .then((response) => response.json())
- .then((json) => {
+ .then(response => response.json())
+ .then(json => {
setMeetings(json);
})
// eslint-disable-next-line
@@ -114,8 +117,8 @@ const MeetingsPage: React.FC = () => {
useEffect(() => {
if (!editingMeeting?.meetingId) return;
fetch(`/api/meeting/${editingMeeting.meetingId}/users`)
- .then((response) => response.json())
- .then((json) => setSelectedUserIds(json.map((d: any) => d.userId)))
+ .then(response => response.json())
+ .then(json => setSelectedUserIds(json.map((d: any) => d.userId)))
// eslint-disable-next-line
.catch(console.error);
}, [editingMeeting?.meetingId, showEditMeetingModal]);
@@ -128,7 +131,7 @@ const MeetingsPage: React.FC = () => {
startTime: meeting.startTime,
endTime: meeting.endTime,
notes: meeting.notes,
- type: meeting.type as 'FULL_BODY' | 'REGULAR',
+ type: meeting.type as 'FULL_BODY' | 'REGULAR'
});
setShowEditMeetingModal(true);
};
@@ -146,15 +149,15 @@ const MeetingsPage: React.FC = () => {
const response = await fetch(`/api/meeting/${editingMeeting.meetingId}`, {
method: 'PUT',
headers: {
- 'Content-Type': 'application/json',
+ 'Content-Type': 'application/json'
},
- body: JSON.stringify(editMeeting),
+ body: JSON.stringify(editMeeting)
});
if (!response.ok) {
const errorData = await response.json();
alert(
- `Failed to update meeting: ${errorData.error || 'Unknown error'}`,
+ `Failed to update meeting: ${errorData.error || 'Unknown error'}`
);
return;
}
@@ -162,7 +165,7 @@ const MeetingsPage: React.FC = () => {
await fetch(`/api/meeting/${editingMeeting.meetingId}/users`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ userIds: selectedUserIds }),
+ body: JSON.stringify({ userIds: selectedUserIds })
});
// Refresh meetings list
@@ -177,7 +180,7 @@ const MeetingsPage: React.FC = () => {
startTime: '',
endTime: '',
notes: '',
- type: 'REGULAR',
+ type: 'REGULAR'
});
alert('Meeting updated successfully!');
} catch {
@@ -189,11 +192,11 @@ const MeetingsPage: React.FC = () => {
useEffect(() => {
if (user?.id) {
fetch(`/api/attendance/user/${user.id}/remaining-absences`)
- .then((response) => response.json())
+ .then(response => response.json())
.then((data: RemainingAbsences) => {
setRemainingAbsences(data);
})
- .catch((error) => {
+ .catch(error => {
alert(`Failed to fetch remaining absences: ${error}`);
});
}
@@ -202,13 +205,13 @@ const MeetingsPage: React.FC = () => {
const [members, setMembers] = useState[]>([]);
const [bulkSelectionActive, setBulkSelectionActive] = useState({
nonEboard: false,
- allMembers: false,
+ allMembers: false
});
useEffect(() => {
fetch('/api/users')
- .then((res) => res.json())
- .then((data) => {
+ .then(res => res.json())
+ .then(data => {
setMembers(data);
})
// eslint-disable-next-line
@@ -220,16 +223,15 @@ const MeetingsPage: React.FC = () => {
[members]
);
const nonEboardMemberIds = useMemo(
- () => nonEboardMembers.map((member) => member.userId),
- [nonEboardMembers],
- );
- const allMemberIds = useMemo(
- () => members.map((member) => member.userId),
- [members],
+ () => nonEboardMembers.map(member => member.userId),
+ [nonEboardMembers]
);
+ const allMemberIds = useMemo(() => members.map(member => member.userId), [
+ members
+ ]);
const selectedAttendeeSet = useMemo(
() => new Set(newMeeting.selectedAttendees),
- [newMeeting.selectedAttendees],
+ [newMeeting.selectedAttendees]
);
const bulkSelectButtonClasses = (active: boolean) =>
@@ -241,17 +243,17 @@ const MeetingsPage: React.FC = () => {
const toggleNonEboardSelection = () => {
if (bulkSelectionActive.nonEboard) {
- setNewMeeting((prev) => ({
+ setNewMeeting(prev => ({
...prev,
selectedAttendees: prev.selectedAttendees.filter(
- (id) => !nonEboardMemberIds.includes(id),
- ),
+ id => !nonEboardMemberIds.includes(id)
+ )
}));
- setBulkSelectionActive((prev) => ({ ...prev, nonEboard: false }));
+ setBulkSelectionActive(prev => ({ ...prev, nonEboard: false }));
} else {
- setNewMeeting((prev) => ({
+ setNewMeeting(prev => ({
...prev,
- selectedAttendees: nonEboardMemberIds,
+ selectedAttendees: nonEboardMemberIds
}));
setBulkSelectionActive({ nonEboard: true, allMembers: false });
}
@@ -259,10 +261,10 @@ const MeetingsPage: React.FC = () => {
const toggleAllMembersSelection = () => {
if (bulkSelectionActive.allMembers) {
- setNewMeeting((prev) => ({ ...prev, selectedAttendees: [] }));
- setBulkSelectionActive((prev) => ({ ...prev, allMembers: false }));
+ setNewMeeting(prev => ({ ...prev, selectedAttendees: [] }));
+ setBulkSelectionActive(prev => ({ ...prev, allMembers: false }));
} else {
- setNewMeeting((prev) => ({ ...prev, selectedAttendees: allMemberIds }));
+ setNewMeeting(prev => ({ ...prev, selectedAttendees: allMemberIds }));
setBulkSelectionActive({ nonEboard: false, allMembers: true });
}
};
@@ -271,9 +273,9 @@ const MeetingsPage: React.FC = () => {
if (!bulkSelectionActive.nonEboard) return;
const allSelected =
nonEboardMemberIds.length > 0 &&
- nonEboardMemberIds.every((id) => selectedAttendeeSet.has(id));
+ nonEboardMemberIds.every(id => selectedAttendeeSet.has(id));
if (!allSelected) {
- setBulkSelectionActive((prev) => ({ ...prev, nonEboard: false }));
+ setBulkSelectionActive(prev => ({ ...prev, nonEboard: false }));
}
}, [bulkSelectionActive.nonEboard, nonEboardMemberIds, selectedAttendeeSet]);
@@ -281,37 +283,37 @@ const MeetingsPage: React.FC = () => {
if (!bulkSelectionActive.allMembers) return;
const allSelected =
allMemberIds.length > 0 &&
- allMemberIds.every((id) => selectedAttendeeSet.has(id));
+ allMemberIds.every(id => selectedAttendeeSet.has(id));
if (!allSelected) {
- setBulkSelectionActive((prev) => ({ ...prev, allMembers: false }));
+ setBulkSelectionActive(prev => ({ ...prev, allMembers: false }));
}
}, [bulkSelectionActive.allMembers, allMemberIds, selectedAttendeeSet]);
// Calculate statistics from real meetings
const today = new Date();
// Calculate statistics from real data
- const attendedMeetings = meetings.filter((m) => {
+ const attendedMeetings = meetings.filter(m => {
const meetingDate = new Date(m.date);
if (meetingDate > today) return false; // Skip upcoming meetings
// Check if current user attended this meeting
return m.attendance.some(
- (a) => a.userId === user?.id && a.status === 'PRESENT',
+ a => a.userId === user?.id && a.status === 'PRESENT'
);
}).length;
- const missedMeetings = meetings.filter((m) => {
+ const missedMeetings = meetings.filter(m => {
const meetingDate = new Date(m.date);
if (meetingDate > today) return false; // Skip upcoming meetings
// Check if current user was absent
return m.attendance.some(
- (a) =>
+ a =>
a.userId === user?.id &&
- (a.status === 'UNEXCUSED_ABSENCE' || a.status === 'EXCUSED_ABSENCE'),
+ (a.status === 'UNEXCUSED_ABSENCE' || a.status === 'EXCUSED_ABSENCE')
);
}).length;
// Filter meetings based on active tab
- const filteredMeetings = meetings.filter((m) => {
+ const filteredMeetings = meetings.filter(m => {
// change to 'meetings' for implementation
const meetingDate = new Date(m.date);
if (activeTab === 'past') {
@@ -325,7 +327,7 @@ const MeetingsPage: React.FC = () => {
}
// Get upcoming meetings for request creation
- const upcomingMeetingsList = meetings.filter((m) => parseEST(m.date) > today);
+ const upcomingMeetingsList = meetings.filter(m => parseEST(m.date) > today);
// Handle request submission
const handleSubmitRequest = async () => {
@@ -360,14 +362,16 @@ const MeetingsPage: React.FC = () => {
: 'IN_PERSON';
// timeAdjustment: can only have one (leavingEarly or comingLate)
- let timeAdjustment: 'ARRIVING_LATE' | 'LEAVING_EARLY' | undefined =
- undefined;
+ let timeAdjustment:
+ | 'ARRIVING_LATE'
+ | 'LEAVING_EARLY'
+ | undefined = undefined;
if (
requestForm.requestTypes.leavingEarly &&
requestForm.requestTypes.comingLate
) {
alert(
- 'Please select only one time adjustment (either leaving early OR coming late)',
+ 'Please select only one time adjustment (either leaving early OR coming late)'
);
return;
} else if (requestForm.requestTypes.leavingEarly) {
@@ -388,19 +392,19 @@ const MeetingsPage: React.FC = () => {
body: JSON.stringify({
userId: user.id,
meetingId: meetingId,
- status: 'PENDING',
- }),
+ status: 'PENDING'
+ })
});
if (!attendanceResponse.ok) {
throw new Error(
- `Failed to create/update attendance for meeting ${meetingId}`,
+ `Failed to create/update attendance for meeting ${meetingId}`
);
}
// Fetch the user's attendance to find the one we just created/updated
const userAttendanceResponse = await fetch(
- `/api/attendance/user/${user.id}`,
+ `/api/attendance/user/${user.id}`
);
if (!userAttendanceResponse.ok) {
throw new Error('Failed to fetch attendance record'); // single quotes
@@ -408,12 +412,12 @@ const MeetingsPage: React.FC = () => {
const userAttendance = await userAttendanceResponse.json();
const attendanceRecord = userAttendance.find(
- (a: any) => a.meetingId === meetingId,
+ (a: any) => a.meetingId === meetingId
);
if (!attendanceRecord || !attendanceRecord.attendanceId) {
throw new Error(
- `Attendance record not found for meeting ${meetingId}`,
+ `Attendance record not found for meeting ${meetingId}`
);
}
@@ -423,7 +427,7 @@ const MeetingsPage: React.FC = () => {
const requestPayload: any = {
attendanceId,
reason: requestForm.explanation,
- attendanceMode,
+ attendanceMode
};
if (timeAdjustment) {
@@ -433,14 +437,14 @@ const MeetingsPage: React.FC = () => {
const requestResponse = await fetch('/api/requests', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(requestPayload),
+ body: JSON.stringify(requestPayload)
});
if (!requestResponse.ok) {
const errorData = await requestResponse.json();
throw new Error(
errorData.error ||
- `Failed to create request for meeting ${meetingId}`,
+ `Failed to create request for meeting ${meetingId}`
);
}
@@ -456,9 +460,9 @@ const MeetingsPage: React.FC = () => {
requestTypes: {
leavingEarly: false,
comingLate: false,
- goingOnline: false,
+ goingOnline: false
},
- explanation: '',
+ explanation: ''
});
setShowCreateRequestModal(false);
} catch (error) {
@@ -472,13 +476,13 @@ const MeetingsPage: React.FC = () => {
try {
const response = await fetch(`/api/meeting/${deleteMeeting.meetingId}`, {
- method: 'DELETE',
+ method: 'DELETE'
});
if (!response.ok) {
const errorData = await response.json();
alert(
- `Failed to delete meeting: ${errorData.error || 'Unknown error'}`,
+ `Failed to delete meeting: ${errorData.error || 'Unknown error'}`
);
return;
}
@@ -497,7 +501,7 @@ const MeetingsPage: React.FC = () => {
// visibleMeetings are meetings post-type-filter
const visibleMeetings = typeFilter
- ? filteredMeetings.filter((m) => m.type === typeFilter)
+ ? filteredMeetings.filter(m => m.type === typeFilter)
: filteredMeetings;
// Determine banner color based on remaining absences
diff --git a/attendance-manager/src/contexts/AuthContext.tsx b/attendance-manager/src/contexts/AuthContext.tsx
index 5ef894a4..71879d62 100644
--- a/attendance-manager/src/contexts/AuthContext.tsx
+++ b/attendance-manager/src/contexts/AuthContext.tsx
@@ -5,7 +5,7 @@ import React, {
useContext,
useState,
useEffect,
- ReactNode,
+ ReactNode
} from 'react';
import { User, LoginCredentials, AuthContextType } from '../types';
import { useRouter } from 'next/navigation';
@@ -36,7 +36,7 @@ export const AuthProvider: React.FC = ({ children }) => {
const checkSession = async () => {
try {
const {
- data: { session },
+ data: { session }
} = await supabase.auth.getSession();
if (session?.user) {
@@ -54,7 +54,7 @@ export const AuthProvider: React.FC = ({ children }) => {
// Listen for auth changes
const {
- data: { subscription },
+ data: { subscription }
} = supabase.auth.onAuthStateChange(async (event, session) => {
if (event === 'SIGNED_IN' && session?.user) {
await loadUserProfile(session.user.id);
@@ -96,7 +96,7 @@ export const AuthProvider: React.FC = ({ children }) => {
// Sign in with Supabase
const { data, error } = await supabase.auth.signInWithPassword({
email: credentials.email,
- password: credentials.password,
+ password: credentials.password
});
if (error) {
@@ -127,7 +127,7 @@ export const AuthProvider: React.FC = ({ children }) => {
login,
// eslint-disable-next-line
logout,
- isLoading,
+ isLoading
};
return {children};
diff --git a/attendance-manager/src/users/users.service.ts b/attendance-manager/src/users/users.service.ts
index f4f261f1..0aa08b33 100644
--- a/attendance-manager/src/users/users.service.ts
+++ b/attendance-manager/src/users/users.service.ts
@@ -5,20 +5,20 @@ export const UsersService = {
async getAllUsers() {
return prisma.user.findMany({
where: { deletedAt: null },
- include: { role: true, attendance: true },
+ include: { role: true, attendance: true }
});
},
async getAllRoles() {
return prisma.role.findMany({
- include: {},
+ include: {}
});
},
async getUserByNUID(nuid: string) {
const user = await prisma.user.findUnique({
where: { nuid },
- include: { role: true },
+ include: { role: true }
});
return user?.deletedAt ? null : user;
},
@@ -26,7 +26,7 @@ export const UsersService = {
async getUserById(userId: string) {
const user = await prisma.user.findUnique({
where: { userId },
- include: { role: true, attendance: true },
+ include: { role: true, attendance: true }
});
return user?.deletedAt ? null : user;
},
@@ -34,7 +34,7 @@ export const UsersService = {
async getUserByEmail(userEmail: string) {
const user = await prisma.user.findUnique({
where: { email: userEmail },
- include: { role: true },
+ include: { role: true }
});
return user?.deletedAt ? null : user;
},
@@ -42,7 +42,7 @@ export const UsersService = {
async getUserByNuid(nuid: string) {
const user = await prisma.user.findUnique({
where: { nuid },
- include: { role: true },
+ include: { role: true }
});
return user?.deletedAt ? null : user;
},
@@ -62,8 +62,8 @@ export const UsersService = {
return prisma.user.create({
data: {
...data,
- password: data.password ?? null,
- },
+ password: data.password ?? null
+ }
});
},
@@ -74,7 +74,7 @@ export const UsersService = {
async getRoleIdByRoleType(roleType: RoleType) {
const role = await prisma.role.findFirst({
where: { roleType },
- select: { roleId: true },
+ select: { roleId: true }
});
return role?.roleId;
},
@@ -93,7 +93,7 @@ export const UsersService = {
) {
return prisma.user.update({
where: { userId },
- data: updates,
+ data: updates
});
},
@@ -101,7 +101,7 @@ export const UsersService = {
// Soft delete attendance records: updates prisma deletedAt field instead of fully deleting
return prisma.user.update({
where: { userId },
- data: { deletedAt: new Date() },
+ data: { deletedAt: new Date() }
});
},
@@ -111,16 +111,16 @@ export const UsersService = {
async deleteRole(roleId: string) {
await prisma.user.deleteMany({
- where: { roleId },
+ where: { roleId }
});
return prisma.role.delete({
- where: { roleId },
+ where: { roleId }
});
},
async getRolesByRoleId(roleId: string) {
return prisma.role.findUnique({
- where: { roleId: roleId },
+ where: { roleId: roleId }
});
},
@@ -129,15 +129,15 @@ export const UsersService = {
where: {
deletedAt: null,
roleType
- },
+ }
});
},
async getUserBySupabaseId(supabaseAuthId: string) {
const user = await prisma.user.findUnique({
where: { supabaseAuthId },
- include: { role: true },
+ include: { role: true }
});
return user?.deletedAt ? null : user;
- },
+ }
};
diff --git a/attendance-manager/src/utils/auth_utils.ts b/attendance-manager/src/utils/auth_utils.ts
index e549d360..38a8bba7 100644
--- a/attendance-manager/src/utils/auth_utils.ts
+++ b/attendance-manager/src/utils/auth_utils.ts
@@ -8,16 +8,16 @@ export const login = async (
credentials: LoginCredentials,
setIsLoading: Dispatch>,
setUser: Dispatch>,
- router: ReturnType,
+ router: ReturnType
) => {
setIsLoading(true);
try {
const res = await fetch(
- `/api/users/get-user-by-email/${credentials.email}`,
+ `/api/users/get-user-by-email/${credentials.email}`
);
if (!res.ok) {
throw new Error(
- `Response status: ${res.status}\n. Response Msg: ${await res.text}`,
+ `Response status: ${res.status}\n. Response Msg: ${await res.text}`
);
}
@@ -28,7 +28,7 @@ export const login = async (
userDetails.roleType === 'SUPER_ADMIN' ||
userDetails.roleType === 'ADMIN' ||
userDetails.roleType === 'EBOARD' ||
- userDetails.roleType === 'MEMBER'
+ userDetails.roleType === 'MEMBER'
)
) {
alert('Incorrect Roles');
@@ -45,7 +45,7 @@ export const login = async (
email: credentials.email,
name: userDetails.firstName + ' ' + userDetails.lastName,
role: userDetails.roleType,
- avatar: undefined,
+ avatar: undefined
};
setUser(user);
diff --git a/attendance-manager/src/utils/permissions.ts b/attendance-manager/src/utils/permissions.ts
index bbac22cf..509e2dfa 100644
--- a/attendance-manager/src/utils/permissions.ts
+++ b/attendance-manager/src/utils/permissions.ts
@@ -10,4 +10,4 @@ export const checkCanEditMeetings = (role?: string) => role === 'EBOARD';
export const checkCanEditProfile = (role?: string) => role === 'EBOARD';
-export const checkCanManageVoting = (role?: string) => role === 'EBOARD';
\ No newline at end of file
+export const checkCanManageVoting = (role?: string) => role === 'EBOARD';
From d3ba4ca3be6afb828b28554241a2a45806b5e48b Mon Sep 17 00:00:00 2001
From: wongcolin45
Date: Mon, 30 Mar 2026 07:08:37 -0400
Subject: [PATCH 15/25] Update ci.yml
---
.github/workflows/ci.yml | 4 ----
1 file changed, 4 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 61acc4cb..1290cca9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,10 +32,6 @@ jobs:
working-directory: attendance-manager
run: npx prisma generate
- - name: Run Database Migrations
- working-directory: attendance-manager
- run: npx prisma migrate deploy
-
- name: Lint
working-directory: attendance-manager
run: npm run lint
From ec1c8c14b0ca7bd05b41cfccbd9a0314edaf91ad Mon Sep 17 00:00:00 2001
From: wongcolin45
Date: Wed, 1 Apr 2026 19:20:21 -0400
Subject: [PATCH 16/25] Lock down backend
---
.../src/app/api/attendance/[attendanceId]/route.ts | 12 ++++++++++++
.../src/app/api/meeting/[id]/route.ts | 13 +++++++++++++
attendance-manager/src/app/api/meeting/route.ts | 8 ++++++++
.../src/app/api/voting-event/[id]/route.ts | 8 ++++++++
.../src/app/api/voting-event/route.ts | 8 ++++++++
5 files changed, 49 insertions(+)
diff --git a/attendance-manager/src/app/api/attendance/[attendanceId]/route.ts b/attendance-manager/src/app/api/attendance/[attendanceId]/route.ts
index 63a7ac99..0064c947 100644
--- a/attendance-manager/src/app/api/attendance/[attendanceId]/route.ts
+++ b/attendance-manager/src/app/api/attendance/[attendanceId]/route.ts
@@ -1,5 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { AttendanceController } from '../../../../attendance/attendance.controller';
+import { requireAuth } from '@/utils/api-auth';
+import { checkCanManageAttendance } from '@/utils/permissions';
/**
* Updates an Attendance
@@ -12,6 +14,11 @@ export async function PATCH(
req: NextRequest,
{ params }: { params: Promise<{ attendanceId: string }> },
) {
+ const { user, error } = await requireAuth();
+ if (error) return error;
+ if (!checkCanManageAttendance(user.roleType)) {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
try {
const { attendanceId } = await params;
const data = await req.json();
@@ -38,6 +45,11 @@ export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ attendanceId: string }> },
) {
+ const { user, error } = await requireAuth();
+ if (error) return error;
+ if (!checkCanManageAttendance(user.roleType)) {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
try {
const { attendanceId } = await params; // Await params
await AttendanceController.deleteAttendance(attendanceId);
diff --git a/attendance-manager/src/app/api/meeting/[id]/route.ts b/attendance-manager/src/app/api/meeting/[id]/route.ts
index fec1442b..779910b9 100644
--- a/attendance-manager/src/app/api/meeting/[id]/route.ts
+++ b/attendance-manager/src/app/api/meeting/[id]/route.ts
@@ -1,4 +1,7 @@
+import { NextResponse } from 'next/server';
import { MeetingController } from '@/meeting/meeting.controller';
+import { requireAuth } from '@/utils/api-auth';
+import { checkCanEditMeetings, checkCanManageMeetings } from '@/utils/permissions';
export async function GET(
request: Request,
@@ -12,6 +15,11 @@ export async function PUT(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
+ const { user, error } = await requireAuth();
+ if (error) return error;
+ if (!checkCanEditMeetings(user.roleType)) {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
const { id } = await params;
return MeetingController.updateMeeting(request, { meetingId: id });
}
@@ -20,6 +28,11 @@ export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
+ const { user, error } = await requireAuth();
+ if (error) return error;
+ if (!checkCanManageMeetings(user.roleType)) {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
const { id } = await params;
return MeetingController.deleteMeeting({ meetingId: id });
}
diff --git a/attendance-manager/src/app/api/meeting/route.ts b/attendance-manager/src/app/api/meeting/route.ts
index 38bebc7e..81bbff1a 100644
--- a/attendance-manager/src/app/api/meeting/route.ts
+++ b/attendance-manager/src/app/api/meeting/route.ts
@@ -1,4 +1,7 @@
+import { NextResponse } from 'next/server';
import { MeetingController } from '@/meeting/meeting.controller';
+import { requireAuth } from '@/utils/api-auth';
+import { checkCanManageMeetings } from '@/utils/permissions';
/**
* @swagger
* /api/users:
@@ -25,5 +28,10 @@ export async function GET() {
* description: Missing required fields.
*/
export async function POST(request: Request) {
+ const { user, error } = await requireAuth();
+ if (error) return error;
+ if (!checkCanManageMeetings(user.roleType)) {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
return MeetingController.createMeeting(request);
}
diff --git a/attendance-manager/src/app/api/voting-event/[id]/route.ts b/attendance-manager/src/app/api/voting-event/[id]/route.ts
index eda18bc0..4877b786 100644
--- a/attendance-manager/src/app/api/voting-event/[id]/route.ts
+++ b/attendance-manager/src/app/api/voting-event/[id]/route.ts
@@ -1,4 +1,7 @@
+import { NextResponse } from 'next/server';
import { VotingController } from '@/voting/voting.controller';
+import { requireAuth } from '@/utils/api-auth';
+import { checkCanManageVoting } from '@/utils/permissions';
/**
* @swagger
@@ -54,6 +57,11 @@ export async function PUT(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
+ const { user, error } = await requireAuth();
+ if (error) return error;
+ if (!checkCanManageVoting(user.roleType)) {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
const { id } = await params;
return VotingController.updateVotingEvent(request, { votingEventId: id });
}
diff --git a/attendance-manager/src/app/api/voting-event/route.ts b/attendance-manager/src/app/api/voting-event/route.ts
index a2cd1a6f..fe979e77 100644
--- a/attendance-manager/src/app/api/voting-event/route.ts
+++ b/attendance-manager/src/app/api/voting-event/route.ts
@@ -1,4 +1,7 @@
+import { NextResponse } from 'next/server';
import { VotingController } from '@/voting/voting.controller';
+import { requireAuth } from '@/utils/api-auth';
+import { checkCanManageVoting } from '@/utils/permissions';
/**
* @swagger
@@ -30,5 +33,10 @@ export async function GET() {
* description: Missing required fields or invalid data.
*/
export async function POST(request: Request) {
+ const { user, error } = await requireAuth();
+ if (error) return error;
+ if (!checkCanManageVoting(user.roleType)) {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
return VotingController.createVotingEvent(request);
}
From fe32bc42ec3dd95d134a0ba2e591ada7dac69877 Mon Sep 17 00:00:00 2001
From: wongcolin45
Date: Wed, 1 Apr 2026 19:35:18 -0400
Subject: [PATCH 17/25] Fix db
---
.github/workflows/ci.yml | 4 ++++
.../20260401000000_remove_none_role_type/migration.sql | 3 +++
2 files changed, 7 insertions(+)
create mode 100644 attendance-manager/prisma/migrations/20260401000000_remove_none_role_type/migration.sql
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1290cca9..66ca8302 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,6 +32,10 @@ jobs:
working-directory: attendance-manager
run: npx prisma generate
+ - name: Run Migrations
+ working-directory: attendance-manager
+ run: npx prisma migrate deploy
+
- name: Lint
working-directory: attendance-manager
run: npm run lint
diff --git a/attendance-manager/prisma/migrations/20260401000000_remove_none_role_type/migration.sql b/attendance-manager/prisma/migrations/20260401000000_remove_none_role_type/migration.sql
new file mode 100644
index 00000000..2e9acb30
--- /dev/null
+++ b/attendance-manager/prisma/migrations/20260401000000_remove_none_role_type/migration.sql
@@ -0,0 +1,3 @@
+-- Convert any NONE role types to MEMBER
+UPDATE "User" SET "roleType" = 'MEMBER' WHERE "roleType"::text = 'NONE';
+UPDATE "Role" SET "roleType" = 'MEMBER' WHERE "roleType"::text = 'NONE';
From 4b17c54475c08060dd462a6cceadb6ec4dd64c8c Mon Sep 17 00:00:00 2001
From: wongcolin45
Date: Wed, 1 Apr 2026 19:38:59 -0400
Subject: [PATCH 18/25] fix test
---
.github/workflows/ci.yml | 4 ----
attendance-manager/src/users/__tests__/users.test.ts | 7 +++++++
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 66ca8302..1290cca9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,10 +32,6 @@ jobs:
working-directory: attendance-manager
run: npx prisma generate
- - name: Run Migrations
- working-directory: attendance-manager
- run: npx prisma migrate deploy
-
- name: Lint
working-directory: attendance-manager
run: npm run lint
diff --git a/attendance-manager/src/users/__tests__/users.test.ts b/attendance-manager/src/users/__tests__/users.test.ts
index a198d1be..a3173d5f 100644
--- a/attendance-manager/src/users/__tests__/users.test.ts
+++ b/attendance-manager/src/users/__tests__/users.test.ts
@@ -28,6 +28,13 @@ describe('UsersService', () => {
let testRoleId: string;
beforeAll(async () => {
+ // Clean up any leftover data from previous runs
+ await prisma.user.deleteMany({
+ where: {
+ nuid: { in: ['001234567', '001234568', '001234570', '001234571'] }
+ }
+ });
+
const role = await prisma.role.create({
data: { roleType: 'MEMBER' }
});
From 98393830994134bbf38538e88c47c652850f3912 Mon Sep 17 00:00:00 2001
From: wongcolin45
Date: Wed, 1 Apr 2026 19:43:33 -0400
Subject: [PATCH 19/25] prettier
---
.../src/users/__tests__/users.test.ts | 2 +-
.../src/utils/__tests__/api-auth.test.ts | 82 ++++++++++---------
2 files changed, 44 insertions(+), 40 deletions(-)
diff --git a/attendance-manager/src/users/__tests__/users.test.ts b/attendance-manager/src/users/__tests__/users.test.ts
index a3173d5f..28f44491 100644
--- a/attendance-manager/src/users/__tests__/users.test.ts
+++ b/attendance-manager/src/users/__tests__/users.test.ts
@@ -546,4 +546,4 @@ describe('POST /api/auth/signup', () => {
RoleType.MEMBER
);
});
-});
\ No newline at end of file
+});
diff --git a/attendance-manager/src/utils/__tests__/api-auth.test.ts b/attendance-manager/src/utils/__tests__/api-auth.test.ts
index 47719f7d..7aab4a04 100644
--- a/attendance-manager/src/utils/__tests__/api-auth.test.ts
+++ b/attendance-manager/src/utils/__tests__/api-auth.test.ts
@@ -8,7 +8,7 @@ jest.setTimeout(20000);
// Mock Supabase server client
jest.mock('@/lib/supabase-server', () => ({
- createServerSupabaseClient: jest.fn(),
+ createServerSupabaseClient: jest.fn()
}));
describe('API Auth Utilities', () => {
@@ -21,7 +21,7 @@ describe('API Auth Utilities', () => {
beforeAll(async () => {
// Create test role
const role = await prisma.role.create({
- data: { roleType: 'MEMBER' },
+ data: { roleType: 'MEMBER' }
});
testRoleId = role.roleId;
@@ -35,8 +35,8 @@ describe('API Auth Utilities', () => {
lastName: 'Test',
roleId: testRoleId,
roleType: 'MEMBER',
- password: null,
- },
+ password: null
+ }
});
testUserId = user.userId;
testSupabaseAuthId = user.supabaseAuthId!;
@@ -49,14 +49,16 @@ describe('API Auth Utilities', () => {
beforeEach(() => {
jest.clearAllMocks();
-
+
mockSupabaseClient = {
auth: {
- getSession: mockGetSession,
- },
+ getSession: mockGetSession
+ }
};
- (createServerSupabaseClient as jest.Mock).mockResolvedValue(mockSupabaseClient);
+ (createServerSupabaseClient as jest.Mock).mockResolvedValue(
+ mockSupabaseClient
+ );
});
describe('getAuthenticatedUser', () => {
@@ -66,11 +68,11 @@ describe('API Auth Utilities', () => {
session: {
user: {
id: testSupabaseAuthId,
- email: 'apitest@example.com',
- },
- },
+ email: 'apitest@example.com'
+ }
+ }
},
- error: null,
+ error: null
});
const user = await getAuthenticatedUser();
@@ -85,7 +87,7 @@ describe('API Auth Utilities', () => {
it('should return null when no session exists', async () => {
mockGetSession.mockResolvedValue({
data: { session: null },
- error: null,
+ error: null
});
const user = await getAuthenticatedUser();
@@ -96,7 +98,7 @@ describe('API Auth Utilities', () => {
it('should return null when session error occurs', async () => {
mockGetSession.mockResolvedValue({
data: { session: null },
- error: { message: 'Session error' },
+ error: { message: 'Session error' }
});
const user = await getAuthenticatedUser();
@@ -110,11 +112,11 @@ describe('API Auth Utilities', () => {
session: {
user: {
id: 'non-existent-supabase-id',
- email: 'notfound@example.com',
- },
- },
+ email: 'notfound@example.com'
+ }
+ }
},
- error: null,
+ error: null
});
const user = await getAuthenticatedUser();
@@ -128,16 +130,18 @@ describe('API Auth Utilities', () => {
session: {
user: {
id: testSupabaseAuthId,
- email: 'apitest@example.com',
- },
- },
+ email: 'apitest@example.com'
+ }
+ }
},
- error: null,
+ error: null
});
// Mock prisma to throw an error
const originalFindUnique = prisma.user.findUnique;
- prisma.user.findUnique = jest.fn().mockRejectedValue(new Error('Database error'));
+ prisma.user.findUnique = jest
+ .fn()
+ .mockRejectedValue(new Error('Database error'));
const user = await getAuthenticatedUser();
@@ -155,11 +159,11 @@ describe('API Auth Utilities', () => {
session: {
user: {
id: testSupabaseAuthId,
- email: 'apitest@example.com',
- },
- },
+ email: 'apitest@example.com'
+ }
+ }
},
- error: null,
+ error: null
});
const result = await requireAuth();
@@ -172,17 +176,17 @@ describe('API Auth Utilities', () => {
it('should return error response when not authenticated', async () => {
mockGetSession.mockResolvedValue({
data: { session: null },
- error: null,
+ error: null
});
const result = await requireAuth();
expect(result.user).toBeNull();
expect(result.error).toBeDefined();
-
+
const errorResponse = result.error as NextResponse;
expect(errorResponse.status).toBe(401);
-
+
const errorData = await errorResponse.json();
expect(errorData.error).toBe('Unauthorized');
});
@@ -190,14 +194,14 @@ describe('API Auth Utilities', () => {
it('should return error response when session error occurs', async () => {
mockGetSession.mockResolvedValue({
data: { session: null },
- error: { message: 'Session error' },
+ error: { message: 'Session error' }
});
const result = await requireAuth();
expect(result.user).toBeNull();
expect(result.error).toBeDefined();
-
+
const errorResponse = result.error as NextResponse;
expect(errorResponse.status).toBe(401);
});
@@ -208,18 +212,18 @@ describe('API Auth Utilities', () => {
session: {
user: {
id: 'non-existent-supabase-id',
- email: 'notfound@example.com',
- },
- },
+ email: 'notfound@example.com'
+ }
+ }
},
- error: null,
+ error: null
});
const result = await requireAuth();
expect(result.user).toBeNull();
expect(result.error).toBeDefined();
-
+
const errorResponse = result.error as NextResponse;
expect(errorResponse.status).toBe(401);
});
@@ -230,7 +234,7 @@ describe('API Auth Utilities', () => {
it('should soft delete a user by setting deletedAt', async () => {
await UsersService.deleteUser(testUserId);
const user = await prisma.user.findUnique({
- where: { userId: testUserId },
+ where: { userId: testUserId }
});
expect(user).toBeDefined();
@@ -245,4 +249,4 @@ describe('API Auth Utilities', () => {
expect(deletedUser).toBeUndefined();
});
});
-});
\ No newline at end of file
+});
From 94d7736853f8edf62a11c69f9e3622e55267ac19 Mon Sep 17 00:00:00 2001
From: wongcolin45
Date: Wed, 1 Apr 2026 19:51:10 -0400
Subject: [PATCH 20/25] Removed checks for voting
---
attendance-manager/src/app/api/voting-event/[id]/route.ts | 8 --------
attendance-manager/src/app/api/voting-event/route.ts | 8 --------
2 files changed, 16 deletions(-)
diff --git a/attendance-manager/src/app/api/voting-event/[id]/route.ts b/attendance-manager/src/app/api/voting-event/[id]/route.ts
index 4877b786..eda18bc0 100644
--- a/attendance-manager/src/app/api/voting-event/[id]/route.ts
+++ b/attendance-manager/src/app/api/voting-event/[id]/route.ts
@@ -1,7 +1,4 @@
-import { NextResponse } from 'next/server';
import { VotingController } from '@/voting/voting.controller';
-import { requireAuth } from '@/utils/api-auth';
-import { checkCanManageVoting } from '@/utils/permissions';
/**
* @swagger
@@ -57,11 +54,6 @@ export async function PUT(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
- const { user, error } = await requireAuth();
- if (error) return error;
- if (!checkCanManageVoting(user.roleType)) {
- return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
- }
const { id } = await params;
return VotingController.updateVotingEvent(request, { votingEventId: id });
}
diff --git a/attendance-manager/src/app/api/voting-event/route.ts b/attendance-manager/src/app/api/voting-event/route.ts
index fe979e77..a2cd1a6f 100644
--- a/attendance-manager/src/app/api/voting-event/route.ts
+++ b/attendance-manager/src/app/api/voting-event/route.ts
@@ -1,7 +1,4 @@
-import { NextResponse } from 'next/server';
import { VotingController } from '@/voting/voting.controller';
-import { requireAuth } from '@/utils/api-auth';
-import { checkCanManageVoting } from '@/utils/permissions';
/**
* @swagger
@@ -33,10 +30,5 @@ export async function GET() {
* description: Missing required fields or invalid data.
*/
export async function POST(request: Request) {
- const { user, error } = await requireAuth();
- if (error) return error;
- if (!checkCanManageVoting(user.roleType)) {
- return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
- }
return VotingController.createVotingEvent(request);
}
From 24d3482570aa9d12faafe868ee5af12a8d59a650 Mon Sep 17 00:00:00 2001
From: wongcolin45
Date: Thu, 2 Apr 2026 18:22:30 -0400
Subject: [PATCH 21/25] Update permissions
---
.../meetings/MeetingStatisticsPanel.tsx | 6 +-
.../src/components/meetings/MeetingsPage.tsx | 2 +-
attendance-manager/src/utils/permissions.ts | 58 ++++++++++++++++---
3 files changed, 55 insertions(+), 11 deletions(-)
diff --git a/attendance-manager/src/components/meetings/MeetingStatisticsPanel.tsx b/attendance-manager/src/components/meetings/MeetingStatisticsPanel.tsx
index 7f8be522..40a046bd 100644
--- a/attendance-manager/src/components/meetings/MeetingStatisticsPanel.tsx
+++ b/attendance-manager/src/components/meetings/MeetingStatisticsPanel.tsx
@@ -6,7 +6,7 @@ interface MeetingStatisticsPanelProps {
attendedMeetings: number;
missedMeetings: number;
upcomingMeetings: number;
- isAdmin: boolean;
+ canManageMeetings: boolean;
setShowCreateMeetingModal: (show: boolean) => void;
}
@@ -15,7 +15,7 @@ const MeetingStatisticsPanel: React.FC = ({
attendedMeetings,
missedMeetings,
upcomingMeetings,
- isAdmin,
+ canManageMeetings,
setShowCreateMeetingModal,
}) => {
return (
@@ -68,7 +68,7 @@ const MeetingStatisticsPanel: React.FC = ({
{/* Create Meeting Button - Only for Admins */}
- {isAdmin && (
+ {canManageMeetings && (
setShowCreateMeetingModal(true)}
diff --git a/attendance-manager/src/components/meetings/MeetingsPage.tsx b/attendance-manager/src/components/meetings/MeetingsPage.tsx
index b2396868..18f928fd 100644
--- a/attendance-manager/src/components/meetings/MeetingsPage.tsx
+++ b/attendance-manager/src/components/meetings/MeetingsPage.tsx
@@ -548,7 +548,7 @@ const MeetingsPage: React.FC = () => {
attendedMeetings={attendedMeetings}
missedMeetings={missedMeetings}
upcomingMeetings={upcomingMeetingsList.length}
- isAdmin={canManageMeetings}
+ canManageMeetings={canManageMeetings}
setShowCreateMeetingModal={setShowCreateMeetingModal}
/>
diff --git a/attendance-manager/src/utils/permissions.ts b/attendance-manager/src/utils/permissions.ts
index 509e2dfa..f537a986 100644
--- a/attendance-manager/src/utils/permissions.ts
+++ b/attendance-manager/src/utils/permissions.ts
@@ -1,13 +1,57 @@
-export const checkCanViewMemberStats = (role?: string) => role === 'EBOARD';
+// Shows: Sidebar with member stats
+export const checkCanViewMemberStats = (role?: string) => {
+ return role === 'EBOARD' ||
+ role === 'ADMIN' ||
+ role === 'SUPER_ADMIN' ||
+ role === 'SENATOR';
+};
-export const checkCanAccessAttendance = (role?: string) => role === 'EBOARD';
+// Shows: AttendancePage (all other roles see Access Denied)
+export const checkCanAccessAttendance = (role?: string) => {
+ return role === 'EBOARD' ||
+ role === 'MEMBER' ||
+ role === 'ADMIN' ||
+ role === 'SUPER_ADMIN' ||
+ role === 'SENATOR';
+};
-export const checkCanManageAttendance = (role?: string) => role === 'EBOARD';
+// Shows: Attendance Check + View Requests buttons in AttendancePage
+// Backend: PATCH /api/attendance/[attendanceId], DELETE /api/attendance/[attendanceId]
+export const checkCanManageAttendance = (role?: string) => {
+ return role === 'EBOARD' ||
+ role === 'ADMIN' ||
+ role === 'SUPER_ADMIN';
+};
-export const checkCanManageMeetings = (role?: string) => role === 'EBOARD';
+// Shows: Create Meeting button in MeetingStatisticsPanel
+// Backend: POST /api/meeting, DELETE /api/meeting/[id]
+export const checkCanManageMeetings = (role?: string) => {
+ return role === 'EBOARD' ||
+ role === 'ADMIN' ||
+ role === 'SUPER_ADMIN';
+};
-export const checkCanEditMeetings = (role?: string) => role === 'EBOARD';
+// Shows: Edit and delete buttons on individual meeting rows in MeetingHistoryPanel
+// Backend: PUT /api/meeting/[id]
+export const checkCanEditMeetings = (role?: string) => {
+ return role === 'EBOARD' ||
+ role === 'ADMIN' ||
+ role === 'SUPER_ADMIN';
+};
-export const checkCanEditProfile = (role?: string) => role === 'EBOARD';
+// Shows: Edit Profile button in ProfilePage
+export const checkCanEditProfile = (role?: string) => {
+ return role === 'EBOARD' ||
+ role === 'MEMBER' ||
+ role === 'ADMIN' ||
+ role === 'SUPER_ADMIN' ||
+ role === 'SENATOR';
+};
-export const checkCanManageVoting = (role?: string) => role === 'EBOARD';
+// Shows: Voting admin panel for creating and ending voting events
+// Backend: POST /api/voting-event, PUT /api/voting-event/[id]
+export const checkCanManageVoting = (role?: string) => {
+ return role === 'EBOARD' ||
+ role === 'ADMIN' ||
+ role === 'SUPER_ADMIN';
+};
From 7588d53dd478574d32d09a2a8c90fe4bd6d26636 Mon Sep 17 00:00:00 2001
From: Colin Wong <150976788+wongcolin45@users.noreply.github.com>
Date: Tue, 7 Apr 2026 05:25:11 -0400
Subject: [PATCH 22/25] Prettier
---
.../src/components/attendance/AttendanceHistory.tsx | 4 +++-
.../src/components/meetings/MeetingHistoryPanel.tsx | 2 +-
.../src/components/meetings/MeetingsPage.tsx | 7 ++++---
.../src/voting-record/__tests__/voting-record.test.ts | 2 +-
4 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/attendance-manager/src/components/attendance/AttendanceHistory.tsx b/attendance-manager/src/components/attendance/AttendanceHistory.tsx
index c4418410..5b1935d1 100644
--- a/attendance-manager/src/components/attendance/AttendanceHistory.tsx
+++ b/attendance-manager/src/components/attendance/AttendanceHistory.tsx
@@ -70,7 +70,9 @@ const AttendanceHistory: React.FC = ({
|
- {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 && (
handleEditMeeting(meeting)}
diff --git a/attendance-manager/src/components/meetings/MeetingsPage.tsx b/attendance-manager/src/components/meetings/MeetingsPage.tsx
index 220ad160..4b8757d0 100644
--- a/attendance-manager/src/components/meetings/MeetingsPage.tsx
+++ b/attendance-manager/src/components/meetings/MeetingsPage.tsx
@@ -5,7 +5,7 @@ import {
RequestApiData,
RemainingAbsences,
MeetingType,
- RequestForm
+ RequestForm,
} from '../../types';
import { z } from 'zod';
import { useAuth } from '../../contexts/AuthContext';
@@ -39,8 +39,9 @@ const normalizeDate = (dateStr: string) => {
const parsed = new Date(dateStr);
if (!Number.isNaN(parsed.getTime())) {
- return `${parsed.getMonth() +
- 1}/${parsed.getDate()}/${parsed.getFullYear()}`;
+ return `${
+ parsed.getMonth() + 1
+ }/${parsed.getDate()}/${parsed.getFullYear()}`;
}
const today = new Date();
@@ -54,7 +55,7 @@ const MeetingsPage: React.FC = () => {
const [showEditMeetingModal, setShowEditMeetingModal] = useState(false);
const [showDeleteMeetingModal, setShowDeleteMeetingModal] = useState(false);
const [editingMeeting, setEditingMeeting] = useState(
- null
+ null,
);
const [newMeeting, setNewMeeting] = useState({
name: '',
@@ -63,7 +64,7 @@ const MeetingsPage: React.FC = () => {
endTime: '',
notes: '',
type: 'REGULAR' as MeetingType, // defaults to REGULAR
- selectedAttendees: [] as string[]
+ selectedAttendees: [] as string[],
});
const [editMeeting, setEditMeeting] = useState({
name: '',
@@ -71,10 +72,10 @@ const MeetingsPage: React.FC = () => {
startTime: '',
endTime: '',
notes: '',
- type: 'REGULAR' as 'FULL_BODY' | 'REGULAR'
+ type: 'REGULAR' as 'FULL_BODY' | 'REGULAR',
});
const [deleteMeeting, setDeleteMeeting] = useState(
- null
+ null,
);
const [meetings, setMeetings] = useState([]);
const [showCreateRequestModal, setShowCreateRequestModal] = useState(false);
@@ -85,9 +86,9 @@ const MeetingsPage: React.FC = () => {
requestTypes: {
leavingEarly: false,
comingLate: false,
- goingOnline: false
+ goingOnline: false,
},
- explanation: ''
+ explanation: '',
});
const [typeFilter, setTypeFilter] = useState(null);
const [selectedUserIds, setSelectedUserIds] = useState([]);
@@ -95,15 +96,13 @@ const MeetingsPage: React.FC = () => {
// Check if user is admin (EBOARD)
const canManageMeetings = checkCanManageMeetings(user?.role);
const isMember = user?.role === 'MEMBER';
- const [
- remainingAbsences,
- setRemainingAbsences
- ] = useState(null);
+ const [remainingAbsences, setRemainingAbsences] =
+ useState(null);
const fetchMeetings = () => {
fetch('/api/meeting')
- .then(response => response.json())
- .then(json => {
+ .then((response) => response.json())
+ .then((json) => {
setMeetings(json);
})
// eslint-disable-next-line
@@ -117,8 +116,8 @@ const MeetingsPage: React.FC = () => {
useEffect(() => {
if (!editingMeeting?.meetingId) return;
fetch(`/api/meeting/${editingMeeting.meetingId}/users`)
- .then(response => response.json())
- .then(json => setSelectedUserIds(json.map((d: any) => d.userId)))
+ .then((response) => response.json())
+ .then((json) => setSelectedUserIds(json.map((d: any) => d.userId)))
// eslint-disable-next-line
.catch(console.error);
}, [editingMeeting?.meetingId, showEditMeetingModal]);
@@ -131,7 +130,7 @@ const MeetingsPage: React.FC = () => {
startTime: meeting.startTime,
endTime: meeting.endTime,
notes: meeting.notes,
- type: meeting.type as 'FULL_BODY' | 'REGULAR'
+ type: meeting.type as 'FULL_BODY' | 'REGULAR',
});
setShowEditMeetingModal(true);
};
@@ -149,15 +148,15 @@ const MeetingsPage: React.FC = () => {
const response = await fetch(`/api/meeting/${editingMeeting.meetingId}`, {
method: 'PUT',
headers: {
- 'Content-Type': 'application/json'
+ 'Content-Type': 'application/json',
},
- body: JSON.stringify(editMeeting)
+ body: JSON.stringify(editMeeting),
});
if (!response.ok) {
const errorData = await response.json();
alert(
- `Failed to update meeting: ${errorData.error || 'Unknown error'}`
+ `Failed to update meeting: ${errorData.error || 'Unknown error'}`,
);
return;
}
@@ -165,7 +164,7 @@ const MeetingsPage: React.FC = () => {
await fetch(`/api/meeting/${editingMeeting.meetingId}/users`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ userIds: selectedUserIds })
+ body: JSON.stringify({ userIds: selectedUserIds }),
});
// Refresh meetings list
@@ -180,7 +179,7 @@ const MeetingsPage: React.FC = () => {
startTime: '',
endTime: '',
notes: '',
- type: 'REGULAR'
+ type: 'REGULAR',
});
alert('Meeting updated successfully!');
} catch {
@@ -192,11 +191,11 @@ const MeetingsPage: React.FC = () => {
useEffect(() => {
if (user?.id) {
fetch(`/api/attendance/user/${user.id}/remaining-absences`)
- .then(response => response.json())
+ .then((response) => response.json())
.then((data: RemainingAbsences) => {
setRemainingAbsences(data);
})
- .catch(error => {
+ .catch((error) => {
alert(`Failed to fetch remaining absences: ${error}`);
});
}
@@ -205,13 +204,13 @@ const MeetingsPage: React.FC = () => {
const [members, setMembers] = useState[]>([]);
const [bulkSelectionActive, setBulkSelectionActive] = useState({
nonEboard: false,
- allMembers: false
+ allMembers: false,
});
useEffect(() => {
fetch('/api/users')
- .then(res => res.json())
- .then(data => {
+ .then((res) => res.json())
+ .then((data) => {
setMembers(data);
})
// eslint-disable-next-line
@@ -219,19 +218,20 @@ const MeetingsPage: React.FC = () => {
}, []);
const nonEboardMembers = useMemo(
- () => members.filter(member => member.roleType !== 'EBOARD'),
- [members]
+ () => members.filter((member) => member.roleType !== 'EBOARD'),
+ [members],
);
const nonEboardMemberIds = useMemo(
- () => nonEboardMembers.map(member => member.userId),
- [nonEboardMembers]
+ () => nonEboardMembers.map((member) => member.userId),
+ [nonEboardMembers],
+ );
+ const allMemberIds = useMemo(
+ () => members.map((member) => member.userId),
+ [members],
);
- const allMemberIds = useMemo(() => members.map(member => member.userId), [
- members
- ]);
const selectedAttendeeSet = useMemo(
() => new Set(newMeeting.selectedAttendees),
- [newMeeting.selectedAttendees]
+ [newMeeting.selectedAttendees],
);
const bulkSelectButtonClasses = (active: boolean) =>
@@ -243,17 +243,17 @@ const MeetingsPage: React.FC = () => {
const toggleNonEboardSelection = () => {
if (bulkSelectionActive.nonEboard) {
- setNewMeeting(prev => ({
+ setNewMeeting((prev) => ({
...prev,
selectedAttendees: prev.selectedAttendees.filter(
- id => !nonEboardMemberIds.includes(id)
- )
+ (id) => !nonEboardMemberIds.includes(id),
+ ),
}));
- setBulkSelectionActive(prev => ({ ...prev, nonEboard: false }));
+ setBulkSelectionActive((prev) => ({ ...prev, nonEboard: false }));
} else {
- setNewMeeting(prev => ({
+ setNewMeeting((prev) => ({
...prev,
- selectedAttendees: nonEboardMemberIds
+ selectedAttendees: nonEboardMemberIds,
}));
setBulkSelectionActive({ nonEboard: true, allMembers: false });
}
@@ -261,10 +261,10 @@ const MeetingsPage: React.FC = () => {
const toggleAllMembersSelection = () => {
if (bulkSelectionActive.allMembers) {
- setNewMeeting(prev => ({ ...prev, selectedAttendees: [] }));
- setBulkSelectionActive(prev => ({ ...prev, allMembers: false }));
+ setNewMeeting((prev) => ({ ...prev, selectedAttendees: [] }));
+ setBulkSelectionActive((prev) => ({ ...prev, allMembers: false }));
} else {
- setNewMeeting(prev => ({ ...prev, selectedAttendees: allMemberIds }));
+ setNewMeeting((prev) => ({ ...prev, selectedAttendees: allMemberIds }));
setBulkSelectionActive({ nonEboard: false, allMembers: true });
}
};
@@ -273,9 +273,9 @@ const MeetingsPage: React.FC = () => {
if (!bulkSelectionActive.nonEboard) return;
const allSelected =
nonEboardMemberIds.length > 0 &&
- nonEboardMemberIds.every(id => selectedAttendeeSet.has(id));
+ nonEboardMemberIds.every((id) => selectedAttendeeSet.has(id));
if (!allSelected) {
- setBulkSelectionActive(prev => ({ ...prev, nonEboard: false }));
+ setBulkSelectionActive((prev) => ({ ...prev, nonEboard: false }));
}
}, [bulkSelectionActive.nonEboard, nonEboardMemberIds, selectedAttendeeSet]);
@@ -283,37 +283,37 @@ const MeetingsPage: React.FC = () => {
if (!bulkSelectionActive.allMembers) return;
const allSelected =
allMemberIds.length > 0 &&
- allMemberIds.every(id => selectedAttendeeSet.has(id));
+ allMemberIds.every((id) => selectedAttendeeSet.has(id));
if (!allSelected) {
- setBulkSelectionActive(prev => ({ ...prev, allMembers: false }));
+ setBulkSelectionActive((prev) => ({ ...prev, allMembers: false }));
}
}, [bulkSelectionActive.allMembers, allMemberIds, selectedAttendeeSet]);
// Calculate statistics from real meetings
const today = new Date();
// Calculate statistics from real data
- const attendedMeetings = meetings.filter(m => {
+ const attendedMeetings = meetings.filter((m) => {
const meetingDate = new Date(m.date);
if (meetingDate > today) return false; // Skip upcoming meetings
// Check if current user attended this meeting
return m.attendance.some(
- a => a.userId === user?.id && a.status === 'PRESENT'
+ (a) => a.userId === user?.id && a.status === 'PRESENT',
);
}).length;
- const missedMeetings = meetings.filter(m => {
+ const missedMeetings = meetings.filter((m) => {
const meetingDate = new Date(m.date);
if (meetingDate > today) return false; // Skip upcoming meetings
// Check if current user was absent
return m.attendance.some(
- a =>
+ (a) =>
a.userId === user?.id &&
- (a.status === 'UNEXCUSED_ABSENCE' || a.status === 'EXCUSED_ABSENCE')
+ (a.status === 'UNEXCUSED_ABSENCE' || a.status === 'EXCUSED_ABSENCE'),
);
}).length;
// Filter meetings based on active tab
- const filteredMeetings = meetings.filter(m => {
+ const filteredMeetings = meetings.filter((m) => {
// change to 'meetings' for implementation
const meetingDate = new Date(m.date);
if (activeTab === 'past') {
@@ -327,7 +327,7 @@ const MeetingsPage: React.FC = () => {
}
// Get upcoming meetings for request creation
- const upcomingMeetingsList = meetings.filter(m => parseEST(m.date) > today);
+ const upcomingMeetingsList = meetings.filter((m) => parseEST(m.date) > today);
// Handle request submission
const handleSubmitRequest = async () => {
@@ -362,16 +362,14 @@ const MeetingsPage: React.FC = () => {
: 'IN_PERSON';
// timeAdjustment: can only have one (leavingEarly or comingLate)
- let timeAdjustment:
- | 'ARRIVING_LATE'
- | 'LEAVING_EARLY'
- | undefined = undefined;
+ let timeAdjustment: 'ARRIVING_LATE' | 'LEAVING_EARLY' | undefined =
+ undefined;
if (
requestForm.requestTypes.leavingEarly &&
requestForm.requestTypes.comingLate
) {
alert(
- 'Please select only one time adjustment (either leaving early OR coming late)'
+ 'Please select only one time adjustment (either leaving early OR coming late)',
);
return;
} else if (requestForm.requestTypes.leavingEarly) {
@@ -392,19 +390,19 @@ const MeetingsPage: React.FC = () => {
body: JSON.stringify({
userId: user.id,
meetingId: meetingId,
- status: 'PENDING'
- })
+ status: 'PENDING',
+ }),
});
if (!attendanceResponse.ok) {
throw new Error(
- `Failed to create/update attendance for meeting ${meetingId}`
+ `Failed to create/update attendance for meeting ${meetingId}`,
);
}
// Fetch the user's attendance to find the one we just created/updated
const userAttendanceResponse = await fetch(
- `/api/attendance/user/${user.id}`
+ `/api/attendance/user/${user.id}`,
);
if (!userAttendanceResponse.ok) {
throw new Error('Failed to fetch attendance record'); // single quotes
@@ -412,12 +410,12 @@ const MeetingsPage: React.FC = () => {
const userAttendance = await userAttendanceResponse.json();
const attendanceRecord = userAttendance.find(
- (a: any) => a.meetingId === meetingId
+ (a: any) => a.meetingId === meetingId,
);
if (!attendanceRecord || !attendanceRecord.attendanceId) {
throw new Error(
- `Attendance record not found for meeting ${meetingId}`
+ `Attendance record not found for meeting ${meetingId}`,
);
}
@@ -427,7 +425,7 @@ const MeetingsPage: React.FC = () => {
const requestPayload: any = {
attendanceId,
reason: requestForm.explanation,
- attendanceMode
+ attendanceMode,
};
if (timeAdjustment) {
@@ -437,14 +435,14 @@ const MeetingsPage: React.FC = () => {
const requestResponse = await fetch('/api/requests', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(requestPayload)
+ body: JSON.stringify(requestPayload),
});
if (!requestResponse.ok) {
const errorData = await requestResponse.json();
throw new Error(
errorData.error ||
- `Failed to create request for meeting ${meetingId}`
+ `Failed to create request for meeting ${meetingId}`,
);
}
@@ -460,9 +458,9 @@ const MeetingsPage: React.FC = () => {
requestTypes: {
leavingEarly: false,
comingLate: false,
- goingOnline: false
+ goingOnline: false,
},
- explanation: ''
+ explanation: '',
});
setShowCreateRequestModal(false);
} catch (error) {
@@ -476,13 +474,13 @@ const MeetingsPage: React.FC = () => {
try {
const response = await fetch(`/api/meeting/${deleteMeeting.meetingId}`, {
- method: 'DELETE'
+ method: 'DELETE',
});
if (!response.ok) {
const errorData = await response.json();
alert(
- `Failed to delete meeting: ${errorData.error || 'Unknown error'}`
+ `Failed to delete meeting: ${errorData.error || 'Unknown error'}`,
);
return;
}
diff --git a/attendance-manager/src/contexts/AuthContext.tsx b/attendance-manager/src/contexts/AuthContext.tsx
index fda0e72d..bd8e7f8f 100644
--- a/attendance-manager/src/contexts/AuthContext.tsx
+++ b/attendance-manager/src/contexts/AuthContext.tsx
@@ -5,7 +5,7 @@ import React, {
useContext,
useState,
useEffect,
- ReactNode
+ ReactNode,
} from 'react';
import { User, LoginCredentials, AuthContextType } from '../types';
import { useRouter } from 'next/navigation';
@@ -36,7 +36,7 @@ export const AuthProvider: React.FC = ({ children }) => {
const checkSession = async () => {
try {
const {
- data: { session }
+ data: { session },
} = await supabase.auth.getSession();
if (session?.user) {
@@ -54,7 +54,7 @@ export const AuthProvider: React.FC = ({ children }) => {
// Listen for auth changes
const {
- data: { subscription }
+ data: { subscription },
} = supabase.auth.onAuthStateChange(async (event, session) => {
if (event === 'SIGNED_IN' && session?.user) {
await loadUserProfile(session.user.id);
@@ -81,7 +81,7 @@ export const AuthProvider: React.FC = ({ children }) => {
email: userDetails.email,
name: `${userDetails.firstName} ${userDetails.lastName}`,
role: userDetails.roleType,
- avatar: undefined
+ avatar: undefined,
};
setUser(user);
@@ -96,7 +96,7 @@ export const AuthProvider: React.FC = ({ children }) => {
// Sign in with Supabase
const { data, error } = await supabase.auth.signInWithPassword({
email: credentials.email,
- password: credentials.password
+ password: credentials.password,
});
if (error) {
@@ -126,7 +126,7 @@ export const AuthProvider: React.FC = ({ children }) => {
login,
// eslint-disable-next-line
logout,
- isLoading
+ isLoading,
};
return {children};
diff --git a/attendance-manager/src/users/users.service.ts b/attendance-manager/src/users/users.service.ts
index 0aa08b33..6549eea4 100644
--- a/attendance-manager/src/users/users.service.ts
+++ b/attendance-manager/src/users/users.service.ts
@@ -5,20 +5,20 @@ export const UsersService = {
async getAllUsers() {
return prisma.user.findMany({
where: { deletedAt: null },
- include: { role: true, attendance: true }
+ include: { role: true, attendance: true },
});
},
async getAllRoles() {
return prisma.role.findMany({
- include: {}
+ include: {},
});
},
async getUserByNUID(nuid: string) {
const user = await prisma.user.findUnique({
where: { nuid },
- include: { role: true }
+ include: { role: true },
});
return user?.deletedAt ? null : user;
},
@@ -26,7 +26,7 @@ export const UsersService = {
async getUserById(userId: string) {
const user = await prisma.user.findUnique({
where: { userId },
- include: { role: true, attendance: true }
+ include: { role: true, attendance: true },
});
return user?.deletedAt ? null : user;
},
@@ -34,7 +34,7 @@ export const UsersService = {
async getUserByEmail(userEmail: string) {
const user = await prisma.user.findUnique({
where: { email: userEmail },
- include: { role: true }
+ include: { role: true },
});
return user?.deletedAt ? null : user;
},
@@ -42,7 +42,7 @@ export const UsersService = {
async getUserByNuid(nuid: string) {
const user = await prisma.user.findUnique({
where: { nuid },
- include: { role: true }
+ include: { role: true },
});
return user?.deletedAt ? null : user;
},
@@ -62,8 +62,8 @@ export const UsersService = {
return prisma.user.create({
data: {
...data,
- password: data.password ?? null
- }
+ password: data.password ?? null,
+ },
});
},
@@ -74,7 +74,7 @@ export const UsersService = {
async getRoleIdByRoleType(roleType: RoleType) {
const role = await prisma.role.findFirst({
where: { roleType },
- select: { roleId: true }
+ select: { roleId: true },
});
return role?.roleId;
},
@@ -89,11 +89,11 @@ export const UsersService = {
roleId: string;
roleType: RoleType;
isVotingMember: boolean;
- }>
+ }>,
) {
return prisma.user.update({
where: { userId },
- data: updates
+ data: updates,
});
},
@@ -101,7 +101,7 @@ export const UsersService = {
// Soft delete attendance records: updates prisma deletedAt field instead of fully deleting
return prisma.user.update({
where: { userId },
- data: { deletedAt: new Date() }
+ data: { deletedAt: new Date() },
});
},
@@ -111,16 +111,16 @@ export const UsersService = {
async deleteRole(roleId: string) {
await prisma.user.deleteMany({
- where: { roleId }
+ where: { roleId },
});
return prisma.role.delete({
- where: { roleId }
+ where: { roleId },
});
},
async getRolesByRoleId(roleId: string) {
return prisma.role.findUnique({
- where: { roleId: roleId }
+ where: { roleId: roleId },
});
},
@@ -128,16 +128,16 @@ export const UsersService = {
return prisma.user.findMany({
where: {
deletedAt: null,
- roleType
- }
+ roleType,
+ },
});
},
async getUserBySupabaseId(supabaseAuthId: string) {
const user = await prisma.user.findUnique({
where: { supabaseAuthId },
- include: { role: true }
+ include: { role: true },
});
return user?.deletedAt ? null : user;
- }
+ },
};
diff --git a/attendance-manager/src/utils/auth_utils.ts b/attendance-manager/src/utils/auth_utils.ts
index 38a8bba7..f69b4d86 100644
--- a/attendance-manager/src/utils/auth_utils.ts
+++ b/attendance-manager/src/utils/auth_utils.ts
@@ -8,16 +8,16 @@ export const login = async (
credentials: LoginCredentials,
setIsLoading: Dispatch>,
setUser: Dispatch>,
- router: ReturnType
+ router: ReturnType,
) => {
setIsLoading(true);
try {
const res = await fetch(
- `/api/users/get-user-by-email/${credentials.email}`
+ `/api/users/get-user-by-email/${credentials.email}`,
);
if (!res.ok) {
throw new Error(
- `Response status: ${res.status}\n. Response Msg: ${await res.text}`
+ `Response status: ${res.status}\n. Response Msg: ${await res.text}`,
);
}
@@ -45,7 +45,7 @@ export const login = async (
email: credentials.email,
name: userDetails.firstName + ' ' + userDetails.lastName,
role: userDetails.roleType,
- avatar: undefined
+ avatar: undefined,
};
setUser(user);
diff --git a/attendance-manager/src/utils/permissions.ts b/attendance-manager/src/utils/permissions.ts
index f537a986..410c316f 100644
--- a/attendance-manager/src/utils/permissions.ts
+++ b/attendance-manager/src/utils/permissions.ts
@@ -1,57 +1,55 @@
// Shows: Sidebar with member stats
export const checkCanViewMemberStats = (role?: string) => {
- return role === 'EBOARD' ||
- role === 'ADMIN' ||
- role === 'SUPER_ADMIN' ||
- role === 'SENATOR';
+ return (
+ role === 'EBOARD' ||
+ role === 'ADMIN' ||
+ role === 'SUPER_ADMIN' ||
+ role === 'SENATOR'
+ );
};
// Shows: AttendancePage (all other roles see Access Denied)
export const checkCanAccessAttendance = (role?: string) => {
- return role === 'EBOARD' ||
- role === 'MEMBER' ||
- role === 'ADMIN' ||
- role === 'SUPER_ADMIN' ||
- role === 'SENATOR';
+ return (
+ role === 'EBOARD' ||
+ role === 'MEMBER' ||
+ role === 'ADMIN' ||
+ role === 'SUPER_ADMIN' ||
+ role === 'SENATOR'
+ );
};
// Shows: Attendance Check + View Requests buttons in AttendancePage
// Backend: PATCH /api/attendance/[attendanceId], DELETE /api/attendance/[attendanceId]
export const checkCanManageAttendance = (role?: string) => {
- return role === 'EBOARD' ||
- role === 'ADMIN' ||
- role === 'SUPER_ADMIN';
+ return role === 'EBOARD' || role === 'ADMIN' || role === 'SUPER_ADMIN';
};
// Shows: Create Meeting button in MeetingStatisticsPanel
// Backend: POST /api/meeting, DELETE /api/meeting/[id]
export const checkCanManageMeetings = (role?: string) => {
- return role === 'EBOARD' ||
- role === 'ADMIN' ||
- role === 'SUPER_ADMIN';
+ return role === 'EBOARD' || role === 'ADMIN' || role === 'SUPER_ADMIN';
};
// Shows: Edit and delete buttons on individual meeting rows in MeetingHistoryPanel
// Backend: PUT /api/meeting/[id]
export const checkCanEditMeetings = (role?: string) => {
- return role === 'EBOARD' ||
- role === 'ADMIN' ||
- role === 'SUPER_ADMIN';
+ return role === 'EBOARD' || role === 'ADMIN' || role === 'SUPER_ADMIN';
};
// Shows: Edit Profile button in ProfilePage
export const checkCanEditProfile = (role?: string) => {
- return role === 'EBOARD' ||
- role === 'MEMBER' ||
- role === 'ADMIN' ||
- role === 'SUPER_ADMIN' ||
- role === 'SENATOR';
+ return (
+ role === 'EBOARD' ||
+ role === 'MEMBER' ||
+ role === 'ADMIN' ||
+ role === 'SUPER_ADMIN' ||
+ role === 'SENATOR'
+ );
};
// Shows: Voting admin panel for creating and ending voting events
// Backend: POST /api/voting-event, PUT /api/voting-event/[id]
export const checkCanManageVoting = (role?: string) => {
- return role === 'EBOARD' ||
- role === 'ADMIN' ||
- role === 'SUPER_ADMIN';
+ return role === 'EBOARD' || role === 'ADMIN' || role === 'SUPER_ADMIN';
};
From aac2692df99a2569dd48fdfb4b0eff1fe8ff8a4a Mon Sep 17 00:00:00 2001
From: Colin Wong <150976788+wongcolin45@users.noreply.github.com>
Date: Tue, 7 Apr 2026 05:41:54 -0400
Subject: [PATCH 24/25] Migrations on test db
---
.github/workflows/ci.yml | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1290cca9..66ca8302 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,6 +32,10 @@ jobs:
working-directory: attendance-manager
run: npx prisma generate
+ - name: Run Migrations
+ working-directory: attendance-manager
+ run: npx prisma migrate deploy
+
- name: Lint
working-directory: attendance-manager
run: npm run lint
From 05165d9664fe5ab5e32e589d700c5b40cdfc28d4 Mon Sep 17 00:00:00 2001
From: Colin Wong <150976788+wongcolin45@users.noreply.github.com>
Date: Tue, 7 Apr 2026 05:57:04 -0400
Subject: [PATCH 25/25] Remove none
---
.../migration.sql | 21 +++++++++++++++++++
1 file changed, 21 insertions(+)
create mode 100644 attendance-manager/prisma/migrations/20260407000000_drop_none_from_role_type_enum/migration.sql
diff --git a/attendance-manager/prisma/migrations/20260407000000_drop_none_from_role_type_enum/migration.sql b/attendance-manager/prisma/migrations/20260407000000_drop_none_from_role_type_enum/migration.sql
new file mode 100644
index 00000000..ebefcf45
--- /dev/null
+++ b/attendance-manager/prisma/migrations/20260407000000_drop_none_from_role_type_enum/migration.sql
@@ -0,0 +1,21 @@
+-- Convert any remaining NONE role types to MEMBER
+UPDATE "User" SET "roleType" = 'MEMBER' WHERE "roleType"::text = 'NONE';
+UPDATE "Role" SET "roleType" = 'MEMBER' WHERE "roleType"::text = 'NONE';
+
+-- Recreate the RoleType enum without NONE
+-- Drop column defaults that reference the enum first
+ALTER TABLE "User" ALTER COLUMN "roleType" DROP DEFAULT;
+
+-- Create new enum without NONE
+CREATE TYPE "RoleType_new" AS ENUM ('SUPER_ADMIN', 'ADMIN', 'SENATOR', 'EBOARD', 'MEMBER');
+
+-- Migrate columns to new enum type
+ALTER TABLE "User" ALTER COLUMN "roleType" TYPE "RoleType_new" USING "roleType"::text::"RoleType_new";
+ALTER TABLE "Role" ALTER COLUMN "roleType" TYPE "RoleType_new" USING "roleType"::text::"RoleType_new";
+
+-- Drop old type and rename
+DROP TYPE "RoleType";
+ALTER TYPE "RoleType_new" RENAME TO "RoleType";
+
+-- Restore default
+ALTER TABLE "User" ALTER COLUMN "roleType" SET DEFAULT 'MEMBER';
| |