From df6a6980985be44a031c3266c9b3c98e9a67258e Mon Sep 17 00:00:00 2001 From: michael Date: Thu, 18 Sep 2025 17:55:30 -0500 Subject: [PATCH] significant code changes and new config file to accommodate deploying to different server with different paths and URLs --- frontend/.env | 7 +- frontend/build.sh | 35 +++- frontend/package.json | 3 +- frontend/public/config.js | 16 ++ frontend/public/config.json | 16 ++ frontend/public/config.template.json | 39 ++++ frontend/public/index.html | 24 +++ frontend/src/App.js | 19 +- frontend/src/auth/AuthProvider.js | 147 +++++++------- frontend/src/auth/authApiClient.js | 24 ++- frontend/src/auth/authConfig.js | 57 +++--- frontend/src/components/Login.js | 15 +- frontend/src/components/ResultDisplay.js | 5 +- frontend/src/utils/chunkedUploader.js | 12 +- frontend/src/utils/configLoader.js | 207 +++++++++++++++++++ frontend/src/utils/pathUtils.js | 244 +++++++++++++++++++++++ 16 files changed, 731 insertions(+), 139 deletions(-) create mode 100644 frontend/public/config.js create mode 100644 frontend/public/config.json create mode 100644 frontend/public/config.template.json create mode 100644 frontend/src/utils/configLoader.js create mode 100644 frontend/src/utils/pathUtils.js diff --git a/frontend/.env b/frontend/.env index ddaae9b..ef95ff8 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1,2 +1,5 @@ -PUBLIC_URL=/video_query -REACT_APP_BASE_URL=/video_query \ No newline at end of file +# Removed hardcoded PUBLIC_URL to enable flexible deployment paths +# The app now uses runtime configuration from config.json for base path detection + +# Optional: Set this if you want to override the runtime base path detection +# REACT_APP_BASE_PATH_OVERRIDE=/your-custom-path \ No newline at end of file diff --git a/frontend/build.sh b/frontend/build.sh index 8ce39f3..726b8d0 100755 --- a/frontend/build.sh +++ b/frontend/build.sh @@ -1,18 +1,31 @@ #!/bin/bash +# Environment-agnostic build script +# The app now detects its base path at runtime from config.json + # Clean any previous build rm -rf build -# Make sure the .env file exists -if [ ! -f .env ]; then - echo "Creating .env file with correct PUBLIC_URL" - echo "PUBLIC_URL=/video_query" > .env - echo "REACT_APP_BASE_URL=/video_query" >> .env -fi +echo "Building application with dynamic base path support..." +echo "The app will detect its base path at runtime from config.json" -# Build with the public URL explicitly set -PUBLIC_URL="/video_query" npm run build +# Build without hardcoded paths +npm run build -echo "Build complete. The 'build' directory now contains files ready for deployment." -echo "Copy these files to your web server's /video_query directory." -echo "Example: scp -r build/* user@your-server:/var/www/html/video_query/" \ No newline at end of file +echo "" +echo "✅ Build complete! The 'build' directory contains files ready for deployment." +echo "" +echo "📝 DEPLOYMENT INSTRUCTIONS:" +echo "1. Copy build files to your web server at any path:" +echo " Example: scp -r build/* user@server:/var/www/html/your-app-path/" +echo "" +echo "2. Update config.json on your server:" +echo " - Set 'basePath' to match your deployment path (e.g., '/video_query', '/video-query')" +echo " - Set 'domain' to your server's domain" +echo " - Update MSAL and API endpoints as needed" +echo "" +echo "3. The app will automatically use the correct paths at runtime!" +echo "" +echo "📋 Example config.json for different deployments:" +echo " Subdirectory: {\"basePath\": \"/video_query\", \"domain\": \"https://myserver.com\"}" +echo " Root deploy: {\"basePath\": \"\", \"domain\": \"https://video-app.com\"}" \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 35bb679..c0068cc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,9 +14,10 @@ "react-scripts": "5.0.1", "showdown": "^2.1.0" }, + "homepage": ".", "scripts": { "start": "react-scripts start", - "build": "PUBLIC_URL=/video_query react-scripts build", + "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, diff --git a/frontend/public/config.js b/frontend/public/config.js new file mode 100644 index 0000000..13f23c6 --- /dev/null +++ b/frontend/public/config.js @@ -0,0 +1,16 @@ +window.__APP_CONFIG__ = { + "_comment": "Dynamic base path configuration - set basePath to override auto-detection", + "basePath": "/video-query", + "domain": "https://brandtechsandbox.oliver.solutions", + "msal": { + "clientId": "01d33c7c-5640-4986-b4db-06af63a7d285", + "authority": "https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385", + "redirectUri": "https://brandtechsandbox.oliver.solutions/video-query/", + "postLogoutRedirectUri": "https://brandtechsandbox.oliver.solutions/video-query/", + "tenantId": "e519c2e6-bc6d-4fdf-8d9c-923c2f002385" + }, + "api": { + "videoProcessingEndpoint": "https://brandtechsandbox.oliver.solutions/video_query_back/api/process", + "chunkedUploadEndpoint": "https://brandtechsandbox.oliver.solutions/video_query_back" + } +}; \ No newline at end of file diff --git a/frontend/public/config.json b/frontend/public/config.json new file mode 100644 index 0000000..9d29a28 --- /dev/null +++ b/frontend/public/config.json @@ -0,0 +1,16 @@ +{ + "_comment": "Dynamic base path configuration - set basePath to override auto-detection", + "basePath": "/video-query", + "domain": "https://brandtechsandbox.oliver.solutions", + "msal": { + "clientId": "01d33c7c-5640-4986-b4db-06af63a7d285", + "authority": "https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385", + "redirectUri": "https://brandtechsandbox.oliver.solutions/video-query/", + "postLogoutRedirectUri": "https://brandtechsandbox.oliver.solutions/video-query/", + "tenantId": "e519c2e6-bc6d-4fdf-8d9c-923c2f002385" + }, + "api": { + "videoProcessingEndpoint": "https://brandtechsandbox.oliver.solutions/video_query_back/api/process", + "chunkedUploadEndpoint": "https://brandtechsandbox.oliver.solutions/video_query_back" + } +} \ No newline at end of file diff --git a/frontend/public/config.template.json b/frontend/public/config.template.json new file mode 100644 index 0000000..685255c --- /dev/null +++ b/frontend/public/config.template.json @@ -0,0 +1,39 @@ +{ + "_note": "This is a template configuration file. Copy this to config.json and update values for your environment.", + "_instructions": { + "basePath": "Set to your app's base path (e.g., '/video_query', '/video-query', '/app'). Leave empty or set to '/' for root deployment. If omitted, the app will auto-detect from URL.", + "domain": "Your application's domain (used for constructing redirect URIs)", + "msal": "Azure AD application registration settings", + "api": "Backend API endpoints" + }, + "basePath": "/your-app-path", + "domain": "https://your-domain.com", + "msal": { + "clientId": "YOUR_AZURE_AD_CLIENT_ID", + "authority": "https://login.microsoftonline.com/YOUR_TENANT_ID", + "tenantId": "YOUR_TENANT_ID" + }, + "api": { + "videoProcessingEndpoint": "https://your-api-domain.com/api/process", + "chunkedUploadEndpoint": "https://your-api-domain.com" + }, + "_examples": { + "deployment_scenarios": { + "subdirectory_deployment": { + "basePath": "/video_query", + "domain": "https://mycompany.com", + "resulting_app_url": "https://mycompany.com/video_query/" + }, + "subdirectory_with_different_name": { + "basePath": "/video-analysis-tool", + "domain": "https://tools.example.org", + "resulting_app_url": "https://tools.example.org/video-analysis-tool/" + }, + "root_deployment": { + "basePath": "", + "domain": "https://video-tool.mycompany.com", + "resulting_app_url": "https://video-tool.mycompany.com/" + } + } + } +} \ No newline at end of file diff --git a/frontend/public/index.html b/frontend/public/index.html index 6156b1d..8a594b6 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -2,6 +2,7 @@ + @@ -15,6 +16,29 @@ Video Query Tool + + diff --git a/frontend/src/App.js b/frontend/src/App.js index 1d894a2..cbfbcae 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -6,6 +6,7 @@ import AuthenticatedContent from './components/AuthenticatedContent'; import Login from './components/Login'; import ChunkedUploader from './utils/chunkedUploader'; import { loginRequest } from './auth/authConfig'; +import { getApiConfig } from './utils/configLoader'; function App() { // MSAL authentication hook @@ -55,25 +56,26 @@ function App() { // Always use chunked upload regardless of file size console.log('Using chunked upload for all files'); - // Create chunked uploader + // Create chunked uploader with runtime config + const apiConfigForUpload = getApiConfig(); const uploader = new ChunkedUploader(selectedFile, (progress) => { console.log(`Upload progress: ${progress}%`); setUploadProgress(progress); - }); - + }, apiConfigForUpload.chunkedUploadEndpoint); + // Variable to store upload result let chunkUploadResult; - + try { // Start the chunked upload console.log('Starting chunked upload process...'); chunkUploadResult = await uploader.uploadFile(); console.log('Upload result:', chunkUploadResult); - + if (!chunkUploadResult.success) { throw new Error('Chunked upload failed'); } - + console.log('Chunked upload complete, starting processing'); console.log('File path:', chunkUploadResult.file_path); console.log('Filename:', chunkUploadResult.filename); @@ -82,10 +84,11 @@ function App() { console.error('Chunked upload error:', uploadError); throw uploadError; } - + // Now process the uploaded file + const apiConfig = getApiConfig(); response = await authApiClient.post( - 'https://ai-sandbox.oliver.solutions/video_query_back/api/process', + apiConfig.videoProcessingEndpoint, { file_path: chunkUploadResult.file_path, filename: chunkUploadResult.filename, diff --git a/frontend/src/auth/AuthProvider.js b/frontend/src/auth/AuthProvider.js index f13edcd..01d7bc7 100644 --- a/frontend/src/auth/AuthProvider.js +++ b/frontend/src/auth/AuthProvider.js @@ -1,39 +1,41 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { MsalProvider } from '@azure/msal-react'; import { PublicClientApplication, EventType, InteractionType } from '@azure/msal-browser'; -import { msalConfig } from './authConfig'; +import { loadConfig, getMsalConfig } from '../utils/configLoader'; -// Use the standard msalConfig but with implicit flow -// This approach should work when the app is registered as a non-SPA client -const msalConfig_enhanced = { - ...msalConfig -}; +// MSAL instance will be created after config is loaded +let msalInstance = null; -// Initialize MSAL -export const msalInstance = new PublicClientApplication(msalConfig_enhanced); - -// Initialize MSAL instance -(async () => { +// Create MSAL instance with loaded configuration +const createMsalInstance = async () => { try { + console.log("Loading configuration for MSAL..."); + await loadConfig(); + + const msalConfig = getMsalConfig(); + console.log("Creating MSAL instance with runtime config..."); + + const instance = new PublicClientApplication(msalConfig); + console.log("Initializing MSAL instance..."); - await msalInstance.initialize(); + await instance.initialize(); console.log("MSAL instance initialized successfully"); - + // Try to set active account after initialization - if (!msalInstance.getActiveAccount() && msalInstance.getAllAccounts().length > 0) { + if (!instance.getActiveAccount() && instance.getAllAccounts().length > 0) { console.log("Setting active account during initialization"); - msalInstance.setActiveAccount(msalInstance.getAllAccounts()[0]); + instance.setActiveAccount(instance.getAllAccounts()[0]); } // Handle any initial redirect response at startup try { console.log("Checking for redirect response at startup..."); - const response = await msalInstance.handleRedirectPromise(); + const response = await instance.handleRedirectPromise(); if (response) { console.log("Found redirect response at startup:", response); if (response.account) { console.log("Setting active account from redirect response:", response.account.name); - msalInstance.setActiveAccount(response.account); + instance.setActiveAccount(response.account); } } else { console.log("No redirect response at startup"); @@ -43,13 +45,13 @@ export const msalInstance = new PublicClientApplication(msalConfig_enhanced); } // Configure event callbacks for authentication events - msalInstance.addEventCallback((event) => { + instance.addEventCallback((event) => { // Handle successful logins if (event.eventType === EventType.LOGIN_SUCCESS) { console.log("Login success event triggered", event); if (event.payload && event.payload.account) { console.log("Setting active account from event:", event.payload.account.name); - msalInstance.setActiveAccount(event.payload.account); + instance.setActiveAccount(event.payload.account); // Force reload to update authentication state if (event.interactionType === "redirect") { window.location.reload(); @@ -81,68 +83,44 @@ export const msalInstance = new PublicClientApplication(msalConfig_enhanced); console.log("Redirect handling completed"); } }); - + console.log("Event callbacks registered"); + return instance; + } catch (error) { - console.error("Error initializing MSAL:", error); + console.error("Error creating MSAL instance:", error); + throw error; } -})(); +}; + +// Export getter for MSAL instance +export const getMsalInstance = () => { + return msalInstance; +}; /** * MSAL Provider Component to wrap the application with authentication context */ export const AuthProvider = ({ children }) => { - const [isInitialized, setIsInitialized] = React.useState(false); - - // Check for authentication on component mount + const [isInitialized, setIsInitialized] = useState(false); + const [initError, setInitError] = useState(null); + + // Initialize MSAL on component mount useEffect(() => { - const initializeAndHandleRedirect = async () => { + const initialize = async () => { try { - // Ensure MSAL is initialized - if (!msalInstance.initialized) { - console.log("Initializing MSAL from component..."); - await msalInstance.initialize(); - console.log("MSAL initialized from component"); - } - - // Handle any redirect response with PKCE auth code flow - try { - console.log("Handling redirect promise with PKCE..."); - // This properly handles auth code + PKCE flow redirects - const response = await msalInstance.handleRedirectPromise(); - - // If we have a response, we just returned from a redirect - if (response) { - console.log("Redirect response from PKCE flow:", response); - if (response.account) { - console.log("Setting active account after PKCE redirect:", response.account.name); - msalInstance.setActiveAccount(response.account); - } - } else { - console.log("No redirect response"); - // Try to set active account if not already set - const accounts = msalInstance.getAllAccounts(); - console.log("Accounts found:", accounts.length); - if (accounts.length > 0 && !msalInstance.getActiveAccount()) { - console.log("Setting active account from cached accounts:", accounts[0].name); - msalInstance.setActiveAccount(accounts[0]); - } - } - } catch (redirectErr) { - console.error("Error handling PKCE redirect:", redirectErr); - console.error("Redirect error details:", JSON.stringify(redirectErr, null, 2)); - } - - // Mark as initialized - setIsInitialized(true); - } catch (initErr) { - console.error("Error during MSAL initialization:", initErr); - // Even if there's an error, mark as initialized to avoid infinite loop + console.log("AuthProvider: Starting initialization..."); + msalInstance = await createMsalInstance(); + console.log("AuthProvider: MSAL instance created successfully"); setIsInitialized(true); + } catch (error) { + console.error("AuthProvider: Error during initialization:", error); + setInitError(error); + setIsInitialized(true); // Mark as initialized even on error to show error message } }; - - initializeAndHandleRedirect(); + + initialize(); }, []); // Show loading until MSAL is initialized @@ -150,9 +128,36 @@ export const AuthProvider = ({ children }) => { return (
- Loading authentication... + Loading configuration... +
+

