217b81c506
Reviewed-on: #2
153 lines
3.8 KiB
TypeScript
153 lines
3.8 KiB
TypeScript
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<EmailOtpType>([
|
|
'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<ResolveEmailAuthCallbackResult> {
|
|
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,
|
|
}
|
|
}
|
|
}
|