casualroom/apps/fenghuo/web/app/[locale]/auth/better-auth-callback/page.tsx

218 lines
9.9 KiB
TypeScript
Raw Normal View History

2025-07-28 07:50:50 +08:00
"use client";
import { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { toast } from "@nice/ui/components/sonner";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@nice/ui/components/card";
import { Button } from "@nice/ui/components/button";
import { client } from "@/lib/auth-client";
export default function BetterAuthCallbackPage() {
const router = useRouter();
const searchParams = useSearchParams();
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
const [message, setMessage] = useState('正在处理认证回调...');
const [authData, setAuthData] = useState<any>(null);
const [session, setSession] = useState<any>(null);
useEffect(() => {
const handleCallback = async () => {
try {
console.log('开始处理 Better Auth 回调...');
console.log('当前 URL:', window.location.href);
// 获取URL参数
const code = searchParams.get('code');
const error = searchParams.get('error');
const errorDescription = searchParams.get('error_description');
const state = searchParams.get('state');
const sessionState = searchParams.get('session_state');
console.log('回调参数:', {
code,
error,
errorDescription,
state,
sessionState,
fullURL: window.location.href
});
// 检查是否有错误
if (error) {
let errorMsg = error;
if (error === 'oauth_code_verification_failed') {
errorMsg = 'OAuth授权码验证失败。可能的原因包括\n1. 授权码已过期\n2. 授权码已被使用\n3. 客户端配置不匹配\n4. PKCE验证失败';
} else if (error === 'account_not_linked') {
errorMsg = '账户未关联。请先创建用户账户或登录现有账户。';
}
throw new Error(errorDescription || errorMsg);
}
// 如果没有授权码,说明可能是直接访问回调页面
if (!code) {
throw new Error('未收到授权码。请从测试页面启动 OAuth 流程。');
}
setMessage('收到授权码,正在完成认证流程...');
// 等待 Better Auth 处理回调
// Better Auth 会自动处理从 URL 参数中的授权码交换令牌
let attempts = 0;
const maxAttempts = 10;
const checkSession = async (): Promise<any> => {
attempts++;
console.log(`尝试检查会话,第 ${attempts}`);
try {
const sessionResult = await client.getSession();
console.log('会话检查结果:', sessionResult);
if (sessionResult.data) {
return sessionResult.data;
} else if (attempts < maxAttempts) {
// 等待一段时间再重试
await new Promise(resolve => setTimeout(resolve, 500));
return checkSession();
} else {
throw new Error('认证完成但未建立会话');
}
} catch (error) {
console.error(`会话检查失败 (尝试 ${attempts}):`, error);
if (attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 500));
return checkSession();
} else {
throw error;
}
}
};
const finalSession = await checkSession();
setStatus('success');
setMessage('OAuth2 认证成功!已建立用户会话。');
setSession(finalSession);
setAuthData({
code,
state,
sessionState,
timestamp: new Date().toISOString(),
user: finalSession.user
});
toast.success('OAuth2 认证成功!');
} catch (error) {
console.error('认证回调失败:', error);
setStatus('error');
setMessage(error instanceof Error ? error.message : '认证失败');
toast.error(`认证失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
};
// 给页面一点时间加载,然后处理回调
const timeoutId = setTimeout(handleCallback, 100);
return () => clearTimeout(timeoutId);
}, [searchParams]);
const handleBackToTest = () => {
router.push('/test');
};
const handleTryAgain = () => {
router.push('/test');
};
return (
<div className="container mx-auto p-6 max-w-2xl">
<Card>
<CardHeader className="text-center">
<div className="mx-auto mb-4">
{status === 'loading' && (
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
)}
{status === 'success' && (
<div className="rounded-full h-12 w-12 bg-green-100 dark:bg-green-900 mx-auto flex items-center justify-center">
<svg className="h-6 w-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
)}
{status === 'error' && (
<div className="rounded-full h-12 w-12 bg-red-100 dark:bg-red-900 mx-auto flex items-center justify-center">
<svg className="h-6 w-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
)}
</div>
<CardTitle className="text-2xl">
{status === 'loading' && 'Better Auth 回调处理中'}
{status === 'success' && '认证成功'}
{status === 'error' && '认证失败'}
</CardTitle>
<CardDescription>
{message}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{authData && (
<div className="space-y-4">
<h3 className="font-semibold">:</h3>
<div className="bg-muted p-4 rounded-md">
<pre className="text-xs overflow-x-auto">
{JSON.stringify(authData, null, 2)}
</pre>
</div>
</div>
)}
{session && (
<div className="space-y-4">
<h3 className="font-semibold">:</h3>
<div className="bg-green-50 dark:bg-green-900/20 p-4 rounded-md">
<div className="text-sm space-y-1">
<div><strong> ID:</strong> {session.user?.id}</div>
<div><strong>:</strong> {session.user?.email}</div>
<div><strong>:</strong> {session.user?.name}</div>
<div><strong> ID:</strong> {session.id}</div>
</div>
</div>
</div>
)}
<div className="flex flex-col gap-3">
{status === 'success' && (
<Button onClick={handleBackToTest} className="w-full">
</Button>
)}
{status === 'error' && (
<Button onClick={handleTryAgain} variant="outline" className="w-full">
</Button>
)}
</div>
<div className="text-sm text-muted-foreground">
<h4 className="font-semibold mb-2">:</h4>
<ul className="list-disc list-inside space-y-1">
<li> Better Auth Generic OAuth </li>
<li> OAuth2/OIDC </li>
<li></li>
<li>Better Auth </li>
<li> "account_not_linked" </li>
</ul>
</div>
</CardContent>
</Card>
</div>
);
}