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:
50
README-google-auth.md
Normal file
50
README-google-auth.md
Normal 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 配置
|
||||||
17
app/auth/callback/route.ts
Normal file
17
app/auth/callback/route.ts
Normal 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));
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
22
middleware.ts
Normal 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).*)',
|
||||||
|
],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user