auth.ts
Rama main · réplica de lectura del repositorio original.
import { headers } from "next/headers";
import { after } from "next/server";
import { findMemberById, findMemberByLoginEmail } from "@/lib/auth/admission";
import { sessionClaims } from "@/lib/auth/access";
import { createAuth, MAGIC_LINK_MINUTES, type AuthEmail } from "@/lib/auth/config";
import type { AppSession } from "@/lib/auth/session";
import type { BrandConfig } from "@/lib/brand";
import { getBrand } from "@/lib/brand/get-brand";
import { getDb } from "@/lib/db/client";
import { sendMail } from "@/lib/email/resend";
import { t } from "@/lib/i18n";
import { familyRole, sessionAccount } from "@/lib/members/family";
import { reportError } from "@/lib/observability";
function authEmailText({ kind, email, url, code }: AuthEmail, brand: BrandConfig): string {
const e = t.authEmail;
switch (kind) {
case "login":
return e.login(brand.appName, url!, code!, MAGIC_LINK_MINUTES);
case "invite":
return e.invite(brand.appName, url!, code!, MAGIC_LINK_MINUTES);
case "notMember":
return e.notMember(brand.appName, email, brand.supportEmail);
case "accountInactive":
return e.accountInactive(brand.appName, brand.supportEmail);
case "membershipInactive":
return e.membershipInactive(brand.appName, brand.membershipUrl ?? null, brand.supportEmail);
case "checkUnavailable":
return e.checkUnavailable(brand.appName);
case "signupRequired":
return e.signupRequired(brand.appName, email, url!, brand.supportEmail);
}
}
// The app's one Better Auth instance (lib/auth/config.ts has the rules),
// built on first use: the build imports this module without a database
// (a preview without DATABASE_URL must still build).
let instance: ReturnType<typeof createAuth> | null = null;
export function getAuthInstance(): ReturnType<typeof createAuth> {
instance ??= buildInstance();
return instance;
}
const buildInstance = () => createAuth(getDb(), {
// Who the address belongs to, which email and the send all run after the
// response: a member's request must not take longer than a stranger's.
defer: (task) => after(task),
async sendAuthEmail(message) {
const brand = await getBrand();
const result = await sendMail({
to: message.email,
subject: t.authEmail.subject(message.kind, brand.appName),
text: authEmailText(message, brand),
});
if ("error" in result) reportError("auth email", new Error(result.error), { kind: message.kind });
},
});
// The signed-in member, in the shape the pages and guards used with Auth.js:
// role, active, memberId and fullName are read from the members table on every
// request, so a deactivated or deleted member is signed out at once.
export async function auth(): Promise<AppSession | null> {
// headers() first: it marks the page dynamic before anything touches the
// database (a statically prerendered page must not open it at build time).
const requestHeaders = await headers();
const session = await getAuthInstance().api.getSession({ headers: requestHeaders });
if (!session) return null;
const person = await findMemberByLoginEmail(session.user.email);
if (person?.householdOf) {
// A person in a family works on the account they joined.
const account = sessionAccount(person, await findMemberById(person.householdOf));
const role = account ? familyRole(person.role, account.role) : null;
if (!account || !role) return null;
return {
user: {
email: session.user.email,
memberId: account.memberId,
personId: person.memberId,
role,
active: true,
fullName: person.fullName,
// The Admin area is personal: the person's own flag, not the account's.
ordersReferent: person.ordersReferent,
},
};
}
const claims = sessionClaims(person);
if (!claims) return null;
return { user: { email: session.user.email, ...claims } };
}
// Server Action: ends this browser's session.
export async function signOut(): Promise<void> {
const requestHeaders = await headers();
await getAuthInstance().api.signOut({ headers: requestHeaders });
}