Skip to content

Commit e866ba5

Browse files
committed
Enable dev/prod environment switching in UI
Adds an environment switcher that persists active env in localStorage and resolves config from Azure runtime or build-time vars Updates env resolution to be environment-aware for backend, login, and OAuth client credentials Adds a user menu action to switch environments; clears facility/location/bay selections, wipes auto-saved script, logs out, sets a switch flag, and forces a reload to re-initialize cleanly Extends startup and injection scripts to surface/inject VITE_DEV_* and VITE_PROD_* values into runtime-config.js Documents required Azure App Service settings and the switcher workflow, including testing and troubleshooting Improves logging for visibility during initialization and switching Moves Azure env vars doc to technical docs Relocates the Azure environment variables guide into the technical docs section for better organization. Updates the technical README to link it, improving discoverability for App Service deployments. Strengthens docs on Azure secret management Clarifies not to commit secrets and to keep .env out of version control Recommends Azure Key Vault for production and adds a reference example Encourages separate OAuth creds for dev/prod and using placeholders in docs
1 parent 2869209 commit e866ba5

File tree

9 files changed

+432
-50
lines changed

9 files changed

+432
-50
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Azure App Service Environment Variables
2+
3+
This document lists all environment variables that need to be configured in Azure App Service for the environment switcher to work properly.
4+
5+
## Overview
6+
7+
The application supports switching between development and production environments from within the UI. To enable this in Azure App Service, you need to configure environment variables for **both** environments.
8+
9+
## Required Environment Variables
10+
11+
### Active Environment (Legacy/Default)
12+
These are used as fallbacks and for backward compatibility:
13+
14+
```bash
15+
VITE_BACKEND_BASE_URL=https://dr-cloud-api-dev.trackmangolfdev.com
16+
VITE_LOGIN_BASE_URL=https://tm-login-dev.trackmangolfdev.com
17+
VITE_OAUTH_WEB_CLIENT_ID=dr-web.4633fada-3b16-490f-8de7-2aa67158a1d6
18+
VITE_OAUTH_WEB_CLIENT_SECRET=7c870264-3703-4ec2-add8-5f8e57251d0e
19+
```
20+
21+
### Development Environment
22+
Required for switching to development environment:
23+
24+
```bash
25+
VITE_DEV_BACKEND_BASE_URL=https://dr-cloud-api-dev.trackmangolfdev.com
26+
VITE_DEV_LOGIN_BASE_URL=https://tm-login-dev.trackmangolfdev.com
27+
VITE_DEV_OAUTH_WEB_CLIENT_ID=<YOUR_DEV_CLIENT_ID>
28+
VITE_DEV_OAUTH_WEB_CLIENT_SECRET=<YOUR_DEV_CLIENT_SECRET>
29+
```
30+
31+
### Production Environment
32+
Required for switching to production environment:
33+
34+
```bash
35+
VITE_PROD_BACKEND_BASE_URL=https://dr-cloud-api.trackmangolf.com
36+
VITE_PROD_LOGIN_BASE_URL=https://tm-login.trackmangolf.com
37+
VITE_PROD_OAUTH_WEB_CLIENT_ID=<YOUR_PROD_CLIENT_ID>
38+
VITE_PROD_OAUTH_WEB_CLIENT_SECRET=<YOUR_PROD_CLIENT_SECRET>
39+
```
40+
41+
### Optional
42+
```bash
43+
VITE_NODE_ENV=production
44+
```
45+
46+
## How to Configure in Azure App Service
47+
48+
1. Go to your Azure App Service in the Azure Portal
49+
2. Navigate to **Configuration** > **Application settings**
50+
3. Click **+ New application setting** for each variable
51+
4. Add the **Name** and **Value** for each environment variable listed above
52+
5. Click **OK** and then **Save**
53+
6. Restart the App Service for changes to take effect
54+
55+
## How the Environment Switcher Works
56+
57+
### In Azure (Cloud)
58+
1. All environment variables are set in Azure App Service configuration
59+
2. `startup.sh` reads these variables and generates `runtime-config.js`
60+
3. The application loads `runtime-config.js` and makes both dev and prod configs available
61+
4. Users can click "Switch to Production/Development" in the user menu
62+
5. The app logs out, switches localStorage, and reloads with the new environment
63+
64+
### Locally (Development)
65+
1. Environment variables are defined in `.env` file
66+
2. Vite loads these at build time as `import.meta.env.*`
67+
3. The environment switcher reads from `.env` values
68+
4. Same switching behavior as in Azure
69+
70+
## Priority Order
71+
72+
The application checks for configuration in this order:
73+
74+
1. **Azure Runtime Config** (`window.runtimeConfig`) - Set by Azure App Service
75+
2. **Environment Switcher** (`localStorage`) - User's environment choice
76+
3. **Build-time Config** (`import.meta.env`) - From `.env` file during build
77+
78+
This ensures Azure configuration always takes precedence when deployed.
79+
80+
## Testing
81+
82+
To test the environment switcher:
83+
84+
1. Log into the application
85+
2. Click on your user avatar (top right)
86+
3. Click "Switch to Production" (or "Switch to Development")
87+
4. The app will log you out and reload
88+
5. You'll be prompted to log in using the new environment's OAuth
89+
6. Verify the backend calls are going to the correct environment
90+
91+
## Troubleshooting
92+
93+
### Menu item doesn't appear
94+
- Check that production OAuth credentials are properly configured (not placeholders)
95+
- Open browser console and check for `isProductionConfigured()` errors
96+
97+
### Switching doesn't work
98+
- Check browser console for localStorage errors
99+
- Verify all environment variables are set in Azure App Service
100+
- Check `runtime-config.js` is being generated correctly (view in browser DevTools)
101+
102+
### Getting 401 errors after switching
103+
- This is expected - you need to log in again after switching
104+
- The app should automatically redirect to OAuth login
105+
- If not, manually go to the login page
106+
107+
## Security Notes
108+
109+
- ⚠️ **Never commit secrets**: OAuth client secrets should never be committed to Git
110+
- 🔒 **Use Azure Key Vault**: For production, use Azure Key Vault references instead of plain text:
111+
```
112+
@Microsoft.KeyVault(SecretUri=https://your-vault.vault.azure.net/secrets/oauth-secret/)
113+
```
114+
- 🔐 **Separate credentials**: Use different OAuth apps/credentials for development and production environments
115+
- 📋 **Document placeholders**: Always use placeholders like `<YOUR_CLIENT_ID>` in documentation
116+
-**`.env` in `.gitignore`**: Ensure `.env` file is excluded from version control

