From be07ab2fa33bec23a8507d117e4ade6250a06736 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 22:52:38 +0000 Subject: [PATCH 1/3] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/integrations/supabase/types.ts | 48 +++++++++++++++++++ ...5_4582e8e4-c42f-48ce-b780-badfd45a3d16.sql | 43 +++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 supabase/migrations/20260417225235_4582e8e4-c42f-48ce-b780-badfd45a3d16.sql 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/supabase/migrations/20260417225235_4582e8e4-c42f-48ce-b780-badfd45a3d16.sql b/supabase/migrations/20260417225235_4582e8e4-c42f-48ce-b780-badfd45a3d16.sql new file mode 100644 index 0000000..167b6e5 --- /dev/null +++ b/supabase/migrations/20260417225235_4582e8e4-c42f-48ce-b780-badfd45a3d16.sql @@ -0,0 +1,43 @@ +create table if not exists public.smtp_settings ( + id uuid primary key default gen_random_uuid(), + host text not null, + port integer not null default 587, + secure boolean not null default false, + username text, + from_email text not null, + from_name text, + reply_to text, + enabled boolean not null default true, + notes text, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + updated_by uuid +); + +alter table public.smtp_settings enable row level security; + +create policy "Admins can read smtp settings" + on public.smtp_settings for select + to authenticated + using (public.is_admin(auth.uid())); + +create policy "Admins can insert smtp settings" + on public.smtp_settings for insert + to authenticated + with check (public.is_admin(auth.uid())); + +create policy "Admins can update smtp settings" + on public.smtp_settings for update + to authenticated + using (public.is_admin(auth.uid())) + with check (public.is_admin(auth.uid())); + +create policy "Admins can delete smtp settings" + on public.smtp_settings for delete + to authenticated + using (public.is_admin(auth.uid())); + +drop trigger if exists trg_smtp_settings_updated_at on public.smtp_settings; +create trigger trg_smtp_settings_updated_at + before update on public.smtp_settings + for each row execute function public.tg_set_updated_at(); \ No newline at end of file From c71f7dbd877e60e249767afa5b480dd04fc18023 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 22:53:27 +0000 Subject: [PATCH 2/3] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.smtp.tsx | 293 ++++++++++++++++++++ src/routes/settings.tsx | 1 + supabase/functions/send-smtp-email/index.ts | 104 +++++++ 3 files changed, 398 insertions(+) create mode 100644 src/routes/settings.smtp.tsx create mode 100644 supabase/functions/send-smtp-email/index.ts 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)} + /> + + +