All files / src/lib auth.ts

74.28% Statements 26/35
80.95% Branches 17/21
100% Functions 5/5
74.28% Lines 26/35

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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116                      1x 1x     10x       1x           1x 1x 1x   1x         1x           1x                     1x 1x 1x 1x                                                           8x 1x     7x 7x 3x   4x                     1x 1x         2x 2x 1x   1x    
import { headers } from "next/headers";
import { auth } from "@/lib/auth-server";
 
export interface Session {
  user?: {
    id: string;
    email: string;
    name?: string;
  };
}
 
const DEV_AUTH_BYPASS_EMAIL = "dev@adored.test";
const DEV_AUTH_BYPASS_NAME = "Dev User";
 
export function isDevAuthBypassEnabled(): boolean {
  return process.env.NODE_ENV === "development" && process.env.AUTH_BYPASS === "1";
}
 
async function getDevBypassSession(): Promise<Session> {
  const [{ eq }, { getDb }, { user: userTable }, { generateId }] = await Promise.all([
    import("drizzle-orm"),
    import("@/lib/db"),
    import("@/lib/db/schema"),
    import("@/lib/id"),
  ]);
  const db = getDb();
  const email = process.env.AUTH_BYPASS_EMAIL || DEV_AUTH_BYPASS_EMAIL;
  const name = process.env.AUTH_BYPASS_NAME || DEV_AUTH_BYPASS_NAME;
 
  const existing = await db
    .select({ id: userTable.id, email: userTable.email, name: userTable.name })
    .from(userTable)
    .where(eq(userTable.email, email))
    .limit(1);
  Iif (existing.length) {
    const { ensureDevSampleCollection } = await import("@/lib/dev-sample-bootstrap");
    await ensureDevSampleCollection(db, existing[0].id);
    return { user: existing[0] };
  }
 
  const [created] = await db
    .insert(userTable)
    .values({
      id: process.env.AUTH_BYPASS_USER_ID || generateId("user"),
      email,
      name,
      emailVerified: true,
    })
    .onConflictDoNothing({ target: userTable.email })
    .returning({ id: userTable.id, email: userTable.email, name: userTable.name });
 
  Eif (created) {
    const { ensureDevSampleCollection } = await import("@/lib/dev-sample-bootstrap");
    await ensureDevSampleCollection(db, created.id);
    return { user: created };
  }
 
  const [raced] = await db
    .select({ id: userTable.id, email: userTable.email, name: userTable.name })
    .from(userTable)
    .where(eq(userTable.email, email))
    .limit(1);
  if (!raced) {
    throw new Error("Unable to create dev auth bypass user");
  }
  const { ensureDevSampleCollection } = await import("@/lib/dev-sample-bootstrap");
  await ensureDevSampleCollection(db, raced.id);
  return { user: raced };
}
 
// THE FAÇADE (Cutover Pass 1). Reads a better-auth session and returns the
// byte-identical shape the ~249 call sites + the DAL (`userId: string`) already
// depend on: { user: { id, email, name } }. better-auth's getSession returns
// { session, user } | null; we project ONLY id/email/name off `user` so no
// extra fields (image/emailVerified/…) leak into the contract. Because we have
// NOT enabled session.cookieCache, auth.api.getSession does a fresh per-request
// DB read — so a profile display-name edit is reflected on the very next call
// (name-freshness) with no stale JWT claim to invalidate.
//
// NextAuth is fully retired (Cutover Pass 2b-i): the old authOptions +
// Credentials/EmailProvider/DrizzleAdapter config is gone, and better-auth now
// owns the whole sign-in/session surface at /api/auth. This module is nothing
// but the read façade over better-auth.
export async function getSession(): Promise<Session | null> {
  if (isDevAuthBypassEnabled()) {
    return getDevBypassSession();
  }
 
  const result = await auth.api.getSession({ headers: await headers() });
  if (!result?.user) {
    return null;
  }
  return {
    user: {
      id: result.user.id,
      email: result.user.email,
      name: result.user.name,
    },
  };
}
 
export class UnauthorizedError extends Error {
  constructor(message = "Unauthorized") {
    super(message);
    this.name = "UnauthorizedError";
  }
}
 
export async function requireAuth(): Promise<{ id: string; email: string; name?: string }> {
  const session = await getSession();
  if (!session?.user) {
    throw new UnauthorizedError("Unauthorized");
  }
  return session.user;
}