Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a Manage Profile page #41

Merged
merged 8 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/common/types/msGraphApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface UserProfileDataBase {
userPrincipalName: string;
displayName?: string;
givenName?: string;
surname?: string;
mail?: string;
otherMails?: string[]
}

export interface UserProfileData extends UserProfileDataBase {
discordUsername?: string;
}
11 changes: 11 additions & 0 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function transformCommaSeperatedName(name: string) {
if (name.includes(",")) {
try {
const split = name.split(",")
return `${split[1].slice(1, split[1].length)} ${split[0]}`
} catch (e) {
return name;
}
}
return name;
}
46 changes: 44 additions & 2 deletions src/ui/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,29 @@ import { ScanTicketsPage } from './pages/tickets/ScanTickets.page';
import { SelectTicketsPage } from './pages/tickets/SelectEventId.page';
import { ViewTicketsPage } from './pages/tickets/ViewTickets.page';
import { ManageIamPage } from './pages/iam/ManageIam.page';
import { ManageProfilePage } from './pages/profile/ManageProfile.page';

const ProfileRediect: React.FC = () => {
const location = useLocation();

// Don't store login-related paths and ALLOW the callback path
const excludedPaths = [
'/login',
'/logout',
'/force_login',
'/a',
'/auth/callback', // Add this to excluded paths
];

if (excludedPaths.includes(location.pathname)) {
return <Navigate to="/login" replace />;
}

// Include search params and hash in the return URL if they exist
const returnPath = location.pathname + location.search + location.hash;
const loginUrl = `/profile?returnTo=${encodeURIComponent(returnPath)}&firstTime=true`;
return <Navigate to={loginUrl} replace />;
};

// Component to handle redirects to login with return path
const LoginRedirect: React.FC = () => {
Expand Down Expand Up @@ -56,6 +79,18 @@ const commonRoutes = [
},
];

const profileRouter = createBrowserRouter([
...commonRoutes,
{
path: '/profile',
element: <ManageProfilePage />,
},
{
path: '*',
element: <ProfileRediect />,
},
]);

