diff --git a/src/App/src/App.tsx b/src/App/src/App.tsx
index 8c28e0de1..8720b5414 100644
--- a/src/App/src/App.tsx
+++ b/src/App/src/App.tsx
@@ -14,7 +14,7 @@ function App() {
const dispatch = useAppDispatch();
// Select state from Redux store
- const { userName, imageGenerationEnabled, showChatHistory } = useAppSelector(state => state.app);
+ const { imageGenerationEnabled, showChatHistory } = useAppSelector(state => state.app);
const { conversationId, conversationTitle, messages, isLoading, generationStatus, historyRefreshTrigger } = useAppSelector(state => state.chat);
const { pendingBrief, confirmedBrief, selectedProducts, availableProducts, generatedContent } = useAppSelector(state => state.content);
@@ -44,7 +44,6 @@ function App() {
{/* Header */}
dispatch(toggleChatHistory())}
/>
diff --git a/src/App/src/components/AppHeader.tsx b/src/App/src/components/AppHeader.tsx
index dd7205737..1bc00d24f 100644
--- a/src/App/src/components/AppHeader.tsx
+++ b/src/App/src/components/AppHeader.tsx
@@ -6,7 +6,6 @@
import React from 'react';
import {
Text,
- Avatar,
Button,
Tooltip,
tokens,
@@ -16,15 +15,14 @@ import {
History24Filled,
} from '@fluentui/react-icons';
import ContosoLogo from '../styles/images/contoso.svg';
+import LoginButton from './LoginButton';
interface AppHeaderProps {
- userName: string;
showChatHistory: boolean;
onToggleChatHistory: () => void;
}
export const AppHeader = React.memo(function AppHeader({
- userName,
showChatHistory,
onToggleChatHistory,
}: AppHeaderProps) {
@@ -53,11 +51,7 @@ export const AppHeader = React.memo(function AppHeader({
aria-label={showChatHistory ? 'Hide chat history' : 'Show chat history'}
/>
-
+
);
diff --git a/src/App/src/components/LoginButton.tsx b/src/App/src/components/LoginButton.tsx
new file mode 100644
index 000000000..0753b6a44
--- /dev/null
+++ b/src/App/src/components/LoginButton.tsx
@@ -0,0 +1,133 @@
+import React, { useCallback } from 'react';
+import {
+ Avatar,
+ Menu,
+ MenuTrigger,
+ MenuPopover,
+ MenuList,
+ MenuItem,
+ Button,
+ makeStyles,
+ tokens,
+} from '@fluentui/react-components';
+import { Person20Regular, SignOut24Regular } from '@fluentui/react-icons';
+import { useAppSelector } from '../store/hooks';
+
+const useStyles = makeStyles({
+ userButton: {
+ minWidth: 'auto',
+ paddingLeft: tokens.spacingHorizontalXS,
+ paddingRight: tokens.spacingHorizontalXS,
+ },
+ menuItem: {
+ paddingLeft: tokens.spacingHorizontalM,
+ paddingRight: tokens.spacingHorizontalM,
+ },
+ userInfo: {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'flex-start',
+ gap: tokens.spacingVerticalXXS,
+ },
+ userName: {
+ fontWeight: tokens.fontWeightSemibold,
+ fontSize: tokens.fontSizeBase200,
+ },
+ userEmail: {
+ fontSize: tokens.fontSizeBase100,
+ color: tokens.colorNeutralForeground2,
+ },
+});
+
+const getUserInitials = (name: string | undefined): string => {
+ if (!name) return 'U';
+ const cleanName = name.replace(/\s*\([^)]*\)/g, '').trim();
+ if (!cleanName) return 'U';
+ const parts = cleanName.split(/\s+/).filter(Boolean);
+ if (parts.length >= 2) {
+ return (parts[0][0] + parts[1][0]).toUpperCase();
+ }
+ return cleanName.charAt(0).toUpperCase();
+};
+
+const LoginButton: React.FC = () => {
+ const styles = useStyles();
+ const userName = useAppSelector(state => state.app.userName);
+ const userId = useAppSelector(state => state.app.userId);
+ const userEmail = useAppSelector(state => state.app.userEmail);
+ const isAuthenticated = Boolean(userId && userId !== 'anonymous');
+
+ const login = useCallback(() => {
+ window.location.href = '/.auth/login/aad';
+ }, []);
+
+ const logout = useCallback(() => {
+ const logoutUrl = '/.auth/logout?post_logout_redirect_uri=' + encodeURIComponent('/');
+ window.location.href = logoutUrl;
+ }, []);
+
+ const displayName = isAuthenticated ? userName || userId || 'User' : 'User';
+
+ if (!isAuthenticated) {
+ return (
+
+ }
+ />
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default LoginButton;
diff --git a/src/App/src/store/appSlice.ts b/src/App/src/store/appSlice.ts
index ebf882e79..f5e826a74 100644
--- a/src/App/src/store/appSlice.ts
+++ b/src/App/src/store/appSlice.ts
@@ -11,6 +11,7 @@ import { httpClient } from '../utils/httpClient';
interface AppState {
userId: string;
userName: string;
+ userEmail: string;
imageGenerationEnabled: boolean;
showChatHistory: boolean;
}
@@ -18,6 +19,7 @@ interface AppState {
const initialState: AppState = {
userId: '',
userName: '',
+ userEmail: '',
imageGenerationEnabled: true,
showChatHistory: true,
};
@@ -30,13 +32,15 @@ export const fetchAppConfig = createAsyncThunk(
}
);
+type AuthClaim = { typ: string; val: string };
+type AuthPayload = Array<{ user_id: string; user_claims: AuthClaim[] }>;
+
export const fetchCurrentUser = createAsyncThunk(
'app/fetchCurrentUser',
async () => {
try {
- const payload = await httpClient.fetchExternal;
- }>>('/.auth/me');
+ const payload = await httpClient.fetchExternal('/.auth/me');
+
const userClaims = payload[0]?.user_claims || [];
const objectIdClaim = userClaims.find(
(claim) =>
@@ -45,12 +49,25 @@ export const fetchCurrentUser = createAsyncThunk(
const nameClaim = userClaims.find(
(claim) => claim.typ === 'name'
);
+
+ let emailVal = '';
+ for (const claim of userClaims) {
+ if (claim.typ === 'preferred_username' ||
+ claim.typ === 'email' ||
+ claim.typ === 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress' ||
+ claim.typ === 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn') {
+ emailVal = claim.val;
+ break;
+ }
+ }
+
return {
- userId: objectIdClaim?.val || 'anonymous',
+ userId: objectIdClaim?.val || payload[0]?.user_id || 'anonymous',
userName: nameClaim?.val || '',
+ userEmail: emailVal,
};
} catch {
- return { userId: 'anonymous', userName: '' };
+ return { userId: 'anonymous', userName: '', userEmail: '' };
}
}
);
@@ -75,10 +92,12 @@ const appSlice = createSlice({
.addCase(fetchCurrentUser.fulfilled, (state, action) => {
state.userId = action.payload.userId;
state.userName = action.payload.userName;
+ state.userEmail = action.payload.userEmail;
})
.addCase(fetchCurrentUser.rejected, (state) => {
state.userId = 'anonymous';
state.userName = '';
+ state.userEmail = '';
});
},
});
diff --git a/src/App/src/utils/httpClient.ts b/src/App/src/utils/httpClient.ts
index 0acadaaaa..a35dd9400 100644
--- a/src/App/src/utils/httpClient.ts
+++ b/src/App/src/utils/httpClient.ts
@@ -69,7 +69,7 @@ class HttpClient {
throw new Error(`${config.method || 'GET'} ${url} failed: ${response.statusText}`);
}
const contentType = response.headers.get('content-type') || '';
- if (!contentType.toLowerCase().includes('application/json')) {
+ if (!contentType.toLowerCase().includes('json')) {
throw new Error(`${config.method || 'GET'} ${url} returned non-JSON response (content-type: ${contentType || 'unknown'})`);
}
return response.json();