All checks were successful
PostgreSQL Compatibility Matrix / PG14 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG15 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG16 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG17 smoke (push) Successful in 6s
PostgreSQL Compatibility Matrix / PG18 smoke (push) Successful in 6s
Implemented backend and frontend support for managing SMTP settings for email notifications. Includes API endpoints, database migration, and UI integration for configuring and testing email alerts.
303 lines
11 KiB
JavaScript
303 lines
11 KiB
JavaScript
import React, { useEffect, useState } from "react";
|
|
import { apiFetch } from "../api";
|
|
import { useAuth } from "../state";
|
|
|
|
export function AdminUsersPage() {
|
|
const { tokens, refresh, me } = useAuth();
|
|
const [users, setUsers] = useState([]);
|
|
const [form, setForm] = useState({ email: "", password: "", role: "viewer" });
|
|
const [emailSettings, setEmailSettings] = useState({
|
|
enabled: false,
|
|
smtp_host: "",
|
|
smtp_port: 587,
|
|
smtp_username: "",
|
|
smtp_password: "",
|
|
clear_smtp_password: false,
|
|
from_email: "",
|
|
use_starttls: true,
|
|
use_ssl: false,
|
|
alert_recipients: "",
|
|
});
|
|
const [smtpState, setSmtpState] = useState({ has_password: false, updated_at: null });
|
|
const [testRecipient, setTestRecipient] = useState("");
|
|
const [smtpInfo, setSmtpInfo] = useState("");
|
|
const [error, setError] = useState("");
|
|
|
|
const load = async () => {
|
|
const [userRows, smtp] = await Promise.all([
|
|
apiFetch("/admin/users", {}, tokens, refresh),
|
|
apiFetch("/admin/settings/email", {}, tokens, refresh),
|
|
]);
|
|
setUsers(userRows);
|
|
setEmailSettings((prev) => ({
|
|
...prev,
|
|
enabled: !!smtp.enabled,
|
|
smtp_host: smtp.smtp_host || "",
|
|
smtp_port: smtp.smtp_port || 587,
|
|
smtp_username: smtp.smtp_username || "",
|
|
smtp_password: "",
|
|
clear_smtp_password: false,
|
|
from_email: smtp.from_email || "",
|
|
use_starttls: !!smtp.use_starttls,
|
|
use_ssl: !!smtp.use_ssl,
|
|
alert_recipients: (smtp.alert_recipients || []).join(", "),
|
|
}));
|
|
setSmtpState({ has_password: !!smtp.has_password, updated_at: smtp.updated_at });
|
|
setTestRecipient(smtp.from_email || "");
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (me?.role === "admin") load().catch((e) => setError(String(e.message || e)));
|
|
}, [me]);
|
|
|
|
if (me?.role !== "admin") return <div className="card">Admins only.</div>;
|
|
|
|
const create = async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
await apiFetch("/admin/users", { method: "POST", body: JSON.stringify(form) }, tokens, refresh);
|
|
setForm({ email: "", password: "", role: "viewer" });
|
|
await load();
|
|
} catch (e) {
|
|
setError(String(e.message || e));
|
|
}
|
|
};
|
|
|
|
const remove = async (id) => {
|
|
try {
|
|
await apiFetch(`/admin/users/${id}`, { method: "DELETE" }, tokens, refresh);
|
|
await load();
|
|
} catch (e) {
|
|
setError(String(e.message || e));
|
|
}
|
|
};
|
|
|
|
const saveSmtp = async (e) => {
|
|
e.preventDefault();
|
|
setError("");
|
|
setSmtpInfo("");
|
|
try {
|
|
const recipients = emailSettings.alert_recipients
|
|
.split(",")
|
|
.map((item) => item.trim())
|
|
.filter(Boolean);
|
|
const payload = {
|
|
...emailSettings,
|
|
smtp_host: emailSettings.smtp_host.trim() || null,
|
|
smtp_username: emailSettings.smtp_username.trim() || null,
|
|
from_email: emailSettings.from_email.trim() || null,
|
|
smtp_password: emailSettings.smtp_password || null,
|
|
alert_recipients: recipients,
|
|
};
|
|
await apiFetch("/admin/settings/email", { method: "PUT", body: JSON.stringify(payload) }, tokens, refresh);
|
|
setSmtpInfo("SMTP settings saved.");
|
|
await load();
|
|
} catch (e) {
|
|
setError(String(e.message || e));
|
|
}
|
|
};
|
|
|
|
const sendTestMail = async () => {
|
|
setError("");
|
|
setSmtpInfo("");
|
|
try {
|
|
const recipient = testRecipient.trim();
|
|
if (!recipient) {
|
|
throw new Error("Please enter a test recipient email.");
|
|
}
|
|
await apiFetch(
|
|
"/admin/settings/email/test",
|
|
{ method: "POST", body: JSON.stringify({ recipient }) },
|
|
tokens,
|
|
refresh
|
|
);
|
|
setSmtpInfo(`Test email sent to ${recipient}.`);
|
|
} catch (e) {
|
|
setError(String(e.message || e));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="admin-settings-page">
|
|
<h2>Admin Settings - Users</h2>
|
|
{error && <div className="card error">{error}</div>}
|
|
|
|
<div className="card">
|
|
<h3>User Management</h3>
|
|
<form className="grid three admin-user-form" onSubmit={create}>
|
|
<input value={form.email} placeholder="E-Mail" onChange={(e) => setForm({ ...form, email: e.target.value })} />
|
|
<input
|
|
type="password"
|
|
value={form.password}
|
|
placeholder="Password"
|
|
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
|
/>
|
|
<select value={form.role} onChange={(e) => setForm({ ...form, role: e.target.value })}>
|
|
<option value="viewer">viewer</option>
|
|
<option value="operator">operator</option>
|
|
<option value="admin">admin</option>
|
|
</select>
|
|
<div className="form-actions field-full">
|
|
<button className="primary-btn" type="submit">Create User</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div className="card admin-users-table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Email</th>
|
|
<th>Role</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{users.map((u) => (
|
|
<tr key={u.id} className="admin-user-row">
|
|
<td className="user-col-id">{u.id}</td>
|
|
<td className="user-col-email">{u.email}</td>
|
|
<td>
|
|
<span className={`pill role-pill role-${u.role}`}>{u.role}</span>
|
|
</td>
|
|
<td>
|
|
{u.id !== me.id && (
|
|
<button className="table-action-btn delete small-btn" onClick={() => remove(u.id)}>
|
|
<span aria-hidden="true">
|
|
<svg viewBox="0 0 24 24" width="12" height="12">
|
|
<path
|
|
d="M9 3h6l1 2h4v2H4V5h4l1-2zm1 6h2v8h-2V9zm4 0h2v8h-2V9zM7 9h2v8H7V9z"
|
|
fill="currentColor"
|
|
/>
|
|
</svg>
|
|
</span>
|
|
<span>Delete</span>
|
|
</button>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div className="card">
|
|
<h3>Alert Email Notifications (SMTP)</h3>
|
|
<p className="muted">
|
|
Configure outgoing SMTP for alert emails. This is send-only; no inbound mailbox is used.
|
|
</p>
|
|
{smtpInfo && <div className="test-connection-result ok">{smtpInfo}</div>}
|
|
<form className="grid two admin-smtp-form" onSubmit={saveSmtp}>
|
|
<label className="toggle-check field-full">
|
|
<input
|
|
type="checkbox"
|
|
checked={emailSettings.enabled}
|
|
onChange={(e) => setEmailSettings({ ...emailSettings, enabled: e.target.checked })}
|
|
/>
|
|
<span className="toggle-ui" />
|
|
<span>
|
|
<strong>Enable alert emails</strong>
|
|
</span>
|
|
</label>
|
|
|
|
<div>
|
|
<label>SMTP host</label>
|
|
<input
|
|
value={emailSettings.smtp_host}
|
|
placeholder="smtp.example.com"
|
|
onChange={(e) => setEmailSettings({ ...emailSettings, smtp_host: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label>SMTP port</label>
|
|
<input
|
|
type="number"
|
|
value={emailSettings.smtp_port}
|
|
onChange={(e) => setEmailSettings({ ...emailSettings, smtp_port: Number(e.target.value || 587) })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label>SMTP username</label>
|
|
<input
|
|
value={emailSettings.smtp_username}
|
|
placeholder="alerts@example.com"
|
|
onChange={(e) => setEmailSettings({ ...emailSettings, smtp_username: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label>SMTP password</label>
|
|
<input
|
|
type="password"
|
|
value={emailSettings.smtp_password}
|
|
placeholder={smtpState.has_password ? "Stored (enter to replace)" : "Set password"}
|
|
onChange={(e) => setEmailSettings({ ...emailSettings, smtp_password: e.target.value, clear_smtp_password: false })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label>From email</label>
|
|
<input
|
|
value={emailSettings.from_email}
|
|
placeholder="noreply@example.com"
|
|
onChange={(e) => setEmailSettings({ ...emailSettings, from_email: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label>Alert recipients (comma-separated)</label>
|
|
<input
|
|
value={emailSettings.alert_recipients}
|
|
placeholder="dba@example.com, oncall@example.com"
|
|
onChange={(e) => setEmailSettings({ ...emailSettings, alert_recipients: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<label className="toggle-check">
|
|
<input
|
|
type="checkbox"
|
|
checked={emailSettings.use_starttls}
|
|
onChange={(e) => setEmailSettings({ ...emailSettings, use_starttls: e.target.checked, use_ssl: e.target.checked ? false : emailSettings.use_ssl })}
|
|
/>
|
|
<span className="toggle-ui" />
|
|
<span>Use STARTTLS</span>
|
|
</label>
|
|
<label className="toggle-check">
|
|
<input
|
|
type="checkbox"
|
|
checked={emailSettings.use_ssl}
|
|
onChange={(e) => setEmailSettings({ ...emailSettings, use_ssl: e.target.checked, use_starttls: e.target.checked ? false : emailSettings.use_starttls })}
|
|
/>
|
|
<span className="toggle-ui" />
|
|
<span>Use SSL/TLS (SMTPS)</span>
|
|
</label>
|
|
|
|
<label className="toggle-check field-full">
|
|
<input
|
|
type="checkbox"
|
|
checked={emailSettings.clear_smtp_password}
|
|
onChange={(e) => setEmailSettings({ ...emailSettings, clear_smtp_password: e.target.checked, smtp_password: e.target.checked ? "" : emailSettings.smtp_password })}
|
|
/>
|
|
<span className="toggle-ui" />
|
|
<span>Clear stored SMTP password</span>
|
|
</label>
|
|
|
|
<div className="form-actions field-full">
|
|
<input
|
|
className="admin-test-recipient"
|
|
value={testRecipient}
|
|
placeholder="test recipient email"
|
|
onChange={(e) => setTestRecipient(e.target.value)}
|
|
/>
|
|
<button className="secondary-btn" type="button" onClick={sendTestMail}>Send Test Mail</button>
|
|
<button className="primary-btn" type="submit">Save SMTP Settings</button>
|
|
</div>
|
|
<small className="muted field-full">
|
|
Last updated: {smtpState.updated_at ? new Date(smtpState.updated_at).toLocaleString() : "not configured yet"}
|
|
</small>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|