// First-Run Setup Application // Purpose: Initializes a fresh server or imports an explicitly selected YAML configuration file through the restricted setup channel. // Scope: Exists only while the server reports setup required; ordinary administration belongs to /admin. import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import CardFrame from '../components/CardFrame/index.jsx'; import SocketConnectionPill from '../components/SocketConnectionPill/index.jsx'; import { useSocket } from '../context/SocketContext.jsx'; import { createFirstAdministrator, getSetupStatus, importConfigurationFile } from './api.js'; export default function SetupApp() { const socket = useSocket(); const [required, setRequired] = useState(null); const [setupCode, setSetupCode] = useState(''); const [username, setUsername] = useState(''); const [discordId, setDiscordId] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [configurationFile, setConfigurationFile] = useState(null); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(''); useEffect(() => { getSetupStatus(socket).then((response) => setRequired(response.required)).catch((error) => setMessage(error.message)); }, [socket]); async function run(work) { setBusy(true); setMessage(''); try { await work(); setRequired(false); setMessage('Setup completed. You can now open the administration application and log in.'); } catch (error) { const details = Array.isArray(error.validationErrors) ? ` ${error.validationErrors.map((entry) => `${entry.path}: ${entry.message}`).join('; ')}` : ''; setMessage(`${error.message}${details}`); } finally { setBusy(false); } } function createAdministrator(event) { event.preventDefault(); if (password !== confirmPassword) { setMessage('Passwords do not match.'); return; } run(() => createFirstAdministrator(socket, { setupCode, username, discordId, password })); } function importSelectedConfiguration(event) { event.preventDefault(); if (!configurationFile) return; run(async () => importConfigurationFile(socket, { setupCode, fileName: configurationFile.name, yaml: await configurationFile.text(), })); } return (
{required ?

Enter the one-time code from setup-code.txt in the server data folder, then create the first lockdown administrator or import an existing configuration.

: null} {required === false ? Open administration : null} {message ?

{message}

: null}
{required ? ( <> setSetupCode(event.target.value)} />
setUsername(event.target.value)} /> setDiscordId(event.target.value)} /> setPassword(event.target.value)} /> setConfirmPassword(event.target.value)} />

Choose an existing YAML configuration explicitly. The server validates and imports it once, and its secrets are never displayed back in the browser.

setConfigurationFile(event.target.files?.[0] || null)} />
) : null}
); }