Files
Redflag/aggregator-web/src/pages/Setup.tsx
jpetree331 f97d4845af feat(security): A-1 Ed25519 key rotation + A-2 replay attack fixes
Complete RedFlag codebase with two major security audit implementations.

== A-1: Ed25519 Key Rotation Support ==

Server:
- SignCommand sets SignedAt timestamp and KeyID on every signature
- signing_keys database table (migration 020) for multi-key rotation
- InitializePrimaryKey registers active key at startup
- /api/v1/public-keys endpoint for rotation-aware agents
- SigningKeyQueries for key lifecycle management

Agent:
- Key-ID-aware verification via CheckKeyRotation
- FetchAndCacheAllActiveKeys for rotation pre-caching
- Cache metadata with TTL and staleness fallback
- SecurityLogger events for key rotation and command signing

== A-2: Replay Attack Fixes (F-1 through F-7) ==

F-5 CRITICAL - RetryCommand now signs via signAndCreateCommand
F-1 HIGH     - v3 format: "{agent_id}:{cmd_id}:{type}:{hash}:{ts}"
F-7 HIGH     - Migration 026: expires_at column with partial index
F-6 HIGH     - GetPendingCommands/GetStuckCommands filter by expires_at
F-2 HIGH     - Agent-side executedIDs dedup map with cleanup
F-4 HIGH     - commandMaxAge reduced from 24h to 4h
F-3 CRITICAL - Old-format commands rejected after 48h via CreatedAt

