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:
2025-06-07 18:32:00 +02:00
parent aa900505b9
commit ae3702c3b1
32 changed files with 2120 additions and 1494 deletions

View File

@@ -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 && (