Refactor auth session handling and server actions across app

- 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`
This commit is contained in:
2026-03-06 17:05:49 -05:00
parent 7a3052250d
commit 73b07f5ff6
34 changed files with 443 additions and 466 deletions
+43 -31
View File
@@ -1,6 +1,7 @@
import { drizzleAdapter } from "@better-auth/drizzle-adapter";
import { APIError, createAuthMiddleware } from "better-auth/api";
import { betterAuth } from "better-auth/minimal";
import { type BetterAuthOptions, betterAuth } from "better-auth/minimal";
import { nextCookies } from "better-auth/next-js";
import { admin, genericOAuth } from "better-auth/plugins";
import {
isOidcAutoRegisterEnabled,
@@ -15,27 +16,6 @@ import {
setSetting,
} from "@/lib/services/settings";
const oidcPlugin = 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,
}),
},
],
}),
]
: [];
const authLog = createLogger("auth");
export const auth = betterAuth({
@@ -60,7 +40,37 @@ export const auth = betterAuth({
trustedProviders: ["oidc"],
},
},
plugins: [admin(), ...oidcPlugin],
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(),
@@ -72,7 +82,7 @@ export const auth = betterAuth({
// 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 = await isRegistrationOpen();
const open = isRegistrationOpen();
if (!open) {
throw new APIError("FORBIDDEN", {
message: "Registration is currently closed",
@@ -84,27 +94,29 @@ export const auth = betterAuth({
databaseHooks: {
user: {
create: {
before: async (userData) => {
before: async (user) => {
// First user becomes admin regardless of auth method
const userCount = await getUserCount();
const userCount = getUserCount();
if (userCount === 0) {
return {
data: {
...userData,
...user,
role: "admin",
},
};
}
return { data: userData };
return { data: user };
},
after: async () => {
// Auto-close registration after first user
const userCount = await getUserCount();
const userCount = getUserCount();
if (userCount === 1) {
await setSetting("registrationOpen", "false");
setSetting("registrationOpen", "false");
}
},
},
},
},
});
} satisfies BetterAuthOptions);
export type Session = typeof auth.$Infer.Session;
+14 -2
View File
@@ -5,9 +5,21 @@ import { auth } from "./server";
/**
* Cached session getter — deduplicated per request via React.cache().
* Use this instead of calling auth.api.getSession() directly in server
* components and layouts to avoid redundant session lookups within a
* single render pass.
* components, route handlers, and server actions to avoid redundant
* session lookups within a single render pass.
*/
export const getSession = cache(async () => {
return auth.api.getSession({ headers: await headers() });
});
export async function requireSession() {
const session = await getSession();
if (!session) throw new Error("Unauthorized");
return session;
}
export async function requireAdmin() {
const session = await requireSession();
if (session.user.role !== "admin") throw new Error("Forbidden");
return session;
}