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:
parent
2409d85b3c
commit
10b6c48f1b
1 changed files with 308 additions and 163 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue