video-master-adapt/static/js/auth.js
nickviljoen 891c36bbfb Add standalone desktop application with web interface
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>
2025-12-31 09:49:04 +02:00

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;