mirror of
https://github.com/temetro/temetro.git
synced 2026-08-19 22:56:17 +00:00
feat: email provider, admin password reset, portal new-patient, chat pill
Chat: history pill now shows a History icon + a Start-new-chat (SquarePen) button; removed the duplicate chat-history list from the sidebar. Email: deployment-wide email provider config (Resend/Postmark/SendGrid/SMTP) in Settings → Developers, with encrypted API key and a Send-test action. sendEmail dispatches via the chosen provider (REST via fetch; SMTP via nodemailer). Forgot password with no provider: alert the clinic admin(s) via a "System" message card in Messages + a bell notification (seeded system user + per-clinic System conversation); clicking deep-links to /settings?tab=careTeam&member=<id>. Admins can set a member's password directly from the employee dialog (PATCH /api/staff/:id/password via Better Auth's internal context — no admin plugin needed). Patient Portal: "New patient" booking path registers a demographics-only patient then books; bookings reject double-booked slots (409). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+126
-26
@@ -1,6 +1,7 @@
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
import { env } from "../env.js";
|
||||
import { getActiveConfig } from "../services/email-config.js";
|
||||
|
||||
type SendArgs = {
|
||||
to: string;
|
||||
@@ -9,10 +10,9 @@ type SendArgs = {
|
||||
html?: string;
|
||||
};
|
||||
|
||||
// Lazily build a transport. With SMTP_HOST configured we send real mail;
|
||||
// otherwise we fall back to logging the message (and any links) to the
|
||||
// server console — zero setup for local / open-source development.
|
||||
const transport = env.SMTP_HOST
|
||||
// SMTP transport (built from env) — used when the active provider is "smtp" or,
|
||||
// for backward compatibility, when SMTP_HOST is set and no provider is chosen.
|
||||
const smtpTransport = env.SMTP_HOST
|
||||
? nodemailer.createTransport({
|
||||
host: env.SMTP_HOST,
|
||||
port: env.SMTP_PORT ?? 587,
|
||||
@@ -24,26 +24,126 @@ const transport = env.SMTP_HOST
|
||||
})
|
||||
: null;
|
||||
|
||||
export async function sendEmail({ to, subject, text, html }: SendArgs): Promise<void> {
|
||||
if (!transport) {
|
||||
console.info(
|
||||
[
|
||||
"",
|
||||
"✉️ [email:console] No SMTP configured — printing instead of sending.",
|
||||
` to: ${to}`,
|
||||
` subject: ${subject}`,
|
||||
` body: ${text}`,
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await transport.sendMail({
|
||||
from: env.SMTP_FROM,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html: html ?? text,
|
||||
});
|
||||
function logToConsole({ to, subject, text }: SendArgs): void {
|
||||
console.info(
|
||||
[
|
||||
"",
|
||||
"✉️ [email:console] No email provider configured — printing instead of sending.",
|
||||
` to: ${to}`,
|
||||
` subject: ${subject}`,
|
||||
` body: ${text}`,
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
async function sendViaResend(
|
||||
apiKey: string,
|
||||
from: string,
|
||||
{ to, subject, text, html }: SendArgs,
|
||||
): Promise<void> {
|
||||
const res = await fetch("https://api.resend.com/emails", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ from, to, subject, text, html: html ?? text }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Resend failed: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
async function sendViaPostmark(
|
||||
apiKey: string,
|
||||
from: string,
|
||||
{ to, subject, text, html }: SendArgs,
|
||||
): Promise<void> {
|
||||
const res = await fetch("https://api.postmarkapp.com/email", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Postmark-Server-Token": apiKey,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
From: from,
|
||||
To: to,
|
||||
Subject: subject,
|
||||
TextBody: text,
|
||||
HtmlBody: html ?? text,
|
||||
MessageStream: "outbound",
|
||||
}),
|
||||
});
|
||||
if (!res.ok)
|
||||
throw new Error(`Postmark failed: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
async function sendViaSendgrid(
|
||||
apiKey: string,
|
||||
from: string,
|
||||
{ to, subject, text, html }: SendArgs,
|
||||
): Promise<void> {
|
||||
const res = await fetch("https://api.sendgrid.com/v3/mail/send", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
personalizations: [{ to: [{ email: to }] }],
|
||||
from: { email: from },
|
||||
subject,
|
||||
content: [
|
||||
{ type: "text/plain", value: text },
|
||||
{ type: "text/html", value: html ?? text },
|
||||
],
|
||||
}),
|
||||
});
|
||||
if (!res.ok)
|
||||
throw new Error(`SendGrid failed: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
// Send an email via the deployment's configured provider. Falls back to logging
|
||||
// when nothing is configured so local/open-source dev needs zero setup.
|
||||
export async function sendEmail(args: SendArgs): Promise<void> {
|
||||
const cfg = await getActiveConfig();
|
||||
// A real "from": the configured address, else the SMTP default.
|
||||
const from = cfg.fromAddress || env.SMTP_FROM;
|
||||
|
||||
switch (cfg.provider) {
|
||||
case "resend":
|
||||
if (!cfg.credentials) return logToConsole(args);
|
||||
return sendViaResend(cfg.credentials, from, args);
|
||||
case "postmark":
|
||||
if (!cfg.credentials) return logToConsole(args);
|
||||
return sendViaPostmark(cfg.credentials, from, args);
|
||||
case "sendgrid":
|
||||
if (!cfg.credentials) return logToConsole(args);
|
||||
return sendViaSendgrid(cfg.credentials, from, args);
|
||||
case "smtp": {
|
||||
if (!smtpTransport) return logToConsole(args);
|
||||
await smtpTransport.sendMail({
|
||||
from,
|
||||
to: args.to,
|
||||
subject: args.subject,
|
||||
text: args.text,
|
||||
html: args.html ?? args.text,
|
||||
});
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
// No provider chosen — honour a pre-existing SMTP env setup if present.
|
||||
if (smtpTransport) {
|
||||
await smtpTransport.sendMail({
|
||||
from,
|
||||
to: args.to,
|
||||
subject: args.subject,
|
||||
text: args.text,
|
||||
html: args.html ?? args.text,
|
||||
});
|
||||
return;
|
||||
}
|
||||
return logToConsole(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user