feat: add email notification settings and test email functionality

- Created a new migration to add email settings to the database.
- Implemented routes for managing notification settings, including retrieving and updating settings.
- Added functionality to send test emails using SMTP configuration from environment variables.
This commit is contained in:
Daniel Volz
2025-12-20 16:07:20 +01:00
parent aac4079c54
commit ce02ab8372
13 changed files with 3792 additions and 268 deletions
@@ -0,0 +1,4 @@
-- Add email notification settings to settings table
ALTER TABLE settings ADD COLUMN email_enabled INTEGER NOT NULL DEFAULT 0;
ALTER TABLE settings ADD COLUMN notification_email TEXT;
ALTER TABLE settings ADD COLUMN reminder_days_before INTEGER NOT NULL DEFAULT 7;
+2 -1
View File
@@ -2,6 +2,7 @@
"entries": [
{ "idx": 0, "version": 1, "when": 1734633120, "tag": "0000_init", "breakpoint": false },
{ "idx": 1, "version": 1, "when": 1734633121, "tag": "0001_add_strips", "breakpoint": false },
{ "idx": 2, "version": 1, "when": 1734633122, "tag": "0002_pack_inventory", "breakpoint": false }
{ "idx": 2, "version": 1, "when": 1734633122, "tag": "0002_pack_inventory", "breakpoint": false },
{ "idx": 3, "version": 1, "when": 1734710400, "tag": "0003_add_email_settings", "breakpoint": false }
]
}
+4
View File
@@ -45,5 +45,9 @@ export const settings = sqliteTable("settings", {
smtpFrom: text("smtp_from"),
smtpSecure: integer("smtp_secure", { mode: "boolean" }).notNull().default(false),
emailsPerDay: integer("emails_per_day").notNull().default(3),
// Email notification settings
emailEnabled: integer("email_enabled", { mode: "boolean" }).notNull().default(false),
notificationEmail: text("notification_email"),
reminderDaysBefore: integer("reminder_days_before").notNull().default(7),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().default(sql`CURRENT_TIMESTAMP`),
});
+2
View File
@@ -9,6 +9,7 @@ import { env } from "./plugins/env.js";
import { healthRoutes } from "./routes/health.js";
import { authRoutes } from "./routes/auth.js";
import { medicationRoutes } from "./routes/medications.js";
import { settingsRoutes } from "./routes/settings.js";
const app = Fastify({
logger: {
@@ -56,6 +57,7 @@ await app.register(jwt, { secret: env.JWT_SECRET, cookie: { cookieName: "access_
await app.register(healthRoutes);
await app.register(authRoutes);
await app.register(medicationRoutes);
await app.register(settingsRoutes);
const start = async () => {
try {
+123
View File
@@ -0,0 +1,123 @@
import { FastifyInstance } from "fastify";
import nodemailer from "nodemailer";
import { readFileSync, writeFileSync, existsSync } from "fs";
import { resolve } from "path";
type SettingsBody = {
emailEnabled: boolean;
notificationEmail: string;
reminderDaysBefore: number;
};
type TestEmailBody = {
email: string;
};
// Notification settings are stored in a JSON file (user-configurable)
// SMTP settings come from .env (admin-configured)
const notificationSettingsFile = resolve(process.cwd(), "data", "notification-settings.json");
type NotificationSettings = {
emailEnabled: boolean;
notificationEmail: string;
reminderDaysBefore: number;
};
function loadNotificationSettings(): NotificationSettings {
try {
if (existsSync(notificationSettingsFile)) {
return JSON.parse(readFileSync(notificationSettingsFile, "utf-8"));
}
} catch {
// ignore
}
return { emailEnabled: false, notificationEmail: "", reminderDaysBefore: 7 };
}
function saveNotificationSettings(settings: NotificationSettings): void {
writeFileSync(notificationSettingsFile, JSON.stringify(settings, null, 2));
}
export async function settingsRoutes(app: FastifyInstance) {
// Get settings - notification from JSON file, SMTP from process.env
app.get("/settings", async (_request, reply) => {
const notification = loadNotificationSettings();
return reply.send({
// Notification settings (user-configurable, stored in JSON)
emailEnabled: notification.emailEnabled,
notificationEmail: notification.notificationEmail,
reminderDaysBefore: notification.reminderDaysBefore,
// SMTP settings (admin-configured, from .env)
smtpHost: process.env.SMTP_HOST ?? "",
smtpPort: parseInt(process.env.SMTP_PORT ?? "587"),
smtpUser: process.env.SMTP_USER ?? "",
smtpFrom: process.env.SMTP_FROM ?? "",
smtpSecure: process.env.SMTP_SECURE === "true",
hasSmtpPassword: !!process.env.SMTP_PASS,
});
});
// Update settings - only notification settings are saved (SMTP comes from .env)
app.put<{ Body: SettingsBody }>("/settings", async (request, reply) => {
const body = request.body;
// Save notification settings to JSON file
saveNotificationSettings({
emailEnabled: body.emailEnabled,
notificationEmail: body.notificationEmail,
reminderDaysBefore: body.reminderDaysBefore,
});
return reply.send({ success: true });
});
// Test email - use SMTP settings from process.env
app.post<{ Body: TestEmailBody }>("/settings/test-email", async (request, reply) => {
const { email } = request.body;
const smtpHost = process.env.SMTP_HOST;
const smtpUser = process.env.SMTP_USER;
const smtpPass = process.env.SMTP_PASS;
const smtpPort = parseInt(process.env.SMTP_PORT ?? "587");
const smtpSecure = process.env.SMTP_SECURE === "true";
const smtpFrom = process.env.SMTP_FROM ?? smtpUser;
if (!smtpHost || !smtpUser) {
return reply.status(400).send({ error: "SMTP not configured" });
}
try {
const transporter = nodemailer.createTransport({
host: smtpHost,
port: smtpPort,
secure: smtpSecure,
auth: {
user: smtpUser,
pass: smtpPass ?? "",
},
});
await transporter.sendMail({
from: smtpFrom,
to: email,
subject: "MedAssist - Test Email",
text: "This is a test email from MedAssist. If you received this, your email configuration is working correctly!",
html: `
<div style="font-family: system-ui, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<h2 style="color: #2563eb;">MedAssist - Test Email</h2>
<p>This is a test email from MedAssist.</p>
<p style="color: #10b981; font-weight: 600;">✓ If you received this, your email configuration is working correctly!</p>
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;" />
<p style="color: #6b7280; font-size: 14px;">Sent from MedAssist Medication Planner</p>
</div>
`,
});
return reply.send({ success: true, message: "Test email sent successfully" });
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
return reply.status(500).send({ error: `Failed to send email: ${errorMessage}` });
}
});
}