Major Features: - 🖥️ Standalone desktop app (VideoMatcher.app) - double-click to run - 🎨 Black & gold branded UI (Montserrat font, #FFC407 accent) - 📁 Local file browser for master/adaptation folders - ⚡ Fast mode processing (10-20x faster, disables AKAZE/AI Vision) - 🤖 Smart AI Vision fallback (auto-retry when no matches found) - 📊 Real-time progress bars (fingerprinting & matching) - 💾 Local processing (no cloud, no authentication) - 📤 CSV export with master filenames Web Application (Enterprise): - 🌐 Flask web app with Azure AD authentication - 📦 Box.com integration for cloud storage - 🐳 Docker support for deployment - 🔐 JWT validation with httpOnly cookies - 🎯 REST API endpoints Enhancements: - Fixed master filename lookup (was showing "Unknown") - Automatic fingerprint recovery (detects missing files) - Improved CSV format (master file next to adaptation) - Port conflict handling (auto-finds available port) - Environment variable fixes for standalone mode Documentation: - Updated README with standalone app section - Added 10+ guide documents (UI improvements, fingerprint recovery, etc.) - Build instructions with PyInstaller - Comprehensive troubleshooting guide Technical: - PyInstaller build configuration (video_matcher.spec) - Launcher with environment setup (launcher.py) - Mock authentication for standalone mode - Video matcher service layer - Metadata parser and AKAZE video matching 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
355 lines
9.3 KiB
JavaScript
355 lines
9.3 KiB
JavaScript
/**
|
|
* Authentication Module for HM QC Report Dashboard
|
|
* Uses Microsoft MSAL Browser library for Azure AD authentication
|
|
*/
|
|
|
|
// 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
|
|
*/
|
|
async function signIn() {
|
|
if (!msalInstance) {
|
|
console.error('MSAL not initialized');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
showLoading();
|
|
|
|
// Perform popup login
|
|
const loginResponse = await msalInstance.loginPopup(loginRequest);
|
|
|
|
if (loginResponse && loginResponse.idToken) {
|
|
// Send token to backend for validation and cookie storage
|
|
const success = await submitTokenToBackend(loginResponse.idToken);
|
|
|
|
if (success) {
|
|
currentUser = loginResponse.account;
|
|
isAuthenticated = true;
|
|
showAuthenticatedState();
|
|
updateUserInfo();
|
|
} else {
|
|
throw new Error('Failed to validate token with backend');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Login failed:', error);
|
|
|
|
if (error.errorCode === 'popup_window_error') {
|
|
showError('Popup was blocked. Please allow popups for this site.');
|
|
} else if (error.errorCode === 'user_cancelled') {
|
|
console.log('User cancelled login');
|
|
showUnauthenticatedState();
|
|
} else {
|
|
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 cookie
|
|
await fetch('/auth/logout', {
|
|
method: 'POST',
|
|
credentials: 'include'
|
|
});
|
|
|
|
// Clear MSAL cache
|
|
if (msalInstance) {
|
|
const currentAccount = msalInstance.getActiveAccount();
|
|
if (currentAccount) {
|
|
await msalInstance.logoutPopup({
|
|
account: currentAccount,
|
|
postLogoutRedirectUri: window.location.origin
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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';
|
|
authRequired.querySelector('.text-center').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;
|
|
}
|
|
|
|
// Setup event listeners
|
|
setupEventListeners();
|
|
|
|
// Check authentication status
|
|
const authenticated = await checkAuthStatus();
|
|
|
|
if (authenticated) {
|
|
console.log('User is authenticated');
|
|
} 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;
|