docs/technical/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
### Deployment & DevOps
3535
- [Azure Deployment Setup](./AZURE_DEPLOYMENT_SETUP.md) - Azure Static Web App deployment configuration
36+
- [Azure Environment Variables](./AZURE_ENVIRONMENT_VARIABLES.md) - Environment variables for Azure App Service deployment
3637
- [Azure Front Door](./AZURE_FRONTDOOR.md) - Azure Front Door setup and configuration
3738
- [Docker Deployment](./DOCKER_DEPLOYMENT.md) - Docker containerization and deployment
3839
- [Deployment Fix](./DEPLOYMENT_FIX.md) - Deployment troubleshooting and fixes

inject-env.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ fi
1515
echo "📁 Found env-config.js file: $ENV_CONFIG_FILE"
1616

1717
# Replace placeholders with actual environment variable values
18+
# Active environment (backward compatibility)
1819
sed -i "s|__VITE_BACKEND_BASE_URL__|${VITE_BACKEND_BASE_URL:-}|g" "$ENV_CONFIG_FILE"
1920
sed -i "s|__VITE_LOGIN_BASE_URL__|${VITE_LOGIN_BASE_URL:-}|g" "$ENV_CONFIG_FILE"
2021
sed -i "s|__VITE_OAUTH_CLIENT_ID__|${VITE_OAUTH_CLIENT_ID:-}|g" "$ENV_CONFIG_FILE"
@@ -23,12 +24,27 @@ sed -i "s|__VITE_GRAPHQL_URL__|${VITE_GRAPHQL_URL:-}|g" "$ENV_CONFIG_FILE"
2324
sed -i "s|__VITE_OAUTH_TOKEN_URL__|${VITE_OAUTH_TOKEN_URL:-}|g" "$ENV_CONFIG_FILE"
2425
sed -i "s|__VITE_APP_COMMIT_SHA__|${VITE_APP_COMMIT_SHA:-}|g" "$ENV_CONFIG_FILE"
2526

