Skip to content

Commit 75e2e9c

Browse files
authored
Create a Manage Profile page (#41)
* enable edit profile support * make the iam page a little nicer * force reroute to profile if we dont know their first and last name * keep the redir behavior outside of the component * tests that fail * fix the tests * fix test * fix tests part 7!
1 parent 968c410 commit 75e2e9c

19 files changed

+744
-378
lines changed

src/common/types/msGraphApi.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export interface UserProfileDataBase {
2+
userPrincipalName: string;
3+
displayName?: string;
4+
givenName?: string;
5+
surname?: string;
6+
mail?: string;
7+
otherMails?: string[]
8+
}
9+
10+
export interface UserProfileData extends UserProfileDataBase {
11+
discordUsername?: string;
12+
}

src/common/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export function transformCommaSeperatedName(name: string) {
2+
if (name.includes(",")) {
3+
try {
4+
const split = name.split(",")
5+
return `${split[1].slice(1, split[1].length)} ${split[0]}`
6+
} catch (e) {
7+
return name;
8+
}
9+
}
10+
return name;
11+
}

src/ui/Router.tsx

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,29 @@ import { ScanTicketsPage } from './pages/tickets/ScanTickets.page';
1717
import { SelectTicketsPage } from './pages/tickets/SelectEventId.page';
1818
import { ViewTicketsPage } from './pages/tickets/ViewTickets.page';
1919
import { ManageIamPage } from './pages/iam/ManageIam.page';
20+
import { ManageProfilePage } from './pages/profile/ManageProfile.page';
21+
22+
const ProfileRediect: React.FC = () => {
23+
const location = useLocation();
24+
25+
// Don't store login-related paths and ALLOW the callback path
26+
const excludedPaths = [
27+
'/login',
28+
'/logout',
29+
'/force_login',
30+
'/a',
31+
'/auth/callback', // Add this to excluded paths
32+
];
33+
34+
if (excludedPaths.includes(location.pathname)) {
35+
return <Navigate to="/login" replace />;
36+
}
37+
38+
// Include search params and hash in the return URL if they exist
39+
const returnPath = location.pathname + location.search + location.hash;
40+
const loginUrl = `/profile?returnTo=${encodeURIComponent(returnPath)}&firstTime=true`;
41+
return <Navigate to={loginUrl} replace />;
42+
};
2043

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

82+
const profileRouter = createBrowserRouter([
83+
...commonRoutes,
84+
{
85+
path: '/profile',
86+
element: <ManageProfilePage />,
87+
},
88+
{
89+
path: '*',
90+
element: <ProfileRediect />,
91+
},
92+
]);
93+
5994
const unauthenticatedRouter = createBrowserRouter([
6095
...commonRoutes,
6196
{
@@ -66,7 +101,6 @@ const unauthenticatedRouter = createBrowserRouter([
66101
path: '/login',
67102
element: <LoginPage />,
68103
},
69-
// Catch-all route that preserves the attempted path
70104
{
71105
path: '*',
72106
element: <LoginRedirect />,
@@ -87,6 +121,10 @@ const authenticatedRouter = createBrowserRouter([
87121
path: '/logout',
88122
element: <LogoutPage />,
89123
},
124+
{
125+
path: '/profile',
126+
element: <ManageProfilePage />,
127+
},
90128
{
91129
path: '/home',
92130
element: <HomePage />,
@@ -163,7 +201,11 @@ const ErrorBoundary: React.FC<ErrorBoundaryProps> = ({ children }) => {
163201

164202
export const Router: React.FC = () => {
165203
const { isLoggedIn } = useAuth();
166-
const router = isLoggedIn ? authenticatedRouter : unauthenticatedRouter;
204+
const router = isLoggedIn
205+
? authenticatedRouter
206+
: isLoggedIn === null
207+
? profileRouter
208+
: unauthenticatedRouter;
167209

168210
return (
169211
<ErrorBoundary>

src/ui/components/AppShell/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { HeaderNavbar } from '../Navbar/index.js';
2525
import { AuthenticatedProfileDropdown } from '../ProfileDropdown/index.js';
2626
import { getCurrentRevision } from '@ui/util/revision.js';
2727

28-
interface AcmAppShellProps {
28+
export interface AcmAppShellProps {
2929
children: ReactNode;
3030
active?: string;
3131
showLoader?: boolean;
@@ -164,7 +164,7 @@ const AcmAppShell: React.FC<AcmAppShellProps> = ({
164164
padding="md"
165165
header={{ height: 60 }}
166166
navbar={{
167-
width: 200,
167+
width: showSidebar ? 200 : 0,
168168
breakpoint: 'sm',
169169
collapsed: { mobile: !opened },
170170
}}

src/ui/components/AuthContext/AuthCallbackHandler.page.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,6 @@ export const AuthCallback: React.FC = () => {
3434
setTimeout(() => {
3535
handleCallback();
3636
}, 100);
37-
38-
// Cleanup function
39-
return () => {
40-
console.log('Callback component unmounting'); // Debug log 8
41-
};
4237
}, [instance, navigate]);
4338

4439
return <FullScreenLoader />;

src/ui/components/AuthContext/index.tsx

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { CACHE_KEY_PREFIX } from '../AuthGuard/index.js';
1919
import FullScreenLoader from './LoadingScreen.js';
2020

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

2325
interface AuthContextDataWrapper {
2426
isLoggedIn: boolean;
@@ -28,6 +30,7 @@ interface AuthContextDataWrapper {
2830
getToken: CallableFunction;
2931
logoutCallback: CallableFunction;
3032
getApiToken: CallableFunction;
33+
setLoginStatus: CallableFunction;
3134
}
3235

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

5457
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
5558
const { instance, inProgress, accounts } = useMsal();
56-
5759
const [userData, setUserData] = useState<AuthContextData | null>(null);
5860
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
5961

@@ -67,11 +69,9 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
6769
if (response) {
6870
handleMsalResponse(response);
6971
} else if (accounts.length > 0) {
70-
// User is already logged in, set the state
71-
const [lastName, firstName] = accounts[0].name?.split(',') || [];
7272
setUserData({
7373
email: accounts[0].username,
74-
name: `${firstName} ${lastName}`,
74+
name: transformCommaSeperatedName(accounts[0].name || ''),
7575
});
7676
setIsLoggedIn(true);
7777
}
@@ -94,29 +94,26 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
9494
})
9595
.then((silentResponse) => {
9696
if (silentResponse?.account?.name) {
97-
const [lastName, firstName] = silentResponse.account.name.split(',');
9897
setUserData({
99-
email: silentResponse.account.username,
100-
name: `${firstName} ${lastName}`,
98+
email: accounts[0].username,
99+
name: transformCommaSeperatedName(accounts[0].name || ''),
101100
});
102101
setIsLoggedIn(true);
103102
}
104103
})
105104
.catch(console.error);
106105
return;
107106
}
108-
109-
// Use response.account instead of accounts[0]
110-
const [lastName, firstName] = response.account.name?.split(',') || [];
111107
setUserData({
112-
email: response.account.username,
113-
name: `${firstName} ${lastName}`,
108+
email: accounts[0].username,
109+
name: transformCommaSeperatedName(accounts[0].name || ''),
114110
});
115111
setIsLoggedIn(true);
116112
}
117113
},
118114
[accounts, instance]
119115
);
116+
120117
const getApiToken = useCallback(
121118
async (service: ValidServices) => {
122119
if (!userData) {
@@ -194,6 +191,9 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
194191
},
195192
[instance]
196193
);
194+
const setLoginStatus = useCallback((val: boolean) => {
195+
setIsLoggedIn(val);
196+
}, []);
197197

198198
const logout = useCallback(async () => {
199199
try {
@@ -209,7 +209,16 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
209209
};
210210
return (
211211
<AuthContext.Provider
212-
value={{ isLoggedIn, userData, loginMsal, logout, getToken, logoutCallback, getApiToken }}
212+
value={{
213+
isLoggedIn,
214+
userData,
215+
setLoginStatus,
216+
loginMsal,
217+
logout,
218+
getToken,
219+
logoutCallback,
220+
getApiToken,
221+
}}
213222
>
214223
{inProgress !== InteractionStatus.None ? (
215224
<MantineProvider>

src/ui/components/AuthGuard/index.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Card, Text, Title } from '@mantine/core';
22
import React, { ReactNode, useEffect, useState } from 'react';
33

4-
import { AcmAppShell } from '@ui/components/AppShell';
4+
import { AcmAppShell, AcmAppShellProps } from '@ui/components/AppShell';
55
import FullScreenLoader from '@ui/components/AuthContext/LoadingScreen';
66
import { getRunEnvironmentConfig, ValidService } from '@ui/config';
77
import { useApi } from '@ui/util/api';
@@ -60,11 +60,13 @@ export const clearAuthCache = () => {
6060
}
6161
};
6262

63-
export const AuthGuard: React.FC<{
64-
resourceDef: ResourceDefinition;
65-
children: ReactNode;
66-
isAppShell?: boolean;
67-
}> = ({ resourceDef, children, isAppShell = true }) => {
63+
export const AuthGuard: React.FC<
64+
{
65+
resourceDef: ResourceDefinition;
66+
children: ReactNode;
67+
isAppShell?: boolean;
68+
} & AcmAppShellProps
69+
> = ({ resourceDef, children, isAppShell = true, ...appShellProps }) => {
6870
const { service, validRoles } = resourceDef;
6971
const { baseEndpoint, authCheckRoute, friendlyName } =
7072
getRunEnvironmentConfig().ServiceConfiguration[service];
@@ -80,6 +82,10 @@ export const AuthGuard: React.FC<{
8082
setIsAuthenticated(true);
8183
return;
8284
}
85+
if (validRoles.length === 0) {
86+
setIsAuthenticated(true);
87+
return;
88+
}
8389

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

165171
if (isAppShell) {
166-
return (
167-
<AcmAppShell>
168-
<Title order={1}>{friendlyName}</Title>
169-
{children}
170-
</AcmAppShell>
171-
);
172+
return <AcmAppShell {...appShellProps}>{children}</AcmAppShell>;
172173
}
173174

174175
return <>{children}</>;

src/ui/components/ProfileDropdown/index.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { useState } from 'react';
1818

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

2223
interface ProfileDropdownProps {
2324
userData?: AuthContextData;
@@ -26,6 +27,7 @@ interface ProfileDropdownProps {
2627
const AuthenticatedProfileDropdown: React.FC<ProfileDropdownProps> = ({ userData }) => {
2728
const [opened, setOpened] = useState(false);
2829
const theme = useMantineTheme();
30+
const navigate = useNavigate();
2931
const { logout } = useAuth();
3032
if (!userData) {
3133
return null;
@@ -111,6 +113,16 @@ const AuthenticatedProfileDropdown: React.FC<ProfileDropdownProps> = ({ userData
111113
</Group>
112114
</UnstyledButton>
113115
<Divider my="sm" />
116+
<Button
117+
variant="primary"
118+
mb="sm"
119+
fullWidth
120+
onClick={() => {
121+
navigate('/profile');
122+
}}
123+
>
124+
Edit Profile
125+
</Button>
114126
<Button
115127
variant="outline"
116128
fullWidth

src/ui/config.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { execCouncilGroupId, execCouncilTestingGroupId } from '@common/config';
33
export const runEnvironments = ['dev', 'prod', 'local-dev'] as const;
44
// local dev should be used when you want to test against a local instance of the API
55

6-
export const services = ['core', 'tickets', 'merch'] as const;
6+
export const services = ['core', 'tickets', 'merch', 'msGraphApi'] as const;
77
export type RunEnvironment = (typeof runEnvironments)[number];
88
export type ValidServices = (typeof services)[number];
99
export type ValidService = ValidServices;
@@ -49,6 +49,12 @@ const environmentConfig: EnvironmentConfigType = {
4949
friendlyName: 'Merch Sales Service (Prod)',
5050
baseEndpoint: 'https://merchapi.acm.illinois.edu',
5151
},
52+
msGraphApi: {
53+
friendlyName: 'Microsoft Graph API',
54+
baseEndpoint: 'https://graph.microsoft.com',
55+
loginScope: 'https://graph.microsoft.com/.default',
56+
apiId: 'https://graph.microsoft.com',
57+
},
5258
},
5359
KnownGroupMappings: {
5460
Exec: execCouncilTestingGroupId,
@@ -72,6 +78,12 @@ const environmentConfig: EnvironmentConfigType = {
7278
friendlyName: 'Merch Sales Service (Prod)',
7379
baseEndpoint: 'https://merchapi.acm.illinois.edu',
7480
},
81+
msGraphApi: {
82+
friendlyName: 'Microsoft Graph API',
83+
baseEndpoint: 'https://graph.microsoft.com',
84+
loginScope: 'https://graph.microsoft.com/.default',
85+
apiId: 'https://graph.microsoft.com',
86+
},
7587
},
7688
KnownGroupMappings: {
7789
Exec: execCouncilTestingGroupId,
@@ -95,6 +107,12 @@ const environmentConfig: EnvironmentConfigType = {
95107
friendlyName: 'Merch Sales Service',
96108
baseEndpoint: 'https://merchapi.acm.illinois.edu',
97109
},
110+
msGraphApi: {
111+
friendlyName: 'Microsoft Graph API',
112+
baseEndpoint: 'https://graph.microsoft.com',
113+
loginScope: 'https://graph.microsoft.com/.default',
114+
apiId: 'https://graph.microsoft.com',
115+
},
98116
},
99117
KnownGroupMappings: {
100118
Exec: execCouncilGroupId,

0 commit comments

Comments
 (0)