const unauthenticatedRouter = createBrowserRouter([
...commonRoutes,
{
Expand All @@ -66,7 +101,6 @@ const unauthenticatedRouter = createBrowserRouter([
path: '/login',
element: <LoginPage />,
},
// Catch-all route that preserves the attempted path
{
path: '*',
element: <LoginRedirect />,
Expand All @@ -87,6 +121,10 @@ const authenticatedRouter = createBrowserRouter([
path: '/logout',
element: <LogoutPage />,
},
{
path: '/profile',
element: <ManageProfilePage />,
},
{
path: '/home',
element: <HomePage />,
Expand Down Expand Up @@ -163,7 +201,11 @@ const ErrorBoundary: React.FC<ErrorBoundaryProps> = ({ children }) => {

export const Router: React.FC = () => {
const { isLoggedIn } = useAuth();
const router = isLoggedIn ? authenticatedRouter : unauthenticatedRouter;
const router = isLoggedIn
? authenticatedRouter
: isLoggedIn === null
? profileRouter
: unauthenticatedRouter;

return (
<ErrorBoundary>
Expand Down
4 changes: 2 additions & 2 deletions src/ui/components/AppShell/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { HeaderNavbar } from '../Navbar/index.js';
import { AuthenticatedProfileDropdown } from '../ProfileDropdown/index.js';
import { getCurrentRevision } from '@ui/util/revision.js';

interface AcmAppShellProps {
export interface AcmAppShellProps {
children: ReactNode;
active?: string;
showLoader?: boolean;
Expand Down Expand Up @@ -164,7 +164,7 @@ const AcmAppShell: React.FC<AcmAppShellProps> = ({
padding="md"
header={{ height: 60 }}
navbar={{
width: 200,
width: showSidebar ? 200 : 0,
breakpoint: 'sm',
collapsed: { mobile: !opened },
}}
Expand Down
5 changes: 0 additions & 5 deletions src/ui/components/AuthContext/AuthCallbackHandler.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,6 @@ export const AuthCallback: React.FC = () => {
setTimeout(() => {
handleCallback();
}, 100);

// Cleanup function
return () => {
console.log('Callback component unmounting'); // Debug log 8
};
}, [instance, navigate]);

return <FullScreenLoader />;
Expand Down
35 changes: 22 additions & 13 deletions src/ui/components/AuthContext/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { CACHE_KEY_PREFIX } from '../AuthGuard/index.js';
import FullScreenLoader from './LoadingScreen.js';

import { getRunEnvironmentConfig, ValidServices } from '@ui/config.js';
import { transformCommaSeperatedName } from '@common/utils.js';
import { useApi } from '@ui/util/api.js';

interface AuthContextDataWrapper {
isLoggedIn: boolean;
Expand All @@ -28,6 +30,7 @@ interface AuthContextDataWrapper {
getToken: CallableFunction;
logoutCallback: CallableFunction;
getApiToken: CallableFunction;
setLoginStatus: CallableFunction;
}

export type AuthContextData = {
Expand All @@ -53,7 +56,6 @@ export const clearAuthCache = () => {

export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const { instance, inProgress, accounts } = useMsal();

const [userData, setUserData] = useState<AuthContextData | null>(null);
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);

Expand All @@ -67,11 +69,9 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
if (response) {
handleMsalResponse(response);
} else if (accounts.length > 0) {
// User is already logged in, set the state
const [lastName, firstName] = accounts[0].name?.split(',') || [];
setUserData({
email: accounts[0].username,
name: `${firstName} ${lastName}`,
name: transformCommaSeperatedName(accounts[0].name || ''),
});
setIsLoggedIn(true);
}
Expand All @@ -94,29 +94,26 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
})
.then((silentResponse) => {
if (silentResponse?.account?.name) {
const [lastName, firstName] = silentResponse.account.name.split(',');
setUserData({
email: silentResponse.account.username,
name: `${firstName} ${lastName}`,
email: accounts[0].username,
name: transformCommaSeperatedName(accounts[0].name || ''),
});
setIsLoggedIn(true);
}
})
.catch(console.error);
return;
}

// Use response.account instead of accounts[0]
const [lastName, firstName] = response.account.name?.split(',') || [];
setUserData({
email: response.account.username,
name: `${firstName} ${lastName}`,
email: accounts[0].username,
name: transformCommaSeperatedName(accounts[0].name || ''),
});
setIsLoggedIn(true);
}
},
[accounts, instance]
);

const getApiToken = useCallback(
async (service: ValidServices) => {
if (!userData) {
Expand Down Expand Up @@ -194,6 +191,9 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
},
[instance]
);
const setLoginStatus = useCallback((val: boolean) => {
setIsLoggedIn(val);
}, []);

const logout = useCallback(async () => {
try {
Expand All @@ -209,7 +209,16 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
};
return (
<AuthContext.Provider
value={{ isLoggedIn, userData, loginMsal, logout, getToken, logoutCallback, getApiToken }}
value={{
isLoggedIn,
userData,
setLoginStatus,
loginMsal,
logout,
getToken,
logoutCallback,
getApiToken,
}}
>
{inProgress !== InteractionStatus.None ? (
<MantineProvider>
Expand Down
25 changes: 13 additions & 12 deletions src/ui/components/AuthGuard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Card, Text, Title } from '@mantine/core';
import React, { ReactNode, useEffect, useState } from 'react';

import { AcmAppShell } from '@ui/components/AppShell';
import { AcmAppShell, AcmAppShellProps } from '@ui/components/AppShell';
import FullScreenLoader from '@ui/components/AuthContext/LoadingScreen';
import { getRunEnvironmentConfig, ValidService } from '@ui/config';
import { useApi } from '@ui/util/api';
Expand Down Expand Up @@ -60,11 +60,13 @@ export const clearAuthCache = () => {
}
};

export const AuthGuard: React.FC<{
resourceDef: ResourceDefinition;
children: ReactNode;
isAppShell?: boolean;
}> = ({ resourceDef, children, isAppShell = true }) => {
export const AuthGuard: React.FC<
{
resourceDef: ResourceDefinition;
children: ReactNode;
isAppShell?: boolean;
} & AcmAppShellProps
> = ({ resourceDef, children, isAppShell = true, ...appShellProps }) => {
const { service, validRoles } = resourceDef;
const { baseEndpoint, authCheckRoute, friendlyName } =
getRunEnvironmentConfig().ServiceConfiguration[service];
Expand All @@ -80,6 +82,10 @@ export const AuthGuard: React.FC<{
setIsAuthenticated(true);
return;
}
if (validRoles.length === 0) {
setIsAuthenticated(true);
return;
}

// Check for cached response first
const cachedData = getCachedResponse(service, authCheckRoute);
Expand Down Expand Up @@ -163,12 +169,7 @@ export const AuthGuard: React.FC<{
}

if (isAppShell) {
return (
<AcmAppShell>
<Title order={1}>{friendlyName}</Title>
{children}
</AcmAppShell>
);
return <AcmAppShell {...appShellProps}>{children}</AcmAppShell>;
}

return <>{children}</>;
Expand Down
12 changes: 12 additions & 0 deletions src/ui/components/ProfileDropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useState } from 'react';

import { AuthContextData, useAuth } from '../AuthContext/index.js';
import classes from '../Navbar/index.module.css';
import { useNavigate } from 'react-router-dom';

interface ProfileDropdownProps {
userData?: AuthContextData;
Expand All @@ -26,6 +27,7 @@ interface ProfileDropdownProps {
const AuthenticatedProfileDropdown: React.FC<ProfileDropdownProps> = ({ userData }) => {
const [opened, setOpened] = useState(false);
const theme = useMantineTheme();
const navigate = useNavigate();
const { logout } = useAuth();
if (!userData) {
return null;
Expand Down Expand Up @@ -111,6 +113,16 @@ const AuthenticatedProfileDropdown: React.FC<ProfileDropdownProps> = ({ userData
</Group>
</UnstyledButton>
<Divider my="sm" />
<Button
variant="primary"
mb="sm"
fullWidth
onClick={() => {
navigate('/profile');
}}
>
Edit Profile
</Button>
<Button
variant="outline"
fullWidth
Expand Down
20 changes: 19 additions & 1 deletion src/ui/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { execCouncilGroupId, execCouncilTestingGroupId } from '@common/config';
export const runEnvironments = ['dev', 'prod', 'local-dev'] as const;
// local dev should be used when you want to test against a local instance of the API

export const services = ['core', 'tickets', 'merch'] as const;
export const services = ['core', 'tickets', 'merch', 'msGraphApi'] as const;
export type RunEnvironment = (typeof runEnvironments)[number];
export type ValidServices = (typeof services)[number];
export type ValidService = ValidServices;
Expand Down Expand Up @@ -49,6 +49,12 @@ const environmentConfig: EnvironmentConfigType = {
friendlyName: 'Merch Sales Service (Prod)',
baseEndpoint: 'https://merchapi.acm.illinois.edu',
},
msGraphApi: {
friendlyName: 'Microsoft Graph API',
baseEndpoint: 'https://graph.microsoft.com',
loginScope: 'https://graph.microsoft.com/.default',
apiId: 'https://graph.microsoft.com',
},
},
KnownGroupMappings: {
Exec: execCouncilTestingGroupId,
Expand All @@ -72,6 +78,12 @@ const environmentConfig: EnvironmentConfigType = {
friendlyName: 'Merch Sales Service (Prod)',
baseEndpoint: 'https://merchapi.acm.illinois.edu',
},
msGraphApi: {
friendlyName: 'Microsoft Graph API',
baseEndpoint: 'https://graph.microsoft.com',
loginScope: 'https://graph.microsoft.com/.default',
apiId: 'https://graph.microsoft.com',
},
},
KnownGroupMappings: {
Exec: execCouncilTestingGroupId,
Expand All @@ -95,6 +107,12 @@ const environmentConfig: EnvironmentConfigType = {
friendlyName: 'Merch Sales Service',
baseEndpoint: 'https://merchapi.acm.illinois.edu',
},
msGraphApi: {
friendlyName: 'Microsoft Graph API',
baseEndpoint: 'https://graph.microsoft.com',
loginScope: 'https://graph.microsoft.com/.default',
apiId: 'https://graph.microsoft.com',
},
},
KnownGroupMappings: {
Exec: execCouncilGroupId,
Expand Down
Loading
Loading