2026-06-19 13:20:52 -04:00

77 lines
2.3 KiB
TypeScript

import { db } from "@/db";
import { user } from "@/db/schema";
import { ensureDelegatedOrganizationForUser } from "@/server/auth/delegated-organization";
import { eq } from "drizzle-orm";
import type { EnsuredUserContext } from "./types";
const LOCAL_ADMIN_USER_ID = "local-admin";
const LOCAL_ADMIN_EMAIL = "admin@localhost";
// Externally-authenticated users (Cloudflare Access, local_noauth) are stored
// in better-auth's `user` table just like hosted users — only the way we
// authenticate them differs (per-request, no better-auth session). Keeping a
// single user table means the OAuth `account` grant and every app table that
// references `user.id` resolve the same way in all auth modes.
function deriveUserName(email: string) {
return email.split("@")[0] || "OpenSEO";
}
async function ensureUserRecord(userId: string, userEmail: string) {
const existing = await db.query.user.findFirst({
columns: { email: true },
where: eq(user.id, userId),
});
if (!existing) {
// Concurrent first-load requests can all see "no row" and race to insert
// the same id; onConflictDoNothing on the PK makes the losers no-ops instead
// of failing. Scoped to the id so a genuine email-unique collision (two
// distinct ids sharing an email) still surfaces loudly.
await db
.insert(user)
.values({
id: userId,
name: deriveUserName(userEmail),
email: userEmail,
emailVerified: true,
})
.onConflictDoNothing({ target: user.id });
return userEmail;
}
if (existing.email !== userEmail) {
await db
.update(user)
.set({ email: userEmail, name: deriveUserName(userEmail) })
.where(eq(user.id, userId));
return userEmail;
}
return existing.email;
}
export async function resolveDelegatedContext(
userId: string,
userEmail: string,
): Promise<EnsuredUserContext> {
const ensuredEmail = await ensureUserRecord(userId, userEmail);
const organizationId = await ensureDelegatedOrganizationForUser(
userId,
ensuredEmail,
);
return {
userId,
userEmail: ensuredEmail,
// Delegated auth (Cloudflare Access / local) has no unverified state.
emailVerified: true,
organizationId,
};
}
export async function resolveLocalNoAuthContext(): Promise<EnsuredUserContext> {
return resolveDelegatedContext(LOCAL_ADMIN_USER_ID, LOCAL_ADMIN_EMAIL);
}