Settings manages folders, thumbnail defaults, and passwords for the signed-in account, and choosing an image now browses saved stills instead of only the desktop file dialog. Co-authored-by: Cursor <cursoragent@cursor.com>
133 lines
3.9 KiB
TypeScript
133 lines
3.9 KiB
TypeScript
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'
|
|
import type { H3Event } from 'h3'
|
|
|
|
export interface SessionUser {
|
|
sub: string
|
|
email?: string
|
|
name?: string
|
|
}
|
|
|
|
const COOKIE = 'aigen_session'
|
|
const STATE_COOKIE = 'aigen_oauth_state'
|
|
const LIBRARY_COOKIE = 'aigen_library'
|
|
|
|
function secret() {
|
|
const config = useRuntimeConfig()
|
|
return config.sessionSecret || 'dev-only-change-me'
|
|
}
|
|
|
|
function sign(value: string) {
|
|
return createHmac('sha256', secret()).update(value).digest('base64url')
|
|
}
|
|
|
|
function seal(payload: unknown) {
|
|
const data = Buffer.from(JSON.stringify(payload)).toString('base64url')
|
|
return `${data}.${sign(data)}`
|
|
}
|
|
|
|
function unseal<T>(token: string | undefined): T | null {
|
|
if (!token || !token.includes('.')) return null
|
|
const [data, sig] = token.split('.')
|
|
const expected = sign(data)
|
|
const a = Buffer.from(sig)
|
|
const b = Buffer.from(expected)
|
|
if (a.length !== b.length || !timingSafeEqual(a, b)) return null
|
|
try {
|
|
return JSON.parse(Buffer.from(data, 'base64url').toString('utf8')) as T
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function cookieOpts(event: H3Event) {
|
|
const proto = getRequestHeader(event, 'x-forwarded-proto') || getRequestProtocol(event)
|
|
return {
|
|
httpOnly: true,
|
|
sameSite: 'lax' as const,
|
|
path: '/',
|
|
secure: proto === 'https',
|
|
maxAge: 60 * 60 * 24 * 14
|
|
}
|
|
}
|
|
|
|
export function getSessionUser(event: H3Event): SessionUser | null {
|
|
return unseal<SessionUser>(getCookie(event, COOKIE))
|
|
}
|
|
|
|
export function setSessionUser(event: H3Event, user: SessionUser) {
|
|
setCookie(event, COOKIE, seal(user), cookieOpts(event))
|
|
}
|
|
|
|
export function clearAuthSession(event: H3Event) {
|
|
deleteCookie(event, COOKIE, { path: '/' })
|
|
}
|
|
|
|
export function createOauthState(event: H3Event) {
|
|
const state = randomBytes(24).toString('hex')
|
|
setCookie(event, STATE_COOKIE, state, { ...cookieOpts(event), maxAge: 600 })
|
|
return state
|
|
}
|
|
|
|
export function consumeOauthState(event: H3Event, incoming: string | undefined) {
|
|
const stored = getCookie(event, STATE_COOKIE)
|
|
deleteCookie(event, STATE_COOKIE, { path: '/' })
|
|
return Boolean(stored && incoming && stored === incoming)
|
|
}
|
|
|
|
export function authEnabled() {
|
|
const config = useRuntimeConfig()
|
|
return Boolean(config.public.authEnabled && config.oidcClientId && config.oidcClientSecret)
|
|
}
|
|
|
|
interface LibraryUnlock {
|
|
version?: string
|
|
folders?: Record<string, string>
|
|
}
|
|
|
|
function readUnlock(event: H3Event): LibraryUnlock {
|
|
return unseal<LibraryUnlock>(getCookie(event, LIBRARY_COOKIE)) || {}
|
|
}
|
|
|
|
export function getFolderUnlocks(event: H3Event) {
|
|
return { ...(readUnlock(event).folders || {}) }
|
|
}
|
|
|
|
export function setFolderUnlock(event: H3Event, folderId: string, version: string) {
|
|
const folders = getFolderUnlocks(event)
|
|
folders[folderId] = version
|
|
setCookie(event, LIBRARY_COOKIE, seal({ folders }), cookieOpts(event))
|
|
}
|
|
|
|
export function setFolderUnlocks(event: H3Event, entries: Record<string, string>) {
|
|
const folders = { ...getFolderUnlocks(event), ...entries }
|
|
setCookie(event, LIBRARY_COOKIE, seal({ folders }), cookieOpts(event))
|
|
}
|
|
|
|
export function clearFolderUnlock(event: H3Event, folderId?: string) {
|
|
if (!folderId) {
|
|
deleteCookie(event, LIBRARY_COOKIE, { path: '/' })
|
|
return
|
|
}
|
|
const folders = getFolderUnlocks(event)
|
|
delete folders[folderId]
|
|
setCookie(event, LIBRARY_COOKIE, seal({ folders }), cookieOpts(event))
|
|
}
|
|
|
|
export function setLibraryUnlock(event: H3Event, version: string) {
|
|
setFolderUnlock(event, '*', version)
|
|
}
|
|
|
|
export function getLibraryUnlockVersion(event: H3Event) {
|
|
return getFolderUnlocks(event)['*'] || readUnlock(event).version || null
|
|
}
|
|
|
|
export function clearLibraryUnlock(event: H3Event) {
|
|
clearFolderUnlock(event)
|
|
}
|
|
|
|
export function publicBaseUrl(event: H3Event) {
|
|
const proto = getRequestHeader(event, 'x-forwarded-proto') || getRequestProtocol(event)
|
|
const host = getRequestHeader(event, 'x-forwarded-host') || getRequestHost(event)
|
|
return `${proto}://${host}`
|
|
}
|