27+
# Development environment variables (for environment switcher)
28+
sed -i "s|__VITE_DEV_BACKEND_BASE_URL__|${VITE_DEV_BACKEND_BASE_URL:-}|g" "$ENV_CONFIG_FILE"
29+
sed -i "s|__VITE_DEV_LOGIN_BASE_URL__|${VITE_DEV_LOGIN_BASE_URL:-}|g" "$ENV_CONFIG_FILE"
30+
sed -i "s|__VITE_DEV_OAUTH_WEB_CLIENT_ID__|${VITE_DEV_OAUTH_WEB_CLIENT_ID:-}|g" "$ENV_CONFIG_FILE"
31+
sed -i "s|__VITE_DEV_OAUTH_WEB_CLIENT_SECRET__|${VITE_DEV_OAUTH_WEB_CLIENT_SECRET:-}|g" "$ENV_CONFIG_FILE"
32+
33+
# Production environment variables (for environment switcher)
34+
sed -i "s|__VITE_PROD_BACKEND_BASE_URL__|${VITE_PROD_BACKEND_BASE_URL:-}|g" "$ENV_CONFIG_FILE"
35+
sed -i "s|__VITE_PROD_LOGIN_BASE_URL__|${VITE_PROD_LOGIN_BASE_URL:-}|g" "$ENV_CONFIG_FILE"
36+
sed -i "s|__VITE_PROD_OAUTH_WEB_CLIENT_ID__|${VITE_PROD_OAUTH_WEB_CLIENT_ID:-}|g" "$ENV_CONFIG_FILE"
37+
sed -i "s|__VITE_PROD_OAUTH_WEB_CLIENT_SECRET__|${VITE_PROD_OAUTH_WEB_CLIENT_SECRET:-}|g" "$ENV_CONFIG_FILE"
38+
2639
echo "✅ Environment variables injected successfully!"
2740
echo "🌐 Backend Base URL: ${VITE_BACKEND_BASE_URL:-'NOT SET'}"
2841
echo "🔐 Login Base URL: ${VITE_LOGIN_BASE_URL:-'NOT SET'}"
2942
echo "🔑 OAuth Client ID: ${VITE_OAUTH_CLIENT_ID:+SET}"
3043
echo "🔒 OAuth Client Secret: ${VITE_OAUTH_CLIENT_SECRET:+SET}"
3144

45+
echo "🔧 Development URLs: ${VITE_DEV_BACKEND_BASE_URL:-'NOT SET'}"
46+
echo "🔧 Production URLs: ${VITE_PROD_BACKEND_BASE_URL:-'NOT SET'}"
47+
3248
# Show the injected config for debugging
3349
echo "📄 Injected env-config.js content:"
3450
head -20 "$ENV_CONFIG_FILE"

