Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
3 changes: 1 addition & 2 deletions src/App/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -44,7 +44,6 @@ function App() {
<div className="app-container">
{/* Header */}
<AppHeader
userName={userName}
showChatHistory={showChatHistory}
onToggleChatHistory={() => dispatch(toggleChatHistory())}
/>
Comment thread
Akhileswara-Microsoft marked this conversation as resolved.
Expand Down
10 changes: 2 additions & 8 deletions src/App/src/components/AppHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import React from 'react';
import {
Text,
Avatar,
Button,
Tooltip,
tokens,
Expand All @@ -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) {
Expand Down Expand Up @@ -53,11 +51,7 @@ export const AppHeader = React.memo(function AppHeader({
aria-label={showChatHistory ? 'Hide chat history' : 'Show chat history'}
/>
</Tooltip>
<Avatar
name={userName || undefined}
color="colorful"
size={36}
/>
<LoginButton/>
</div>
Comment thread
Akhileswara-Microsoft marked this conversation as resolved.
</header>
);
Expand Down
133 changes: 133 additions & 0 deletions src/App/src/components/LoginButton.tsx
Original file line number Diff line number Diff line change
@@ -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(' ');
if (parts.length >= 2) {
return (parts[0][0] + parts[1][0]).toUpperCase();
}
return cleanName.charAt(0).toUpperCase();
Comment thread
Akhileswara-Microsoft marked this conversation as resolved.
};
Comment thread
Akhileswara-Microsoft marked this conversation as resolved.

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 = userName || userId || 'User';
Comment thread
Akhileswara-Microsoft marked this conversation as resolved.
Outdated

if (!isAuthenticated) {
return (
<Button
appearance="subtle"
className={styles.userButton}
onClick={login}
title="Sign in"
Comment thread
Akhileswara-Microsoft marked this conversation as resolved.
aria-label="Sign in"
icon={
<Avatar
name={displayName}
initials={getUserInitials(displayName)}
size={28}
color="colorful"
style={{ fontWeight: 'bold' }}
/>
}
/>
);
}

return (
<Menu>
<MenuTrigger disableButtonEnhancement>
<Button
appearance="subtle"
className={styles.userButton}
title={`Signed in as ${displayName}`}
Comment thread
Akhileswara-Microsoft marked this conversation as resolved.
aria-label={`User menu for ${displayName}`}
icon={
<Avatar
name={displayName}
initials={getUserInitials(displayName)}
size={28}
color="colorful"
style={{ fontWeight: 'bold' }}
/>
}
/>
</MenuTrigger>
Comment thread
Akhileswara-Microsoft marked this conversation as resolved.

<MenuPopover>
<MenuList>
<MenuItem className={styles.menuItem} icon={<Person20Regular />} disabled style={{ cursor: 'default' }}>
<div className={styles.userInfo}>
<div className={styles.userName}>{displayName}</div>
{userEmail && <div className={styles.userEmail}>{userEmail}</div>}
</div>
</MenuItem>
<MenuItem
className={styles.menuItem}
icon={<SignOut24Regular />}
onClick={logout}
>
Sign out
</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
);
};

export default LoginButton;
34 changes: 28 additions & 6 deletions src/App/src/store/appSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import { httpClient } from '../utils/httpClient';
interface AppState {
userId: string;
userName: string;
userEmail: string;
imageGenerationEnabled: boolean;
showChatHistory: boolean;
}

const initialState: AppState = {
userId: '',
userName: '',
userEmail: '',
imageGenerationEnabled: true,
showChatHistory: true,
};
Expand All @@ -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<Array<{
user_claims?: Array<{ typ: string; val: string }>;
}>>('/.auth/me');
const payload = await httpClient.fetchExternal<AuthPayload>('/.auth/me');

const userClaims = payload[0]?.user_claims || [];
const objectIdClaim = userClaims.find(
(claim) =>
Expand All @@ -45,12 +49,28 @@ export const fetchCurrentUser = createAsyncThunk(
const nameClaim = userClaims.find(
(claim) => claim.typ === 'name'
);
return {

// Search each email claim type individually for reliability
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;
}
}

const userData = {
userId: objectIdClaim?.val || 'anonymous',
userName: nameClaim?.val || '',
userEmail: emailVal,
};
} catch {
return { userId: 'anonymous', userName: '' };

return userData;
} catch (error) {
return { userId: 'anonymous', userName: '', userEmail: '' };
}
}
);
Expand All @@ -75,10 +95,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 = '';
});
},
});
Expand Down