Add Google sign-in functionality to Login and Register pages, including error handling and UI updates for better user experience.

This commit is contained in:
2025-04-23 20:52:04 +08:00
parent ced29201da
commit 0203cb4041
5 changed files with 159 additions and 24 deletions

50
README-google-auth.md Normal file
View File

@@ -0,0 +1,50 @@
# 配置 Google 登录功能
为了启用 Google 登录功能,您需要在 Supabase 和 Google Cloud Platform 进行配置。
## 步骤 1: 创建 Google OAuth 客户端
1. 访问 [Google Cloud Console](https://console.cloud.google.com/)
2. 创建一个新项目或选择现有项目
3. 在左侧菜单中导航到 "API 和服务" > "OAuth 同意屏幕"
4. 选择用户类型(外部或内部),然后点击"创建"
5. 填写必要的信息(应用名称、用户支持电子邮件等)并保存
6. 导航到 "API 和服务" > "凭据"
7. 点击"创建凭据" > "OAuth 客户端 ID"
8. 应用类型选择 "Web 应用"
9. 名称中输入您的应用名称
10. 添加以下已获授权的重定向 URI:
- `https://mwwvqwevplndzvmqmrxa.supabase.co/auth/v1/callback`
11. 点击"创建"
12. 复制生成的 "客户端 ID" 和 "客户端密钥"
## 步骤 2: 在 Supabase 中配置 Google 提供商
1. 登录 [Supabase 仪表板](https://app.supabase.com)
2. 选择您的项目
3. 导航到 "身份验证" > "提供商"
4. 找到 Google 提供商并启用它
5. 粘贴您刚才获取的 "客户端 ID" 和 "客户端密钥"
6. 保存配置
## 步骤 3: 更新重定向 URL如有需要
如果您的应用需要在登录后重定向到特定页面,请确保在 Google Cloud Console 和 Supabase 中配置了正确的重定向 URL。
在 Supabase 中:
1. 导航到 "身份验证" > "URL 配置"
2. 添加您的前端 URL 到站点 URL 字段中
3. 设置重定向 URL通常是您的前端 URL
## 测试
1. 在您的应用中,尝试使用 Google 登录
2. 验证认证流程,确保可以成功登录并重定向到应用
3. 检查 Supabase 中的用户数据,确认新用户已创建
## 故障排除
- 确保重定向 URI 完全匹配
- 确保 OAuth 同意屏幕已正确配置
- 查看 Supabase 和应用程序中的日志以获取详细的错误信息
- 如果遇到 CORS 错误,检查您的站点 URL 配置

View File

@@ -0,0 +1,17 @@
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get('code');
if (code) {
const cookieStore = cookies();
const supabase = createRouteHandlerClient({ cookies: () => cookieStore });
await supabase.auth.exchangeCodeForSession(code);
}
// URL to redirect to after sign in process completes
return NextResponse.redirect(new URL('/analytics', request.url));
}

View File

@@ -21,7 +21,7 @@ function MessageHandler({ setMessage }: { setMessage: (message: { type: string,
export default function LoginPage() { export default function LoginPage() {
const router = useRouter(); const router = useRouter();
const { signIn, user } = useAuth(); const { signIn, signInWithGoogle, user } = useAuth();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
@@ -67,6 +67,28 @@ export default function LoginPage() {
} }
}; };
const handleGoogleSignIn = async () => {
try {
setIsLoading(true);
setMessage({ type: '', content: '' });
const { error } = await signInWithGoogle();
if (error) {
throw new Error(error.message);
}
// Google OAuth will handle the redirect
} catch (error) {
console.error('Google login error:', error);
setMessage({
type: 'error',
content: error instanceof Error ? error.message : 'Failed to sign in with Google'
});
setIsLoading(false);
}
};
return ( return (
<div className="flex items-center justify-center min-h-screen bg-gray-100"> <div className="flex items-center justify-center min-h-screen bg-gray-100">
{/* Wrap the component using useSearchParams in Suspense */} {/* Wrap the component using useSearchParams in Suspense */}
@@ -101,7 +123,31 @@ export default function LoginPage() {
</div> </div>
)} )}
<form onSubmit={handleEmailSignIn} className="mt-8 space-y-6"> {/* Google Sign In Button */}
<button
type="button"
onClick={handleGoogleSignIn}
disabled={isLoading}
className="w-full flex items-center justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<svg className="h-5 w-5 mr-2" viewBox="0 0 24 24" width="24" height="24">
<g transform="matrix(1, 0, 0, 1, 27.009001, -39.238998)">
<path fill="#4285F4" d="M -3.264 51.509 C -3.264 50.719 -3.334 49.969 -3.454 49.239 L -14.754 49.239 L -14.754 53.749 L -8.284 53.749 C -8.574 55.229 -9.424 56.479 -10.684 57.329 L -10.684 60.329 L -6.824 60.329 C -4.564 58.239 -3.264 55.159 -3.264 51.509 Z"/>
<path fill="#34A853" d="M -14.754 63.239 C -11.514 63.239 -8.804 62.159 -6.824 60.329 L -10.684 57.329 C -11.764 58.049 -13.134 58.489 -14.754 58.489 C -17.884 58.489 -20.534 56.379 -21.484 53.529 L -25.464 53.529 L -25.464 56.619 C -23.494 60.539 -19.444 63.239 -14.754 63.239 Z"/>
<path fill="#FBBC05" d="M -21.484 53.529 C -21.734 52.809 -21.864 52.039 -21.864 51.239 C -21.864 50.439 -21.724 49.669 -21.484 48.949 L -21.484 45.859 L -25.464 45.859 C -26.284 47.479 -26.754 49.299 -26.754 51.239 C -26.754 53.179 -26.284 54.999 -25.464 56.619 L -21.484 53.529 Z"/>
<path fill="#EA4335" d="M -14.754 43.989 C -12.984 43.989 -11.404 44.599 -10.154 45.789 L -6.734 42.369 C -8.804 40.429 -11.514 39.239 -14.754 39.239 C -19.444 39.239 -23.494 41.939 -25.464 45.859 L -21.484 48.949 C -20.534 46.099 -17.884 43.989 -14.754 43.989 Z"/>
</g>
</svg>
{isLoading ? 'Signing in...' : 'Sign in with Google'}
</button>
<div className="mt-6 flex items-center justify-center">
<div className="border-t border-gray-300 flex-grow mr-3"></div>
<span className="text-sm text-gray-500">or</span>
<div className="border-t border-gray-300 flex-grow ml-3"></div>
</div>
<form onSubmit={handleEmailSignIn} className="mt-6 space-y-6">
<div> <div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700"> <label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email address Email address
@@ -144,7 +190,7 @@ export default function LoginPage() {
disabled={isLoading} disabled={isLoading}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
> >
{isLoading ? 'Signing in...' : 'Sign in'} {isLoading ? 'Signing in...' : 'Sign in with Email'}
</button> </button>
</div> </div>
</form> </form>

View File

@@ -12,44 +12,44 @@ export default function RegisterPage() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { signUp, signInWithGoogle } = useAuth(); const { signUp, signInWithGoogle } = useAuth();
// 处理注册表单提交 // Handle registration form submission
const handleSubmit = async (e: FormEvent) => { const handleSubmit = async (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
setError(null); setError(null);
// 验证密码 // Validate passwords
if (password !== confirmPassword) { if (password !== confirmPassword) {
setError('两次输入的密码不一致'); setError('Passwords do not match');
return; return;
} }
// 密码强度验证 // Password strength validation
if (password.length < 6) { if (password.length < 6) {
setError('密码长度至少为6个字符'); setError('Password must be at least 6 characters');
return; return;
} }
setIsLoading(true); setIsLoading(true);
try { try {
await signUp(email, password); await signUp(email, password);
// 注册成功后会跳转到登录页面,提示用户验证邮箱 // After successful registration, redirect to login page with email verification prompt
} catch (error) { } catch (error) {
console.error('Registration error:', error); console.error('Registration error:', error);
setError('注册失败,请稍后再试或使用其他邮箱'); setError('Registration failed. Please try again later or use a different email');
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
// 处理Google注册/登录 // Handle Google registration/login
const handleGoogleSignIn = async () => { const handleGoogleSignIn = async () => {
setError(null); setError(null);
try { try {
await signInWithGoogle(); await signInWithGoogle();
// 登录流程会重定向到Google然后回到应用 // Login flow will redirect to Google and then back to the application
} catch (error) { } catch (error) {
console.error('Google sign in error:', error); console.error('Google sign in error:', error);
setError('Google登录失败,请稍后再试'); setError('Google login failed. Please try again later');
} }
}; };
@@ -57,13 +57,13 @@ export default function RegisterPage() {
<div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900"> <div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900">
<div className="w-full max-w-md p-8 space-y-8 bg-white dark:bg-gray-800 rounded-lg shadow-md"> <div className="w-full max-w-md p-8 space-y-8 bg-white dark:bg-gray-800 rounded-lg shadow-md">
<div className="text-center"> <div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100"></h1> <h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Register</h1>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400"> <p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
访 Create your account to access the analytics dashboard
</p> </p>
</div> </div>
{/* 错误提示 */} {/* Error message */}
{error && ( {error && (
<div className="p-4 mb-4 text-sm text-red-700 bg-red-100 dark:bg-red-900 dark:text-red-200 rounded-lg"> <div className="p-4 mb-4 text-sm text-red-700 bg-red-100 dark:bg-red-900 dark:text-red-200 rounded-lg">
{error} {error}
@@ -74,7 +74,7 @@ export default function RegisterPage() {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Email Address
</label> </label>
<input <input
id="email" id="email"
@@ -90,7 +90,7 @@ export default function RegisterPage() {
</div> </div>
<div> <div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Password
</label> </label>
<input <input
id="password" id="password"
@@ -106,7 +106,7 @@ export default function RegisterPage() {
</div> </div>
<div> <div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Confirm Password
</label> </label>
<input <input
id="confirmPassword" id="confirmPassword"
@@ -128,7 +128,7 @@ export default function RegisterPage() {
disabled={isLoading} disabled={isLoading}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
> >
{isLoading ? '注册中...' : '注册'} {isLoading ? 'Registering...' : 'Register'}
</button> </button>
</div> </div>
@@ -137,7 +137,7 @@ export default function RegisterPage() {
<div className="w-full border-t border-gray-300 dark:border-gray-600"></div> <div className="w-full border-t border-gray-300 dark:border-gray-600"></div>
</div> </div>
<div className="relative flex justify-center text-sm"> <div className="relative flex justify-center text-sm">
<span className="px-2 bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400"></span> <span className="px-2 bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400">or</span>
</div> </div>
</div> </div>
@@ -173,19 +173,19 @@ export default function RegisterPage() {
/> />
</g> </g>
</svg> </svg>
使Google账号注册 Sign up with Google
</button> </button>
</div> </div>
</form> </form>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-sm text-gray-600 dark:text-gray-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
{' '} Already have an account?{' '}
<Link <Link
href="/login" href="/login"
className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300" className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300"
> >
Log in
</Link> </Link>
</p> </p>
</div> </div>

22
middleware.ts Normal file
View File

@@ -0,0 +1,22 @@
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(req: NextRequest) {
const res = NextResponse.next();
// Create a Supabase client configured to use cookies
const supabase = createMiddlewareClient({ req, res });
// Refresh session if expired - required for Server Components
await supabase.auth.getSession();
return res;
}
// Specify the paths where this middleware should run
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico).*)',
],
};