All files / src/lib auth-server.ts

75% Statements 6/8
93.33% Branches 14/15
100% Functions 1/1
85.71% Lines 6/7

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81                                                                        6x 6x 6x 6x 6x           6x                                                                    
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { nextCookies } from "better-auth/next-js";
import { getDb } from "@/lib/db";
import { env, previewTrustedOrigins } from "@/lib/env";
import { user, session, account, verification } from "@/lib/db/schema";
 
// better-auth `auth` instance — the ONLY auth backend now (NextAuth retired in
// Cutover Pass 2b-i). Mounted at /api/auth; the façade (auth.ts), middleware,
// and UI all read/drive it.
//
// - drizzleAdapter maps better-auth's four core models onto our canonical
//   Drizzle tables (`user`/`session`/`account`/`verification`). Pass 2b-ii
//   collapsed the old `user`↔`ba_user` split into this single `user` table:
//   better-auth OWNS it and every domain FK resolves straight to it — no more
//   mirroring bridge.
// - Google is the only supported sign-in method for Adored. Email/password,
//   magic-link, and email verification are intentionally not configured.
// - Google creds are optional at construction time so local/test builds boot,
//   but a deployment with empty creds has no usable login path.
// - nextCookies() MUST be the LAST plugin: plugins wrap each other like
//   middleware, and the cookie-writer must see the final response to attach the
//   Set-Cookie that carries the session.
//
// Secret/base URL prefer the better-auth-specific vars, falling back to the
// VALIDATED `env.AUTH_SECRET`/`env.AUTH_URL` (not raw process.env) so a single
// secret can serve both AND preview deploys — which have no explicit AUTH_URL —
// get env.ts's VERCEL_URL-derived base URL instead of `undefined`.
// better-auth rate-limits auth requests per-IP and is ON in production builds.
// The e2e suite runs a production `next start` and creates multiple signed
// session fixtures from a single loopback IP (baseline + per-user authz
// fixtures), which can trip the limiter (429). Disable it only for loopback
// base URLs — real
// deployments (https origin) keep the protection. Host is parsed defensively;
// a malformed URL leaves rate limiting ON (fail-safe).
function isLoopbackBaseUrl(): boolean {
  const raw = process.env.BETTER_AUTH_URL ?? env.AUTH_URL;
  Iif (!raw) return false;
  try {
    const host = new URL(raw).hostname;
    return host === "localhost" || host === "127.0.0.1" || host === "::1";
  } catch {
    return false;
  }
}
 
export const auth = betterAuth({
  rateLimit: { enabled: !isLoopbackBaseUrl() },
  database: drizzleAdapter(getDb(), {
    provider: "pg",
    schema: {
      user,
      session,
      account,
      verification,
    },
  }),
  secret: process.env.BETTER_AUTH_SECRET ?? env.AUTH_SECRET,
  baseURL: process.env.BETTER_AUTH_URL ?? env.AUTH_URL,
  // Mounted at the default /api/auth (Cutover Pass 2b-i): NextAuth is retired,
  // so the [...nextauth] collision is gone and better-auth owns /api/auth. It
  // builds all its route + callback URLs from this basePath, so it must match
  // the src/app/api/auth/[...all] mount — and the Google callback is therefore
  // …/api/auth/callback/google (matches the setup checklist).
  basePath: "/api/auth",
  // better-auth's default trusted set is exactly [origin(baseURL)]; this ADDS the
  // preview branch alias so sign-in works on the URL the PR comment links to. It is
  // [] everywhere but preview — production's trusted set is unchanged. See env.ts.
  trustedOrigins: previewTrustedOrigins(),
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID ?? "",
      clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? "",
    },
  },
  plugins: [
    // Keep nextCookies LAST — see note above.
    nextCookies(),
  ],
});