File size: 3,189 Bytes
dff8593
 
 
dd6516e
dff8593
 
 
b00f0f1
549bb48
cc4c5f9
549bb48
dff8593
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b00f0f1
 
dff8593
 
 
7447362
 
 
 
 
 
 
 
 
 
 
 
 
dff8593
 
 
b00f0f1
 
 
 
 
 
 
dff8593
 
 
413c7f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dff8593
 
 
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
import { cookies, headers } from 'next/headers'
import { NextResponse, NextRequest } from 'next/server'
import { createHmac } from 'crypto'
import { isHuggingFaceSpace } from 'lib/supabase/publicEnv'

const APP_SESSION_COOKIE = 'letschat_session'
const DEFAULT_MAX_AGE_SECONDS = 30 * 24 * 60 * 60 // 30 days
const isProd = process.env.NODE_ENV === 'production'
// Prefer broadest compatibility in production (embeds/iframe) -> None; dev -> Lax.
const sameSite: 'lax' | 'none' =
  isProd || process.env.FORCE_SAMESITE_NONE === '1' || isHuggingFaceSpace ? 'none' : 'lax'

type AppSessionPayload = {
  userId: string
  email: string
  exp: number
}

const secret = process.env.APP_SECRET || process.env.SUPABASE_SERVICE_ROLE_KEY || 'dev-secret'

function sign(payload: AppSessionPayload) {
  const data = Buffer.from(JSON.stringify(payload)).toString('base64url')
  const sig = createHmac('sha256', secret).update(data).digest('base64url')
  return `${data}.${sig}`
}

function verify(token: string): AppSessionPayload | null {
  const [data, sig] = token.split('.')
  if (!data || !sig) return null
  const expected = createHmac('sha256', secret).update(data).digest('base64url')
  if (expected !== sig) return null
  try {
    const payload = JSON.parse(Buffer.from(data, 'base64url').toString()) as AppSessionPayload
    if (payload.exp && Date.now() > payload.exp) return null
    return payload
  } catch {
    return null
  }
}

export async function setAppSession(res: NextResponse, userId: string, email: string) {
  const exp = Date.now() + DEFAULT_MAX_AGE_SECONDS * 1000
  const token = sign({ userId, email, exp })
  res.cookies.set(APP_SESSION_COOKIE, token, {
    httpOnly: true,
    secure: isProd,
    sameSite,
    maxAge: DEFAULT_MAX_AGE_SECONDS,
    path: '/'
  })
  // Also set a non-httpOnly cookie so client JS / proxies can read it if necessary
  try {
    res.cookies.set(`${APP_SESSION_COOKIE}_pub`, token, {
      httpOnly: false,
      secure: isProd,
      sameSite,
      maxAge: DEFAULT_MAX_AGE_SECONDS,
      path: '/'
    })
  } catch (e) {
    // ignore in environments that disallow multiple Set-Cookie settings
  }
  return token
}

export async function clearAppSession(res: NextResponse) {
  res.cookies.set(APP_SESSION_COOKIE, '', {
    httpOnly: true,
    secure: isProd,
    sameSite,
    maxAge: 0,
    path: '/'
  })
}

export async function getAppSession(req?: NextRequest): Promise<AppSessionPayload | null> {
  // Try req cookies first (server handler may pass NextRequest)
  let token = req?.cookies?.get(APP_SESSION_COOKIE)?.value

  // Fallback to Next.js cookies() helper
  if (!token) {
    token = (await cookies()).get(APP_SESSION_COOKIE)?.value
  }

  // As a last resort, try to parse Cookie header string directly (some proxies/platforms)
  if (!token && req?.headers?.get) {
    const cookieHeader = req.headers.get('cookie') || ''
    if (cookieHeader) {
      const match = cookieHeader.split(';').map((s) => s.trim()).find((c) => c.startsWith(`${APP_SESSION_COOKIE}=`))
      if (match) token = match.split('=')[1]
    }
  }

  console.log('[getAppSession] token present=', !!token)
  if (!token) return null
  return verify(token)
}