Verification fixes: migration idempotency (ETHOS #4), log format
compliance (ETHOS #1), stale comments updated.

All 24 tests passing. Docker --no-cache build verified.
See docs/ for full audit reports and deviation log (DEV-001 to DEV-019).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 21:25:47 -04:00

722 lines
31 KiB
TypeScript

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Settings, Database, User, Shield, Eye, EyeOff, CheckCircle, Key } from 'lucide-react';
import { toast } from 'react-hot-toast';
import { setupApi } from '@/lib/api';
import { useAuthStore } from '@/lib/store';
interface SetupFormData {
adminUser: string;
adminPassword: string;
dbHost: string;
dbPort: string;
dbName: string;
dbUser: string;
dbPassword: string;
serverHost: string;
serverPort: string;
maxSeats: string;
}
interface SigningKeys {
public_key: string;
private_key: string;
fingerprint: string;
algorithm: string;
}
const Setup: React.FC = () => {
const navigate = useNavigate();
const { logout } = useAuthStore();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [envContent, setEnvContent] = useState<string | null>(null);
const [showSuccess, setShowSuccess] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showDbPassword, setShowDbPassword] = useState(false);
const [signingKeys, setSigningKeys] = useState<SigningKeys | null>(null);
const [generatingKeys, setGeneratingKeys] = useState(false);
const [configType, setConfigType] = useState<'env' | 'swarm'>('env');
const [formData, setFormData] = useState<SetupFormData>({
adminUser: 'admin',
adminPassword: '',
dbHost: 'postgres',
dbPort: '5432',
dbName: 'redflag',
dbUser: 'redflag',
dbPassword: 'redflag',
serverHost: '0.0.0.0',
serverPort: '8080',
maxSeats: '50',
});
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleGenerateKeys = async () => {
setGeneratingKeys(true);
try {
const response = await fetch('/api/setup/generate-keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) {
throw new Error('Failed to generate keys');
}
const keys: SigningKeys = await response.json();
setSigningKeys(keys);
toast.success('Signing keys generated successfully!');
} catch (error: any) {
toast.error(error.message || 'Failed to generate keys');
setError('Failed to generate signing keys');
} finally {
setGeneratingKeys(false);
}
};
const validateForm = (): boolean => {
if (!formData.adminUser.trim()) {
setError('Admin username is required');
return false;
}
if (!formData.adminPassword.trim()) {
setError('Admin password is required');
return false;
}
if (!formData.dbHost.trim()) {
setError('Database host is required');
return false;
}
if (!formData.dbPort.trim()) {
setError('Database port is required');
return false;
}
const dbPort = parseInt(formData.dbPort);
if (isNaN(dbPort) || dbPort <= 0 || dbPort > 65535) {
setError('Database port must be between 1 and 65535');
return false;
}
if (!formData.dbName.trim()) {
setError('Database name is required');
return false;
}
if (!formData.dbUser.trim()) {
setError('Database user is required');
return false;
}
if (!formData.dbPassword.trim()) {
setError('Database password is required');
return false;
}
if (!formData.serverPort.trim()) {
setError('Server port is required');
return false;
}
const serverPort = parseInt(formData.serverPort);
if (isNaN(serverPort) || serverPort <= 0 || serverPort > 65535) {
setError('Server port must be between 1 and 65535');
return false;
}
const maxSeats = parseInt(formData.maxSeats);
if (isNaN(maxSeats) || maxSeats <= 0) {
setError('Maximum agent seats must be greater than 0');
return false;
}
return true;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (!validateForm()) {
return;
}
logout();
setIsLoading(true);
try {
const result = await setupApi.configure(formData);
let configContent = '';
if (configType === 'env') {
configContent = generateEnvContent(result, signingKeys);
} else {
configContent = generateDockerSecretCommands(result, signingKeys);
}
setEnvContent(configContent || null);
setShowSuccess(true);
toast.success(result.message || 'Configuration saved successfully!');
} catch (error: any) {
console.error('Setup error:', error);
const errorMessage = error.response?.data?.error || error.message || 'Setup failed';
setError(errorMessage);
toast.error(errorMessage);
} finally {
setIsLoading(false);
}
};
const generateEnvContent = (result: any, keys: SigningKeys | null): string => {
if (!result.envContent) return '';
let envContent = result.envContent;
if (keys) {
envContent += `\n# Ed25519 Signing Keys (for agent updates)\nREDFLAG_SIGNING_PRIVATE_KEY=${keys.private_key}\n`;
}
return envContent;
};
const generateDockerSecretCommands = (result: any, keys: SigningKeys | null): string => {
if (!result.envContent) return '';
// Parse the envContent to extract values
const envLines = result.envContent.split('\n');
const envVars: Record<string, string> = {};
envLines.forEach(line => {
const match = line.match(/^([^#=]+)=(.+)$/);
if (match) {
envVars[match[1].trim()] = match[2].trim();
}
});
// Add signing keys if available
if (keys) {
envVars['REDFLAG_SIGNING_PRIVATE_KEY'] = keys.private_key;
}
// Generate Docker secret commands
const commands = [
'# RedFlag Docker Secrets Configuration',
'# Generated by web setup on 2025-12-13',
'# [WARNING] SECURITY CRITICAL: Backup the signing key or you will lose access to all agents',
'#',
'# Run these commands on your Docker host to create the secrets:',
'#',
`printf '%s' '${envVars['REDFLAG_ADMIN_PASSWORD'] || ''}' | docker secret create redflag_admin_password -`,
`printf '%s' '${envVars['REDFLAG_JWT_SECRET'] || ''}' | docker secret create redflag_jwt_secret -`,
`printf '%s' '${envVars['REDFLAG_DB_PASSWORD'] || ''}' | docker secret create redflag_db_password -`,
`printf '%s' '${envVars['REDFLAG_SIGNING_PRIVATE_KEY'] || ''}' | docker secret create redflag_signing_private_key -`,
'',
'# After creating the secrets, restart your RedFlag server:',
'# docker compose down && docker compose up -d',
'',
'# Optional: Save these values securely (password manager, encrypted storage)',
`# Admin Password: ${envVars['REDFLAG_ADMIN_PASSWORD'] || ''}`,
`# JWT Secret: ${envVars['REDFLAG_JWT_SECRET'] || ''}`,
`# DB Password: ${envVars['REDFLAG_DB_PASSWORD'] || ''}`,
].join('\n');
return commands;
};
// Success screen with configuration display
if (showSuccess && envContent) {
return (
<div className="px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto">
{/* Header */}
<div className="mb-8">
<div className="flex items-center justify-center mb-4">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
</div>
<h1 className="text-3xl font-bold text-gray-900 text-center mb-2">
Configuration Complete!
</h1>
<p className="text-gray-600 text-center">
Your RedFlag server is ready to use
</p>
</div>
{/* Success Card */}
<div className="bg-white rounded-lg border border-gray-200 shadow-sm p-6">
{/* Admin Credentials Section */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3">Administrator Credentials</h3>
<div className="bg-gray-50 border border-gray-200 rounded-md p-4">
<div className="grid grid-cols-1 gap-3">
<div>
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">Username</label>
<div className="mt-1 p-2 bg-white border border-gray-300 rounded text-sm font-mono">{formData.adminUser}</div>
</div>
<div>
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">Password</label>
<div className="mt-1 p-2 bg-white border border-gray-300 rounded text-sm font-mono"></div>
</div>
</div>
</div>
<div className="mt-3 p-3 bg-green-50 border border-green-200 rounded-md">
<p className="text-sm text-green-800">
<strong>Important:</strong> Save these credentials securely. You'll use them to login to the RedFlag dashboard.
</p>
</div>
</div>
{/* Configuration Content Section */}
{envContent && (
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900">
{configType === 'env' ? 'Environment Configuration (.env)' : 'Docker Swarm Secrets'}
</h3>
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-600">.env</span>
<button
onClick={() => setConfigType(configType === 'env' ? 'swarm' : 'env')}
className={`relative inline-flex h-6 w-11 items-center rounded-full ${configType === 'swarm' ? 'bg-indigo-600' : 'bg-gray-200'}`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition ${configType === 'swarm' ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
<span className="text-sm text-gray-600">Swarm</span>
</div>
</div>
<div className="bg-gray-50 border border-gray-200 rounded-md p-4">
<textarea
readOnly
value={envContent}
className="w-full h-64 p-3 text-xs font-mono text-gray-800 bg-white border border-gray-300 rounded-md resize-none focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
{configType === 'env' ? (
<>
<button
onClick={() => {
navigator.clipboard.writeText(envContent);
toast.success('.env content copied to clipboard!');
}}
className="mt-3 w-full flex justify-center py-2 px-4 border border-transparent rounded-md text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
Copy .env Content
</button>
<div className="mt-3 p-3 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-sm text-blue-800">
<strong>Next Steps:</strong> Save this content to <code className="bg-blue-100 px-1 rounded">config/.env</code> and run <code className="bg-blue-100 px-1 rounded">docker compose down && docker compose up -d</code> to apply the configuration.
</p>
</div>
<div className="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
<p className="text-sm text-yellow-800">
<strong>Security Note:</strong> The <code className="bg-yellow-100 px-1 rounded">config/.env</code> file contains sensitive credentials. Ensure it has restricted permissions (<code className="bg-yellow-100 px-1 rounded">chmod 600</code>) and is excluded from version control.
</p>
</div>
</>
) : (
<>
<button
onClick={() => {
navigator.clipboard.writeText(envContent);
toast.success('Docker secret commands copied to clipboard!');
}}
className="mt-3 w-full flex justify-center py-2 px-4 border border-transparent rounded-md text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
Copy Docker Secret Commands
</button>
<div className="mt-3 p-3 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-sm text-blue-800">
<strong>Requirements:</strong> Docker Swarm mode is required. Run <code className="bg-blue-100 px-1 rounded">docker swarm init</code> on your Docker host before creating secrets.
</p>
</div>
<div className="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
<p className="text-sm text-yellow-800">
<strong>Next Steps:</strong> Run the copied commands on your Docker host, then update <code className="bg-yellow-100 px-1 rounded">docker-compose.yml</code> to mount the secrets and restart.
</p>
</div>
</>
)}
</div>
)}
{/* Next Steps */}
<div className="border-t border-gray-200 pt-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3">Next Steps</h3>
{configType === 'env' ? (
<ol className="list-decimal list-inside space-y-2 text-sm text-gray-600">
<li>Copy the .env content using the green button above</li>
<li>Save it to <code className="bg-gray-100 px-1 rounded">config/.env</code></li>
<li>Run <code className="bg-gray-100 px-1 rounded">docker compose down && docker compose up -d</code></li>
<li>Login to the dashboard with your admin username and password</li>
</ol>
) : (
<ol className="list-decimal list-inside space-y-2 text-sm text-gray-600">
<li>Initialize Docker Swarm: <code className="bg-gray-100 px-1 rounded">docker swarm init</code></li>
<li>Copy the Docker secret commands using the green button above</li>
<li>Run the commands on your Docker host to create the secrets</li>
<li>Update <code className="bg-gray-100 px-1 rounded">docker-compose.yml</code> to mount the secrets</li>
<li>Restart RedFlag with <code className="bg-gray-100 px-1 rounded">docker compose down && docker compose up -d</code></li>
<li>Login to the dashboard with your admin username and password</li>
</ol>
)}
</div>
<div className="mt-6 pt-6 border-t border-gray-200 space-y-3">
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-md">
<p className="text-sm text-yellow-800">
<strong>Important:</strong> You must restart the containers to apply the configuration before logging in.
</p>
</div>
<button
onClick={() => {
toast.success('Please run: docker-compose down && docker-compose up -d');
}}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md text-sm font-medium text-white bg-yellow-600 hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500"
>
Show Restart Command
</button>
<button
onClick={() => {
setTimeout(() => navigate('/login'), 500);
}}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Continue to Login (After Restart)
</button>
</div>
</div>
</div>
</div>
);
}
return (
<div className="px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="mb-8">
<div className="flex items-center justify-center mb-4">
<div className="w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center">
<span className="text-2xl">🚩</span>
</div>
</div>
<h1 className="text-3xl font-bold text-gray-900 text-center mb-2">
Configure RedFlag Server
</h1>
<p className="text-gray-600 text-center">
Set up your update management server configuration
</p>
</div>
{/* Setup Form */}
<div className="bg-white rounded-lg border border-gray-200 shadow-sm p-6">
<form onSubmit={handleSubmit} className="space-y-8">
{/* Error Display */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<div className="text-sm text-red-800">{error}</div>
</div>
)}
{/* Administrator Account */}
<div>
<div className="flex items-center mb-4">
<User className="h-5 w-5 text-indigo-600 mr-2" />
<h3 className="text-lg font-semibold text-gray-900">Administrator Account</h3>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label htmlFor="adminUser" className="block text-sm font-medium text-gray-700 mb-1">
Admin Username
</label>
<input
type="text"
id="adminUser"
name="adminUser"
value={formData.adminUser}
onChange={handleInputChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="admin"
required
/>
</div>
<div>
<label htmlFor="adminPassword" className="block text-sm font-medium text-gray-700 mb-1">
Admin Password
</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
id="adminPassword"
name="adminPassword"
value={formData.adminPassword}
onChange={handleInputChange}
className="block w-full px-3 py-2 pr-10 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="Enter secure password"
required
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-4 w-4 text-gray-400" />
) : (
<Eye className="h-4 w-4 text-gray-400" />
)}
</button>
</div>
</div>
</div>
</div>
{/* Security Keys Section */}
<div>
<div className="flex items-center mb-4">
<Key className="h-5 w-5 text-indigo-600 mr-2" />
<h3 className="text-lg font-semibold text-gray-900">Security Keys</h3>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-md p-4 mb-4">
<p className="text-sm text-blue-800">
Generate Ed25519 signing keys for secure agent updates.
<strong> Save the private key securely</strong> - it will be included in your configuration.
</p>
</div>
{!signingKeys ? (
<button
type="button"
onClick={handleGenerateKeys}
disabled={generatingKeys}
className="w-full py-2 px-4 border border-indigo-600 text-indigo-600 rounded-md hover:bg-indigo-50 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
>
{generatingKeys ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-indigo-600 mr-2"></div>
Generating Keys...
</>
) : (
<>
<Key className="h-4 w-4 mr-2" />
Generate Signing Keys
</>
)}
</button>
) : (
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Public Key Fingerprint
</label>
<input
readOnly
value={signingKeys.fingerprint}
className="block w-full px-3 py-2 bg-gray-100 border border-gray-300 rounded-md font-mono text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Algorithm
</label>
<input
readOnly
value={signingKeys.algorithm.toUpperCase()}
className="block w-full px-3 py-2 bg-gray-100 border border-gray-300 rounded-md text-sm"
/>
</div>
<div className="bg-green-50 border border-green-200 rounded-md p-3">
<p className="text-sm text-green-800">
✓ Keys generated! Private key will be securely included in your configuration file.
</p>
</div>
</div>
)}
</div>
{/* Database Configuration */}
<div>
<div className="flex items-center mb-4">
<Database className="h-5 w-5 text-indigo-600 mr-2" />
<h3 className="text-lg font-semibold text-gray-900">Database Configuration</h3>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div>
<label htmlFor="dbHost" className="block text-sm font-medium text-gray-700 mb-1">
Database Host
</label>
<input
type="text"
id="dbHost"
name="dbHost"
value={formData.dbHost}
onChange={handleInputChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="postgres"
required
/>
</div>
<div>
<label htmlFor="dbPort" className="block text-sm font-medium text-gray-700 mb-1">
Database Port
</label>
<input
type="number"
id="dbPort"
name="dbPort"
value={formData.dbPort}
onChange={handleInputChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
required
/>
</div>
<div>
<label htmlFor="dbName" className="block text-sm font-medium text-gray-700 mb-1">
Database Name
</label>
<input
type="text"
id="dbName"
name="dbName"
value={formData.dbName}
onChange={handleInputChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="redflag"
required
/>
</div>
<div>
<label htmlFor="dbUser" className="block text-sm font-medium text-gray-700 mb-1">
Database User
</label>
<input
type="text"
id="dbUser"
name="dbUser"
value={formData.dbUser}
onChange={handleInputChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="redflag"
required
/>
</div>
<div>
<label htmlFor="dbPassword" className="block text-sm font-medium text-gray-700 mb-1">
Database Password
</label>
<div className="relative">
<input
type={showDbPassword ? 'text' : 'password'}
id="dbPassword"
name="dbPassword"
value={formData.dbPassword}
onChange={handleInputChange}
className="block w-full px-3 py-2 pr-10 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="Enter database password"
required
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowDbPassword(!showDbPassword)}
>
{showDbPassword ? (
<EyeOff className="h-4 w-4 text-gray-400" />
) : (
<Eye className="h-4 w-4 text-gray-400" />
)}
</button>
</div>
</div>
</div>
</div>
{/* Server Configuration */}
<div>
<div className="flex items-center mb-4">
<Settings className="h-5 w-5 text-indigo-600 mr-2" />
<h3 className="text-lg font-semibold text-gray-900">Server Configuration</h3>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label htmlFor="serverHost" className="block text-sm font-medium text-gray-700 mb-1">
Server Host
</label>
<input
type="text"
id="serverHost"
name="serverHost"
value={formData.serverHost}
onChange={handleInputChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="0.0.0.0"
required
/>
</div>
<div>
<label htmlFor="serverPort" className="block text-sm font-medium text-gray-700 mb-1">
Server Port
</label>
<input
type="number"
id="serverPort"
name="serverPort"
value={formData.serverPort}
onChange={handleInputChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="8080"
required
/>
</div>
<div>
<label htmlFor="maxSeats" className="block text-sm font-medium text-gray-700 mb-1">
Maximum Agent Seats
</label>
<input
type="number"
id="maxSeats"
name="maxSeats"
value={formData.maxSeats}
onChange={handleInputChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
min="1"
max="1000"
placeholder="50"
required
/>
<p className="mt-1 text-xs text-gray-500">Security limit for agent registration</p>
</div>
</div>
</div>
{/* Submit Button */}
<div className="pt-6 border-t border-gray-200">
<button
type="submit"
disabled={isLoading}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? (
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Configuring RedFlag Server...
</div>
) : (
<div className="flex items-center">
<Shield className="w-4 h-4 mr-2" />
Configure RedFlag Server
</div>
)}
</button>
</div>
</form>
</div>
</div>
</div>
);
};
export default Setup;