import { createClient, type EmailOtpType, type Session, type SupabaseClient, } from '@supabase/supabase-js' const supabaseUrl = import.meta.env.VITE_SUPABASE_URL const supabaseKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY const knownEmailOtpTypes = new Set([ 'signup', 'invite', 'magiclink', 'recovery', 'email_change', 'email', ]) type AuthCallbackParams = { accessToken: string | null code: string | null error: string | null errorCode: string | null errorDescription: string | null refreshToken: string | null tokenHash: string | null type: EmailOtpType | null } type ResolveEmailAuthCallbackOptions = { allowedTypes: EmailOtpType[] client: SupabaseClient } type ResolveEmailAuthCallbackResult = | { ok: true session: Session | null type: EmailOtpType | null } | { ok: false error: string } function parseEmailOtpType(value: string | null): EmailOtpType | null { if (!value || !knownEmailOtpTypes.has(value as EmailOtpType)) { return null } return value as EmailOtpType } export function readAuthCallbackParams(): AuthCallbackParams { const search = new URLSearchParams(window.location.search) const hash = new URLSearchParams(window.location.hash.slice(1)) return { code: search.get('code'), tokenHash: search.get('token_hash') ?? hash.get('token_hash'), accessToken: hash.get('access_token'), refreshToken: hash.get('refresh_token'), type: parseEmailOtpType(search.get('type') ?? hash.get('type')), error: search.get('error') ?? hash.get('error'), errorCode: search.get('error_code') ?? hash.get('error_code'), errorDescription: search.get('error_description') ?? hash.get('error_description'), } } export function clearAuthCallbackUrl() { window.history.replaceState(null, '', window.location.pathname) } export function createDetachedAuthClient() { return createClient(supabaseUrl, supabaseKey, { auth: { autoRefreshToken: false, detectSessionInUrl: false, persistSession: false, }, }) } export async function resolveEmailAuthCallback( options: ResolveEmailAuthCallbackOptions, ): Promise { const { client, allowedTypes } = options const params = readAuthCallbackParams() const allowedTypeSet = new Set(allowedTypes) if (params.error || params.errorCode || params.errorDescription) { return { ok: false, error: params.errorDescription || '链接无效或已过期。', } } try { let session: Session | null = null if (params.tokenHash && params.type && allowedTypeSet.has(params.type)) { const { data, error } = await client.auth.verifyOtp({ token_hash: params.tokenHash, type: params.type, }) if (error) throw error session = data.session } else if (params.code) { const { data, error } = await client.auth.exchangeCodeForSession(params.code) if (error) throw error session = data.session } else if ( params.accessToken && params.refreshToken && params.type && allowedTypeSet.has(params.type) ) { const { data, error } = await client.auth.setSession({ access_token: params.accessToken, refresh_token: params.refreshToken, }) if (error) throw error session = data.session } else { return { ok: false, error: '链接无效或已过期。', } } if (!session) { const { data, error } = await client.auth.getSession() if (error) throw error session = data.session } clearAuthCallbackUrl() return { ok: true, session, type: params.type, } } catch (error) { const message = error instanceof Error ? error.message : '链接无效或已过期。' return { ok: false, error: message, } } }