mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 01:35:39 -04:00
- Overhaul `lib/auth/server.ts` and `lib/auth/session.ts`; update all API routes and server actions to use the revised session pattern - Refactor server actions (settings, titles, watchlist, setup) for consistency with new auth layer - Extract `SetupForm` into its own client component with `useActionState`, animated steps, and copyable env snippets - Move landing page redirect logic into `app/page.tsx`; slim down `LandingPage` component - Add `proxy.ts` for local dev proxying - Minor cleanup to `NavBar`, `MobileTabBar`, and `TitleCard`
123 lines
3.4 KiB
TypeScript
123 lines
3.4 KiB
TypeScript
import { drizzleAdapter } from "@better-auth/drizzle-adapter";
|
|
import { APIError, createAuthMiddleware } from "better-auth/api";
|
|
import { type BetterAuthOptions, betterAuth } from "better-auth/minimal";
|
|
import { nextCookies } from "better-auth/next-js";
|
|
import { admin, genericOAuth } from "better-auth/plugins";
|
|
import {
|
|
isOidcAutoRegisterEnabled,
|
|
isOidcConfigured,
|
|
isPasswordLoginDisabled,
|
|
} from "@/lib/config";
|
|
import { db } from "@/lib/db/client";
|
|
import { createLogger } from "@/lib/logger";
|
|
import {
|
|
getUserCount,
|
|
isRegistrationOpen,
|
|
setSetting,
|
|
} from "@/lib/services/settings";
|
|
|
|
const authLog = createLogger("auth");
|
|
|
|
export const auth = betterAuth({
|
|
logger: {
|
|
// Suppress unset secret/low entropy warnings during build
|
|
disabled: process.env.NEXT_PHASE === "phase-production-build",
|
|
level: "debug",
|
|
log: (level, message, ...args) => {
|
|
const fn = authLog[level as keyof typeof authLog];
|
|
if (fn) fn(message, ...args);
|
|
},
|
|
},
|
|
database: drizzleAdapter(db, {
|
|
provider: "sqlite",
|
|
}),
|
|
emailAndPassword: {
|
|
enabled: !isPasswordLoginDisabled(),
|
|
},
|
|
account: {
|
|
accountLinking: {
|
|
enabled: true,
|
|
trustedProviders: ["oidc"],
|
|
},
|
|
},
|
|
session: {
|
|
cookieCache: {
|
|
enabled: true,
|
|
maxAge: 5 * 60,
|
|
},
|
|
},
|
|
plugins: [
|
|
admin(),
|
|
...(isOidcConfigured()
|
|
? [
|
|
genericOAuth({
|
|
config: [
|
|
{
|
|
providerId: "oidc",
|
|
clientId: process.env.OIDC_CLIENT_ID ?? "",
|
|
clientSecret: process.env.OIDC_CLIENT_SECRET ?? "",
|
|
discoveryUrl: `${process.env.OIDC_ISSUER_URL}/.well-known/openid-configuration`,
|
|
scopes: ["openid", "email", "profile"],
|
|
pkce: true,
|
|
disableImplicitSignUp: !isOidcAutoRegisterEnabled(),
|
|
mapProfileToUser: (profile) => ({
|
|
name:
|
|
profile.name || profile.preferred_username || profile.email,
|
|
}),
|
|
},
|
|
],
|
|
}),
|
|
]
|
|
: []),
|
|
nextCookies(), // must be last
|
|
],
|
|
advanced: {
|
|
database: {
|
|
generateId: () => Bun.randomUUIDv7(),
|
|
},
|
|
},
|
|
hooks: {
|
|
before: createAuthMiddleware(async (ctx) => {
|
|
// Block email/password sign-up when registration is closed.
|
|
// This is endpoint-level so it doesn't affect OIDC user creation
|
|
// (which is gated by the genericOAuth plugin's disableImplicitSignUp).
|
|
if (ctx.path === "/sign-up/email") {
|
|
const open = isRegistrationOpen();
|
|
if (!open) {
|
|
throw new APIError("FORBIDDEN", {
|
|
message: "Registration is currently closed",
|
|
});
|
|
}
|
|
}
|
|
}),
|
|
},
|
|
databaseHooks: {
|
|
user: {
|
|
create: {
|
|
before: async (user) => {
|
|
// First user becomes admin regardless of auth method
|
|
const userCount = getUserCount();
|
|
if (userCount === 0) {
|
|
return {
|
|
data: {
|
|
...user,
|
|
role: "admin",
|
|
},
|
|
};
|
|
}
|
|
return { data: user };
|
|
},
|
|
after: async () => {
|
|
// Auto-close registration after first user
|
|
const userCount = getUserCount();
|
|
if (userCount === 1) {
|
|
setSetting("registrationOpen", "false");
|
|
}
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} satisfies BetterAuthOptions);
|
|
|
|
export type Session = typeof auth.$Infer.Session;
|