feat: implement setup wizard for initial configuration and user registration

This commit is contained in:
LaurensBartelds committed 2026-04-25 16:48:23 +02:00
1 parent f7b93f8612
commit ad777ef1c3
9 files changed
+576 -8

No files matched your search

+313
View File
@@ -0,0 +1,313 @@
import { useState } from 'react'
import { apiFetch, setToken } from '../utils/api'
interface Props {
onComplete: () => void
}
export default function SetupWizard({ onComplete }: Readonly<Props>) {
const [step, setStep] = useState(1)
const [email, setEmail] = useState('')
const [displayName, setDisplayName] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [smtpHost, setSmtpHost] = useState('')
const [smtpPort, setSmtpPort] = useState('587')
const [smtpUser, setSmtpUser] = useState('')
const [smtpPass, setSmtpPass] = useState('')
const [smtpFrom, setSmtpFrom] = useState('')
const [smtpSecure, setSmtpSecure] = useState('auto')
const [appUrl, setAppUrl] = useState(window.location.origin)
const [error, setError] = useState<string | null>(null)
const [submitting, setSubmitting] = useState(false)
const validateStep1 = () => {
if (!email.trim() || !email.includes('@')) {
setError('Please enter a valid email address')
return false
}
if (!displayName.trim()) {
setError('Display name is required')
return false
}
if (password.length < 8) {
setError('Password must be at least 8 characters')
return false
}
if (password !== confirmPassword) {
setError('Passwords do not match')
return false
}
return true
}
const handleNext = () => {
setError(null)
if (validateStep1()) setStep(2)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError(null)
setSubmitting(true)
try {
const data = await apiFetch<{
message: string
token: string
user: {
id: number
email: string
displayName: string
role: 'user' | 'admin'
}
}>('/api/setup', {
method: 'POST',
body: JSON.stringify({
email,
password,
displayName: displayName.trim(),
smtpHost,
smtpPort,
smtpUser,
smtpPass,
smtpFrom: smtpFrom || 'BrainWave <noreply@brainwave.local>',
smtpSecure,
appUrl,
}),
})
setToken(data.token)
onComplete()
} catch (err) {
setError(
err instanceof Error ? err.message : 'Setup failed. Please try again.',
)
} finally {
setSubmitting(false)
}
}
return (
<div className="setup-page">
<div className="setup-card">
<div className="brand-mark" style={{ margin: '0 auto 16px' }} />
<h1 className="setup-title">Welcome to BrainWave</h1>
<p className="setup-subtitle">
Let's get your instance set up. This only takes a minute.
</p>
<div className="setup-steps">
<div className={`setup-step ${step >= 1 ? 'active' : ''}`}>
<span className="setup-step-num">1</span>
<span className="setup-step-label">Admin Account</span>
</div>
<div className="setup-step-line" />
<div className={`setup-step ${step >= 2 ? 'active' : ''}`}>
<span className="setup-step-num">2</span>
<span className="setup-step-label">Email Settings</span>
</div>
</div>
{error && <div className="auth-error">{error}</div>}
{step === 1 && (
<>
<div className="auth-field">
<label className="auth-label" htmlFor="setup-email">
Email
</label>
<input
id="setup-email"
className="input"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="admin@example.com"
required
autoComplete="email"
/>
</div>
<div className="auth-field">
<label className="auth-label" htmlFor="setup-name">
Display Name
</label>
<input
id="setup-name"
className="input"
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="Your name"
required
autoComplete="name"
/>
</div>
<div className="auth-field">
<label className="auth-label" htmlFor="setup-password">
Password
</label>
<input
id="setup-password"
className="input"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Min. 8 characters"
required
minLength={8}
autoComplete="new-password"
/>
</div>
<div className="auth-field">
<label className="auth-label" htmlFor="setup-confirm">
Confirm Password
</label>
<input
id="setup-confirm"
className="input"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
required
minLength={8}
autoComplete="new-password"
/>
</div>
<button
className="btn btn-primary auth-submit"
onClick={handleNext}
style={{ marginTop: 8 }}
>
Continue →
</button>
</>
)}
{step === 2 && (
<form onSubmit={(e) => void handleSubmit(e)}>
<p className="setup-hint">
Configure your outgoing email server so BrainWave can send
verification and password-reset emails.
</p>
<p className="setup-hint">
You can leave these blank and configure them later in the admin
dashboard.
</p>
<div className="auth-field">
<label className="auth-label" htmlFor="smtp-host">
SMTP Host
</label>
<input
id="smtp-host"
className="input"
type="text"
value={smtpHost}
onChange={(e) => setSmtpHost(e.target.value)}
placeholder="smtp.example.com"
/>
</div>
<div className="auth-field">
<label className="auth-label" htmlFor="smtp-port">
SMTP Port
</label>
<input
id="smtp-port"
className="input"
type="number"
value={smtpPort}
onChange={(e) => setSmtpPort(e.target.value)}
placeholder="587"
/>
</div>
<div className="auth-field">
<label className="auth-label" htmlFor="smtp-user">
SMTP Username
</label>
<input
id="smtp-user"
className="input"
type="text"
value={smtpUser}
onChange={(e) => setSmtpUser(e.target.value)}
placeholder="user@example.com"
/>
</div>
<div className="auth-field">
<label className="auth-label" htmlFor="smtp-pass">
SMTP Password
</label>
<input
id="smtp-pass"
className="input"
type="password"
value={smtpPass}
onChange={(e) => setSmtpPass(e.target.value)}
placeholder="••••••••"
/>
</div>
<div className="auth-field">
<label className="auth-label" htmlFor="smtp-from">
From Address
</label>
<input
id="smtp-from"
className="input"
type="text"
value={smtpFrom}
onChange={(e) => setSmtpFrom(e.target.value)}
placeholder="BrainWave <noreply@brainwave.local>"
/>
</div>
<div className="auth-field">
<label className="auth-label" htmlFor="smtp-secure">
Security
</label>
<select
id="smtp-secure"
className="input"
value={smtpSecure}
onChange={(e) => setSmtpSecure(e.target.value)}
>
<option value="auto">Auto (use SSL on port 465)</option>
<option value="true">SSL/TLS</option>
<option value="false">None / STARTTLS</option>
</select>
</div>
<div className="auth-field">
<label className="auth-label" htmlFor="app-url">
App URL
</label>
<input
id="app-url"
className="input"
type="url"
value={appUrl}
onChange={(e) => setAppUrl(e.target.value)}
placeholder="https://brainwave.example.com"
required
/>
</div>
<div className="setup-actions">
<button
type="button"
className="btn btn-ghost"
onClick={() => setStep(1)}
>
← Back
</button>
<button
className="btn btn-primary auth-submit"
type="submit"
disabled={submitting}
>
{submitting ? 'Setting up…' : 'Complete Setup'}
</button>
</div>
</form>
)}
</div>
</div>
)
}