feat(smtp): update SMTP management page with test functionality and email templates link

- Redesign SMTP management page with two-column layout

- Add SMTP connection test functionality

- Add link to email templates page

- Show current SMTP status

- Add quick help section
This commit is contained in:
Ashwin Kumar 2026-04-10 04:55:44 +02:00
parent 2409d85b3c
commit 10b6c48f1b

View file

@ -1,4 +1,6 @@
import { Show, createSignal, onMount } from 'solid-js';
import { Show, createSignal, onMount, createResource } from "solid-js";
import { useNavigate } from "@solidjs/router";
import { api } from "~/lib/api";
type SmtpConfig = {
host: string;
@ -13,87 +15,54 @@ type SmtpConfig = {
};
const DEFAULT_CONFIG: SmtpConfig = {
host: '',
host: "",
port: 587,
secure: false,
username: '',
password: '',
fromEmail: '',
fromName: 'NxtGIG',
replyToEmail: '',
username: "",
password: "",
fromEmail: "",
fromName: "NXTGAUGE",
replyToEmail: "",
enabled: true,
};
const READ_ENDPOINTS = [
'/api/admin/smtp-config',
'/api/admin/settings/smtp',
'/api/admin/system-config/smtp',
'/api/gateway/admin/smtp-config',
];
const WRITE_ENDPOINTS = [
'/api/admin/smtp-config',
'/api/admin/settings/smtp',
'/api/admin/system-config/smtp',
'/api/gateway/admin/smtp-config',
];
function authHeaders() {
const token = typeof sessionStorage !== 'undefined'
? (sessionStorage.getItem('nxtgauge_admin_access_token') || '')
: '';
return {
'Content-Type': 'application/json',
Accept: 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
}
function normalizePayload(payload: any): SmtpConfig {
const src = payload?.config || payload?.data || payload || {};
return {
host: String(src.host || ''),
port: Number(src.port || 587),
secure: src.secure === true || String(src.secure || '').toLowerCase() === 'true',
username: String(src.username || src.user || ''),
password: String(src.password || ''),
fromEmail: String(src.fromEmail || src.from_email || ''),
fromName: String(src.fromName || src.from_name || 'NxtGIG'),
replyToEmail: String(src.replyToEmail || src.reply_to_email || ''),
enabled: src.enabled !== false,
};
}
export default function SmtpManagementPage() {
const [loading, setLoading] = createSignal(false);
const navigate = useNavigate();
const [saving, setSaving] = createSignal(false);
const [error, setError] = createSignal('');
const [success, setSuccess] = createSignal('');
const [testing, setTesting] = createSignal(false);
const [error, setError] = createSignal("");
const [success, setSuccess] = createSignal("");
const [showPassword, setShowPassword] = createSignal(false);
const [testEmail, setTestEmail] = createSignal("");
const [cfg, setCfg] = createSignal<SmtpConfig>(DEFAULT_CONFIG);
const load = async () => {
setLoading(true);
setError('');
const [smtpStatus] = createResource(async () => {
try {
let loaded = false;
for (const endpoint of READ_ENDPOINTS) {
const res = await fetch(endpoint, { method: 'GET', headers: authHeaders(), credentials: 'include' }).catch(() => null);
if (!res || !res.ok) continue;
const payload = await res.json().catch(() => ({}));
setCfg(normalizePayload(payload));
loaded = true;
break;
}
if (!loaded) setCfg(DEFAULT_CONFIG);
} catch (e: any) {
setError(e?.message || 'Failed to load SMTP configuration.');
} finally {
setLoading(false);
// Get current env config from backend
const res = await api.get("/admin/smtp-config");
return res.data;
} catch (e) {
return null;
}
};
});
onMount(() => void load());
onMount(() => {
// Load from environment if available
const envHost = import.meta.env.VITE_SMTP_HOST || "";
const envPort = import.meta.env.VITE_SMTP_PORT || 587;
const envUser = import.meta.env.VITE_SMTP_USER || "";
const envFrom = import.meta.env.VITE_SMTP_FROM || "";
if (envHost) {
setCfg((prev) => ({
...prev,
host: envHost,
port: Number(envPort),
username: envUser,
fromEmail: envFrom,
}));
}
});
const setField = <K extends keyof SmtpConfig>(key: K, value: SmtpConfig[K]) => {
setCfg((prev) => ({ ...prev, [key]: value }));
@ -102,10 +71,11 @@ export default function SmtpManagementPage() {
const save = async (e: Event) => {
e.preventDefault();
setSaving(true);
setError('');
setSuccess('');
setError("");
setSuccess("");
try {
const payload = {
await api.post("/admin/smtp-config", {
host: cfg().host.trim(),
port: Number(cfg().port || 0),
secure: cfg().secure,
@ -115,116 +85,291 @@ export default function SmtpManagementPage() {
from_name: cfg().fromName.trim(),
reply_to_email: cfg().replyToEmail.trim(),
enabled: cfg().enabled,
};
});
let saved = false;
for (const endpoint of WRITE_ENDPOINTS) {
const methods: Array<'PUT' | 'PATCH' | 'POST'> = ['PUT', 'PATCH', 'POST'];
for (const method of methods) {
const res = await fetch(endpoint, {
method,
headers: authHeaders(),
credentials: 'include',
body: JSON.stringify(payload),
}).catch(() => null);
if (!res || !res.ok) continue;
saved = true;
break;
}
if (saved) break;
}
if (!saved) throw new Error('Could not save SMTP configuration. Please verify backend endpoint wiring.');
setSuccess('SMTP configuration saved successfully.');
await load();
setSuccess("SMTP configuration saved successfully.");
} catch (e: any) {
setError(e?.message || 'Failed to save SMTP configuration.');
setError(e?.message || "Failed to save SMTP configuration.");
} finally {
setSaving(false);
}
};
const inputCls = 'w-full rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13]';
const labelCls = 'mb-1.5 block text-sm font-medium text-gray-700';
const testConnection = async () => {
if (!testEmail()) {
setError("Please enter a test email address");
return;
}
setTesting(true);
setError("");
setSuccess("");
try {
await api.post("/admin/smtp-test", {
to_email: testEmail(),
config: {
host: cfg().host.trim(),
port: Number(cfg().port || 0),
secure: cfg().secure,
username: cfg().username.trim(),
password: cfg().password,
from_email: cfg().fromEmail.trim(),
from_name: cfg().fromName.trim(),
},
});
setSuccess(`Test email sent to ${testEmail()}. Check your inbox!`);
setTestEmail("");
} catch (e: any) {
setError(e?.message || "Failed to send test email. Check your SMTP settings.");
} finally {
setTesting(false);
}
};
const inputCls =
"w-full rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500";
const labelCls = "mb-1.5 block text-sm font-medium text-gray-700";
return (
<div class="w-full space-y-6 pb-8">
<div style="margin-bottom:1.5rem">
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">SMTP Management</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">Manage transactional email provider credentials and sender defaults.</p>
<div class="p-6 max-w-6xl mx-auto">
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">SMTP Management</h1>
<p class="mt-1 text-gray-600">
Configure transactional email settings and test connectivity.
</p>
</div>
<Show when={error()}>
<div class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{error()}
</div>
</Show>
<Show when={success()}>
<div class="rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">{success()}</div>
<div class="mb-4 rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-700">
{success()}
</div>
</Show>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm p-6">
<Show when={loading()}>
<p class="text-sm text-gray-500">Loading configuration...</p>
</Show>
<div class="grid lg:grid-cols-3 gap-6">
{/* SMTP Configuration Form */}
<div class="lg:col-span-2 space-y-6">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm p-6">
<h2 class="text-lg font-semibold mb-4">SMTP Configuration</h2>
<form onSubmit={save} class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label class={labelCls}>SMTP Host</label>
<input class={inputCls} value={cfg().host} onInput={(e) => setField('host', e.currentTarget.value)} placeholder="smtp.example.com" />
</div>
<div>
<label class={labelCls}>Port</label>
<input type="number" class={inputCls} value={String(cfg().port)} onInput={(e) => setField('port', Number(e.currentTarget.value || 0))} />
</div>
<div>
<label class={labelCls}>Username</label>
<input class={inputCls} value={cfg().username} onInput={(e) => setField('username', e.currentTarget.value)} />
</div>
<div>
<label class={labelCls}>Password</label>
<div class="flex gap-2">
<input
type={showPassword() ? 'text' : 'password'}
class={inputCls}
value={cfg().password}
onInput={(e) => setField('password', e.currentTarget.value)}
/>
<button type="button" class="rounded-lg border border-gray-200 px-3 py-2 text-xs font-semibold text-gray-700 hover:bg-gray-50" onClick={() => setShowPassword((v) => !v)}>
{showPassword() ? 'Hide' : 'Show'}
<form onSubmit={save} class="space-y-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class={labelCls}>SMTP Host</label>
<input
class={inputCls}
value={cfg().host}
onInput={(e) => setField("host", e.currentTarget.value)}
placeholder="smtp.gmail.com"
/>
</div>
<div>
<label class={labelCls}>Port</label>
<input
type="number"
class={inputCls}
value={String(cfg().port)}
onInput={(e) => setField("port", Number(e.currentTarget.value || 0))}
/>
</div>
<div>
<label class={labelCls}>Username</label>
<input
class={inputCls}
value={cfg().username}
onInput={(e) => setField("username", e.currentTarget.value)}
placeholder="your-email@gmail.com"
/>
</div>
<div>
<label class={labelCls}>Password / App Password</label>
<div class="flex gap-2">
<input
type={showPassword() ? "text" : "password"}
class={inputCls}
value={cfg().password}
onInput={(e) => setField("password", e.currentTarget.value)}
placeholder="••••••••"
/>
<button
type="button"
class="px-3 py-2 text-sm border border-gray-200 rounded-lg hover:bg-gray-50"
onClick={() => setShowPassword((v) => !v)}
>
{showPassword() ? "Hide" : "Show"}
</button>
</div>
</div>
<div>
<label class={labelCls}>From Email</label>
<input
type="email"
class={inputCls}
value={cfg().fromEmail}
onInput={(e) => setField("fromEmail", e.currentTarget.value)}
placeholder="noreply@nxtgauge.com"
/>
</div>
<div>
<label class={labelCls}>From Name</label>
<input
class={inputCls}
value={cfg().fromName}
onInput={(e) => setField("fromName", e.currentTarget.value)}
placeholder="NXTGAUGE"
/>
</div>
</div>
<div class="flex items-center gap-6 pt-2">
<label class="flex items-center gap-2 text-sm text-gray-700">
<input
type="checkbox"
checked={cfg().secure}
onChange={(e) => setField("secure", e.currentTarget.checked)}
class="rounded text-orange-500 focus:ring-orange-500"
/>
Use SSL/TLS (Port 465)
</label>
<label class="flex items-center gap-2 text-sm text-gray-700">
<input
type="checkbox"
checked={cfg().enabled}
onChange={(e) => setField("enabled", e.currentTarget.checked)}
class="rounded text-orange-500 focus:ring-orange-500"
/>
SMTP Enabled
</label>
</div>
<div class="flex items-center justify-end gap-3 pt-4 border-t">
<button
type="submit"
class="px-6 py-2 bg-orange-500 text-white rounded-lg font-medium hover:bg-orange-600 transition-colors disabled:opacity-50"
disabled={saving()}
>
{saving() ? "Saving..." : "Save Configuration"}
</button>
</div>
</form>
</section>
{/* Email Templates Link */}
<section class="rounded-xl border border-gray-200 bg-white shadow-sm p-6">
<h2 class="text-lg font-semibold mb-2">Email Templates</h2>
<p class="text-gray-600 text-sm mb-4">
Manage and preview the 35 branded email templates used for notifications.
</p>
<button
onClick={() => navigate("/admin/email-management")}
class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors"
>
Go to Email Templates
</button>
</section>
</div>
{/* Test Connection Panel */}
<div class="space-y-6">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm p-6">
<h2 class="text-lg font-semibold mb-4">Test Connection</h2>
<p class="text-sm text-gray-600 mb-4">
Send a test email to verify your SMTP configuration is working correctly.
</p>
<div class="space-y-3">
<div>
<label class={labelCls}>Test Email Address</label>
<input
type="email"
class={inputCls}
value={testEmail()}
onInput={(e) => setTestEmail(e.currentTarget.value)}
placeholder="your@email.com"
/>
</div>
<button
onClick={testConnection}
disabled={testing() || !testEmail()}
class="w-full px-4 py-2 bg-green-500 text-white rounded-lg font-medium hover:bg-green-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<Show when={testing()} fallback="Send Test Email">
<span class="flex items-center justify-center gap-2">
<span class="animate-spin w-4 h-4 border-2 border-white border-t-transparent rounded-full" />
Sending...
</span>
</Show>
</button>
</div>
</div>
<div>
<label class={labelCls}>From Email</label>
<input type="email" class={inputCls} value={cfg().fromEmail} onInput={(e) => setField('fromEmail', e.currentTarget.value)} placeholder="no-reply@nxtgig.com" />
</div>
<div>
<label class={labelCls}>From Name</label>
<input class={inputCls} value={cfg().fromName} onInput={(e) => setField('fromName', e.currentTarget.value)} />
</div>
<div>
<label class={labelCls}>Reply-To Email</label>
<input type="email" class={inputCls} value={cfg().replyToEmail} onInput={(e) => setField('replyToEmail', e.currentTarget.value)} />
</div>
<div class="flex items-end gap-4 pb-2">
<label class="inline-flex items-center gap-2 text-sm text-gray-700">
<input type="checkbox" checked={cfg().secure} onChange={(e) => setField('secure', e.currentTarget.checked)} />
Use SSL/TLS
</label>
<label class="inline-flex items-center gap-2 text-sm text-gray-700">
<input type="checkbox" checked={cfg().enabled} onChange={(e) => setField('enabled', e.currentTarget.checked)} />
SMTP Enabled
</label>
</div>
<div class="sm:col-span-2 flex items-center justify-end gap-2 border-t border-gray-100 pt-4">
<button type="button" class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" onClick={() => void load()}>
Reload
</button>
<button type="submit" class="rounded-lg bg-[#0D0D2A] px-4 py-2 text-sm font-semibold text-white hover:bg-[#17173f]" disabled={saving()}>
{saving() ? 'Saving...' : 'Save Configuration'}
</button>
</div>
</form>
</section>
</section>
{/* SMTP Status */}
<section class="rounded-xl border border-gray-200 bg-white shadow-sm p-6">
<h2 class="text-lg font-semibold mb-4">Current Status</h2>
<Show when={smtpStatus.loading}>
<p class="text-sm text-gray-500">Loading status...</p>
</Show>
<Show when={!smtpStatus.loading}>
<div class="space-y-3">
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">SMTP Status</span>
<span
class={`px-2 py-1 rounded-full text-xs font-medium ${
cfg().enabled && cfg().host
? "bg-green-100 text-green-800"
: "bg-gray-100 text-gray-800"
}`}
>
{cfg().enabled && cfg().host ? "Configured" : "Not Configured"}
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Host</span>
<span class="text-sm font-medium">{cfg().host || "—"}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Port</span>
<span class="text-sm font-medium">{cfg().port || "—"}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Security</span>
<span class="text-sm font-medium">
{cfg().secure ? "SSL/TLS" : "STARTTLS/None"}
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">From</span>
<span class="text-sm font-medium">{cfg().fromEmail || "—"}</span>
</div>
</div>
</Show>
</section>
{/* Quick Help */}
<section class="rounded-xl border border-blue-200 bg-blue-50 p-6">
<h2 class="text-lg font-semibold mb-2 text-blue-900">Quick Help</h2>
<ul class="text-sm text-blue-800 space-y-2">
<li> Use App Password for Gmail</li>
<li> Port 587 for STARTTLS</li>
<li> Port 465 for SSL/TLS</li>
<li> Enable 2FA for security</li>
</ul>
</section>
</div>
</div>
</div>
);
}