优化密码找回流程、管理后台批量操作和上传体验 (#2)

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-05-31 17:00:16 +08:00
parent e087dd46e2
commit 217b81c506
15 changed files with 736 additions and 158 deletions
+152
View File
@@ -0,0 +1,152 @@
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,
}
}
}
+6 -1
View File
@@ -7,4 +7,9 @@ if (!supabaseUrl || !supabaseKey) {
throw new Error('Missing Supabase environment variables')
}
export const supabase = createClient(supabaseUrl, supabaseKey)
export const supabase = createClient(supabaseUrl, supabaseKey, {
auth: {
// Email confirmation and password recovery are handled by route components.
detectSessionInUrl: false,
},
})