The popup login flow was broken because the Flask 302 redirect from / to /reporting/index caused MSAL in the popup to consume the auth code hash before the parent window could detect it, leaving the parent stuck on "Authenticating..." while the popup rendered the full app. - Switch signIn() from loginPopup() to loginRedirect() - Add handleRedirectPromise() at start of initAuth() to process the auth code on page load after returning from Microsoft - Change root route from 302 redirect to direct template render so the #code=... hash fragment is preserved for MSAL - Switch signOut() from logoutPopup() to clearCache() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
367 lines
10 KiB
JavaScript
367 lines
10 KiB
JavaScript
/**
|
|
* Authentication Module for HM QC Report Dashboard
|
|
* Uses Microsoft MSAL Browser library for Azure AD authentication
|
|
* Uses redirect flow (not popup) to avoid cross-window timing issues.
|
|
*/
|
|
|
|
// Azure AD Configuration
|
|
const msalConfig = {
|
|
auth: {
|
|
clientId: '9079054c-9620-4757-a256-23413042f1ef',
|
|
authority: 'https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385',
|
|
// Use localhost:7183 for local dev (already registered in Azure AD)
|
|
redirectUri: (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')
|
|
? 'http://localhost:7183'
|
|
: window.location.origin,
|
|
navigateToLoginRequestUrl: false
|
|
},
|
|
cache: {
|
|
cacheLocation: 'sessionStorage',
|
|
storeAuthStateInCookie: false
|
|
},
|
|
system: {
|
|
allowNativeBroker: false,
|
|
loggerOptions: {
|
|
loggerCallback: (level, message, containsPii) => {
|
|
if (containsPii) return;
|
|
switch (level) {
|
|
case msal.LogLevel.Error:
|
|
console.error(message);
|
|
return;
|
|
case msal.LogLevel.Warning:
|
|
console.warn(message);
|
|
return;
|
|
default:
|
|
return;
|
|
}
|
|
},
|
|
logLevel: msal.LogLevel.Warning
|
|
}
|
|
}
|
|
};
|
|
|
|
// Login request configuration
|
|
const loginRequest = {
|
|
scopes: ['openid', 'profile', 'email']
|
|
};
|
|
|
|
// Global variables
|
|
let msalInstance = null;
|
|
let currentUser = null;
|
|
let isAuthenticated = false;
|
|
|
|
/**
|
|
* Initialize MSAL instance
|
|
*/
|
|
function initializeMsal() {
|
|
try {
|
|
if (typeof msal === 'undefined') {
|
|
console.error('MSAL library not loaded');
|
|
showError('Authentication library not loaded. Please check your internet connection.');
|
|
return false;
|
|
}
|
|
|
|
msalInstance = new msal.PublicClientApplication(msalConfig);
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Failed to initialize MSAL:', error);
|
|
showError('Failed to initialize authentication system.');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check current authentication status
|
|
*/
|
|
async function checkAuthStatus() {
|
|
try {
|
|
const response = await fetch('/auth/status', {
|
|
method: 'GET',
|
|
credentials: 'include'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.authenticated) {
|
|
isAuthenticated = true;
|
|
currentUser = data.user || {};
|
|
showAuthenticatedState();
|
|
updateUserInfo();
|
|
} else {
|
|
isAuthenticated = false;
|
|
currentUser = null;
|
|
showUnauthenticatedState();
|
|
}
|
|
|
|
return data.authenticated;
|
|
} catch (error) {
|
|
console.error('Error checking auth status:', error);
|
|
showUnauthenticatedState();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sign in with Microsoft using redirect flow.
|
|
* The page navigates to Microsoft login, then redirects back.
|
|
* On return, handleRedirectPromise() in initAuth() processes the response.
|
|
*/
|
|
async function signIn() {
|
|
if (!msalInstance) {
|
|
console.error('MSAL not initialized');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
showLoading();
|
|
// Redirect the whole page to Microsoft login.
|
|
// After auth, Microsoft redirects back to redirectUri.
|
|
// initAuth() -> handleRedirectPromise() will pick up the response.
|
|
await msalInstance.loginRedirect(loginRequest);
|
|
} catch (error) {
|
|
console.error('Login redirect failed:', error);
|
|
showError('Login failed: ' + (error.errorMessage || error.message || 'Unknown error'));
|
|
showUnauthenticatedState();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Submit token to backend for validation
|
|
*/
|
|
async function submitTokenToBackend(idToken) {
|
|
try {
|
|
const response = await fetch('/auth/login', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({ token: idToken })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.success) {
|
|
return true;
|
|
} else {
|
|
console.error('Backend token validation failed:', data.error);
|
|
showError('Authentication failed: ' + (data.error || 'Unknown error'));
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error submitting token to backend:', error);
|
|
showError('Failed to communicate with authentication server.');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sign out
|
|
*/
|
|
async function signOut() {
|
|
try {
|
|
showLoading();
|
|
|
|
// Clear backend session
|
|
await fetch('/auth/logout', {
|
|
method: 'POST',
|
|
credentials: 'include'
|
|
});
|
|
|
|
// Clear MSAL cache
|
|
if (msalInstance) {
|
|
const accounts = msalInstance.getAllAccounts();
|
|
if (accounts.length > 0) {
|
|
// Clear local MSAL state without redirecting to Microsoft logout
|
|
msalInstance.clearCache();
|
|
}
|
|
}
|
|
|
|
// Reset state
|
|
isAuthenticated = false;
|
|
currentUser = null;
|
|
|
|
// Show unauthenticated state
|
|
showUnauthenticatedState();
|
|
|
|
} catch (error) {
|
|
console.error('Logout error:', error);
|
|
// Force logout even if there's an error
|
|
isAuthenticated = false;
|
|
currentUser = null;
|
|
showUnauthenticatedState();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show loading state
|
|
*/
|
|
function showLoading() {
|
|
hideAllContainers();
|
|
const loadingElement = document.getElementById('authLoading');
|
|
if (loadingElement) {
|
|
loadingElement.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show authenticated state
|
|
*/
|
|
function showAuthenticatedState() {
|
|
hideAllContainers();
|
|
const mainContent = document.getElementById('mainContent');
|
|
if (mainContent) {
|
|
mainContent.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show unauthenticated state
|
|
*/
|
|
function showUnauthenticatedState() {
|
|
hideAllContainers();
|
|
const authRequired = document.getElementById('authRequired');
|
|
if (authRequired) {
|
|
authRequired.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide all containers
|
|
*/
|
|
function hideAllContainers() {
|
|
const containers = ['authLoading', 'authRequired', 'mainContent'];
|
|
containers.forEach(id => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Show error message
|
|
*/
|
|
function showError(message) {
|
|
console.error(message);
|
|
|
|
// Try to show error in UI
|
|
const authRequired = document.getElementById('authRequired');
|
|
if (authRequired) {
|
|
let errorDiv = authRequired.querySelector('.alert-danger');
|
|
if (!errorDiv) {
|
|
errorDiv = document.createElement('div');
|
|
errorDiv.className = 'alert alert-danger mt-3';
|
|
const textCenter = authRequired.querySelector('.text-center');
|
|
if (textCenter) {
|
|
textCenter.appendChild(errorDiv);
|
|
}
|
|
}
|
|
errorDiv.textContent = message;
|
|
} else {
|
|
alert(message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update user info display
|
|
*/
|
|
function updateUserInfo() {
|
|
const userNameElement = document.getElementById('userName');
|
|
if (userNameElement && currentUser) {
|
|
const displayName = currentUser.name || currentUser.username || currentUser.email || 'User';
|
|
userNameElement.textContent = displayName;
|
|
}
|
|
|
|
const userInfoElement = document.getElementById('userInfo');
|
|
if (userInfoElement) {
|
|
userInfoElement.style.display = isAuthenticated ? 'flex' : 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup event listeners
|
|
*/
|
|
function setupEventListeners() {
|
|
// Login button
|
|
const loginBtn = document.getElementById('loginBtn');
|
|
if (loginBtn) {
|
|
loginBtn.addEventListener('click', signIn);
|
|
}
|
|
|
|
// Logout button
|
|
const logoutBtn = document.getElementById('logoutBtn');
|
|
if (logoutBtn) {
|
|
logoutBtn.addEventListener('click', signOut);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize authentication on page load
|
|
*/
|
|
async function initAuth() {
|
|
console.log('Initializing authentication...');
|
|
|
|
showLoading();
|
|
|
|
// Initialize MSAL
|
|
const msalInitialized = initializeMsal();
|
|
if (!msalInitialized) {
|
|
showError('Failed to initialize authentication system.');
|
|
return;
|
|
}
|
|
|
|
// Handle redirect response from Microsoft login.
|
|
// After loginRedirect(), the page reloads with auth code in the URL hash.
|
|
// handleRedirectPromise() extracts the code, exchanges it for tokens,
|
|
// and cleans up the URL hash.
|
|
try {
|
|
const redirectResponse = await msalInstance.handleRedirectPromise();
|
|
|
|
if (redirectResponse && redirectResponse.idToken) {
|
|
console.log('Redirect login successful, sending token to backend...');
|
|
|
|
// Send token to backend for session creation
|
|
const success = await submitTokenToBackend(redirectResponse.idToken);
|
|
|
|
if (success) {
|
|
currentUser = redirectResponse.account;
|
|
isAuthenticated = true;
|
|
|
|
// Setup event listeners and show authenticated state
|
|
setupEventListeners();
|
|
showAuthenticatedState();
|
|
updateUserInfo();
|
|
return;
|
|
} else {
|
|
console.error('Backend token validation failed after redirect');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error handling redirect response:', error);
|
|
}
|
|
|
|
// Setup event listeners
|
|
setupEventListeners();
|
|
|
|
// No redirect response - check if we have an existing backend session
|
|
const authenticated = await checkAuthStatus();
|
|
|
|
if (authenticated) {
|
|
console.log('User is authenticated (existing session)');
|
|
} else {
|
|
console.log('User is not authenticated');
|
|
}
|
|
}
|
|
|
|
// Initialize when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initAuth);
|
|
} else {
|
|
initAuth();
|
|
}
|
|
|
|
// Expose functions globally for inline event handlers
|
|
window.signIn = signIn;
|
|
window.signOut = signOut;
|
|
window.checkAuthStatus = checkAuthStatus;
|