Files
CubeAdmin/lib/auth/index.ts
2026-03-08 17:01:36 +01:00

148 lines
5.4 KiB
TypeScript

import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { organization } from "better-auth/plugins";
import { magicLink } from "better-auth/plugins/magic-link";
import { count, eq } from "drizzle-orm";
import { db } from "@/lib/db";
import * as schema from "@/lib/db/schema";
const isProduction = process.env.NODE_ENV === "production";
export const auth = betterAuth({
// -------------------------------------------------------------------------
// Core
// -------------------------------------------------------------------------
secret: process.env.BETTER_AUTH_SECRET!,
baseURL: process.env.BETTER_AUTH_URL ?? "http://localhost:3000",
// -------------------------------------------------------------------------
// Database adapter (Drizzle + bun:sqlite)
// -------------------------------------------------------------------------
database: drizzleAdapter(db, {
provider: "sqlite",
// Keys must match Better Auth's internal model names (singular).
// usePlural: false (default) → "user", "session", "account", "verification"
schema: {
user: schema.users,
session: schema.sessions,
account: schema.accounts,
verification: schema.verifications,
},
}),
// -------------------------------------------------------------------------
// Custom user fields
// -------------------------------------------------------------------------
user: {
additionalFields: {
role: {
type: "string",
required: false,
defaultValue: "moderator",
input: false, // Not settable by the user directly
},
},
},
// -------------------------------------------------------------------------
// Email + password authentication
// -------------------------------------------------------------------------
emailAndPassword: {
enabled: true,
requireEmailVerification: false,
minPasswordLength: 8,
maxPasswordLength: 128,
},
// -------------------------------------------------------------------------
// Database hooks — first registered user becomes admin automatically
// -------------------------------------------------------------------------
databaseHooks: {
user: {
create: {
after: async (user) => {
// Count all users; if this is the very first, promote to admin
const [{ total }] = await db
.select({ total: count() })
.from(schema.users);
if (total === 1) {
await db
.update(schema.users)
.set({ role: "admin" } as Record<string, unknown>)
.where(eq(schema.users.id, user.id));
console.log(`[Auth] First user ${user.id} (${user.email}) promoted to admin`);
}
},
},
},
},
// -------------------------------------------------------------------------
// Plugins
// -------------------------------------------------------------------------
plugins: [
// Organization / role support
organization(),
// Magic link — used for invitation acceptance flows
magicLink({
expiresIn: 60 * 60, // 1 hour
disableSignUp: true, // magic links are only for invited users
sendMagicLink: async ({ email, url, token }) => {
// Delegate to the application's email module. The email module is
// responsible for importing and calling Resend (or whichever mailer
// is configured). We do a dynamic import so that this file does not
// pull in email dependencies at auth-initialisation time on the edge.
const { sendMagicLinkEmail } = await import("@/lib/email/index");
await sendMagicLinkEmail({ email, url, token });
},
}),
],
// -------------------------------------------------------------------------
// Trusted origins — allow env-configured list plus localhost in dev
// -------------------------------------------------------------------------
trustedOrigins: process.env.BETTER_AUTH_TRUSTED_ORIGINS
? process.env.BETTER_AUTH_TRUSTED_ORIGINS.split(",").map((o) => o.trim())
: ["http://localhost:3000"],
// -------------------------------------------------------------------------
// Cookie / session security
// -------------------------------------------------------------------------
advanced: {
useSecureCookies: isProduction,
defaultCookieAttributes: {
httpOnly: true,
secure: isProduction,
sameSite: "strict",
path: "/",
},
},
});
// ---------------------------------------------------------------------------
// Type helpers for use across the application
// ---------------------------------------------------------------------------
export type Auth = typeof auth;
/** The server-side session type returned by auth.api.getSession */
export type Session = typeof auth.$Infer.Session.session;
/** The user type embedded in every session, with custom additionalFields */
export type User = typeof auth.$Infer.Session.user & {
role?: "superadmin" | "admin" | "moderator" | null;
};
type RawSession = NonNullable<Awaited<ReturnType<typeof auth.api.getSession>>>;
/** Typed wrapper around auth.api.getSession that includes the role field */
export async function getAuthSession(
headers: Headers,
): Promise<(Omit<RawSession, "user"> & { user: User }) | null> {
return auth.api.getSession({ headers }) as Promise<
(Omit<RawSession, "user"> & { user: User }) | null
>;
}