Backup: 2025-06-07 18:32 - Production setup complete
[Restore from backup: vip-coordinator-backup-2025-06-07-18-32-production-setup-complete]
This commit is contained in:
@@ -1,16 +1,14 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { apiCall, API_BASE_URL } from '../config/api';
|
||||
import { apiCall } from '../config/api';
|
||||
import { generateTestVips, getTestOrganizations, generateVipSchedule } from '../utils/testVipData';
|
||||
|
||||
interface ApiKeys {
|
||||
aviationStackKey?: string;
|
||||
googleMapsKey?: string;
|
||||
twilioKey?: string;
|
||||
auth0Domain?: string;
|
||||
auth0ClientId?: string;
|
||||
auth0ClientSecret?: string;
|
||||
auth0Audience?: string;
|
||||
googleClientId?: string;
|
||||
googleClientSecret?: string;
|
||||
}
|
||||
|
||||
interface SystemSettings {
|
||||
@@ -22,89 +20,92 @@ interface SystemSettings {
|
||||
|
||||
const AdminDashboard: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [adminPassword, setAdminPassword] = useState('');
|
||||
const [apiKeys, setApiKeys] = useState<ApiKeys>({});
|
||||
const [systemSettings, setSystemSettings] = useState<SystemSettings>({});
|
||||
const [testResults, setTestResults] = useState<{ [key: string]: string }>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saveStatus, setSaveStatus] = useState<string | null>(null);
|
||||
const [showKeys, setShowKeys] = useState<{ [key: string]: boolean }>({});
|
||||
const [savedKeys, setSavedKeys] = useState<{ [key: string]: boolean }>({});
|
||||
const [maskedKeyHints, setMaskedKeyHints] = useState<{ [key: string]: string }>({});
|
||||
const [testDataLoading, setTestDataLoading] = useState(false);
|
||||
const [testDataStatus, setTestDataStatus] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const buildAuthHeaders = (includeJson = false) => {
|
||||
const headers: Record<string, string> = {};
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null;
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
useEffect(() => {
|
||||
// Check if already authenticated
|
||||
const authStatus = sessionStorage.getItem('adminAuthenticated');
|
||||
if (authStatus === 'true') {
|
||||
setIsAuthenticated(true);
|
||||
loadSettings();
|
||||
}
|
||||
if (includeJson) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}, []);
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/authenticate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password: adminPassword })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setIsAuthenticated(true);
|
||||
sessionStorage.setItem('adminAuthenticated', 'true');
|
||||
loadSettings();
|
||||
} else {
|
||||
alert('Invalid admin password');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Authentication failed');
|
||||
}
|
||||
return headers;
|
||||
};
|
||||
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await apiCall('/api/admin/settings', {
|
||||
headers: buildAuthHeaders()
|
||||
const response = await fetch('/api/admin/settings', {
|
||||
headers: {
|
||||
'Admin-Auth': sessionStorage.getItem('adminAuthenticated') || ''
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
// Track which keys are already saved (masked keys start with ***)
|
||||
const saved: { [key: string]: boolean } = {};
|
||||
const maskedHints: { [key: string]: string } = {};
|
||||
const cleanedApiKeys: ApiKeys = {};
|
||||
|
||||
if (data.apiKeys) {
|
||||
Object.entries(data.apiKeys).forEach(([key, value]) => {
|
||||
if (value && (value as string).startsWith('***')) {
|
||||
saved[key] = true;
|
||||
maskedHints[key] = value as string;
|
||||
} else if (value) {
|
||||
}
|
||||
});
|
||||
}
|
||||
setSavedKeys(saved);
|
||||
// Don't load masked keys as actual values - keep them empty
|
||||
const cleanedApiKeys: ApiKeys = {};
|
||||
if (data.apiKeys) {
|
||||
Object.entries(data.apiKeys).forEach(([key, value]) => {
|
||||
// Only set the value if it's not a masked key
|
||||
if (value && !(value as string).startsWith('***')) {
|
||||
cleanedApiKeys[key as keyof ApiKeys] = value as string;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setSavedKeys(saved);
|
||||
setMaskedKeyHints(maskedHints);
|
||||
setApiKeys(cleanedApiKeys);
|
||||
setSystemSettings(data.systemSettings || {});
|
||||
} else if (response.status === 403) {
|
||||
setError('You need administrator access to view this page.');
|
||||
} else if (response.status === 401) {
|
||||
setError('Authentication required. Please sign in again.');
|
||||
} else {
|
||||
setError('Failed to load admin settings.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load settings:', err);
|
||||
setError('Failed to load admin settings.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
}, []);
|
||||
|
||||
const handleApiKeyChange = (key: keyof ApiKeys, value: string) => {
|
||||
setApiKeys(prev => ({ ...prev, [key]: value }));
|
||||
// If user is typing a new key, mark it as not saved anymore
|
||||
if (value && !value.startsWith('***')) {
|
||||
setSavedKeys(prev => ({ ...prev, [key]: false }));
|
||||
setMaskedKeyHints(prev => {
|
||||
const next = { ...prev };
|
||||
delete next[key];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -116,9 +117,12 @@ const AdminDashboard: React.FC = () => {
|
||||
setTestResults(prev => ({ ...prev, [apiType]: 'Testing...' }));
|
||||
|
||||
try {
|
||||
const response = await apiCall(`/api/admin/test-api/${apiType}`, {
|
||||
const response = await fetch(`/api/admin/test-api/${apiType}`, {
|
||||
method: 'POST',
|
||||
headers: buildAuthHeaders(true),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Admin-Auth': sessionStorage.getItem('adminAuthenticated') || ''
|
||||
},
|
||||
body: JSON.stringify({
|
||||
apiKey: apiKeys[apiType as keyof ApiKeys]
|
||||
})
|
||||
@@ -146,13 +150,16 @@ const AdminDashboard: React.FC = () => {
|
||||
};
|
||||
|
||||
const saveSettings = async () => {
|
||||
setSaving(true);
|
||||
setLoading(true);
|
||||
setSaveStatus(null);
|
||||
|
||||
try {
|
||||
const response = await apiCall('/api/admin/settings', {
|
||||
const response = await fetch('/api/admin/settings', {
|
||||
method: 'POST',
|
||||
headers: buildAuthHeaders(true),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Admin-Auth': sessionStorage.getItem('adminAuthenticated') || ''
|
||||
},
|
||||
body: JSON.stringify({
|
||||
apiKeys,
|
||||
systemSettings
|
||||
@@ -161,8 +168,16 @@ const AdminDashboard: React.FC = () => {
|
||||
|
||||
if (response.ok) {
|
||||
setSaveStatus('Settings saved successfully!');
|
||||
// Refresh the latest settings so saved states/labels stay accurate
|
||||
await loadSettings();
|
||||
// Mark keys as saved if they have values
|
||||
const newSavedKeys: { [key: string]: boolean } = {};
|
||||
Object.entries(apiKeys).forEach(([key, value]) => {
|
||||
if (value && !value.startsWith('***')) {
|
||||
newSavedKeys[key] = true;
|
||||
}
|
||||
});
|
||||
setSavedKeys(prev => ({ ...prev, ...newSavedKeys }));
|
||||
// Clear the input fields after successful save
|
||||
setApiKeys({});
|
||||
setTimeout(() => setSaveStatus(null), 3000);
|
||||
} else {
|
||||
setSaveStatus('Failed to save settings');
|
||||
@@ -170,14 +185,14 @@ const AdminDashboard: React.FC = () => {
|
||||
} catch (error) {
|
||||
setSaveStatus('Error saving settings');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('authToken');
|
||||
sessionStorage.removeItem('adminAuthenticated');
|
||||
setIsAuthenticated(false);
|
||||
navigate('/');
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
// Test VIP functions
|
||||
@@ -337,29 +352,37 @@ const AdminDashboard: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex justify-center items-center">
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8 flex items-center space-x-4 border border-slate-200/60">
|
||||
<div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
|
||||
<span className="text-lg font-medium text-slate-700">Loading admin settings...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex justify-center items-center">
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8 w-full max-w-xl border border-rose-200/70">
|
||||
<h2 className="text-2xl font-bold text-rose-700 mb-4">Admin access required</h2>
|
||||
<p className="text-slate-600 mb-6">{error}</p>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
Return to dashboard
|
||||
</button>
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8 w-full max-w-md border border-slate-200/60">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-amber-500 to-orange-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<div className="w-8 h-8 bg-white rounded-full flex items-center justify-center">
|
||||
<div className="w-4 h-4 bg-amber-500 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-slate-800">Admin Login</h2>
|
||||
<p className="text-slate-600 mt-2">Enter your admin password to continue</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="password" className="form-label">Admin Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={adminPassword}
|
||||
onChange={(e) => setAdminPassword(e.target.value)}
|
||||
className="form-input"
|
||||
placeholder="Enter admin password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -415,20 +438,24 @@ const AdminDashboard: React.FC = () => {
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4 items-end">
|
||||
<div className="lg:col-span-2">
|
||||
<label className="form-label">API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder={savedKeys.aviationStackKey && maskedKeyHints.aviationStackKey
|
||||
? `Saved (${maskedKeyHints.aviationStackKey.slice(-4)})`
|
||||
: 'Enter AviationStack API key'}
|
||||
value={apiKeys.aviationStackKey || ''}
|
||||
onChange={(e) => handleApiKeyChange('aviationStackKey', e.target.value)}
|
||||
className="form-input"
|
||||
/>
|
||||
{savedKeys.aviationStackKey && maskedKeyHints.aviationStackKey && !apiKeys.aviationStackKey && (
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Currently saved key ends with {maskedKeyHints.aviationStackKey.slice(-4)}. Enter a new value to replace it.
|
||||
</p>
|
||||
)}
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showKeys.aviationStackKey ? 'text' : 'password'}
|
||||
placeholder={savedKeys.aviationStackKey ? 'Key saved (enter new key to update)' : 'Enter AviationStack API key'}
|
||||
value={apiKeys.aviationStackKey || ''}
|
||||
onChange={(e) => handleApiKeyChange('aviationStackKey', e.target.value)}
|
||||
className="form-input pr-12"
|
||||
/>
|
||||
{savedKeys.aviationStackKey && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowKeys(prev => ({ ...prev, aviationStackKey: !prev.aviationStackKey }))}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
{showKeys.aviationStackKey ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Get your key from: https://aviationstack.com/dashboard
|
||||
</p>
|
||||
@@ -457,91 +484,72 @@ const AdminDashboard: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auth0 Credentials */}
|
||||
{/* Google OAuth Credentials */}
|
||||
<div className="form-section">
|
||||
<div className="form-section-header">
|
||||
<h3 className="form-section-title">Auth0 Configuration</h3>
|
||||
{(savedKeys.auth0Domain || savedKeys.auth0ClientId || savedKeys.auth0ClientSecret) && (
|
||||
<h3 className="form-section-title">Google OAuth Credentials</h3>
|
||||
{(savedKeys.googleClientId && savedKeys.googleClientSecret) && (
|
||||
<span className="bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
|
||||
Configured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="form-group">
|
||||
<label className="form-label">Auth0 Domain</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={savedKeys.auth0Domain && maskedKeyHints.auth0Domain
|
||||
? `Saved (${maskedKeyHints.auth0Domain.slice(-4)})`
|
||||
: 'e.g. dev-1234abcd.us.auth0.com'}
|
||||
value={apiKeys.auth0Domain || ''}
|
||||
onChange={(e) => handleApiKeyChange('auth0Domain', e.target.value)}
|
||||
className="form-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Client ID</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder={savedKeys.auth0ClientId && maskedKeyHints.auth0ClientId
|
||||
? `Saved (${maskedKeyHints.auth0ClientId.slice(-4)})`
|
||||
: 'Enter Auth0 application Client ID'}
|
||||
value={apiKeys.auth0ClientId || ''}
|
||||
onChange={(e) => handleApiKeyChange('auth0ClientId', e.target.value)}
|
||||
className="form-input"
|
||||
/>
|
||||
{savedKeys.auth0ClientId && maskedKeyHints.auth0ClientId && !apiKeys.auth0ClientId && (
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Saved client ID ends with {maskedKeyHints.auth0ClientId.slice(-4)}. Provide a new ID to update it.
|
||||
</p>
|
||||
)}
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showKeys.googleClientId ? 'text' : 'password'}
|
||||
placeholder={savedKeys.googleClientId ? 'Client ID saved' : 'Enter Google OAuth Client ID'}
|
||||
value={apiKeys.googleClientId || ''}
|
||||
onChange={(e) => handleApiKeyChange('googleClientId', e.target.value)}
|
||||
className="form-input pr-12"
|
||||
/>
|
||||
{savedKeys.googleClientId && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowKeys(prev => ({ ...prev, googleClientId: !prev.googleClientId }))}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
{showKeys.googleClientId ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Client Secret</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder={savedKeys.auth0ClientSecret && maskedKeyHints.auth0ClientSecret
|
||||
? `Saved (${maskedKeyHints.auth0ClientSecret.slice(-4)})`
|
||||
: 'Enter Auth0 application Client Secret'}
|
||||
value={apiKeys.auth0ClientSecret || ''}
|
||||
onChange={(e) => handleApiKeyChange('auth0ClientSecret', e.target.value)}
|
||||
className="form-input"
|
||||
/>
|
||||
{savedKeys.auth0ClientSecret && maskedKeyHints.auth0ClientSecret && !apiKeys.auth0ClientSecret && (
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Saved client secret ends with {maskedKeyHints.auth0ClientSecret.slice(-4)}. Provide a new secret to rotate it.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">API Audience (Identifier)</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={apiKeys.auth0Audience || 'https://your-api-identifier'}
|
||||
value={apiKeys.auth0Audience || ''}
|
||||
onChange={(e) => handleApiKeyChange('auth0Audience', e.target.value)}
|
||||
className="form-input"
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Create an API in Auth0 and use its Identifier here (e.g. https://vip-coordinator-api).
|
||||
</p>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showKeys.googleClientSecret ? 'text' : 'password'}
|
||||
placeholder={savedKeys.googleClientSecret ? 'Client Secret saved' : 'Enter Google OAuth Client Secret'}
|
||||
value={apiKeys.googleClientSecret || ''}
|
||||
onChange={(e) => handleApiKeyChange('googleClientSecret', e.target.value)}
|
||||
className="form-input pr-12"
|
||||
/>
|
||||
{savedKeys.googleClientSecret && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowKeys(prev => ({ ...prev, googleClientSecret: !prev.googleClientSecret }))}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
{showKeys.googleClientSecret ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mt-4">
|
||||
<h4 className="font-semibold text-blue-900 mb-2">Setup Instructions</h4>
|
||||
<ol className="text-sm text-blue-800 space-y-1 list-decimal list-inside">
|
||||
<li>Sign in to the Auth0 Dashboard</li>
|
||||
<li>Create a <strong>Single Page Application</strong> for the frontend</li>
|
||||
<li>Set Allowed Callback URL to <code>https://bsa.madeamess.online/auth/callback</code></li>
|
||||
<li>Set Allowed Logout URL to <code>https://bsa.madeamess.online/</code></li>
|
||||
<li>Set Allowed Web Origins to <code>https://bsa.madeamess.online</code></li>
|
||||
<li>Create an <strong>API</strong> in Auth0 for the backend and use its Identifier as the audience</li>
|
||||
<li>Go to Google Cloud Console</li>
|
||||
<li>Create or select a project</li>
|
||||
<li>Enable the Google+ API</li>
|
||||
<li>Go to "Credentials" → "Create Credentials" → "OAuth 2.0 Client IDs"</li>
|
||||
<li>Set authorized redirect URI: http://bsa.madeamess.online:3000/auth/google/callback</li>
|
||||
<li>Set authorized JavaScript origins: http://bsa.madeamess.online:5173</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
@@ -751,7 +759,7 @@ const AdminDashboard: React.FC = () => {
|
||||
</p>
|
||||
<button
|
||||
className="btn btn-primary w-full mb-2"
|
||||
onClick={() => window.open(`${API_BASE_URL}/api-docs.html`, '_blank')}
|
||||
onClick={() => window.open('http://localhost:3000/api-docs.html', '_blank')}
|
||||
>
|
||||
Open API Documentation
|
||||
</button>
|
||||
@@ -803,9 +811,9 @@ const AdminDashboard: React.FC = () => {
|
||||
<button
|
||||
className="btn btn-success text-lg px-8 py-4"
|
||||
onClick={saveSettings}
|
||||
disabled={saving}
|
||||
disabled={loading}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save All Settings'}
|
||||
{loading ? 'Saving...' : 'Save All Settings'}
|
||||
</button>
|
||||
|
||||
{saveStatus && (
|
||||
|
||||
Reference in New Issue
Block a user