diff --git a/api/apiFunctions.interface.ts b/api/apiFunctions.interface.ts index ed8352ba..e5d1f6fb 100644 --- a/api/apiFunctions.interface.ts +++ b/api/apiFunctions.interface.ts @@ -9,6 +9,16 @@ export interface IAccountData { } export interface IUser { documentId: string; + // for the appwrite auth collection + id: string; + email: string; + leagues: string[]; + labels: string[]; +} + +export interface ICollectionUser { + documentId: string; + // for the custom user collection id: string; email: string; leagues: string[]; diff --git a/api/apiFunctions.ts b/api/apiFunctions.ts index 93041dd3..2f3b072d 100644 --- a/api/apiFunctions.ts +++ b/api/apiFunctions.ts @@ -8,6 +8,7 @@ import { ILeague, IGameWeek, IUser, + ICollectionUser, IWeeklyPicks, INFLTeam, IRecoveryToken, @@ -149,7 +150,9 @@ export async function updateUserEmail({ * @param userId - The user ID * @returns {Models.DocumentList | Error} - The user object or an error */ -export async function getCurrentUser(userId: IUser['id']): Promise { +export async function getCurrentUser( + userId: IUser['id'], +): Promise { try { const user = await databases.listDocuments( appwriteConfig.databaseId, diff --git a/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.test.tsx b/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.test.tsx index 78b69c1d..516631f6 100644 --- a/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.test.tsx +++ b/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.test.tsx @@ -186,6 +186,8 @@ describe('League Week Picks', () => { // Wait for the main content to be displayed await waitFor(() => { expect(screen.getByTestId('weekly-picks')).toBeInTheDocument(); + expect(screen.getByTestId('week__week-number')).toHaveTextContent('Week 1'); + expect(screen.getByTestId('week__entry-name')).toHaveTextContent('Entry 1'); }); expect(screen.queryByTestId('global-spinner')).not.toBeInTheDocument(); diff --git a/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.tsx b/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.tsx index db581d42..6edab010 100644 --- a/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.tsx +++ b/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.tsx @@ -277,12 +277,14 @@ const Week = ({ entry, league, NFLTeams, week }: IWeekProps): JSX.Element => { {`Week ${week} pick`} {`Entry ${entryName}`} + className='pb-8 text-muted-foreground' + data-testid='week__entry-name' + >{entryName} {pickHistory.length > 0 && (
({ id: '1234', email: 'test@test.com', leagues: ['league1'], + labels: [], }, allLeagues: [ { @@ -82,6 +83,7 @@ describe('Leagues Component', () => { leagues: [], }, allLeagues: [], + labels: [], }); render(); @@ -105,6 +107,7 @@ describe('Leagues Component', () => { leagues: [], }, allLeagues: [], + labels: [], }); render(); @@ -121,6 +124,7 @@ describe('Leagues Component', () => { email: 'test@test.com', id: '123', leagues: [], + labels: [], }, allLeagues: [ { @@ -150,6 +154,7 @@ describe('Leagues Component', () => { email: 'test@test.com', id: '123', leagues: [], + labels: [], }; const league = { @@ -202,6 +207,7 @@ describe('Leagues Component', () => { user.id, user.email, [...user.leagues, league.leagueId], + user.labels, ); expect(toast.custom).toHaveBeenCalledWith( { email: 'test@test.com', id: '123', leagues: [], + labels: [], }; const league = { diff --git a/app/(main)/league/all/page.tsx b/app/(main)/league/all/page.tsx index 56c04323..8e8a77de 100644 --- a/app/(main)/league/all/page.tsx +++ b/app/(main)/league/all/page.tsx @@ -100,10 +100,13 @@ const Leagues = (): JSX.Element => { }); setLeagues([...leagues, league]); - updateUser(user.documentId, user.id, user.email, [ - ...user.leagues, - league.leagueId, - ]); + updateUser( + user.documentId, + user.id, + user.email, + [...user.leagues, league.leagueId], + user.labels, + ); toast.custom( { />, ); - updateUser(user.documentId, user.id, email, user.leagues); + updateUser(user.documentId, user.id, email, user.leagues, user.labels); form.reset({ email: email || '', password: '' }); } catch (error) { console.error('Email Update Failed', error); diff --git a/context/AuthContextProvider.tsx b/context/AuthContextProvider.tsx index d607d6b0..eb1adb9b 100644 --- a/context/AuthContextProvider.tsx +++ b/context/AuthContextProvider.tsx @@ -8,10 +8,11 @@ import { account } from '@/api/config'; import { useRouter } from 'next/navigation'; import { useDataStore } from '@/store/dataStore'; import type { DataStore } from '@/store/dataStore'; -import { IUser } from '@/api/apiFunctions.interface'; +import { ICollectionUser, IUser } from '@/api/apiFunctions.interface'; import { getCurrentUser } from '@/api/apiFunctions'; import { loginAccount, logoutHandler } from './AuthHelper'; import { usePathname } from 'next/navigation'; +import { isAuthRequiredPath } from '@/utils/utils'; type UserCredentials = { email: string; @@ -50,8 +51,12 @@ export const AuthContextProvider = ({ getUser(); return; } + setIsSignedIn(true); - }, [user]); + if (pathname.startsWith('/admin')) { + !user.labels.includes('admin') && router.push('/'); + } + }, [user, pathname]); /** * Authenticate and set session state @@ -81,11 +86,7 @@ export const AuthContextProvider = ({ */ const getUser = async (): Promise => { if (!isSessionInLocalStorage()) { - if ( - pathname !== '/register' && - pathname !== '/account/recovery' && - pathname !== '/recover-password' - ) { + if (isAuthRequiredPath(pathname)) { router.push('/login'); } return; @@ -93,14 +94,25 @@ export const AuthContextProvider = ({ try { const user = await account.get(); - const userData: IUser = await getCurrentUser(user.$id); + const userData: ICollectionUser = await getCurrentUser(user.$id); + + const currentUser: IUser = { + documentId: userData.documentId, + id: userData.id, + email: userData.email, + leagues: userData.leagues, + labels: user.labels, + }; + updateUser( - userData.documentId, - userData.id, - userData.email, - userData.leagues, + currentUser.documentId, + currentUser.id, + currentUser.email, + currentUser.leagues, + user.labels, ); - return userData; + + return currentUser; } catch (error) { resetUser(); setIsSignedIn(false); diff --git a/store/dataStore.test.ts b/store/dataStore.test.ts index ab4f9acf..d3012f1f 100644 --- a/store/dataStore.test.ts +++ b/store/dataStore.test.ts @@ -7,6 +7,7 @@ const userData = { id: '123', email: 'test@email.com', leagues: ['123456'], + labels: ['admin'], }; const NFLTeams = [ @@ -61,11 +62,14 @@ describe('Data Store', () => { userData.userId, userData.userEmail, userData.leagues, + userData.labels, ); }); expect(result.current.user.id).toBe(userData.userId); expect(result.current.user.email).toBe(userData.userEmail); + expect(result.current.user.labels).toStrictEqual(userData.labels); + expect(result.current.user.leagues).toStrictEqual(userData.leagues); }); it('Checks the reset user state matches default', () => { const { result } = renderHook(() => useDataStore()); @@ -76,12 +80,15 @@ describe('Data Store', () => { userData.userId, userData.userEmail, userData.leagues, + userData.labels, ); result.current.resetUser(); }); expect(result.current.user.id).toBe(''); expect(result.current.user.email).toBe(''); + expect(result.current.user.labels).toStrictEqual([]); + expect(result.current.user.leagues).toStrictEqual([]); }); }); diff --git a/store/dataStore.ts b/store/dataStore.ts index 1acc84e5..e0cef710 100644 --- a/store/dataStore.ts +++ b/store/dataStore.ts @@ -34,6 +34,7 @@ interface IDataStoreAction { id: IUser['id'], email: IUser['email'], leagues: IUser['leagues'], + labels: IUser['labels'], ) => void; updateWeeklyPicks: ({ leagueId, @@ -63,6 +64,7 @@ const initialState: IDataStoreState = { id: '', email: '', leagues: [], + labels: [], }, weeklyPicks: { leagueId: '', @@ -111,13 +113,14 @@ export const useDataStore = create((set) => ({ * @param selectedLeagues - The user selected league * @returns {void} */ - updateUser: (documentId, id, email, leagues): void => + updateUser: (documentId, id, email, leagues, labels): void => set( produce((state: IDataStoreState) => { state.user.documentId = documentId; state.user.id = id; state.user.email = email; state.user.leagues = [...leagues]; + state.user.labels = [...labels]; }), ), /** diff --git a/utils/utils.test.ts b/utils/utils.test.ts index 408a3b89..1c471924 100644 --- a/utils/utils.test.ts +++ b/utils/utils.test.ts @@ -5,6 +5,7 @@ import { getUserPick, parseUserPick, getUserLeagues, + isAuthRequiredPath, } from './utils'; import { getCurrentLeague, getAllWeeklyPicks } from '@/api/apiFunctions'; @@ -179,6 +180,33 @@ describe('utils', () => { }); }); }); + describe('isAuthRequiredPath', () => { + it('should return false for non-auth paths', () => { + expect(isAuthRequiredPath('/register')).toBe(false); + expect(isAuthRequiredPath('/account/recovery')).toBe(false); + expect(isAuthRequiredPath('/recover-password')).toBe(false); + }); + + it('should return true for auth-required paths', () => { + expect(isAuthRequiredPath('/')).toBe(true); + expect(isAuthRequiredPath('/dashboard')).toBe(true); + expect(isAuthRequiredPath('/profile')).toBe(true); + expect(isAuthRequiredPath('/settings')).toBe(true); + }); + + it('should handle edge cases', () => { + expect(isAuthRequiredPath('')).toBe(true); + expect(isAuthRequiredPath('/register/')).toBe(true); // Trailing slash + expect(isAuthRequiredPath('/REGISTER')).toBe(true); // Case sensitivity + expect(isAuthRequiredPath('/register/subpage')).toBe(true); + }); + + it('should handle unusual inputs', () => { + expect(isAuthRequiredPath(' /register ')).toBe(true); // Spaces + expect(isAuthRequiredPath('/account/recovery?param=value')).toBe(true); // Query parameters + }); + }); + xdescribe('getUserLeagues', () => { it('should return the list of leagues the user is a part of', async () => { (getCurrentLeague as jest.Mock).mockResolvedValue(mockLeague); diff --git a/utils/utils.ts b/utils/utils.ts index 51aee08e..ca28a1da 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -147,6 +147,15 @@ export const getUserEntries = async (userId: IUser['id'], leagueId: ILeague['lea return await getCurrentUserEntries(userId, leagueId); } +/** + * Check if the route is an /admin route + * @param path - The path to check + * @returns {boolean} - Whether the route is an /admin route + */ +export const isAdminRoute = (path: string): boolean => { + return path.startsWith('/admin'); +}; + /** * Returns if the team has already been picked by the user * @param teamName - The team name @@ -167,3 +176,13 @@ export const hasTeamBeenPicked = (teamName: string, selectedTeams: string[]): bo export const getNFLTeamLogo = (NFLTeams: INFLTeam[], teamName: string): string => { return NFLTeams.find((teams) => teams.teamName === teamName)?.teamLogo as string; } + +/** + * Checks if the current path requires authentication + * @param pathname - The current path + * @returns {boolean} - Whether the current path requires authentication + */ +export const isAuthRequiredPath = (pathname: string): boolean => { + const nonAuthPaths = ['/register', '/account/recovery', '/recover-password']; + return !nonAuthPaths.includes(pathname); +};