src/App.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,43 @@ export default function App() {
7171
// Enable auto-save to browser's localStorage (debounced, async)
7272
useAutoSaveScript(script, true);
7373

74+
// Function to clear all facility/location/bay selections (for environment switching)
75+
const handleClearSelections = async () => {
76+
console.log('🧹 Clearing all facility/location/bay selections...');
77+
try {
78+
// Clear backend-persisted selections
79+
await saveSelection('FACILITY_ID', null);
80+
await saveSelection('LOCATION_ID', null);
81+
await saveSelection('BAY_ID', null);
82+
83+
// Clear local state
84+
setSelectedFacility(null);
85+
setSelectedFacilityId(null);
86+
setSelectedLocation(null);
87+
setSelectedBayId(null);
88+
setSelectedBayObj(null);
89+
90+
console.log('✅ All selections cleared successfully');
91+
} catch (error) {
92+
console.error('❌ Error clearing selections:', error);
93+
throw error;
94+
}
95+
};
96+
7497
// Restore auto-saved script on mount (only once)
7598
useEffect(() => {
7699
if (hasRestoredAutoSave.current) return;
77100

101+
// Check if we're in the middle of an environment switch
102+
const isSwitchingEnvironment = localStorage.getItem('environment-switching') === 'true';
103+
if (isSwitchingEnvironment) {
104+
console.log('🔄 Environment switch detected, clearing auto-save and skipping restore prompt');
105+
clearAutoSavedScript();
106+
localStorage.removeItem('environment-switching');
107+
hasRestoredAutoSave.current = true;
108+
return;
109+
}
110+
78111
const autoSaved = loadAutoSavedScript();
79112
if (autoSaved) {
80113
// Ask user if they want to restore
@@ -600,6 +633,7 @@ export default function App() {
600633
selectedFacility={selectedFacility}
601634
selectedFacilityId={selectedFacilityId}
602635
onFacilitySelect={handleFacilitySelect}
636+
onClearSelections={handleClearSelections}
603637
activeTab={activeTab}
604638
setActiveTab={setActiveTab}
605639
state={state}

src/app/AppShell.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface AppShellProps {
1818
selectedFacility: any;
1919
selectedFacilityId: string | null;
2020
onFacilitySelect: (f: any) => void;
21+
onClearSelections?: () => Promise<void>;
2122
activeTab: TabType;
2223
setActiveTab: (t: TabType) => void;
2324
// editor state and handlers (kept generic to avoid tight coupling in this step)
@@ -51,6 +52,7 @@ export const AppShell: React.FC<AppShellProps> = (props) => {
5152
selectedFacility,
5253
selectedFacilityId,
5354
onFacilitySelect,
55+
onClearSelections,
5456
activeTab,
5557
setActiveTab,
5658
state,
@@ -88,6 +90,7 @@ export const AppShell: React.FC<AppShellProps> = (props) => {
8890
selectedFacility={selectedFacility}
8991
selectedFacilityId={selectedFacilityId}
9092
onFacilitySelect={onFacilitySelect}
93+
onClearSelections={onClearSelections}
9194
/>
9295
<TabBar
9396
activeTab={activeTab}

src/components/TopBar/index.tsx

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import React, { useState, useRef, useEffect } from 'react';
22
import { FacilitySelectorPortal } from '../FacilitySelector';
33
import { BuildVersion } from './BuildVersion';
44
import { useAuth } from '../../lib/AuthProvider';
5+
import {
6+
getActiveEnvironment,
7+
setActiveEnvironment,
8+
getEnvironmentLabel,
9+
isProductionConfigured
10+
} from '../../lib/environment-switcher';
11+
import { clearAutoSavedScript } from '../../hooks/useAutoSaveScript';
512

613
interface Facility {
714
id: string;
@@ -11,20 +18,24 @@ interface Facility {
1118
apiDeveloperAccess?: string | null;
1219
}
1320

14-
interface TopBarProps {
21+
export interface TopBarProps {
1522
selectedFacility: Facility | null;
1623
selectedFacilityId: string | null;
1724
onFacilitySelect: (facility: Facility | null) => void;
25+
onClearSelections?: () => Promise<void>;
1826
}
1927

2028
export const TopBar: React.FC<TopBarProps> = ({
2129
selectedFacility,
2230
selectedFacilityId,
23-
onFacilitySelect
31+
onFacilitySelect,
32+
onClearSelections
2433
}) => {
2534
const { isAuthenticated, isLoading, logout, profile } = useAuth();
2635
const [menuOpen, setMenuOpen] = useState(false);
2736
const avatarRef = useRef<HTMLDivElement>(null);
37+
const [currentEnv, setCurrentEnv] = useState(getActiveEnvironment());
38+
const prodConfigured = isProductionConfigured();
2839

2940
// Close menu on outside click for avatar menu
3041
useEffect(() => {
@@ -43,6 +54,49 @@ export const TopBar: React.FC<TopBarProps> = ({
4354
logout();
4455
setMenuOpen(false);
4556
};
57+
58+
const handleEnvironmentSwitch = async () => {
59+
const targetEnv: 'dev' | 'prod' = currentEnv === 'dev' ? 'prod' : 'dev';
60+
console.log(`🔄 Starting environment switch: ${currentEnv}${targetEnv}`);
61+
setMenuOpen(false);
62+
63+
try {
64+
// Step 1: Clear facility/location/bay selections from backend
65+
console.log('📝 Clearing facility/location/bay selections...');
66+
if (onClearSelections) {
67+
await onClearSelections();
68+
console.log('✅ Selections cleared');
69+
}
70+
71+
// Step 2: Clear auto-saved script from browser cache
72+
console.log('🗑️ Clearing auto-saved script...');
73+
clearAutoSavedScript();
74+
console.log('✅ Auto-saved script cleared');
75+
76+
// Step 3: Logout from current environment
77+
console.log('🔐 Logging out from current environment...');
78+
await logout();
79+
console.log('✅ Logged out');
80+
81+
// Step 4: Set target environment in localStorage EXPLICITLY
82+
// Don't use switchEnvironment() because logout may have cleared localStorage
83+
console.log(`🔀 Setting environment to: ${targetEnv}`);
84+
setActiveEnvironment(targetEnv);
85+
86+
// Set a flag to indicate we're switching environments (used to skip restore prompt)
87+
localStorage.setItem('environment-switching', 'true');
88+
console.log(`✅ Environment set to: ${targetEnv}`);
89+
90+
// Step 5: Force full page reload to re-initialize with new environment
91+
// This ensures we don't try to read from localStorage - the reload will handle everything
92+
console.log('🔄 Reloading page...');
93+
window.location.href = '/';
94+
} catch (error) {
95+
console.error('❌ Error during environment switch:', error);
96+
// Even on error, try to reload to get to a clean state
97+
window.location.href = '/';
98+
}
99+
};
46100

47101
const handleFacilitySelect = (facility: Facility | null) => {
48102
onFacilitySelect(facility);
@@ -95,6 +149,28 @@ export const TopBar: React.FC<TopBarProps> = ({
95149
<div className="top-bar-user-menu topbar-user-menu-absolute">
96150
<div className="top-bar-user-menu-title">{profile.fullName || ''}</div>
97151
<hr className="top-bar-user-menu-divider" />
152+
153+
{/* Environment Switcher - Only show if production is configured */}
154+
{prodConfigured && (
155+
<>
156+
<button
157+
className="top-bar-user-menu-item top-bar-user-menu-link"
158+
onMouseDown={handleEnvironmentSwitch}
159+
>
160+
<span className="top-bar-user-menu-row">
161+
{/* Environment Switch SVG */}
162+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
163+
<path fillRule="evenodd" clipRule="evenodd" d="M8 1a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0v-5.5A.75.75 0 0 1 8 1z" fill="#414141"/>
164+
<path fillRule="evenodd" clipRule="evenodd" d="M3 8a5 5 0 1 1 10 0A5 5 0 0 1 3 8zm5-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13z" fill="#414141"/>
165+
<path d="M11 8.5a.5.5 0 0 0-1 0v1.793l-.646-.647a.5.5 0 1 0-.708.708l1.5 1.5a.5.5 0 0 0 .708 0l1.5-1.5a.5.5 0 0 0-.708-.708L11 10.293V8.5z" fill="#414141"/>
166+
</svg>
167+
Switch to {currentEnv === 'dev' ? 'Production' : 'Development'}
168+
</span>
169+
</button>
170+
<hr className="top-bar-user-menu-divider" />
171+
</>
172+
)}
173+
98174
<button
99175
className="top-bar-user-menu-item top-bar-user-menu-link"
100176
onMouseDown={handleLogoutClick}

0 commit comments

Comments
 (0)