diff --git a/src/frontend/declarations.d.ts b/src/frontend/declarations.d.ts index 00f9c47c..e0fe54d7 100644 --- a/src/frontend/declarations.d.ts +++ b/src/frontend/declarations.d.ts @@ -2,4 +2,16 @@ declare module "*.png" { const value: string; export default value; } - \ No newline at end of file + +interface AppConfig { + API_URL?: string; + REACT_APP_MSAL_AUTH_CLIENTID?: string; + REACT_APP_MSAL_AUTH_AUTHORITY?: string; + REACT_APP_MSAL_REDIRECT_URL?: string; + REACT_APP_MSAL_POST_REDIRECT_URL?: string; + ENABLE_AUTH?: boolean; +} + +interface Window { + appConfig?: AppConfig; +} diff --git a/src/frontend/src/components/Header/Header.tsx b/src/frontend/src/components/Header/Header.tsx index baccc3bc..b78ff168 100644 --- a/src/frontend/src/components/Header/Header.tsx +++ b/src/frontend/src/components/Header/Header.tsx @@ -1,5 +1,7 @@ import React from "react"; import { Subtitle2 } from "@fluentui/react-components"; +import UserProfile from "./UserProfile"; + /** * @component * @name Header @@ -16,7 +18,19 @@ type HeaderProps = { children?: React.ReactNode; }; +// Determine once whether MSAL authentication is enabled, so the hooks inside +// UserProfile (which require MsalProvider in the tree) are only mounted when safe. +// window.appConfig is set in main.jsx after fetching /config; falls back to +// false when the config has not loaded or auth is disabled. +const isAuthEnabled = (): boolean => { + if (typeof window === "undefined") return false; + return Boolean(window.appConfig && window.appConfig.ENABLE_AUTH); +}; + const Header: React.FC = ({ title = "Contoso", subtitle, children }) => { + const authEnabled = isAuthEnabled(); + const hasToolbarContent = React.Children.count(children) > 0 || authEnabled; + return (
= ({ title = "Contoso", subtitle, children } - {/* HEADER TOOLBAR (rendered only if passed as a child) */} - {children} + {/* HEADER TOOLBAR (rendered only when there is toolbar content + or the auth-enabled user profile menu to display) */} + {hasToolbarContent && ( +
+ {children} + {authEnabled && } +
+ )}
); }; diff --git a/src/frontend/src/components/Header/UserProfile.tsx b/src/frontend/src/components/Header/UserProfile.tsx new file mode 100644 index 00000000..50be2d49 --- /dev/null +++ b/src/frontend/src/components/Header/UserProfile.tsx @@ -0,0 +1,136 @@ +import React from "react"; +import { + Avatar, + Button, + Menu, + MenuTrigger, + MenuPopover, + MenuList, + MenuItem, + MenuDivider, + Tooltip, +} from "@fluentui/react-components"; +import { + Person20Regular, + SignOut20Regular, +} from "@fluentui/react-icons"; +import useAuth from "../../msal-auth/useAuth"; + +/** + * @component UserProfile + * @description Renders an avatar in the header. Clicking opens a menu showing + * the signed-in user's name and email along with a Logout option. + * Designed to be rendered only when MSAL authentication is enabled. + */ +const getInitials = (name?: string, username?: string): string => { + const source = name && name.trim().length > 0 ? name : username || ""; + if (!source) return "U"; + + if (source.includes("@")) { + const prefix = source.split("@")[0]; + const parts = prefix.split(/[._-]/).filter(Boolean); + if (parts.length >= 2) { + return (parts[0][0] + parts[1][0]).toUpperCase(); + } + return prefix.substring(0, 2).toUpperCase(); + } + + return source + .split(" ") + .filter(Boolean) + .map((n) => n[0]) + .join("") + .toUpperCase() + .slice(0, 2); +}; + +const UserProfile: React.FC = () => { + const { isAuthenticated, accounts, logout } = useAuth(); + + if (!isAuthenticated || !accounts || accounts.length === 0) { + return null; + } + + const account = accounts[0]; + const name = account?.name || account?.username || "User"; + const email = account?.username || ""; + const initials = getInitials(account?.name, account?.username); + + const handleLogout = (e: React.MouseEvent) => { + e.stopPropagation(); + logout(); + }; + + return ( + + + + + + + + + } + disabled + style={{ cursor: "default", opacity: 1 }} + > +
+ + {name} + + {email && ( + + {email} + + )} +
+
+ + } onClick={handleLogout}> + Logout + +
+
+
+ ); +}; + +export default UserProfile;