diff --git a/.github/workflows/validate-bicep-params.yml b/.github/workflows/validate-bicep-params.yml
index bcb03f097..656299974 100644
--- a/.github/workflows/validate-bicep-params.yml
+++ b/.github/workflows/validate-bicep-params.yml
@@ -34,9 +34,16 @@ jobs:
- name: Validate infra/ parameters
id: validate_infra
continue-on-error: true
+ env:
+ ACCELERATOR_NAME: ${{ env.accelerator_name }}
run: |
set +e
- python infra/scripts/validate_bicep_params.py --dir infra --strict --no-color --json-output infra_results.json 2>&1 | tee infra_output.txt
+ RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
+ python infra/scripts/validate_bicep_params.py --dir infra --strict --no-color \
+ --json-output infra_results.json \
+ --html-output email_body.html \
+ --accelerator-name "${ACCELERATOR_NAME}" \
+ --run-url "${RUN_URL}" 2>&1 | tee infra_output.txt
EXIT_CODE=${PIPESTATUS[0]}
set -e
echo "## Infra Param Validation" >> "$GITHUB_STEP_SUMMARY"
@@ -61,24 +68,25 @@ jobs:
name: bicep-validation-results
path: |
infra_results.json
+ email_body.html
retention-days: 30
- name: Send schedule notification on failure
if: github.event_name == 'schedule' && steps.result.outputs.status == 'failure'
env:
LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}
- GITHUB_REPOSITORY: ${{ github.repository }}
- GITHUB_RUN_ID: ${{ github.run_id }}
ACCELERATOR_NAME: ${{ env.accelerator_name }}
run: |
- RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
- INFRA_OUTPUT=$(sed 's/&/\&/g; s/\</g; s/>/\>/g' infra_output.txt)
+ if [ -f email_body.html ]; then
+ EMAIL_BODY=$(cat email_body.html)
+ else
+ EMAIL_BODY="
Bicep parameter validation failed but no HTML report was generated. Check workflow logs for details.
"
+ fi
jq -n \
--arg name "${ACCELERATOR_NAME}" \
- --arg infra "$INFRA_OUTPUT" \
- --arg url "$RUN_URL" \
- '{subject: ("Bicep Parameter Validation Report - " + $name + " - Issues Detected"), body: ("Dear Team,
The scheduled Bicep Parameter Validation for " + $name + " has detected parameter mapping errors.
infra/ Results:
" + $infra + " Run URL: " + $url + "
Please fix the parameter mapping issues at your earliest convenience.
Best regards, Your Automation Team
")}' \
+ --arg body "$EMAIL_BODY" \
+ '{subject: ("Bicep Parameter Validation Report - " + $name + " - Issues Detected"), body: $body}' \
| curl -X POST "${LOGICAPP_URL}" \
-H "Content-Type: application/json" \
-d @- || echo "Failed to send notification"
@@ -87,18 +95,18 @@ jobs:
if: github.event_name == 'schedule' && steps.result.outputs.status == 'success'
env:
LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}
- GITHUB_REPOSITORY: ${{ github.repository }}
- GITHUB_RUN_ID: ${{ github.run_id }}
ACCELERATOR_NAME: ${{ env.accelerator_name }}
run: |
- RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
- INFRA_OUTPUT=$(sed 's/&/\&/g; s/\</g; s/>/\>/g' infra_output.txt)
+ if [ -f email_body.html ]; then
+ EMAIL_BODY=$(cat email_body.html)
+ else
+ EMAIL_BODY="Bicep parameter validation passed but no HTML report was generated (no parameter file pairs found).
"
+ fi
jq -n \
--arg name "${ACCELERATOR_NAME}" \
- --arg infra "$INFRA_OUTPUT" \
- --arg url "$RUN_URL" \
- '{subject: ("Bicep Parameter Validation Report - " + $name + " - Passed"), body: ("Dear Team,
The scheduled Bicep Parameter Validation for " + $name + " has completed successfully. All parameter mappings are valid.
infra/ Results:
" + $infra + " Run URL: " + $url + "
Best regards, Your Automation Team
")}' \
+ --arg body "$EMAIL_BODY" \
+ '{subject: ("Bicep Parameter Validation Report - " + $name + " - Passed"), body: $body}' \
| curl -X POST "${LOGICAPP_URL}" \
-H "Content-Type: application/json" \
-d @- || echo "Failed to send notification"
diff --git a/docs/QuotaCheck.md b/docs/QuotaCheck.md
index 3f73376a3..f67f4f5ad 100644
--- a/docs/QuotaCheck.md
+++ b/docs/QuotaCheck.md
@@ -72,7 +72,7 @@ The final table lists regions with available quota. You can select any of these
**To check quota for the deployment**
```sh
- curl -L -o quota_check_params.sh "https://raw.githubusercontent.com/microsoft/content-generation-solution-accelerator/main/content-gen/infra/scripts/quota_check_params.sh"
+ curl -L -o quota_check_params.sh "https://raw.githubusercontent.com/microsoft/content-generation-solution-accelerator/main/infra/scripts/quota_check_params.sh"
chmod +x quota_check_params.sh
./quota_check_params.sh
```
diff --git a/docs/create_new_app_registration.md b/docs/create_new_app_registration.md
index 7dcf2c402..732f0aa75 100644
--- a/docs/create_new_app_registration.md
+++ b/docs/create_new_app_registration.md
@@ -20,7 +20,7 @@

