218 lines
9.9 KiB
TypeScript
218 lines
9.9 KiB
TypeScript
|
|
"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>
|
|||
|
|
);
|
|||
|
|
}
|