Files
temetro/backend/src/services/auth-fallback.ts
T
Khalid Abdi 90e6ec4cc0 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>
2026-06-20 19:52:55 +03:00

69 lines
2.2 KiB
TypeScript

import { and, eq, inArray } from "drizzle-orm";
import { db } from "../db/index.js";
import { member } from "../db/schema/auth.js";
import { emitToUser } from "../realtime.js";
import { createSystemMessage } from "./messaging.js";
import { createNotification } from "./notifications.js";
// When an employee asks for a password reset but no email provider is configured,
// alert the admin(s) of each clinic the user belongs to: a "System" message card
// in Messages + a bell notification, both deep-linking to that member's settings
// so an admin can set a new password. The reset URL is never exposed.
export async function notifyAdminsPasswordReset(u: {
id: string;
name: string;
email: string;
}): Promise<void> {
const orgs = await db
.select({ organizationId: member.organizationId })
.from(member)
.where(eq(member.userId, u.id));
const orgIds = [...new Set(orgs.map((o) => o.organizationId))];
for (const orgId of orgIds) {
try {
const admins = await db
.select({ userId: member.userId })
.from(member)
.where(
and(
eq(member.organizationId, orgId),
inArray(member.role, ["owner", "admin"]),
),
);
const adminIds = admins.map((a) => a.userId).filter((id) => id !== u.id);
if (adminIds.length === 0) continue;
const body = `${u.name} requested a password reset, but no email provider is configured. Reset their password from their settings.`;
const { message, recipientIds } = await createSystemMessage(
orgId,
adminIds,
body,
{
kind: "passwordReset",
userId: u.id,
userName: u.name,
userEmail: u.email,
},
);
for (const rid of recipientIds) emitToUser(rid, "message:new", message);
for (const rid of adminIds) {
const n = await createNotification({
orgId,
userId: rid,
type: "password_reset",
text: `${u.name} needs a password reset`,
entityType: "user",
entityId: u.id,
actorName: u.name,
});
if (n) emitToUser(rid, "notification:new", n);
}
} catch (err) {
console.error("password-reset fallback failed:", err);
}
}
}