All files / web/src/lib viewer.ts

100% Statements 167/167
100% Branches 23/23
100% Functions 4/4
100% Lines 167/167

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 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 1681x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 28x 28x 28x 28x 28x 28x 28x 28x 28x 28x 28x 11x 11x 11x 11x 11x 17x 17x 17x 17x 17x 17x 17x 22x 12x 12x 12x 12x 12x 5x 5x 5x 5x 16x 28x 28x 28x 3x 3x 3x 3x 3x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 28x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 10x 10x 10x 10x 10x 10x 2x 2x 2x 10x 10x 10x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 16x 16x 16x 16x 16x 16x 16x 7x 7x 7x 7x 7x 16x 8x 8x 8x 8x 8x 8x 8x 8x 8x 16x 1x 16x 16x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 5x 5x 5x 5x 5x 2x 2x 2x 2x 1x 1x 1x 1x 5x 2x 2x 5x 1x 5x 5x  
import { eq } from 'drizzle-orm'
import type { Session } from 'next-auth'
import { cookies, headers } from 'next/headers'
import { auth } from '@/auth'
import { db, schema } from '@/db'
import { GUEST_COOKIE_NAME, verifyGuestToken } from './guest-token'
 
/**
 * Unified viewer utility for server components
 *
 * Gets the current viewer (guest or authenticated user) in a type-safe way.
 * Use this in server components instead of calling auth() directly.
 *
 * @returns Viewer information with discriminated union type
 */
export async function getViewer(): Promise<
  { kind: 'user'; session: Session } | { kind: 'guest'; guestId: string } | { kind: 'unknown' }
> {
  const start = performance.now()
 
  // Check if user is authenticated via NextAuth
  let t = performance.now()
  const session = await auth()
  const authTime = performance.now() - t
 
  if (session) {
    console.log(
      `[PERF] getViewer (user): ${(performance.now() - start).toFixed(1)}ms | auth=${authTime.toFixed(1)}ms`
    )
    return { kind: 'user', session }
  }
 
  // Check for guest ID in header (set by middleware)
  t = performance.now()
  const headerStore = await headers()
  const headerGuestId = headerStore.get('x-guest-id')
  const headersTime = performance.now() - t
 
  if (headerGuestId) {
    console.log(
      `[PERF] getViewer (guest-header): ${(performance.now() - start).toFixed(1)}ms | auth=${authTime.toFixed(1)}ms, headers=${headersTime.toFixed(1)}ms`
    )
    return { kind: 'guest', guestId: headerGuestId }
  }
 
  // Fallback: check for guest cookie
  t = performance.now()
  const cookieStore = await cookies()
  const guestCookie = cookieStore.get(GUEST_COOKIE_NAME)?.value
  const cookieTime = performance.now() - t
 
  if (!guestCookie) {
    console.log(
      `[PERF] getViewer (unknown): ${(performance.now() - start).toFixed(1)}ms | auth=${authTime.toFixed(1)}ms, headers=${headersTime.toFixed(1)}ms, cookies=${cookieTime.toFixed(1)}ms`
    )
    return { kind: 'unknown' }
  }
 
  try {
    t = performance.now()
    const { sid } = await verifyGuestToken(guestCookie)
    const verifyTime = performance.now() - t
    console.log(
      `[PERF] getViewer (guest-cookie): ${(performance.now() - start).toFixed(1)}ms | auth=${authTime.toFixed(1)}ms, headers=${headersTime.toFixed(1)}ms, cookies=${cookieTime.toFixed(1)}ms, verify=${verifyTime.toFixed(1)}ms`
    )
    return { kind: 'guest', guestId: sid }
  } catch {
    console.log(
      `[PERF] getViewer (unknown-failed): ${(performance.now() - start).toFixed(1)}ms | auth=${authTime.toFixed(1)}ms, headers=${headersTime.toFixed(1)}ms, cookies=${cookieTime.toFixed(1)}ms`
    )
    return { kind: 'unknown' }
  }
}
 
/**
 * Get or create a user record from a guestId
 *
 * This is the core function for converting a guest session identifier
 * into a database user record. If no user exists for the guestId,
 * one is created automatically.
 *
 * @param guestId - The guest session identifier
 * @returns The user record from the database
 */
async function getOrCreateUserFromGuestId(guestId: string) {
  let user = await db.query.users.findFirst({
    where: eq(schema.users.guestId, guestId),
  })
 
  if (!user) {
    const [newUser] = await db.insert(schema.users).values({ guestId }).returning()
    user = newUser
  }
 
  return user
}
 
/**
 * Get the stable database user.id for the current viewer
 *
 * For authenticated users: returns session.user.id directly
 * For guests: looks up or creates the user record by guestId, returns user.id
 * For unknown: throws an error
 *
 * This is the single canonical way to get the current user's ID on the server.
 *
 * @throws Error if no valid viewer found
 */
export async function getUserId(): Promise<string> {
  const start = performance.now()
  const viewer = await getViewer()
  const viewerTime = performance.now() - start
 
  switch (viewer.kind) {
    case 'user':
      // Authenticated users already have a database user.id in their session
      console.log(
        `[PERF] getUserId (user): ${(performance.now() - start).toFixed(1)}ms | getViewer=${viewerTime.toFixed(1)}ms`
      )
      return viewer.session.user!.id
    case 'guest': {
      // Guests need to look up their user record by guestId
      const t = performance.now()
      const user = await getOrCreateUserFromGuestId(viewer.guestId)
      const userLookupTime = performance.now() - t
      console.log(
        `[PERF] getUserId (guest): ${(performance.now() - start).toFixed(1)}ms | getViewer=${viewerTime.toFixed(1)}ms, userLookup=${userLookupTime.toFixed(1)}ms`
      )
      return user.id
    }
    case 'unknown':
      throw new Error('No valid viewer session found')
  }
}
 
/**
 * Get the full user record for the current viewer
 *
 * This returns the complete database user record, useful when you need
 * more than just the user.id (e.g., for checking user properties).
 *
 * For authenticated users: looks up user by session.user.id
 * For guests: looks up or creates user by guestId
 * For unknown: throws an error
 *
 * @throws Error if no valid viewer found
 */
export async function getViewerUser() {
  const viewer = await getViewer()
 
  switch (viewer.kind) {
    case 'user': {
      const user = await db.query.users.findFirst({
        where: eq(schema.users.id, viewer.session.user!.id),
      })
      if (!user) {
        throw new Error('Authenticated user not found in database')
      }
      return user
    }
    case 'guest': {
      return getOrCreateUserFromGuestId(viewer.guestId)
    }
    case 'unknown':
      throw new Error('No valid viewer session found')
  }
}