diff --git a/ui/src/pages/vendor/EmailConfigPage.tsx b/ui/src/pages/vendor/EmailConfigPage.tsx new file mode 100644 index 0000000..f1fae4d --- /dev/null +++ b/ui/src/pages/vendor/EmailConfigPage.tsx @@ -0,0 +1,296 @@ +import { useState } from 'react'; +import { + Alert, + Button, + Card, + FormField, + Input, + Spinner, + useToast, +} from '@cameleer/design-system'; +import { Send, Trash2, Save, Power } from 'lucide-react'; +import { + useEmailConnector, + useSaveEmailConnector, + useDeleteEmailConnector, + useTestEmailConnector, + useToggleRegistration, +} from '../../api/email-connector-hooks'; +import styles from '../../styles/platform.module.css'; + +export function EmailConfigPage() { + const { toast } = useToast(); + const { data: connector, isLoading, isError } = useEmailConnector(); + const saveMutation = useSaveEmailConnector(); + const deleteMutation = useDeleteEmailConnector(); + const testMutation = useTestEmailConnector(); + const toggleMutation = useToggleRegistration(); + + const [editing, setEditing] = useState(false); + const [host, setHost] = useState(''); + const [port, setPort] = useState('587'); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [fromEmail, setFromEmail] = useState(''); + const [testTo, setTestTo] = useState(''); + const [confirmDelete, setConfirmDelete] = useState(false); + + const isConfigured = connector != null; + const showForm = !isConfigured || editing; + + function startEditing() { + if (connector) { + setHost(connector.host); + setPort(String(connector.port)); + setUsername(connector.username); + setPassword(''); + setFromEmail(connector.fromEmail); + } + setEditing(true); + } + + async function handleSave() { + if (!host || !username || !password || !fromEmail) { + toast({ title: 'All fields are required', variant: 'error' }); + return; + } + try { + await saveMutation.mutateAsync({ + host, + port: parseInt(port, 10) || 587, + username, + password, + fromEmail, + }); + toast({ title: 'Email connector saved', variant: 'success' }); + setEditing(false); + setPassword(''); + } catch (err) { + toast({ title: 'Failed to save', description: String(err), variant: 'error' }); + } + } + + async function handleDelete() { + try { + await deleteMutation.mutateAsync(); + toast({ title: 'Email connector removed', variant: 'success' }); + setConfirmDelete(false); + setEditing(false); + } catch (err) { + toast({ title: 'Failed to delete', description: String(err), variant: 'error' }); + } + } + + async function handleTest() { + if (!testTo) { + toast({ title: 'Enter a recipient email address', variant: 'error' }); + return; + } + try { + const result = await testMutation.mutateAsync(testTo); + if (result.status === 'sent') { + toast({ title: 'Test email sent', description: result.message, variant: 'success' }); + } else { + toast({ title: 'Test failed', description: result.message, variant: 'error' }); + } + } catch (err) { + toast({ title: 'Test failed', description: String(err), variant: 'error' }); + } + } + + async function handleToggleRegistration() { + if (!connector) return; + const newValue = !connector.registrationEnabled; + try { + await toggleMutation.mutateAsync(newValue); + toast({ + title: newValue ? 'Registration enabled' : 'Registration disabled', + variant: 'success', + }); + } catch (err) { + toast({ title: 'Failed to toggle registration', description: String(err), variant: 'error' }); + } + } + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( +
+ + Could not fetch email connector data. Please refresh. + +
+ ); + } + + return ( +
+

Email Connector

+ + {!isConfigured && ( + + Self-service registration is disabled. Configure an SMTP connector to enable email + verification for sign-up and password reset. + + )} + +
+ {/* Current config card (when configured and not editing) */} + {isConfigured && !editing && ( + +
+
+ Host + {connector.host} +
+
+ Port + {connector.port} +
+
+ Username + {connector.username} +
+
+ Password + {'*'.repeat(8)} +
+
+ From Email + {connector.fromEmail} +
+
+ + {!confirmDelete ? ( + + ) : ( + <> + + + + )} +
+
+
+ )} + + {/* SMTP form (when unconfigured or editing) */} + {showForm && ( + +
+ + setHost(e.target.value)} + /> + + + setPort(e.target.value)} + /> + + + setUsername(e.target.value)} + /> + + + setPassword(e.target.value)} + /> + + + setFromEmail(e.target.value)} + /> + +
+ + {editing && ( + + )} +
+
+
+ )} + + {/* Registration toggle (when configured) */} + {isConfigured && !editing && ( + +
+

+ {connector.registrationEnabled + ? 'New users can register with their email address and verify via a code sent to their inbox.' + : 'Registration is disabled. Only admin-invited users can sign in.'} +

+
+ +
+
+
+ )} + + {/* Test email (when configured and not editing) */} + {isConfigured && !editing && ( + +
+ + setTestTo(e.target.value)} + /> + + +
+
+ )} +
+
+ ); +}