diff --git a/web-integrations/google-secure-signals/react-client-side/package-lock.json b/web-integrations/google-secure-signals/react-client-side/package-lock.json index cb3da66..5add2a3 100644 --- a/web-integrations/google-secure-signals/react-client-side/package-lock.json +++ b/web-integrations/google-secure-signals/react-client-side/package-lock.json @@ -23,6 +23,7 @@ }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.0", + "cross-env": "^7.0.3", "dotenv-cli": "^10.0.0" } }, @@ -5495,6 +5496,24 @@ "node": ">=14" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", diff --git a/web-integrations/google-secure-signals/react-client-side/package.json b/web-integrations/google-secure-signals/react-client-side/package.json index 60e3894..10cd53c 100644 --- a/web-integrations/google-secure-signals/react-client-side/package.json +++ b/web-integrations/google-secure-signals/react-client-side/package.json @@ -18,6 +18,7 @@ }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.0", + "cross-env": "^7.0.3", "dotenv-cli": "^10.0.0" }, "overrides": { @@ -28,7 +29,7 @@ }, "scripts": { "start": "node server.js", - "dev": "PORT=3044 dotenv -e ../../../.env -- react-scripts start", + "dev": "cross-env PORT=3044 dotenv -e ../../../.env -- react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" diff --git a/web-integrations/google-secure-signals/react-client-side/server.js b/web-integrations/google-secure-signals/react-client-side/server.js index 871a320..e96cf72 100644 --- a/web-integrations/google-secure-signals/react-client-side/server.js +++ b/web-integrations/google-secure-signals/react-client-side/server.js @@ -1,6 +1,11 @@ -require('dotenv').config({ path: '../../../.env' }); - -console.log('process.env', process.env); +// Load environment variables from .env file (for local development) +// In Docker, env_file in docker-compose.yml already provides these, so this is optional +try { + require('dotenv').config({ path: '../../../.env' }); +} catch (error) { + // Silently fail if .env file doesn't exist (e.g., in Docker where env vars are provided via env_file) + // This is expected behavior when running in Docker +} const fs = require('fs'); const path = require('path'); @@ -21,9 +26,19 @@ app.get('/ops/healthcheck', (req, res) => { // Helper function to serve index.html with environment variable replacement function serveIndexHtml(req, res) { // Try build directory first (production), then public (development) - let indexPath = path.join(__dirname, 'build', 'index.html'); - if (!fs.existsSync(indexPath)) { - indexPath = path.join(__dirname, 'public', 'index.html'); + const buildPath = path.join(__dirname, 'build'); + const buildIndexPath = path.join(buildPath, 'index.html'); + const publicIndexPath = path.join(__dirname, 'public', 'index.html'); + + let indexPath; + if (fs.existsSync(buildIndexPath)) { + indexPath = buildIndexPath; + } else if (fs.existsSync(publicIndexPath)) { + console.warn('Warning: build directory not found. Serving from public directory. Run "npm run build" to build the React app.'); + indexPath = publicIndexPath; + } else { + res.status(500).send('Error: Neither build nor public index.html found. Please run "npm run build" first.'); + return; } let html = fs.readFileSync(indexPath, 'utf8'); @@ -36,18 +51,40 @@ function serveIndexHtml(req, res) { html = html.replace(/__UID_JS_SDK_URL_PLACEHOLDER__/g, uidJsSdkUrl); html = html.replace(/__PUBLIC_URL_PLACEHOLDER__/g, publicUrl); - // Debug: log if replacement happened + // Verify replacement worked - if placeholder still exists, log error if (html.includes('__UID_JS_SDK_URL_PLACEHOLDER__')) { - console.warn('Warning: Placeholder __UID_JS_SDK_URL_PLACEHOLDER__ was not replaced in', indexPath); + console.error('ERROR: Placeholder __UID_JS_SDK_URL_PLACEHOLDER__ was not replaced in', indexPath); } + // Inject runtime environment variables as a script tag + // This allows the React app to read environment variables at runtime (for Kubernetes) + const runtimeEnv = { + REACT_APP_UID_JS_SDK_NAME: process.env.REACT_APP_UID_JS_SDK_NAME, + REACT_APP_UID_CLIENT_BASE_URL: process.env.REACT_APP_UID_CLIENT_BASE_URL, + REACT_APP_UID_SECURE_SIGNALS_SDK_URL: process.env.REACT_APP_UID_SECURE_SIGNALS_SDK_URL, + REACT_APP_UID_SECURE_SIGNALS_STORAGE_KEY: process.env.REACT_APP_UID_SECURE_SIGNALS_STORAGE_KEY, + REACT_APP_IDENTITY_NAME: process.env.REACT_APP_IDENTITY_NAME, + REACT_APP_DOCS_BASE_URL: process.env.REACT_APP_DOCS_BASE_URL, + REACT_APP_UID_CSTG_SUBSCRIPTION_ID: process.env.REACT_APP_UID_CSTG_SUBSCRIPTION_ID, + REACT_APP_UID_CSTG_SERVER_PUBLIC_KEY: process.env.REACT_APP_UID_CSTG_SERVER_PUBLIC_KEY, + }; + + // Inject script tag before closing tag + const envScript = ``; + html = html.replace('', `${envScript}`); + + // Set content type explicitly + res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(html); } -// Route handler for index - must be before static middleware +// Route handlers for index - must be before static middleware +// These must be registered BEFORE express.static() to ensure they intercept index.html requests app.get('/', serveIndexHtml); +app.get('/index.html', serveIndexHtml); // Serve static files from build directory (production) or public (development) +// IMPORTANT: This must come AFTER the route handlers to ensure index.html is processed by serveIndexHtml const buildPath = path.join(__dirname, 'build'); const publicPath = path.join(__dirname, 'public'); @@ -74,5 +111,14 @@ app.get('*', (req, res, next) => { }); app.listen(port, () => { - console.log(`Example app listening at http://localhost:${port}`); + const buildPath = path.join(__dirname, 'build'); + if (fs.existsSync(buildPath)) { + console.log(`Example app listening at http://localhost:${port}`); + console.log('Serving production build from build/ directory'); + } else { + console.log(`Example app listening at http://localhost:${port}`); + console.warn('WARNING: build/ directory not found. Serving from public/ directory.'); + console.warn('For production, run "npm run build" first.'); + console.warn('For development, use "npm run dev" instead.'); + } }); diff --git a/web-integrations/google-secure-signals/react-client-side/src/SecureSignalsApp.tsx b/web-integrations/google-secure-signals/react-client-side/src/SecureSignalsApp.tsx index 9ae8536..7d40c8f 100644 --- a/web-integrations/google-secure-signals/react-client-side/src/SecureSignalsApp.tsx +++ b/web-integrations/google-secure-signals/react-client-side/src/SecureSignalsApp.tsx @@ -7,23 +7,46 @@ declare global { getAdvertisingToken: any; google: any; googletag: any; + __REACT_APP_ENV__?: { + REACT_APP_UID_JS_SDK_NAME?: string; + REACT_APP_UID_CLIENT_BASE_URL?: string; + REACT_APP_UID_SECURE_SIGNALS_SDK_URL?: string; + REACT_APP_UID_SECURE_SIGNALS_STORAGE_KEY?: string; + REACT_APP_IDENTITY_NAME?: string; + REACT_APP_DOCS_BASE_URL?: string; + REACT_APP_UID_CSTG_SUBSCRIPTION_ID?: string; + REACT_APP_UID_CSTG_SERVER_PUBLIC_KEY?: string; + }; } } // Declare global variables declare const google: any; -// Environment variables -const UID_JS_SDK_NAME = process.env.REACT_APP_UID_JS_SDK_NAME; -const UID_BASE_URL = process.env.REACT_APP_UID_CLIENT_BASE_URL; -const SECURE_SIGNALS_SDK_URL = process.env.REACT_APP_UID_SECURE_SIGNALS_SDK_URL; -const SECURE_SIGNALS_STORAGE_KEY = process.env.REACT_APP_UID_SECURE_SIGNALS_STORAGE_KEY; -const IDENTITY_NAME = process.env.REACT_APP_IDENTITY_NAME; -const DOCS_BASE_URL = process.env.REACT_APP_DOCS_BASE_URL; +// Helper function to get environment variables from runtime (Kubernetes) or build-time +function getEnvVar(key: string): string | undefined { + // First try runtime environment (injected by server.js for Kubernetes) + if (typeof window !== 'undefined' && window.__REACT_APP_ENV__) { + const value = window.__REACT_APP_ENV__[key as keyof typeof window.__REACT_APP_ENV__]; + if (value !== undefined && value !== null) { + return value; + } + } + // Fallback to build-time environment variable + return process.env[key]; +} + +// Environment variables (read from runtime or build-time) +const UID_JS_SDK_NAME = getEnvVar('REACT_APP_UID_JS_SDK_NAME') || '__uid2'; +const UID_BASE_URL = getEnvVar('REACT_APP_UID_CLIENT_BASE_URL'); +const SECURE_SIGNALS_SDK_URL = getEnvVar('REACT_APP_UID_SECURE_SIGNALS_SDK_URL') || ''; +const SECURE_SIGNALS_STORAGE_KEY = getEnvVar('REACT_APP_UID_SECURE_SIGNALS_STORAGE_KEY') || ''; +const IDENTITY_NAME = getEnvVar('REACT_APP_IDENTITY_NAME') || 'UID2'; +const DOCS_BASE_URL = getEnvVar('REACT_APP_DOCS_BASE_URL') || 'https://unifiedid.com/docs'; const clientSideIdentityOptions = { - subscriptionId: process.env.REACT_APP_UID_CSTG_SUBSCRIPTION_ID, - serverPublicKey: process.env.REACT_APP_UID_CSTG_SERVER_PUBLIC_KEY, + subscriptionId: getEnvVar('REACT_APP_UID_CSTG_SUBSCRIPTION_ID'), + serverPublicKey: getEnvVar('REACT_APP_UID_CSTG_SERVER_PUBLIC_KEY'), }; const SecureSignalsApp = () => { @@ -49,10 +72,19 @@ const SecureSignalsApp = () => { const loginAttemptedRef = useRef(false); // Helper function to get SDK instance - const getSDK = () => window[UID_JS_SDK_NAME]; + const getSDK = () => { + const sdk = window[UID_JS_SDK_NAME]; + if (!sdk) { + console.error(`SDK not found at window.${UID_JS_SDK_NAME}. Make sure the SDK script is loaded.`); + } + return sdk; + }; const updateElements = useCallback((status) => { const sdk = getSDK(); + if (!sdk) { + return; + } const token = sdk.getAdvertisingToken(); // Check for opt-out: only if user attempted login, and we got identity null with no token @@ -178,9 +210,40 @@ const SecureSignalsApp = () => { useEffect(() => { // Add callbacks for UID2/EUID JS SDK let sdk = getSDK(); - sdk = sdk || { callbacks: [] }; + if (!sdk) { + // SDK not loaded yet, wait for it + const checkSDK = setInterval(() => { + sdk = getSDK(); + if (sdk) { + clearInterval(checkSDK); + if (!sdk.callbacks) { + sdk.callbacks = []; + } + sdk.callbacks.push(onIdentityUpdated); + sdk.callbacks.push((eventType: string, payload: any) => { + if (eventType === 'SdkLoaded') { + sdk.init({ + baseUrl: UID_BASE_URL, + }); + } + if (eventType === 'InitCompleted') { + if (sdk.isLoginRequired()) { + sdk.setIdentity(identity); + setIdentity(identity); + } + } + }); + } + }, 100); + return () => clearInterval(checkSDK); + } + + // SDK is available, set up callbacks + if (!sdk.callbacks) { + sdk.callbacks = []; + } sdk.callbacks.push(onIdentityUpdated); - sdk.callbacks.push((eventType, payload) => { + sdk.callbacks.push((eventType: string, payload: any) => { if (eventType === 'SdkLoaded') { sdk.init({ baseUrl: UID_BASE_URL, @@ -261,6 +324,18 @@ const SecureSignalsApp = () => { try { const sdk = getSDK(); + if (!sdk) { + console.error('SDK not available. Make sure the SDK script is loaded.'); + return; + } + + // Check if crypto.subtle is available (required for SDK) + if (typeof window !== 'undefined' && !window.crypto?.subtle) { + console.error('crypto.subtle is not available. This requires HTTPS or a secure context.'); + alert('crypto.subtle is not available. Please access this page over HTTPS.'); + return; + } + await sdk.setIdentityFromEmail(email, clientSideIdentityOptions); loadSecureSignals(); } catch (e) { diff --git a/web-integrations/javascript-sdk/react-client-side/package-lock.json b/web-integrations/javascript-sdk/react-client-side/package-lock.json index 2fbe785..3531894 100644 --- a/web-integrations/javascript-sdk/react-client-side/package-lock.json +++ b/web-integrations/javascript-sdk/react-client-side/package-lock.json @@ -23,6 +23,7 @@ }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.0", + "cross-env": "^7.0.3", "dotenv-cli": "^10.0.0" } }, @@ -5523,6 +5524,24 @@ "node": ">=14" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", diff --git a/web-integrations/javascript-sdk/react-client-side/package.json b/web-integrations/javascript-sdk/react-client-side/package.json index ff49107..8de1da8 100644 --- a/web-integrations/javascript-sdk/react-client-side/package.json +++ b/web-integrations/javascript-sdk/react-client-side/package.json @@ -18,6 +18,7 @@ }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.0", + "cross-env": "^7.0.3", "dotenv-cli": "^10.0.0" }, "overrides": { @@ -28,7 +29,7 @@ }, "scripts": { "start": "node server.js", - "dev": "PORT=3034 dotenv -e ../../../.env -- react-scripts start", + "dev": "cross-env PORT=3034 dotenv -e ../../../.env -- react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" diff --git a/web-integrations/javascript-sdk/react-client-side/server.js b/web-integrations/javascript-sdk/react-client-side/server.js index d219188..4e9feef 100644 --- a/web-integrations/javascript-sdk/react-client-side/server.js +++ b/web-integrations/javascript-sdk/react-client-side/server.js @@ -1,6 +1,11 @@ -require('dotenv').config({ path: '../../../.env' }); - -console.log('process.env', process.env); +// Load environment variables from .env file (for local development) +// In Docker, env_file in docker-compose.yml already provides these, so this is optional +try { + require('dotenv').config({ path: '../../../.env' }); +} catch (error) { + // Silently fail if .env file doesn't exist (e.g., in Docker where env vars are provided via env_file) + // This is expected behavior when running in Docker +} const fs = require('fs'); const path = require('path'); @@ -21,9 +26,19 @@ app.get('/ops/healthcheck', (req, res) => { // Helper function to serve index.html with environment variable replacement function serveIndexHtml(req, res) { // Try build directory first (production), then public (development) - let indexPath = path.join(__dirname, 'build', 'index.html'); - if (!fs.existsSync(indexPath)) { - indexPath = path.join(__dirname, 'public', 'index.html'); + const buildPath = path.join(__dirname, 'build'); + const buildIndexPath = path.join(buildPath, 'index.html'); + const publicIndexPath = path.join(__dirname, 'public', 'index.html'); + + let indexPath; + if (fs.existsSync(buildIndexPath)) { + indexPath = buildIndexPath; + } else if (fs.existsSync(publicIndexPath)) { + console.warn('Warning: build directory not found. Serving from public directory. Run "npm run build" to build the React app.'); + indexPath = publicIndexPath; + } else { + res.status(500).send('Error: Neither build nor public index.html found. Please run "npm run build" first.'); + return; } let html = fs.readFileSync(indexPath, 'utf8'); @@ -36,6 +51,21 @@ function serveIndexHtml(req, res) { html = html.replace(/__UID_JS_SDK_URL_PLACEHOLDER__/g, uidJsSdkUrl); html = html.replace(/__PUBLIC_URL_PLACEHOLDER__/g, publicUrl); + // Inject runtime environment variables as a script tag + // This allows the React app to read environment variables at runtime (for Kubernetes) + const runtimeEnv = { + REACT_APP_UID_JS_SDK_NAME: process.env.REACT_APP_UID_JS_SDK_NAME, + REACT_APP_UID_CLIENT_BASE_URL: process.env.REACT_APP_UID_CLIENT_BASE_URL, + REACT_APP_IDENTITY_NAME: process.env.REACT_APP_IDENTITY_NAME, + REACT_APP_DOCS_BASE_URL: process.env.REACT_APP_DOCS_BASE_URL, + REACT_APP_UID_CSTG_SUBSCRIPTION_ID: process.env.REACT_APP_UID_CSTG_SUBSCRIPTION_ID, + REACT_APP_UID_CSTG_SERVER_PUBLIC_KEY: process.env.REACT_APP_UID_CSTG_SERVER_PUBLIC_KEY, + }; + + // Inject script tag before closing tag + const envScript = ``; + html = html.replace('', `${envScript}`); + // Debug: log if replacement happened if (html.includes('__UID_JS_SDK_URL_PLACEHOLDER__')) { console.warn('Warning: Placeholder __UID_JS_SDK_URL_PLACEHOLDER__ was not replaced in', indexPath); @@ -74,6 +104,15 @@ app.get('*', (req, res, next) => { }); app.listen(port, () => { - console.log(`Example app listening at http://localhost:${port}`); + const buildPath = path.join(__dirname, 'build'); + if (fs.existsSync(buildPath)) { + console.log(`Example app listening at http://localhost:${port}`); + console.log('Serving production build from build/ directory'); + } else { + console.log(`Example app listening at http://localhost:${port}`); + console.warn('WARNING: build/ directory not found. Serving from public/ directory.'); + console.warn('For production, run "npm run build" first.'); + console.warn('For development, use "npm run dev" instead.'); + } }); diff --git a/web-integrations/javascript-sdk/react-client-side/src/ClientSideApp.tsx b/web-integrations/javascript-sdk/react-client-side/src/ClientSideApp.tsx index 539e6a8..6507f43 100644 --- a/web-integrations/javascript-sdk/react-client-side/src/ClientSideApp.tsx +++ b/web-integrations/javascript-sdk/react-client-side/src/ClientSideApp.tsx @@ -7,15 +7,34 @@ declare global { } } -// Environment variables -const UID_JS_SDK_NAME = process.env.REACT_APP_UID_JS_SDK_NAME; -const UID_BASE_URL = process.env.REACT_APP_UID_CLIENT_BASE_URL; -const IDENTITY_NAME = process.env.REACT_APP_IDENTITY_NAME; -const DOCS_BASE_URL = process.env.REACT_APP_DOCS_BASE_URL; +// Extend Window interface for runtime environment variables +interface ReactAppEnv { + [key: string]: string | undefined; +} + +// Helper function to get environment variables from runtime (Kubernetes) or build-time +function getEnvVar(key: string): string | undefined { + // First try runtime environment (injected by server.js for Kubernetes) + if (typeof window !== 'undefined' && (window as any).__REACT_APP_ENV__) { + const env = (window as any).__REACT_APP_ENV__ as ReactAppEnv; + const value = env[key]; + if (value !== undefined && value !== null) { + return value; + } + } + // Fallback to build-time environment variable + return process.env[key]; +} + +// Environment variables (read from runtime or build-time) +const UID_JS_SDK_NAME = getEnvVar('REACT_APP_UID_JS_SDK_NAME') || '__uid2'; +const UID_BASE_URL = getEnvVar('REACT_APP_UID_CLIENT_BASE_URL'); +const IDENTITY_NAME = getEnvVar('REACT_APP_IDENTITY_NAME') || 'UID2'; +const DOCS_BASE_URL = getEnvVar('REACT_APP_DOCS_BASE_URL') || 'https://unifiedid.com/docs'; const clientSideIdentityOptions = { - subscriptionId: process.env.REACT_APP_UID_CSTG_SUBSCRIPTION_ID, - serverPublicKey: process.env.REACT_APP_UID_CSTG_SERVER_PUBLIC_KEY, + subscriptionId: getEnvVar('REACT_APP_UID_CSTG_SUBSCRIPTION_ID'), + serverPublicKey: getEnvVar('REACT_APP_UID_CSTG_SERVER_PUBLIC_KEY'), }; const ClientSideApp = () => {