-6. Click on `+ Add a platform`.
+6. Click on `+ Add Redirect URI`.

diff --git a/docs/images/AddDetails.png b/docs/images/AddDetails.png
index f36b596f2..f5946c6db 100644
Binary files a/docs/images/AddDetails.png and b/docs/images/AddDetails.png differ
diff --git a/docs/images/AddPlatform.png b/docs/images/AddPlatform.png
index 6c74919b4..2424f2a8f 100644
Binary files a/docs/images/AddPlatform.png and b/docs/images/AddPlatform.png differ
diff --git a/docs/images/Web.png b/docs/images/Web.png
index 35f846453..d997cbd3a 100644
Binary files a/docs/images/Web.png and b/docs/images/Web.png differ
diff --git a/infra/scripts/validate_bicep_params.py b/infra/scripts/validate_bicep_params.py
index 85df7d149..4fd76a293 100644
--- a/infra/scripts/validate_bicep_params.py
+++ b/infra/scripts/validate_bicep_params.py
@@ -340,6 +340,246 @@ def print_report(results: list[ValidationResult], *, use_color: bool = True) ->
print(f"{c['ERROR']}Parameter mapping issues detected!{c['RESET']}")
+# ---------------------------------------------------------------------------
+# HTML email report
+# ---------------------------------------------------------------------------
+
+def _html_escape(text: str) -> str:
+ """Escape HTML special characters."""
+ return (
+ text.replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace('"', """)
+ )
+
+
+def generate_html_report(
+ results: list[ValidationResult],
+ *,
+ accelerator_name: str = "",
+ run_url: str = "",
+ scan_dir: str = "",
+) -> str:
+ """Build a structured HTML email body from validation results."""
+ total_errors = sum(
+ 1 for r in results for i in r.issues if i.severity == "ERROR"
+ )
+ total_warnings = sum(
+ 1 for r in results for i in r.issues if i.severity == "WARNING"
+ )
+ has_errors = total_errors > 0
+ overall_status = "Issues Detected" if has_errors else "Passed"
+ status_color = "#D32F2F" if has_errors else "#2E7D32"
+ status_bg = "#FFEBEE" if has_errors else "#E8F5E9"
+ status_icon = "❌" if has_errors else "✅"
+
+ parts: list[str] = []
+
+ # --- Document wrapper (Outlook-compatible, no gradient/border-radius/box-shadow) ---
+ parts.append(
+ ' '
+ ''
+ ''
+ ''
+ ''
+ )
+
+ # --- Header banner (solid color, Outlook-safe) ---
+ parts.append(
+ f''
+ f''
+ f'Bicep Parameter Validation Report '
+ f''
+ f'{_html_escape(accelerator_name) if accelerator_name else "Accelerator"}'
+ f' — Automated Check
'
+ f' '
+ )
+
+ # --- Summary card ---
+ parts.append(
+ f''
+ f''
+ f''
+ f''
+ f'{status_icon} Overall Status: {overall_status} '
+ f' '
+ f''
+ f''
+ )
+ # Accelerator name pill
+ if accelerator_name:
+ parts.append(
+ f''
+ f'Accelerator '
+ f'{_html_escape(accelerator_name)}'
+ f' '
+ )
+ # Scan directory pill
+ if scan_dir:
+ parts.append(
+ f''
+ f'Scan Directory '
+ f'{_html_escape(scan_dir)}/ '
+ f' '
+ )
+ # Error count pill
+ err_pill_color = "#D32F2F" if total_errors > 0 else "#2E7D32"
+ parts.append(
+ f''
+ f'Errors '
+ f''
+ f'{total_errors} '
+ )
+ # Warning count pill
+ warn_pill_color = "#F57C00" if total_warnings > 0 else "#2E7D32"
+ parts.append(
+ f''
+ f'Warnings '
+ f''
+ f'{total_warnings} '
+ )
+ parts.append("
")
+
+ # --- Per-pair detail sections ---
+ parts.append('')
+ for r in results:
+ errors = [i for i in r.issues if i.severity == "ERROR"]
+ warnings = [i for i in r.issues if i.severity == "WARNING"]
+
+ if not r.issues:
+ badge = (
+ 'PASS '
+ )
+ elif errors:
+ badge = (
+ 'FAIL '
+ )
+ else:
+ badge = (
+ 'WARN '
+ )
+
+ parts.append(
+ f''
+ f''
+ f'{badge} '
+ f''
+ f'{_html_escape(r.pair)} '
+ f''
+ f'{len(errors)} error(s), {len(warnings)} warning(s) '
+ f' '
+ )
+
+ if r.issues:
+ # --- Errors section ---
+ if errors:
+ parts.append(
+ ''
+ ''
+ '● Errors '
+ ''
+ ''
+ ''
+ 'Parameter '
+ 'Details '
+ )
+ for idx, issue in enumerate(errors):
+ bg = "#ffffff" if idx % 2 == 0 else "#fff5f5"
+ parts.append(
+ f''
+ f''
+ f'{_html_escape(issue.param_name)} '
+ f'{_html_escape(issue.message)} '
+ f' '
+ )
+ parts.append("
")
+
+ # --- Warnings section ---
+ if warnings:
+ parts.append(
+ ''
+ ''
+ '● Warnings '
+ ''
+ ''
+ ''
+ 'Parameter '
+ 'Details '
+ )
+ for idx, issue in enumerate(warnings):
+ bg = "#ffffff" if idx % 2 == 0 else "#fffaf0"
+ parts.append(
+ f''
+ f''
+ f'{_html_escape(issue.param_name)} '
+ f'{_html_escape(issue.message)} '
+ f' '
+ )
+ parts.append("
")
+ else:
+ parts.append(
+ 'All parameters validated successfully.'
+ ' '
+ )
+
+ parts.append("
")
+
+ parts.append(" ")
+
+ # --- Footer with run URL ---
+ footer_parts: list[str] = []
+ if run_url:
+ footer_parts.append(
+ f'View Workflow Run '
+ )
+ if has_errors:
+ footer_parts.append(
+ ''
+ 'Please fix the parameter mapping issues at your earliest convenience.
'
+ )
+ footer_parts.append(
+ ''
+ 'Best regards, Your Automation Team
'
+ )
+ parts.append(
+ f''
+ f'{"".join(footer_parts)} '
+ )
+
+ # --- Close wrapper ---
+ parts.append("
")
+ return "".join(parts)
+
+
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
@@ -378,6 +618,23 @@ def main() -> int:
type=Path,
help="Write results as JSON to the given file path.",
)
+ parser.add_argument(
+ "--html-output",
+ type=Path,
+ help="Write a structured HTML email report to the given file path.",
+ )
+ parser.add_argument(
+ "--accelerator-name",
+ type=str,
+ default="",
+ help="Accelerator display name for the HTML report header.",
+ )
+ parser.add_argument(
+ "--run-url",
+ type=str,
+ default="",
+ help="Workflow run URL to include in the HTML report footer.",
+ )
args = parser.parse_args()
results: list[ValidationResult] = []
@@ -388,6 +645,23 @@ def main() -> int:
pairs = discover_pairs(args.dir)
if not pairs:
print(f"No (bicep, parameters.json) pairs found under {args.dir}")
+ # Generate empty reports so downstream workflow steps (e.g. cat
+ # email_body.html) do not fail when no pairs are discovered.
+ if args.json_output:
+ args.json_output.parent.mkdir(parents=True, exist_ok=True)
+ args.json_output.write_text(
+ json.dumps([], indent=2), encoding="utf-8"
+ )
+ if args.html_output:
+ scan_dir = str(args.dir) if args.dir else ""
+ html = generate_html_report(
+ [],
+ accelerator_name=args.accelerator_name,
+ run_url=args.run_url,
+ scan_dir=scan_dir,
+ )
+ args.html_output.parent.mkdir(parents=True, exist_ok=True)
+ args.html_output.write_text(html, encoding="utf-8")
return 0
for bicep_path, params_path in pairs:
results.append(validate_pair(bicep_path, params_path))
@@ -414,6 +688,19 @@ def main() -> int:
)
print(f"\nJSON report written to {args.json_output}")
+ # Optional HTML email report
+ if args.html_output:
+ scan_dir = str(args.dir) if args.dir else ""
+ html = generate_html_report(
+ results,
+ accelerator_name=args.accelerator_name,
+ run_url=args.run_url,
+ scan_dir=scan_dir,
+ )
+ args.html_output.parent.mkdir(parents=True, exist_ok=True)
+ args.html_output.write_text(html, encoding="utf-8")
+ print(f"HTML report written to {args.html_output}")
+
has_errors = any(r.has_errors for r in results)
return 1 if args.strict and has_errors else 0
diff --git a/scripts/post_deploy.py b/scripts/post_deploy.py
index 202d7ebeb..72e8d0a67 100644
--- a/scripts/post_deploy.py
+++ b/scripts/post_deploy.py
@@ -244,8 +244,8 @@ def get_api_headers(config: ResourceConfig) -> Dict[str, str]:
return headers
-async def check_admin_api_health(config: ResourceConfig, max_retries: int = 5, retry_delay: int = 10) -> bool:
- """Check if the admin API is available with retry logic for cold starts."""
+async def check_admin_api_health(config: ResourceConfig, max_retries: int = 8, retry_delay: int = 20) -> bool:
+ """Check if the admin API is available with retry logic for cold starts and container restarts."""
print_step("Checking admin API health...")
async with httpx.AsyncClient(timeout=30.0) as client:
@@ -256,8 +256,32 @@ async def check_admin_api_health(config: ResourceConfig, max_retries: int = 5, r
headers=get_api_headers(config)
)
if response.status_code == 200:
- print_success("Admin API is healthy")
- return True
+ # Validate the response is actually from the backend admin API,
+ # not the frontend catch-all serving index.html
+ try:
+ data = response.json()
+ if data.get("status") == "healthy":
+ print_success("Admin API is healthy")
+ return True
+ else:
+ print_warning(
+ f"Attempt {attempt}/{max_retries}: Admin API returned unexpected JSON: {data}"
+ )
+ except (ValueError, json.JSONDecodeError):
+ # Response is not JSON.
+ # This means the API proxy is not forwarding requests to the backend.
+ content_preview = response.text[:200]
+ is_html = " {
- // Disable buffering for streaming responses
- if (proxyRes.headers['content-type']?.includes('text/event-stream')) {
- res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
- res.setHeader('X-Accel-Buffering', 'no');
- res.setHeader('Connection', 'keep-alive');
- res.flushHeaders();
+ const apiProxy = createProxyMiddleware({
+ target: BACKEND_URL,
+ changeOrigin: true,
+ pathRewrite: {
+ '^/': '/api/'
+ },
+ agent: httpAgent,
+ // Increase timeout for long-running requests (10 minutes)
+ proxyTimeout: 600000,
+ timeout: 600000,
+ // Support streaming responses (SSE)
+ onProxyRes: (proxyRes, req, res) => {
+ // Disable buffering for streaming responses
+ if (proxyRes.headers['content-type']?.includes('text/event-stream')) {
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('X-Accel-Buffering', 'no');
+ res.setHeader('Connection', 'keep-alive');
+ res.flushHeaders();
+ }
+ },
+ onError: (err, req, res) => {
+ console.error('Proxy error:', err.message);
+ if (!res.headersSent) {
+ res.status(502).json({ error: 'Backend service unavailable', details: err.message });
+ }
}
- // Log response for debugging
- console.log(`Proxy response: ${req.method} ${req.path} -> ${proxyRes.statusCode}`);
- },
- onProxyReq: (proxyReq, req, res) => {
- // Log request for debugging
- console.log(`Proxy request: ${req.method} ${req.path}`);
- },
- onError: (err, req, res) => {
- console.error('Proxy error:', err.message);
- if (!res.headersSent) {
- res.status(502).json({ error: 'Backend service unavailable', details: err.message });
- }
- }
-}));
+ });
+ app.use('/api', apiProxy);
+} else {
+ // When BACKEND_URL is not set, return a clear error for API requests
+ // instead of letting them fall through to the SPA catch-all (which returns index.html with 200)
+ app.use('/api', (req, res) => {
+ res.status(503).json({
+ error: 'Backend not configured',
+ message: 'BACKEND_URL environment variable is not set. The API proxy is not available.',
+ });
+ });
}
+
// Serve static files from the build directory
app.use(express.static(path.join(__dirname, 'static')));
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/ChatInput.tsx b/src/App/src/components/ChatInput.tsx
index b51983af2..bed099ee9 100644
--- a/src/App/src/components/ChatInput.tsx
+++ b/src/App/src/components/ChatInput.tsx
@@ -122,6 +122,7 @@ export const ChatInput = React.memo(function ChatInput({
size="small"
onClick={handleSubmit}
disabled={!inputValue.trim() || isLoading}
+ aria-label="Send"
style={{
minWidth: '32px',
height: '32px',
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 (
+
+
+
+ }
+ />
+
+
+
+
+ } disabled style={{ cursor: 'default' }}>
+
+
{displayName}
+ {userEmail &&
{userEmail}
}
+
+
+ }
+ onClick={logout}
+ >
+ Sign out
+
+
+
+
+ );
+};
+
+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();
diff --git a/tests/e2e-test/pages/HomePage.py b/tests/e2e-test/pages/HomePage.py
index fc0209ee1..a819ad345 100644
--- a/tests/e2e-test/pages/HomePage.py
+++ b/tests/e2e-test/pages/HomePage.py
@@ -24,7 +24,7 @@ class HomePage(BasePage):
# Input and send locators
ASK_QUESTION_TEXTAREA = "//input[@placeholder='Type a message']"
- SEND_BUTTON = "//button[2]//span[1]"
+ SEND_BUTTON = "//button[@aria-label='Send']"
# Response and status locators
TYPING_INDICATOR = "//div[@class='typing-indicator']"