'use client'; import { useEffect, useState } from 'react'; import { Save, Loader2, RotateCcw, Sparkles } from 'lucide-react'; import { apiFetch } from '@/lib/browser-navigation'; interface ConfigEntry { value: unknown; source: 'admin' | 'env' | 'default'; } export default function AdminAuthPage() { const [config, setConfig] = useState>({}); const [edits, setEdits] = useState>({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); useEffect(() => { fetchConfig(); }, []); async function fetchConfig() { setLoading(true); const res = await apiFetch('/api/admin/config'); if (res.ok) setConfig(await res.json()); setLoading(false); } function handleChange(key: string, value: unknown) { setEdits(prev => ({ ...prev, [key]: value })); setMessage(null); } function currentValue(key: string): unknown { if (key in edits) return edits[key]; return config[key]?.value; } async function handleSave() { if (Object.keys(edits).length === 0) return; setSaving(true); setMessage(null); const res = await apiFetch('/api/admin/config', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(edits), }); if (res.ok) { setMessage({ type: 'success', text: 'Authentication settings saved.' }); setEdits({}); await fetchConfig(); } else { const data = await res.json(); setMessage({ type: 'error', text: data.error || 'Failed to save' }); } setSaving(false); } async function handleRevert(key: string) { const res = await apiFetch('/api/admin/config', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key }), }); if (res.ok) { setEdits(prev => { const next = { ...prev }; delete next[key]; return next; }); await fetchConfig(); } } const [setupRunning, setSetupRunning] = useState(false); const [setupOauthOnly, setSetupOauthOnly] = useState(false); async function handleAutoSetup() { if (typeof window === 'undefined') return; const oauthOnlyText = setupOauthOnly ? '\n\n • Disable password login (OAuth only)' : ''; const ok = window.confirm( `Auto-configure OAuth between this webmail and the connected Stalwart server?\n\nThis will:\n • Create or update an OAuth client called "bulwark-webmail" on the Stalwart server\n • Generate a new client secret\n • Register redirect URIs for ${window.location.origin}\n • Save OAuth settings to admin config (survives env changes)${oauthOnlyText}\n\nYour Stalwart user must have admin permissions.` ); if (!ok) return; setSetupRunning(true); setMessage(null); try { const res = await apiFetch('/api/admin/oauth/setup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ origin: window.location.origin, oauthOnly: setupOauthOnly, }), }); const data = await res.json(); if (res.ok) { setMessage({ type: 'success', text: `OAuth client ${data.action} on Stalwart. ${data.redirectUriCount} redirect URI(s) registered. Webmail config updated.`, }); setEdits({}); await fetchConfig(); } else { const detail = data.detail ? ` (${typeof data.detail === 'string' ? data.detail : JSON.stringify(data.detail).slice(0, 200)})` : ''; setMessage({ type: 'error', text: (data.error || 'Setup failed') + detail }); } } catch (err) { setMessage({ type: 'error', text: err instanceof Error ? err.message : 'Setup failed' }); } finally { setSetupRunning(false); } } const hasEdits = Object.keys(edits).length > 0; if (loading) { return
Loading...
; } return (

Authentication

OAuth, SSO, and session configuration

{hasEdits && ( )}
{message && (
{message.text}
)} {/* Auto-setup */}

Auto-configure OAuth (Stalwart)

Registers an OAuth client on the connected Stalwart server, generates a client secret, and saves the settings here. Requires your Stalwart account to have admin permissions.

{/* OAuth */}
{/* SSO */}
{/* Session & Security */}
onChange(configKey, e.target.value)} placeholder={placeholder} className="h-8 w-64 rounded-md border border-input bg-background px-2.5 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" /> {source === 'admin' && ( )}
); } function Toggle({ label, description, configKey, value, source, onChange, onRevert }: { label: string; description?: string; configKey: string; value: boolean; source?: string; onChange: (k: string, v: unknown) => void; onRevert: (k: string) => void; }) { return (
{label}
{description &&

{description}

}
{source === 'admin' && ( )}
); } function Select({ label, configKey, value, source, options, onChange, onRevert }: { label: string; configKey: string; value: string; source?: string; options: string[]; onChange: (k: string, v: unknown) => void; onRevert: (k: string) => void; }) { return (
{label}
{source === 'admin' && ( )}
); }