Skip to content
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
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(/\s+/).filter(Boolean);
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 = isAuthenticated ? userName || userId || 'User' : 'User';

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;
29 changes: 24 additions & 5 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,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: '' };
}
}
);
Expand All @@ -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 = '';
});
},
});
Expand Down
2 changes: 1 addition & 1 deletion src/App/src/utils/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down