significant code changes and new config file to accommodate deploying to different server with different paths and URLs

This commit is contained in:
michael 2025-09-18 17:55:30 -05:00
parent ba0391bbc6
commit df6a698098
16 changed files with 731 additions and 139 deletions

View file

@ -1,2 +1,5 @@
PUBLIC_URL=/video_query
REACT_APP_BASE_URL=/video_query
# 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

View file

@ -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/"
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\"}"

View file

@ -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"
},

16
frontend/public/config.js Normal file
View file

@ -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"
}
};

View file

@ -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"
}
}

View file

@ -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/"
}
}
}
}

View file

@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<base id="base-path-tag" href="./">
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
@ -15,6 +16,29 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<title>Video Query Tool</title>
<script>
// Early base path detection for static asset loading
(function() {
try {
var pathname = window.location.pathname;
var basePath = './';
if (pathname !== '/') {
var segments = pathname.split('/').filter(function(s) { return s.length > 0; });
if (segments.length > 0) {
// If we're in a subdirectory, use relative paths from that directory
basePath = './';
}
}
document.getElementById('base-path-tag').setAttribute('href', basePath);
console.log('Early base path set to:', basePath, 'for pathname:', pathname);
} catch (e) {
console.warn('Early base path detection failed, using default');
}
})();
</script>
<script src="./config.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View file

@ -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,

View file

@ -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 (
<div className="d-flex justify-content-center align-items-center" style={{ height: '100vh' }}>
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading authentication...</span>
<span className="visually-hidden">Loading configuration...</span>
</div>
<p className="ms-3">Loading configuration and initializing authentication...</p>
</div>
);
}
// Show error if initialization failed
if (initError) {
return (
<div className="d-flex justify-content-center align-items-center" style={{ height: '100vh' }}>
<div className="alert alert-danger" role="alert">
<h4 className="alert-heading">Configuration Error</h4>
<p>Failed to load application configuration:</p>
<p><strong>{initError.message}</strong></p>
<hr />
<p className="mb-0">Please ensure config.json is available and properly configured.</p>
</div>
</div>
);
}
// Only render MSAL provider if instance was created successfully
if (!msalInstance) {
return (
<div className="d-flex justify-content-center align-items-center" style={{ height: '100vh' }}>
<div className="alert alert-warning" role="alert">
<h4 className="alert-heading">Initialization Error</h4>
<p>MSAL instance was not created properly. Please refresh the page.</p>
</div>
<p className="ms-3">Initializing authentication...</p>
</div>
);
}

View file

@ -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...");

View file

@ -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"
};
// 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];
}
});

View file

@ -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);

View file

@ -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,

View file

@ -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: {

View file

@ -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<Object>} 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
};
};

View file

@ -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()
};
};