Loading configuration and initializing authentication...

+
+ ); + } + + // Show error if initialization failed + if (initError) { + return ( +
+
+

Configuration Error

+

Failed to load application configuration:

+

{initError.message}

+
+

Please ensure config.json is available and properly configured.

+
+
+ ); + } + + // Only render MSAL provider if instance was created successfully + if (!msalInstance) { + return ( +
+
+

Initialization Error

+

MSAL instance was not created properly. Please refresh the page.

-

Initializing authentication...

); } diff --git a/frontend/src/auth/authApiClient.js b/frontend/src/auth/authApiClient.js index dc87765..871ab7b 100644 --- a/frontend/src/auth/authApiClient.js +++ b/frontend/src/auth/authApiClient.js @@ -1,5 +1,5 @@ import axios from 'axios'; -import { msalInstance } from './AuthProvider'; +import { getMsalInstance } from './AuthProvider'; import { loginRequest } from './authConfig'; /** @@ -27,10 +27,13 @@ const redirectToLogin = async () => { // Clear MSAL caches before redirecting try { console.log("API: Clearing MSAL cache before redirect"); - msalInstance.clearCache(); - await msalInstance.logout({ - onRedirectNavigate: () => false // Don't redirect yet, we'll do it manually - }); + const msalInstance = getMsalInstance(); + if (msalInstance) { + msalInstance.clearCache(); + await msalInstance.logout({ + onRedirectNavigate: () => false // Don't redirect yet, we'll do it manually + }); + } } catch (e) { console.error("API: Error clearing cache:", e); } @@ -38,8 +41,9 @@ const redirectToLogin = async () => { // Set a flag in sessionStorage to indicate we're expecting a login sessionStorage.setItem("redirectedForLogin", "true"); - // Redirect to login page - window.location.href = window.location.origin + "/video_query/"; + // Redirect to login page using dynamic base path + const { navigateToPath } = require('../utils/pathUtils'); + navigateToPath('/'); }; // Add request interceptor to add auth token to all API requests @@ -78,6 +82,12 @@ authApiClient.interceptors.request.use( console.log("API: No manual token, trying MSAL"); // Get active account and check if token exists + const msalInstance = getMsalInstance(); + if (!msalInstance) { + console.warn("API: MSAL instance not available! Redirecting to login..."); + await redirectToLogin(); + return config; + } const account = msalInstance.getActiveAccount(); if (!account) { console.warn("API: No active account! Redirecting to login..."); diff --git a/frontend/src/auth/authConfig.js b/frontend/src/auth/authConfig.js index 007ff45..79e5123 100644 --- a/frontend/src/auth/authConfig.js +++ b/frontend/src/auth/authConfig.js @@ -1,35 +1,25 @@ /* * MSAL configuration for authentication + * This module exports configuration that will be loaded at runtime */ -// Get the public URL from environment or use default -const publicUrl = process.env.REACT_APP_BASE_URL || '/video_query'; +import { getMsalConfig, getApiConfig } from '../utils/configLoader'; -export const msalConfig = { - auth: { - clientId: "9079054c-9620-4757-a256-23413042f1ef", - authority: "https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385", - redirectUri: "https://ai-sandbox.oliver.solutions/video_query/", - postLogoutRedirectUri: "https://ai-sandbox.oliver.solutions/video_query/", - navigateToLoginRequestUrl: true - }, - cache: { - cacheLocation: "sessionStorage", - storeAuthStateInCookie: true, - }, - system: { - allowRedirectInIframe: true, - tokenRenewalOffsetSeconds: 300, - // Log all messages for debugging - loggerOptions: { - loggerCallback: (level, message) => { - console.log(`MSAL: ${message}`); - }, - logLevel: 4 // Verbose - } - } +// Export a getter function instead of a static object +// This ensures the config is loaded from runtime configuration +export const getMsalConfigFromRuntime = () => { + return getMsalConfig(); }; +// Legacy export for backward compatibility +// This will throw an error if config hasn't been loaded yet +export const msalConfig = new Proxy({}, { + get: (target, prop) => { + const config = getMsalConfig(); + return config[prop]; + } +}); + // Add scopes here for access token request // For more information about scopes visit: // https://learn.microsoft.com/en-us/azure/active-directory/develop/permissions-consent-overview @@ -38,7 +28,16 @@ export const loginRequest = { }; // Add endpoints here for API calls -export const apiConfig = { - videoProcessingEndpoint: "https://ai-sandbox.oliver.solutions/video_query_back/api/process", - chunkedUploadEndpoint: "https://ai-sandbox.oliver.solutions/video_query_back/api" -}; \ No newline at end of file +// These are now loaded from runtime configuration + +export const getApiConfigFromRuntime = () => { + return getApiConfig(); +}; + +// Legacy export for backward compatibility +export const apiConfig = new Proxy({}, { + get: (target, prop) => { + const config = getApiConfig(); + return config[prop]; + } +}); \ No newline at end of file diff --git a/frontend/src/components/Login.js b/frontend/src/components/Login.js index 1677a88..0faf330 100644 --- a/frontend/src/components/Login.js +++ b/frontend/src/components/Login.js @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useMsal } from '@azure/msal-react'; import { loginRequest } from '../auth/authConfig'; +import { getManualAuthConfig } from '../utils/configLoader'; const Login = () => { const { instance, accounts, inProgress } = useMsal(); @@ -59,11 +60,19 @@ const Login = () => { console.log("Login: No existing accounts found, proceeding with login"); } + // Get configuration values from runtime config + try { + var authConfig = getManualAuthConfig(); + } catch (error) { + console.error("Configuration not loaded for login:", error); + return; + } + // Open Azure AD login page directly // Use id_token for better backend compatibility - const tenantId = "e519c2e6-bc6d-4fdf-8d9c-923c2f002385"; - const clientId = "9079054c-9620-4757-a256-23413042f1ef"; - const redirectUri = encodeURIComponent("https://ai-sandbox.oliver.solutions/video_query/"); + const tenantId = authConfig.tenantId; + const clientId = authConfig.clientId; + const redirectUri = encodeURIComponent(authConfig.redirectUri); const responseType = "id_token+token"; // Get both ID token and access token const scope = encodeURIComponent("openid profile email User.Read"); const nonce = Math.random().toString(36).substring(2, 15); diff --git a/frontend/src/components/ResultDisplay.js b/frontend/src/components/ResultDisplay.js index 2b2e725..daacb3d 100644 --- a/frontend/src/components/ResultDisplay.js +++ b/frontend/src/components/ResultDisplay.js @@ -1,6 +1,7 @@ import React, { useRef, useEffect, useState } from 'react'; import showdown from 'showdown'; import mermaid from 'mermaid'; +import { getApiConfig } from '../utils/configLoader'; const ResultDisplay = ({ result, isLoading, uploadProgress = 0 }) => { const resultRef = useRef(null); @@ -271,8 +272,10 @@ const ResultDisplay = ({ result, isLoading, uploadProgress = 0 }) => { // Make API request to generate PDF const authApiClient = require('../auth/authApiClient').authApiClient; + const apiConfig = getApiConfig(); + const pdfEndpoint = `${apiConfig.chunkedUploadEndpoint}/api/generate-pdf`; const response = await authApiClient.post( - 'https://ai-sandbox.oliver.solutions/video_query_back/api/generate-pdf', + pdfEndpoint, { html: htmlToSend, textDiagrams: textDiagrams, diff --git a/frontend/src/utils/chunkedUploader.js b/frontend/src/utils/chunkedUploader.js index d338017..007c3a4 100644 --- a/frontend/src/utils/chunkedUploader.js +++ b/frontend/src/utils/chunkedUploader.js @@ -5,14 +5,14 @@ import { authApiClient } from '../auth/authApiClient'; const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB chunks -const BACKEND_URL = 'https://ai-sandbox.oliver.solutions/video_query_back'; class ChunkedUploader { - constructor(file, onProgress) { + constructor(file, onProgress, backendUrl = 'https://ai-sandbox.oliver.solutions/video_query_back') { this.file = file; this.onProgress = onProgress || (() => {}); this.uploadId = null; this.aborted = false; + this.backendUrl = backendUrl; } /** @@ -21,7 +21,7 @@ class ChunkedUploader { async initUpload() { try { const response = await authApiClient.post( - `${BACKEND_URL}/api/init-upload`, + `${this.backendUrl}/api/init-upload`, { filename: this.file.name, size: this.file.size, @@ -67,7 +67,7 @@ class ChunkedUploader { try { const response = await authApiClient.post( - `${BACKEND_URL}/api/upload-chunk/${this.uploadId}`, + `${this.backendUrl}/api/upload-chunk/${this.uploadId}`, formData, { headers: { @@ -100,7 +100,7 @@ class ChunkedUploader { async completeUpload() { try { const response = await authApiClient.post( - `${BACKEND_URL}/api/complete-upload/${this.uploadId}`, + `${this.backendUrl}/api/complete-upload/${this.uploadId}`, {}, { headers: { @@ -139,7 +139,7 @@ class ChunkedUploader { try { const response = await authApiClient.post( - `${BACKEND_URL}/api/cancel-upload/${this.uploadId}`, + `${this.backendUrl}/api/cancel-upload/${this.uploadId}`, {}, { headers: { diff --git a/frontend/src/utils/configLoader.js b/frontend/src/utils/configLoader.js new file mode 100644 index 0000000..141ff4e --- /dev/null +++ b/frontend/src/utils/configLoader.js @@ -0,0 +1,207 @@ +/** + * Runtime configuration loader + * Fetches configuration from public/config.json at application startup + */ + +let cachedConfig = null; +let configPromise = null; + +/** + * Loads configuration from the public config.json file + * @returns {Promise} The configuration object + */ +export const loadConfig = async () => { + if (cachedConfig) { + return cachedConfig; + } + + if (configPromise) { + return configPromise; + } + + configPromise = (async () => { + try { + console.log('Loading runtime configuration...'); + + // Check if config was already loaded by config.js script + if (typeof window !== 'undefined' && window.__APP_CONFIG__) { + console.log('Using config from window.__APP_CONFIG__'); + const config = window.__APP_CONFIG__; + + // Validate required configuration + if (!config.msal) { + throw new Error('Missing MSAL configuration in config'); + } + + if (!config.msal.clientId || !config.msal.authority) { + throw new Error('Missing required MSAL configuration fields (clientId, authority)'); + } + + console.log('Runtime configuration loaded successfully from script'); + cachedConfig = config; + return config; + } + + // Fallback to fetch config.json if not loaded by script + console.log('Fetching config.json as fallback...'); + const response = await fetch('./config.json'); + + if (!response.ok) { + throw new Error(`Failed to load config: ${response.status} ${response.statusText}`); + } + + const config = await response.json(); + + // Validate required configuration + if (!config.msal) { + throw new Error('Missing MSAL configuration in config.json'); + } + + if (!config.msal.clientId || !config.msal.authority || !config.msal.redirectUri) { + throw new Error('Missing required MSAL configuration fields (clientId, authority, redirectUri)'); + } + + console.log('Runtime configuration loaded successfully'); + cachedConfig = config; + return config; + + } catch (error) { + console.error('Error loading configuration:', error); + throw error; + } + })(); + + return configPromise; +}; + +/** + * Gets the cached configuration (synchronous) + * Must call loadConfig() first to ensure config is loaded + * @returns {Object|null} The cached configuration or null if not loaded + */ +export const getConfig = () => { + return cachedConfig; +}; + +/** + * Clears the cached configuration (useful for testing) + */ +export const clearConfig = () => { + cachedConfig = null; + configPromise = null; +}; + +/** + * Gets MSAL configuration from the loaded config + * @returns {Object} MSAL configuration object + */ +export const getMsalConfig = () => { + if (!cachedConfig) { + throw new Error('Configuration not loaded. Call loadConfig() first.'); + } + + // Import pathUtils here to avoid circular dependencies + const { getRedirectUri } = require('./pathUtils'); + + return { + auth: { + clientId: cachedConfig.msal.clientId, + authority: cachedConfig.msal.authority, + redirectUri: getRedirectUri('/'), + postLogoutRedirectUri: getRedirectUri('/'), + navigateToLoginRequestUrl: true + }, + cache: { + cacheLocation: "sessionStorage", + storeAuthStateInCookie: true, + }, + system: { + allowRedirectInIframe: true, + tokenRenewalOffsetSeconds: 300, + loggerOptions: { + loggerCallback: (level, message) => { + console.log(`MSAL: ${message}`); + }, + logLevel: 4 // Verbose + } + } + }; +}; + +/** + * Gets API configuration from the loaded config + * @returns {Object} API configuration object + */ +export const getApiConfig = () => { + if (!cachedConfig) { + throw new Error('Configuration not loaded. Call loadConfig() first.'); + } + + return cachedConfig.api || {}; +}; + +/** + * Gets the tenant ID for manual auth flows + * @returns {string} The tenant ID + */ +export const getTenantId = () => { + if (!cachedConfig) { + throw new Error('Configuration not loaded. Call loadConfig() first.'); + } + + return cachedConfig.msal.tenantId || cachedConfig.msal.authority.split('/').pop(); +}; + +/** + * Gets the base path from configuration + * @returns {string|undefined} The configured base path + */ +export const getBasePath = () => { + if (!cachedConfig) { + return undefined; + } + return cachedConfig.basePath; +}; + +/** + * Gets the domain from configuration + * @returns {string|undefined} The configured domain + */ +export const getDomain = () => { + if (!cachedConfig) { + return undefined; + } + return cachedConfig.domain; +}; + +/** + * Constructs a redirect URI using configuration + * @param {string} path - Optional path to append (defaults to '/') + * @returns {string} Complete redirect URI + */ +export const getRedirectUri = (path = '/') => { + if (!cachedConfig) { + throw new Error('Configuration not loaded. Call loadConfig() first.'); + } + + // Import pathUtils here to avoid circular dependencies + const { getFullUrl } = require('./pathUtils'); + return getFullUrl(path); +}; + +/** + * Gets configuration for manual authentication flows (Login.js) + * @returns {Object} Configuration for constructing auth URLs + */ +export const getManualAuthConfig = () => { + if (!cachedConfig) { + throw new Error('Configuration not loaded. Call loadConfig() first.'); + } + + return { + tenantId: getTenantId(), + clientId: cachedConfig.msal.clientId, + redirectUri: getRedirectUri('/'), + authority: cachedConfig.msal.authority + }; +}; \ No newline at end of file diff --git a/frontend/src/utils/pathUtils.js b/frontend/src/utils/pathUtils.js new file mode 100644 index 0000000..6567df9 --- /dev/null +++ b/frontend/src/utils/pathUtils.js @@ -0,0 +1,244 @@ +/** + * Path utilities for dynamic base path detection and management + * Enables "build once, deploy anywhere" functionality + */ + +import { getConfig } from './configLoader'; + +let detectedBasePath = null; +let configBasePath = null; + +/** + * Detects the base path from the current URL + * Examples: + * - https://example.com/video_query/ → /video_query + * - https://example.com/video-query/ → /video-query + * - https://example.com/app/ → /app + * - https://example.com/ → / + */ +const detectBasePath = () => { + if (detectedBasePath !== null) { + return detectedBasePath; + } + + try { + const currentPath = window.location.pathname; + console.log('PathUtils: Current pathname:', currentPath); + + // If we're at the root, base path is empty + if (currentPath === '/') { + detectedBasePath = ''; + return detectedBasePath; + } + + // Look for common app indicators in the path + // This assumes the app is served from a subdirectory + const pathSegments = currentPath.split('/').filter(segment => segment.length > 0); + + if (pathSegments.length === 0) { + detectedBasePath = ''; + return detectedBasePath; + } + + // Common patterns for where React apps are deployed + const commonAppPaths = [ + 'video_query', 'video-query', 'videoquery', + 'app', 'frontend', 'client', 'ui', 'web' + ]; + + // Check if the first path segment matches common app patterns + const firstSegment = pathSegments[0].toLowerCase(); + if (commonAppPaths.includes(firstSegment)) { + detectedBasePath = `/${pathSegments[0]}`; + console.log('PathUtils: Detected base path from common patterns:', detectedBasePath); + return detectedBasePath; + } + + // If no common pattern found, assume the first segment is the app path + // This handles cases like /my-custom-app/ + if (pathSegments.length >= 1) { + detectedBasePath = `/${pathSegments[0]}`; + console.log('PathUtils: Detected base path from first segment:', detectedBasePath); + return detectedBasePath; + } + + // Fallback to root + detectedBasePath = ''; + console.log('PathUtils: Defaulting to root path'); + return detectedBasePath; + + } catch (error) { + console.error('PathUtils: Error detecting base path:', error); + detectedBasePath = ''; + return detectedBasePath; + } +}; + +/** + * Gets the base path from configuration (if specified) + */ +const getConfigBasePath = () => { + if (configBasePath !== null) { + return configBasePath; + } + + // Check for early config loaded by HTML script + if (typeof window !== 'undefined' && window.__EARLY_CONFIG__) { + const config = window.__EARLY_CONFIG__; + if (config && config.basePath !== undefined) { + configBasePath = config.basePath; + // Ensure it starts with / and doesn't end with / (unless it's just /) + if (configBasePath && configBasePath !== '/') { + if (!configBasePath.startsWith('/')) { + configBasePath = '/' + configBasePath; + } + if (configBasePath.endsWith('/')) { + configBasePath = configBasePath.slice(0, -1); + } + } else if (configBasePath === '/') { + configBasePath = ''; + } + console.log('PathUtils: Using early loaded config base path:', configBasePath); + return configBasePath; + } + } + + try { + const config = getConfig(); + if (config && config.basePath !== undefined) { + configBasePath = config.basePath; + // Ensure it starts with / and doesn't end with / (unless it's just /) + if (configBasePath && configBasePath !== '/') { + if (!configBasePath.startsWith('/')) { + configBasePath = '/' + configBasePath; + } + if (configBasePath.endsWith('/')) { + configBasePath = configBasePath.slice(0, -1); + } + } else if (configBasePath === '/') { + configBasePath = ''; + } + console.log('PathUtils: Using configured base path:', configBasePath); + return configBasePath; + } + } catch (error) { + console.log('PathUtils: No configuration available yet, using detection'); + } + + configBasePath = null; + return null; +}; + +/** + * Gets the effective base path (config override > detected > root) + * @returns {string} The base path without trailing slash (empty string for root) + */ +export const getBasePath = () => { + // Priority: configured > detected > root + const configured = getConfigBasePath(); + if (configured !== null) { + return configured; + } + + return detectBasePath(); +}; + +/** + * Constructs a full URL with the correct base path + * @param {string} path - The path to append (should start with /) + * @returns {string} Full URL with base path + * + * Example: + * - getFullUrl('/') with basePath '/video_query' → 'https://example.com/video_query/' + * - getFullUrl('/api/test') with basePath '/app' → 'https://example.com/app/api/test' + */ +export const getFullUrl = (path = '/') => { + const basePath = getBasePath(); + const origin = window.location.origin; + + // Ensure path starts with / + if (!path.startsWith('/')) { + path = '/' + path; + } + + const fullUrl = `${origin}${basePath}${path}`; + console.log(`PathUtils: Constructed URL: ${fullUrl} (basePath: ${basePath}, path: ${path})`); + return fullUrl; +}; + +/** + * Gets just the path part with base path (no domain) + * @param {string} path - The path to append (should start with /) + * @returns {string} Full path with base path + * + * Example: + * - getFullPath('/') with basePath '/video_query' → '/video_query/' + * - getFullPath('/callback') with basePath '/app' → '/app/callback' + */ +export const getFullPath = (path = '/') => { + const basePath = getBasePath(); + + // Ensure path starts with / + if (!path.startsWith('/')) { + path = '/' + path; + } + + return `${basePath}${path}`; +}; + +/** + * Constructs a redirect URI for authentication + * Uses the current domain and detected base path + * @param {string} path - Optional path after base (defaults to '/') + * @returns {string} Full redirect URI + */ +export const getRedirectUri = (path = '/') => { + return getFullUrl(path); +}; + +/** + * Checks if the current URL is at the expected base path + * Useful for determining if redirects worked correctly + */ +export const isAtBasePath = () => { + const basePath = getBasePath(); + const currentPath = window.location.pathname; + + if (basePath === '') { + return currentPath === '/'; + } + + return currentPath === basePath || currentPath === basePath + '/'; +}; + +/** + * Navigates to a path within the app (respects base path) + * @param {string} path - Path to navigate to + */ +export const navigateToPath = (path = '/') => { + const fullUrl = getFullUrl(path); + window.location.href = fullUrl; +}; + +/** + * Clears cached base path detection (useful for testing) + */ +export const clearCache = () => { + detectedBasePath = null; + configBasePath = null; +}; + +/** + * Gets debug information about path detection + */ +export const getDebugInfo = () => { + return { + currentPathname: window.location.pathname, + currentOrigin: window.location.origin, + detectedBasePath: detectBasePath(), + configBasePath: getConfigBasePath(), + effectiveBasePath: getBasePath(), + sampleRedirectUri: getRedirectUri(), + isAtBasePath: isAtBasePath() + }; +}; \ No newline at end of file