From ade6c22b5740e8728f91f71658d85f6f3733e418 Mon Sep 17 00:00:00 2001 From: JasonIsAzn Date: Mon, 6 Jan 2025 16:36:30 -0600 Subject: [PATCH 1/6] Reduce Lead's access to manage high-risk features --- src/components/MOTMCard.tsx | 5 +- src/helpers/rolesUtils.ts | 10 +++ src/screens/committees/CommitteeInfo.tsx | 7 +- src/screens/committees/Committees.tsx | 9 +-- src/screens/events/EventCard.tsx | 5 +- src/screens/events/EventInfo.tsx | 81 ++++++++++++----------- src/screens/events/Events.tsx | 13 ++-- src/screens/events/PastEvents.tsx | 9 +-- src/screens/home/Home.tsx | 15 +++-- src/screens/resources/Resources.tsx | 8 +-- src/screens/resources/ResumeCard.tsx | 6 +- src/screens/userProfile/PublicProfile.tsx | 5 +- 12 files changed, 101 insertions(+), 72 deletions(-) create mode 100644 src/helpers/rolesUtils.ts diff --git a/src/components/MOTMCard.tsx b/src/components/MOTMCard.tsx index db47ee22..c3dfb79a 100644 --- a/src/components/MOTMCard.tsx +++ b/src/components/MOTMCard.tsx @@ -8,6 +8,7 @@ import { useFocusEffect } from '@react-navigation/core'; import { UserContext } from '../context/UserContext'; import { truncateStringWithEllipsis } from '../helpers/stringUtils'; import { auth } from '../config/firebaseConfig'; +import { hasPrivileges } from '../helpers/rolesUtils'; const MOTMCard: React.FC = ({ navigation }) => { const userContext = useContext(UserContext); @@ -18,7 +19,7 @@ const MOTMCard: React.FC = ({ navigation }) => { const colorScheme = useColorScheme(); const darkMode = useSystemDefault ? colorScheme === 'dark' : fixDarkMode; - const hasPrivileges = (userInfo?.publicInfo?.roles?.admin?.valueOf() || userInfo?.publicInfo?.roles?.officer?.valueOf() || userInfo?.publicInfo?.roles?.developer?.valueOf() || userInfo?.publicInfo?.roles?.lead?.valueOf() || userInfo?.publicInfo?.roles?.representative?.valueOf()); + const isAdmin = hasPrivileges(userInfo!, ['admin', 'officer', 'developer', 'representative']); const [MOTM, setMOTM] = useState(); const [currentUser, setCurrentUser] = useState(auth.currentUser); @@ -78,7 +79,7 @@ const MOTMCard: React.FC = ({ navigation }) => { return; } - if (hasPrivileges) { + if (isAdmin) { fetchMOTM(); } }, [currentUser]) diff --git a/src/helpers/rolesUtils.ts b/src/helpers/rolesUtils.ts new file mode 100644 index 00000000..cd09a2a1 --- /dev/null +++ b/src/helpers/rolesUtils.ts @@ -0,0 +1,10 @@ +import { Roles, User } from "../types/user"; + +export const hasPrivileges = ( + userInfo: User, + roles: (keyof Roles)[] = ['admin', 'officer', 'developer', 'lead', 'representative'] +) => { + const userRoles = userInfo?.publicInfo?.roles || {}; + return roles.some(role => userRoles[role]?.valueOf()); +}; + diff --git a/src/screens/committees/CommitteeInfo.tsx b/src/screens/committees/CommitteeInfo.tsx index 2ad504b1..4521c38a 100644 --- a/src/screens/committees/CommitteeInfo.tsx +++ b/src/screens/committees/CommitteeInfo.tsx @@ -18,6 +18,7 @@ import { PublicUserInfo } from '../../types/user'; import { CommitteesStackParams } from '../../types/navigation'; import MembersList from '../../components/MembersList'; import EventCard from '../events/EventCard'; +import { hasPrivileges } from '../../helpers/rolesUtils'; const CommitteeInfo: React.FC = ({ route, navigation }) => { const initialCommittee = route.params.committee; @@ -34,7 +35,7 @@ const CommitteeInfo: React.FC = ({ route, navigat const colorScheme = useColorScheme(); const darkMode = useSystemDefault ? colorScheme === 'dark' : fixDarkMode; - const hasPrivileges = (userInfo?.publicInfo?.roles?.admin?.valueOf() || userInfo?.publicInfo?.roles?.officer?.valueOf() || userInfo?.publicInfo?.roles?.developer?.valueOf() || userInfo?.publicInfo?.roles?.lead?.valueOf() || userInfo?.publicInfo?.roles?.representative?.valueOf()); + const isAdminLead = hasPrivileges(userInfo!, ['admin', 'officer', 'developer', 'representative', 'lead']); const [events, setEvents] = useState([]); const [members, setMembers] = useState([]); @@ -128,7 +129,7 @@ const CommitteeInfo: React.FC = ({ route, navigat useFocusEffect( useCallback(() => { - if (hasPrivileges) { + if (isAdminLead) { fetchTeamMemberData(); } }, [fetchTeamMemberData]) @@ -208,7 +209,7 @@ const CommitteeInfo: React.FC = ({ route, navigat navigation.goBack()} className='py-2 px-4'> - {hasPrivileges && ( + {isAdminLead && ( { navigation.navigate("CommitteeEditor", { committee: initialCommittee }) }} className='items-center justify-center px-4 py-1' diff --git a/src/screens/committees/Committees.tsx b/src/screens/committees/Committees.tsx index 72b84df0..15f00fcf 100644 --- a/src/screens/committees/Committees.tsx +++ b/src/screens/committees/Committees.tsx @@ -12,6 +12,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { CommitteesStackParams } from '../../types/navigation'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { StatusBar } from 'expo-status-bar'; +import { hasPrivileges } from '../../helpers/rolesUtils'; const Committees = ({ navigation }: NativeStackScreenProps) => { const userContext = useContext(UserContext); @@ -22,7 +23,7 @@ const Committees = ({ navigation }: NativeStackScreenProps([]); const [filteredCommittees, setFilteredCommittees] = useState([]); @@ -79,10 +80,10 @@ const Committees = ({ navigation }: NativeStackScreenProps { - if (hasPrivileges) { + if (isAdminLead) { fetchCommittees(); } - }, [hasPrivileges]) + }, [isAdminLead]) ); return ( @@ -151,7 +152,7 @@ const Committees = ({ navigation }: NativeStackScreenProps {/* Create Committee */} - {hasPrivileges && ( + {isAdminLead && ( { const userContext = useContext(UserContext); @@ -15,7 +16,7 @@ const EventCard = ({ event, navigation }: { event: SHPEEvent, navigation: any }) const colorScheme = useColorScheme(); const darkMode = useSystemDefault ? colorScheme === 'dark' : fixDarkMode; - const hasPrivileges = (userInfo?.publicInfo?.roles?.admin?.valueOf() || userInfo?.publicInfo?.roles?.officer?.valueOf() || userInfo?.publicInfo?.roles?.developer?.valueOf() || userInfo?.publicInfo?.roles?.lead?.valueOf() || userInfo?.publicInfo?.roles?.representative?.valueOf()); + const isAdminLead = hasPrivileges(userInfo!, ['admin', 'officer', 'developer', 'representative', 'lead']); return ( {formatDate(event.startTime?.toDate()!)} - {hasPrivileges && ( + {isAdminLead && ( { navigation.navigate("QRCode", { event: event }) }} diff --git a/src/screens/events/EventInfo.tsx b/src/screens/events/EventInfo.tsx index be6f43a3..197f7a8a 100644 --- a/src/screens/events/EventInfo.tsx +++ b/src/screens/events/EventInfo.tsx @@ -22,6 +22,7 @@ import ClockIconBlack from '../../../assets/clock-solid_black.svg' import ClockIconWhite from '../../../assets/clock-solid_white.svg' import LocationDotIconBlack from '../../../assets/location-dot-solid_black.svg' import LocationDotIconWhite from '../../../assets/location-dot-solid_white.svg' +import { hasPrivileges } from '../../helpers/rolesUtils'; @@ -42,7 +43,8 @@ const EventInfo = ({ navigation }: EventProps) => { const colorScheme = useColorScheme(); const darkMode = useSystemDefault ? colorScheme === 'dark' : fixDarkMode; - const hasPrivileges = (userInfo?.publicInfo?.roles?.admin?.valueOf() || userInfo?.publicInfo?.roles?.officer?.valueOf() || userInfo?.publicInfo?.roles?.developer?.valueOf() || userInfo?.publicInfo?.roles?.lead?.valueOf() || userInfo?.publicInfo?.roles?.representative?.valueOf()); + const isAdminLead = hasPrivileges(userInfo!, ['admin', 'officer', 'developer', 'representative', 'lead']); + const isAdmin = hasPrivileges(userInfo!, ['admin', 'officer', 'developer', 'representative']); const [creatorData, setCreatorData] = useState(null); const [loadingUserEventLog, setLoadingUserEventLog] = useState(true); @@ -203,7 +205,7 @@ const EventInfo = ({ navigation }: EventProps) => { > - {hasPrivileges && ( + {isAdminLead && ( { setShowOptionMenu(!showOptionMenu) }} @@ -228,39 +230,44 @@ const EventInfo = ({ navigation }: EventProps) => { > Edit Event - - { - setShowOptionMenu(false); - setSignInModalVisible(true) - fetchAllData(); - }} - > - Manual Sign In - - - { - setShowOptionMenu(false); - setSignOutModalVisible(true) - fetchAllData(); - }} - > - Manual Sign Out - - - { - setShowOptionMenu(false); - setUsersLoggedModalVisible(true); - fetchAllData(); - }} - > - Manual Delete Log - + + {isAdmin && ( + <> + + { + setShowOptionMenu(false); + setSignInModalVisible(true) + fetchAllData(); + }} + > + Manual Sign In + + + { + setShowOptionMenu(false); + setSignOutModalVisible(true) + fetchAllData(); + }} + > + Manual Sign Out + + + { + setShowOptionMenu(false); + setUsersLoggedModalVisible(true); + fetchAllData(); + }} + > + Manual Delete Log + + + )} )} @@ -269,7 +276,7 @@ const EventInfo = ({ navigation }: EventProps) => { - {hasPrivileges && ( + {isAdminLead && ( { navigation.navigate("QRCode", { event: event }) @@ -288,7 +295,7 @@ const EventInfo = ({ navigation }: EventProps) => { - {hasPrivileges && !loading && ( + {isAdminLead && !loading && ( { const [committeeEvents, setCommitteeEvents] = useState({ today: [], upcoming: [], past: [] }); const [filter, setFilter] = useState<"main" | "intramural" | "committee">("main"); - const hasPrivileges = (userInfo?.publicInfo?.roles?.admin?.valueOf() || userInfo?.publicInfo?.roles?.officer?.valueOf() || userInfo?.publicInfo?.roles?.developer?.valueOf() || userInfo?.publicInfo?.roles?.lead?.valueOf() || userInfo?.publicInfo?.roles?.representative?.valueOf()); + const isAdminLead = hasPrivileges(userInfo!, ['admin', 'officer', 'developer', 'representative', 'lead']); const selectedEvents = filter === "main" ? mainEvents : filter === "intramural" ? intramuralEvents : committeeEvents; @@ -57,7 +58,7 @@ const Events = ({ navigation }: EventsProps) => { const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000); const filterEvents = (events: SHPEEvent[], condition: (event: SHPEEvent) => boolean) => - events.filter(event => (hasPrivileges || !event.hiddenEvent) && condition(event)); + events.filter(event => (isAdminLead || !event.hiddenEvent) && condition(event)); const todayEvents = filterEvents(filteredUpcomingEvents, (event: SHPEEvent) => { const startTime = event.startTime?.toDate() || new Date(0); @@ -139,10 +140,10 @@ const Events = ({ navigation }: EventsProps) => { useFocusEffect( useCallback(() => { - if (hasPrivileges) { + if (isAdminLead) { fetchEvents(); } - }, [hasPrivileges]) + }, [isAdminLead]) ); @@ -284,7 +285,7 @@ const Events = ({ navigation }: EventsProps) => { - {hasPrivileges && ( + {isAdminLead && ( { navigation.navigate("QRCode", { event: event }); @@ -343,7 +344,7 @@ const Events = ({ navigation }: EventsProps) => { {/* Create Event */} - {hasPrivileges && ( + {isAdminLead && ( ) => { const userContext = useContext(UserContext); @@ -19,7 +20,7 @@ const PastEvents = ({ navigation }: NativeStackScreenProps) = const colorScheme = useColorScheme(); const darkMode = useSystemDefault ? colorScheme === 'dark' : fixDarkMode; - const hasPrivileges = (userInfo?.publicInfo?.roles?.admin?.valueOf() || userInfo?.publicInfo?.roles?.officer?.valueOf() || userInfo?.publicInfo?.roles?.developer?.valueOf() || userInfo?.publicInfo?.roles?.lead?.valueOf() || userInfo?.publicInfo?.roles?.representative?.valueOf()); + const isAdminLead = hasPrivileges(userInfo!, ['admin', 'officer', 'developer', 'representative', 'lead']); const [pastEvents, setPastEvents] = useState([]); const [loading, setLoading] = useState(false); @@ -51,10 +52,10 @@ const PastEvents = ({ navigation }: NativeStackScreenProps) = useFocusEffect( useCallback(() => { - if (hasPrivileges) { + if (isAdminLead) { fetchInitialEvents(); } - }, [hasPrivileges]) + }, [isAdminLead]) ); @@ -68,7 +69,7 @@ const PastEvents = ({ navigation }: NativeStackScreenProps) = loadMoreEvents(); }, [loading, endOfData, setPastEvents]); - const visibleEvents = hasPrivileges ? pastEvents : pastEvents.filter(event => !event.hiddenEvent); + const visibleEvents = isAdminLead ? pastEvents : pastEvents.filter(event => !event.hiddenEvent); return ( diff --git a/src/screens/home/Home.tsx b/src/screens/home/Home.tsx index 56e49e4e..9bdd8d2b 100644 --- a/src/screens/home/Home.tsx +++ b/src/screens/home/Home.tsx @@ -22,6 +22,7 @@ import { useFocusEffect } from '@react-navigation/core'; import { compareVersions } from 'compare-versions'; import { incrementAppLaunchCount, requestReview } from '../../helpers/appReview'; import { Animated } from 'react-native'; +import { hasPrivileges } from '../../helpers/rolesUtils'; const pkg = require("../../../package.json"); /** @@ -43,7 +44,9 @@ const Home = ({ navigation, route }: NativeStackScreenProps) => const colorScheme = useColorScheme(); const darkMode = useSystemDefault ? colorScheme === 'dark' : fixDarkMode; - const hasPrivileges = (userInfo?.publicInfo?.roles?.admin?.valueOf() || userInfo?.publicInfo?.roles?.officer?.valueOf() || userInfo?.publicInfo?.roles?.developer?.valueOf() || userInfo?.publicInfo?.roles?.lead?.valueOf() || userInfo?.publicInfo?.roles?.representative?.valueOf()); + const isAdmin = hasPrivileges(userInfo!, ['admin', 'officer', 'developer', 'representative']); + + const isAdminLead = hasPrivileges(userInfo!, ['admin', 'officer', 'developer', 'representative', 'lead']); const [isVerified, setIsVerified] = useState(true); // By default hide "become a member" banner const [isSignedIn, setIsSignedIn] = useState(undefined); @@ -122,7 +125,7 @@ const Home = ({ navigation, route }: NativeStackScreenProps) => fetchEvents(); getOfficeCount(); - if (hasPrivileges) { + if (isAdmin) { getOfficerStatus(); } }, [auth.currentUser]); @@ -152,10 +155,10 @@ const Home = ({ navigation, route }: NativeStackScreenProps) => useFocusEffect( useCallback(() => { - if (hasPrivileges) { + if (isAdminLead) { fetchEvents(); } - }, [hasPrivileges]) + }, [isAdminLead]) ); const isInterestChanged = () => { @@ -339,7 +342,7 @@ const Home = ({ navigation, route }: NativeStackScreenProps) => ) : (() => { - const visibleEvents = myEvents.filter((event: SHPEEvent) => hasPrivileges || !event.hiddenEvent); + const visibleEvents = myEvents.filter((event: SHPEEvent) => isAdminLead || !event.hiddenEvent); if (visibleEvents.length === 0) { return ( @@ -427,7 +430,7 @@ const Home = ({ navigation, route }: NativeStackScreenProps) => )} {/* Office Dashboard Office Sign In*/} - {hasPrivileges && ( + {isAdmin && ( ([]); @@ -128,7 +128,7 @@ const Resources = ({ navigation }: { navigation: NativeStackNavigationProp ( void }> = ({ resumeData, navigation, onResumeRemoved }) => { const { uid, photoURL, name, resumePublicURL, major, classYear, roles, nationalExpiration, chapterExpiration } = resumeData @@ -21,7 +22,8 @@ const ResumeCard: React.FC void }> = ({ r const colorScheme = useColorScheme(); const darkMode = useSystemDefault ? colorScheme === 'dark' : fixDarkMode; - const hasPrivileges = (userInfo?.publicInfo?.roles?.admin?.valueOf() || userInfo?.publicInfo?.roles?.officer?.valueOf() || userInfo?.publicInfo?.roles?.developer?.valueOf() || userInfo?.publicInfo?.roles?.lead?.valueOf() || userInfo?.publicInfo?.roles?.representative?.valueOf()); + const isAdminLead = hasPrivileges(userInfo!, ['admin', 'officer', 'developer', 'representative', 'lead']); + const [confirmVisible, setConfirmVisible] = useState(false); const [loading, setLoading] = useState(false); @@ -78,7 +80,7 @@ const ResumeCard: React.FC void }> = ({ r - {hasPrivileges && ( + {isAdminLead && ( setConfirmVisible(true)} diff --git a/src/screens/userProfile/PublicProfile.tsx b/src/screens/userProfile/PublicProfile.tsx index c38b3a04..37c40fec 100644 --- a/src/screens/userProfile/PublicProfile.tsx +++ b/src/screens/userProfile/PublicProfile.tsx @@ -20,6 +20,7 @@ import { DocumentSnapshot, Timestamp } from 'firebase/firestore'; import { truncateStringWithEllipsis } from '../../helpers/stringUtils'; import { reverseFormattedFirebaseName } from '../../types/committees'; import { formatDateWithYear } from '../../helpers/timeUtils'; +import { hasPrivileges } from '../../helpers/rolesUtils'; export type PublicProfileScreenProps = { route: RouteProp; @@ -51,7 +52,7 @@ const PublicProfileScreen: React.FC = ({ route, naviga const [endOfData, setEndOfData] = useState(false); const lastVisibleRef = useRef(null); - const hasPrivileges = (userInfo?.publicInfo?.roles?.admin?.valueOf() || userInfo?.publicInfo?.roles?.officer?.valueOf() || userInfo?.publicInfo?.roles?.developer?.valueOf() || userInfo?.publicInfo?.roles?.lead?.valueOf() || userInfo?.publicInfo?.roles?.representative?.valueOf()); + const isAdmin = hasPrivileges(userInfo!, ['admin', 'officer', 'developer', 'representative']); useEffect(() => { const fetchUserData = async () => { @@ -188,7 +189,7 @@ const PublicProfileScreen: React.FC = ({ route, naviga {/* Edit Role Button */} - {hasPrivileges && + {isAdmin && setShowRoleModal(true)} className="rounded-xl px-3 py-2 mt-4" From bf263ac62226050b86e97b67b17690bbc1b6097d Mon Sep 17 00:00:00 2001 From: JasonIsAzn Date: Mon, 6 Jan 2025 17:05:15 -0600 Subject: [PATCH 2/6] reduce lead access to mange manually sign in/out for events --- functions/src/events.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/functions/src/events.ts b/functions/src/events.ts index 6b1d451b..ee5845ba 100644 --- a/functions/src/events.ts +++ b/functions/src/events.ts @@ -111,7 +111,7 @@ export const eventSignIn = functions.https.onCall(async (data, context) => { // Only allow privileged users to sign-in for other users const token = context.auth.token; - if (uid !== context.auth.uid && (token.admin !== true && token.officer !== true && token.developer !== true && token.secretary !== true && token.lead !== true && token.representative !== true)) { + if (uid !== context.auth.uid && (token.admin !== true && token.officer !== true && token.developer !== true && token.secretary !== true && token.representative !== true)) { functions.logger.warn(`${context.auth.token} attempted to sign in as ${uid} with invalid permissions.`); throw new functions.https.HttpsError("permission-denied", `Invalid credentials`); } @@ -188,7 +188,7 @@ export const eventSignOut = functions.https.onCall(async (data, context) => { // Only allow privileged users to sign-out for other users const token = context.auth.token; - if (uid !== context.auth.uid && (token.admin !== true && token.officer !== true && token.developer !== true && token.secretary !== true && token.lead !== true && token.representative !== true)) { + if (uid !== context.auth.uid && (token.admin !== true && token.officer !== true && token.developer !== true && token.secretary !== true && token.representative !== true)) { functions.logger.warn(`${context.auth.token} attempted to sign in as ${uid} with invalid permissions.`); throw new functions.https.HttpsError("permission-denied", `Invalid credentials`); } @@ -261,7 +261,7 @@ export const addInstagramPoints = functions.https.onCall(async (data, context) = } const token = context.auth.token; - if (token.admin !== true && token.officer !== true && token.developer !== true && token.lead !== true && token.representative !== true) { + if (token.admin !== true && token.officer !== true && token.developer !== true && token.representative !== true) { throw new functions.https.HttpsError("permission-denied", `Invalid credentials`); } @@ -313,7 +313,7 @@ export const eventLogDelete = functions.https.onCall(async (data, context) => { // Only allow privileged users to sign-out for other users const token = context.auth.token; - if (uid !== context.auth.uid && (token.admin !== true && token.officer !== true && token.developer !== true && token.secretary !== true && token.lead !== true && token.representative !== true)) { + if (uid !== context.auth.uid && (token.admin !== true && token.officer !== true && token.developer !== true && token.secretary !== true && token.representative !== true)) { functions.logger.warn(`${context.auth.token} attempted to sign in as ${uid} with invalid permissions.`); throw new functions.https.HttpsError("permission-denied", `Invalid credentials`); } From 49e14ef4a4ee81458fc61f67dc9d48d4fdc27a4a Mon Sep 17 00:00:00 2001 From: JasonIsAzn Date: Mon, 6 Jan 2025 17:05:31 -0600 Subject: [PATCH 3/6] remove lead power to update points in points sheet --- functions/src/pointSheet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/functions/src/pointSheet.ts b/functions/src/pointSheet.ts index c4e41f09..f6ce90ca 100644 --- a/functions/src/pointSheet.ts +++ b/functions/src/pointSheet.ts @@ -114,7 +114,7 @@ export const updateUserPoints = functions.https.onCall(async (data, context) => } const token = context.auth.token; - if (token.admin !== true && token.officer !== true && token.developer !== true && token.secretary !== true && token.lead !== true && token.representative !== true) { + if (token.admin !== true && token.officer !== true && token.developer !== true && token.secretary !== true && token.representative !== true) { throw new functions.https.HttpsError("permission-denied", `Invalid credentials`); } @@ -138,7 +138,7 @@ export const updateAllUserPoints = functions.https.onCall(async (_, context) => } const token = context.auth.token; - if (token.admin !== true && token.officer !== true && token.developer !== true && token.secretary !== true && token.lead !== true && token.representative !== true) { + if (token.admin !== true && token.officer !== true && token.developer !== true && token.secretary !== true && token.representative !== true) { throw new functions.https.HttpsError("permission-denied", `Invalid credentials`); } From c52426f7a06bb252237acafa4986ca7ecb859472 Mon Sep 17 00:00:00 2001 From: JasonIsAzn Date: Mon, 6 Jan 2025 17:06:33 -0600 Subject: [PATCH 4/6] add coach role to improve events management --- src/screens/events/EventCard.tsx | 3 ++- src/screens/events/EventInfo.tsx | 7 ++++--- src/screens/events/Events.tsx | 9 +++++---- src/screens/events/PastEvents.tsx | 5 +++-- src/screens/userProfile/PublicProfile.tsx | 13 +++++++------ src/types/user.ts | 1 + 6 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/screens/events/EventCard.tsx b/src/screens/events/EventCard.tsx index 497b4ee7..1a110a14 100644 --- a/src/screens/events/EventCard.tsx +++ b/src/screens/events/EventCard.tsx @@ -17,6 +17,7 @@ const EventCard = ({ event, navigation }: { event: SHPEEvent, navigation: any }) const darkMode = useSystemDefault ? colorScheme === 'dark' : fixDarkMode; const isAdminLead = hasPrivileges(userInfo!, ['admin', 'officer', 'developer', 'representative', 'lead']); + const isCoach = hasPrivileges(userInfo!, ['coach']); return ( {formatDate(event.startTime?.toDate()!)} - {isAdminLead && ( + {(isAdminLead || isCoach) && ( { navigation.navigate("QRCode", { event: event }) }} diff --git a/src/screens/events/EventInfo.tsx b/src/screens/events/EventInfo.tsx index 197f7a8a..c3365faa 100644 --- a/src/screens/events/EventInfo.tsx +++ b/src/screens/events/EventInfo.tsx @@ -45,6 +45,7 @@ const EventInfo = ({ navigation }: EventProps) => { const isAdminLead = hasPrivileges(userInfo!, ['admin', 'officer', 'developer', 'representative', 'lead']); const isAdmin = hasPrivileges(userInfo!, ['admin', 'officer', 'developer', 'representative']); + const isCoach = hasPrivileges(userInfo!, ['coach']); const [creatorData, setCreatorData] = useState(null); const [loadingUserEventLog, setLoadingUserEventLog] = useState(true); @@ -205,7 +206,7 @@ const EventInfo = ({ navigation }: EventProps) => { > - {isAdminLead && ( + {(isAdminLead || isCoach) && ( { setShowOptionMenu(!showOptionMenu) }} @@ -276,7 +277,7 @@ const EventInfo = ({ navigation }: EventProps) => { - {isAdminLead && ( + {(isAdminLead || isCoach) && ( { navigation.navigate("QRCode", { event: event }) @@ -295,7 +296,7 @@ const EventInfo = ({ navigation }: EventProps) => { - {isAdminLead && !loading && ( + {(isAdminLead || isCoach) && !loading && ( { const [filter, setFilter] = useState<"main" | "intramural" | "committee">("main"); const isAdminLead = hasPrivileges(userInfo!, ['admin', 'officer', 'developer', 'representative', 'lead']); + const isCoach = hasPrivileges(userInfo!, ['coach']); const selectedEvents = filter === "main" ? mainEvents : filter === "intramural" ? intramuralEvents : committeeEvents; @@ -140,10 +141,10 @@ const Events = ({ navigation }: EventsProps) => { useFocusEffect( useCallback(() => { - if (isAdminLead) { + if ((isAdminLead || isCoach)) { fetchEvents(); } - }, [isAdminLead]) + }, [isAdminLead, isCoach]) ); @@ -285,7 +286,7 @@ const Events = ({ navigation }: EventsProps) => { - {isAdminLead && ( + {(isAdminLead || isCoach) && ( { navigation.navigate("QRCode", { event: event }); @@ -344,7 +345,7 @@ const Events = ({ navigation }: EventsProps) => { {/* Create Event */} - {isAdminLead && ( + {(isAdminLead || isCoach) && ( ) = const darkMode = useSystemDefault ? colorScheme === 'dark' : fixDarkMode; const isAdminLead = hasPrivileges(userInfo!, ['admin', 'officer', 'developer', 'representative', 'lead']); + const isCoach = hasPrivileges(userInfo!, ['coach']); const [pastEvents, setPastEvents] = useState([]); const [loading, setLoading] = useState(false); @@ -52,10 +53,10 @@ const PastEvents = ({ navigation }: NativeStackScreenProps) = useFocusEffect( useCallback(() => { - if (isAdminLead) { + if ((isAdminLead || isCoach)) { fetchInitialEvents(); } - }, [isAdminLead]) + }, [isAdminLead, isCoach]) ); diff --git a/src/screens/userProfile/PublicProfile.tsx b/src/screens/userProfile/PublicProfile.tsx index 37c40fec..229350c9 100644 --- a/src/screens/userProfile/PublicProfile.tsx +++ b/src/screens/userProfile/PublicProfile.tsx @@ -383,6 +383,11 @@ const PublicProfileScreen: React.FC = ({ route, naviga isActive={modifiedRoles?.lead || false} onToggle={() => setModifiedRoles({ ...modifiedRoles, lead: !modifiedRoles?.lead })} /> + setModifiedRoles({ ...modifiedRoles, coach: !modifiedRoles?.coach })} + /> {/* Action Buttons */} @@ -390,13 +395,13 @@ const PublicProfileScreen: React.FC = ({ route, naviga { // checks if has role but no custom title - if ((modifiedRoles?.admin || modifiedRoles?.developer || modifiedRoles?.officer || modifiedRoles?.secretary || modifiedRoles?.representative || modifiedRoles?.lead) && !modifiedRoles?.customTitle && !modifiedRoles?.customTitle?.length) { + if ((modifiedRoles?.admin || modifiedRoles?.developer || modifiedRoles?.officer || modifiedRoles?.secretary || modifiedRoles?.representative || modifiedRoles?.lead || modifiedRoles?.coach) && !modifiedRoles?.customTitle && !modifiedRoles?.customTitle?.length) { Alert.alert("Missing Title", "You must enter a title "); return; } // Checks if has custom title but no role - if (!modifiedRoles?.admin && !modifiedRoles?.developer && !modifiedRoles?.officer && !modifiedRoles?.secretary && !modifiedRoles?.representative && !modifiedRoles?.lead && modifiedRoles?.customTitle) { + if (!modifiedRoles?.admin && !modifiedRoles?.developer && !modifiedRoles?.officer && !modifiedRoles?.secretary && !modifiedRoles?.representative && !modifiedRoles?.lead && !modifiedRoles?.coach && modifiedRoles?.customTitle) { Alert.alert("Missing Role", "If a custom title is entered, you must select a role."); return; } @@ -437,8 +442,4 @@ const PublicProfileScreen: React.FC = ({ route, naviga ) } -const formatTimestamp = (timestamp: Timestamp | null | undefined) => { - return timestamp ? new Date(timestamp.toDate()).toLocaleString() : 'N/A'; -}; - export default PublicProfileScreen; \ No newline at end of file diff --git a/src/types/user.ts b/src/types/user.ts index 82867100..64b94c5c 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -14,6 +14,7 @@ export interface Roles { representative?: boolean; lead?: boolean; secretary?: boolean; + coach?: boolean; customTitle?: string; }; From 2b8c991ad1878705cc87320b2b04ff3a8fb2a153 Mon Sep 17 00:00:00 2001 From: LucientZ Date: Tue, 7 Jan 2025 09:21:47 -0600 Subject: [PATCH 5/6] Added jsdoc to hasPrivileges function --- src/helpers/rolesUtils.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/helpers/rolesUtils.ts b/src/helpers/rolesUtils.ts index cd09a2a1..a7dc23cf 100644 --- a/src/helpers/rolesUtils.ts +++ b/src/helpers/rolesUtils.ts @@ -1,9 +1,22 @@ import { Roles, User } from "../types/user"; +/** + * Returns a boolean stating whether or not a user object has the given roles. + * + * Note that these are not the same as *claims* and should only be used for frontend things like + * accessing certain screens or showing certain buttons. + * + * @param userInfo The user information object. + * @param roles The roles to check for the given user. + * @returns {boolean} Whether or not the given `User` object has the specified roles. + * + * @example + * const isSuperuser = hasPrivileges(user, ['admin', 'developer']); + */ export const hasPrivileges = ( userInfo: User, roles: (keyof Roles)[] = ['admin', 'officer', 'developer', 'lead', 'representative'] -) => { +): boolean => { const userRoles = userInfo?.publicInfo?.roles || {}; return roles.some(role => userRoles[role]?.valueOf()); }; From a939921493e8db4b4acfeea695bc90dc9aa80818 Mon Sep 17 00:00:00 2001 From: JasonIsAzn Date: Tue, 7 Jan 2025 19:32:14 -0600 Subject: [PATCH 6/6] version bump --- app.json | 4 ++-- package.json | 2 +- src/config/firebaseConfig.ts | 32 ++++++++++++++++---------------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app.json b/app.json index 80fb1821..090cc39b 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "TAMU SHPE", "slug": "TAMU-SHPE", - "version": "1.0.22", + "version": "1.0.23", "owner": "tamu-shpe", "orientation": "portrait", "icon": "./assets/icon.png", @@ -54,7 +54,7 @@ "permissions": [ "android.permission.RECORD_AUDIO" ], - "versionCode": 105, + "versionCode": 110, "userInterfaceStyle": "automatic", "config": { "googleMaps": { diff --git a/package.json b/package.json index 3cb86fb1..889071e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shpe-app", - "version": "1.0.22", + "version": "1.0.23", "scripts": { "start": "npx expo start --dev-client", "test": "jest --coverage=true --verbose --bail --config ./jest.config.ts", diff --git a/src/config/firebaseConfig.ts b/src/config/firebaseConfig.ts index da080682..e4dd5b98 100644 --- a/src/config/firebaseConfig.ts +++ b/src/config/firebaseConfig.ts @@ -38,21 +38,21 @@ const db = getFirestore(app); const storage = getStorage(app); const functions = getFunctions(app); -if (process.env.FIREBASE_EMULATOR_ADDRESS !== undefined) { - const address = process.env.FIREBASE_EMULATOR_ADDRESS; - console.debug("Connecting to firebase emulators."); - if (process.env.FIREBASE_AUTH_PORT !== undefined) { - connectAuthEmulator(auth, `http://${address}:${process.env.FIREBASE_AUTH_PORT}`); - } - if (process.env.FIREBASE_FIRESTORE_PORT !== undefined) { - connectFirestoreEmulator(db, address, Number(process.env.FIREBASE_FIRESTORE_PORT)); - } - if (process.env.FIREBASE_CLOUD_FUNCTIONS_PORT !== undefined) { - connectFunctionsEmulator(functions, address, Number(process.env.FIREBASE_CLOUD_FUNCTIONS_PORT)); - } - if (process.env.FIREBASE_STORAGE_PORT !== undefined) { - connectStorageEmulator(storage, address, Number(process.env.FIREBASE_STORAGE_PORT)); - } -} +// if (process.env.FIREBASE_EMULATOR_ADDRESS !== undefined) { +// const address = process.env.FIREBASE_EMULATOR_ADDRESS; +// console.debug("Connecting to firebase emulators."); +// if (process.env.FIREBASE_AUTH_PORT !== undefined) { +// connectAuthEmulator(auth, `http://${address}:${process.env.FIREBASE_AUTH_PORT}`); +// } +// if (process.env.FIREBASE_FIRESTORE_PORT !== undefined) { +// connectFirestoreEmulator(db, address, Number(process.env.FIREBASE_FIRESTORE_PORT)); +// } +// if (process.env.FIREBASE_CLOUD_FUNCTIONS_PORT !== undefined) { +// connectFunctionsEmulator(functions, address, Number(process.env.FIREBASE_CLOUD_FUNCTIONS_PORT)); +// } +// if (process.env.FIREBASE_STORAGE_PORT !== undefined) { +// connectStorageEmulator(storage, address, Number(process.env.FIREBASE_STORAGE_PORT)); +// } +// } export { db, auth, storage, functions };