diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index f6151a2..d940019 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -1786,6 +1786,54 @@ export type Database = { } Relationships: [] } + smtp_settings: { + Row: { + created_at: string + enabled: boolean + from_email: string + from_name: string | null + host: string + id: string + notes: string | null + port: number + reply_to: string | null + secure: boolean + updated_at: string + updated_by: string | null + username: string | null + } + Insert: { + created_at?: string + enabled?: boolean + from_email: string + from_name?: string | null + host: string + id?: string + notes?: string | null + port?: number + reply_to?: string | null + secure?: boolean + updated_at?: string + updated_by?: string | null + username?: string | null + } + Update: { + created_at?: string + enabled?: boolean + from_email?: string + from_name?: string | null + host?: string + id?: string + notes?: string | null + port?: number + reply_to?: string | null + secure?: boolean + updated_at?: string + updated_by?: string | null + username?: string | null + } + Relationships: [] + } status_updates: { Row: { body: string diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 2ed1b6c..fb097a5 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -28,6 +28,7 @@ import { Route as CasesIndexRouteImport } from './routes/cases.index' import { Route as CalendarIndexRouteImport } from './routes/calendar.index' import { Route as SettingsWorkflowsRouteImport } from './routes/settings.workflows' import { Route as SettingsWorkflowRouteImport } from './routes/settings.workflow' +import { Route as SettingsSmtpRouteImport } from './routes/settings.smtp' import { Route as SettingsProfileRouteImport } from './routes/settings.profile' import { Route as SettingsFeesRouteImport } from './routes/settings.fees' import { Route as InvoicesInvoiceIdRouteImport } from './routes/invoices.$invoiceId' @@ -137,6 +138,11 @@ const SettingsWorkflowRoute = SettingsWorkflowRouteImport.update({ path: '/workflow', getParentRoute: () => SettingsRoute, } as any) +const SettingsSmtpRoute = SettingsSmtpRouteImport.update({ + id: '/smtp', + path: '/smtp', + getParentRoute: () => SettingsRoute, +} as any) const SettingsProfileRoute = SettingsProfileRouteImport.update({ id: '/profile', path: '/profile', @@ -218,6 +224,7 @@ export interface FileRoutesByFullPath { '/invoices/$invoiceId': typeof InvoicesInvoiceIdRoute '/settings/fees': typeof SettingsFeesRoute '/settings/profile': typeof SettingsProfileRoute + '/settings/smtp': typeof SettingsSmtpRoute '/settings/workflow': typeof SettingsWorkflowRoute '/settings/workflows': typeof SettingsWorkflowsRoute '/calendar/': typeof CalendarIndexRoute @@ -251,6 +258,7 @@ export interface FileRoutesByTo { '/invoices/$invoiceId': typeof InvoicesInvoiceIdRoute '/settings/fees': typeof SettingsFeesRoute '/settings/profile': typeof SettingsProfileRoute + '/settings/smtp': typeof SettingsSmtpRoute '/settings/workflow': typeof SettingsWorkflowRoute '/settings/workflows': typeof SettingsWorkflowsRoute '/calendar': typeof CalendarIndexRoute @@ -286,6 +294,7 @@ export interface FileRoutesById { '/invoices/$invoiceId': typeof InvoicesInvoiceIdRoute '/settings/fees': typeof SettingsFeesRoute '/settings/profile': typeof SettingsProfileRoute + '/settings/smtp': typeof SettingsSmtpRoute '/settings/workflow': typeof SettingsWorkflowRoute '/settings/workflows': typeof SettingsWorkflowsRoute '/calendar/': typeof CalendarIndexRoute @@ -322,6 +331,7 @@ export interface FileRouteTypes { | '/invoices/$invoiceId' | '/settings/fees' | '/settings/profile' + | '/settings/smtp' | '/settings/workflow' | '/settings/workflows' | '/calendar/' @@ -355,6 +365,7 @@ export interface FileRouteTypes { | '/invoices/$invoiceId' | '/settings/fees' | '/settings/profile' + | '/settings/smtp' | '/settings/workflow' | '/settings/workflows' | '/calendar' @@ -389,6 +400,7 @@ export interface FileRouteTypes { | '/invoices/$invoiceId' | '/settings/fees' | '/settings/profile' + | '/settings/smtp' | '/settings/workflow' | '/settings/workflows' | '/calendar/' @@ -575,6 +587,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SettingsWorkflowRouteImport parentRoute: typeof SettingsRoute } + '/settings/smtp': { + id: '/settings/smtp' + path: '/smtp' + fullPath: '/settings/smtp' + preLoaderRoute: typeof SettingsSmtpRouteImport + parentRoute: typeof SettingsRoute + } '/settings/profile': { id: '/settings/profile' path: '/profile' @@ -672,6 +691,7 @@ declare module '@tanstack/react-router' { interface SettingsRouteChildren { SettingsFeesRoute: typeof SettingsFeesRoute SettingsProfileRoute: typeof SettingsProfileRoute + SettingsSmtpRoute: typeof SettingsSmtpRoute SettingsWorkflowRoute: typeof SettingsWorkflowRoute SettingsWorkflowsRoute: typeof SettingsWorkflowsRoute SettingsIndexRoute: typeof SettingsIndexRoute @@ -680,6 +700,7 @@ interface SettingsRouteChildren { const SettingsRouteChildren: SettingsRouteChildren = { SettingsFeesRoute: SettingsFeesRoute, SettingsProfileRoute: SettingsProfileRoute, + SettingsSmtpRoute: SettingsSmtpRoute, SettingsWorkflowRoute: SettingsWorkflowRoute, SettingsWorkflowsRoute: SettingsWorkflowsRoute, SettingsIndexRoute: SettingsIndexRoute, diff --git a/src/routes/settings.smtp.tsx b/src/routes/settings.smtp.tsx new file mode 100644 index 0000000..742a8a8 --- /dev/null +++ b/src/routes/settings.smtp.tsx @@ -0,0 +1,293 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "@/lib/auth"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Switch } from "@/components/ui/switch"; +import { Loader2, Mail, Send, KeyRound } from "lucide-react"; +import { toast } from "sonner"; + +export const Route = createFileRoute("/settings/smtp")({ + component: SmtpSettingsPage, +}); + +const EMPTY = { + host: "", + port: "587", + secure: false, + username: "", + from_email: "", + from_name: "", + reply_to: "", + enabled: true, + notes: "", +}; + +function SmtpSettingsPage() { + const { user, isAdmin } = useAuth(); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [testing, setTesting] = useState(false); + const [recordId, setRecordId] = useState(null); + const [form, setForm] = useState({ ...EMPTY }); + const [testTo, setTestTo] = useState(""); + + const load = async () => { + setLoading(true); + const { data } = await supabase + .from("smtp_settings") + .select("*") + .order("updated_at", { ascending: false }) + .limit(1) + .maybeSingle(); + if (data) { + setRecordId(data.id); + setForm({ + host: data.host ?? "", + port: String(data.port ?? 587), + secure: !!data.secure, + username: data.username ?? "", + from_email: data.from_email ?? "", + from_name: data.from_name ?? "", + reply_to: data.reply_to ?? "", + enabled: data.enabled ?? true, + notes: data.notes ?? "", + }); + } + setLoading(false); + }; + + useEffect(() => { + if (isAdmin) load(); + else setLoading(false); + }, [isAdmin]); + + const update = (k: keyof typeof EMPTY, v: string | boolean) => + setForm((f) => ({ ...f, [k]: v })); + + const save = async () => { + if (!form.host || !form.from_email) { + toast.error("Host and From email are required"); + return; + } + setSaving(true); + const payload = { + host: form.host.trim(), + port: parseInt(form.port, 10) || 587, + secure: form.secure, + username: form.username || null, + from_email: form.from_email.trim(), + from_name: form.from_name || null, + reply_to: form.reply_to || null, + enabled: form.enabled, + notes: form.notes || null, + updated_by: user?.id, + }; + const { error } = recordId + ? await supabase.from("smtp_settings").update(payload).eq("id", recordId) + : await supabase.from("smtp_settings").insert(payload); + setSaving(false); + if (error) { + toast.error("Could not save", { description: error.message }); + return; + } + toast.success("SMTP settings saved"); + load(); + }; + + const sendTest = async () => { + if (!testTo) { + toast.error("Enter a test recipient email"); + return; + } + setTesting(true); + const { data, error } = await supabase.functions.invoke("send-smtp-email", { + body: { + to: testTo, + subject: "SMTP test from your firm CRM", + html: `

This is a test email sent from ${form.from_email} via ${form.host}:${form.port}.

If you received this, your SMTP settings are working.

`, + text: `Test email from ${form.from_email} via ${form.host}:${form.port}.`, + }, + }); + setTesting(false); + if (error || (data as any)?.error) { + toast.error("Send failed", { + description: (error as any)?.message || (data as any)?.error, + }); + return; + } + toast.success(`Test email sent to ${testTo}`); + }; + + if (!isAdmin) { + return ( +
+ Only administrators can manage SMTP settings. +
+ ); + } + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ + + + SMTP server + + + + + update("host", e.target.value)} + placeholder="smtp.gmail.com" + /> + + + update("port", e.target.value)} + placeholder="587" + /> + + + update("username", e.target.value)} + placeholder="user@yourdomain.com" + /> + +
+ +
+ + Stored securely as the SMTP_PASSWORD secret. +
+
+
+
+
Use TLS / SSL
+
+ Enable for port 465 (implicit TLS). Leave off for 587 (STARTTLS). +
+
+ update("secure", v)} + /> +
+
+
+ + + + Sender identity + + + + update("from_email", e.target.value)} + placeholder="notifications@yourfirm.com" + /> + + + update("from_name", e.target.value)} + placeholder="Your Firm" + /> + + + update("reply_to", e.target.value)} + /> + + +