significant code changes and new config file to accommodate deploying to different server with different paths and URLs
This commit is contained in:
parent
ba0391bbc6
commit
df6a698098
16 changed files with 731 additions and 139 deletions
|
|
@ -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
|
||||
|
|
@ -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\"}"
|
||||
|
|
@ -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
16
frontend/public/config.js
Normal 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"
|
||||
}
|
||||
};
|
||||
16
frontend/public/config.json
Normal file
16
frontend/public/config.json
Normal 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"
|
||||
}
|
||||
}
|
||||
39
frontend/public/config.template.json
Normal file
39
frontend/public/config.template.json
Normal 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/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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...");
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
207
frontend/src/utils/configLoader.js
Normal file
207
frontend/src/utils/configLoader.js
Normal 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
|
||||
};
|
||||
};
|
||||
244
frontend/src/utils/pathUtils.js
Normal file
244
frontend/src/utils/pathUtils.js
Normal 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()
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue