Compare commits

2 Commits

6 changed files with 204 additions and 69 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

@@ -47,11 +47,11 @@ function CreateShortUrlForm() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false); const [success, setSuccess] = useState(false);
// 使用 useEffect 在加载时添加用户信息到表单数据中 // Use useEffect to add user information to form data on load
useEffect(() => { useEffect(() => {
if (user) { if (user) {
console.log('当前用户:', user.email); console.log('Current user:', user.email);
// 可以在这里添加用户相关数据到表单中 // Can add user-related data to the form here
} }
}, [user]); }, [user]);
@@ -93,32 +93,32 @@ function CreateShortUrlForm() {
setError(null); setError(null);
try { try {
// 验证必填字段 // Validate required fields
if (!formData.originalUrl) { if (!formData.originalUrl) {
throw new Error('原始 URL 是必填项'); throw new Error('Original URL is required');
} }
if (!formData.title) { if (!formData.title) {
throw new Error('标题是必填项'); throw new Error('Title is required');
} }
if (!formData.teamId) { if (!formData.teamId) {
throw new Error('团队是必填项'); throw new Error('Team is required');
} }
if (!formData.projectId) { if (!formData.projectId) {
throw new Error('项目是必填项'); throw new Error('Project is required');
} }
if (!formData.domain) { if (!formData.domain) {
throw new Error('域名是必填项'); throw new Error('Domain is required');
} }
// 按照API要求构建请求数据 // Construct request data according to API requirements
const requestData = { const requestData = {
type: "shorturl", type: "shorturl",
attributes: { attributes: {
// 可以添加任何额外属性但attributes不能为空 // Can add any additional attributes, but attributes cannot be empty
icon: "" icon: ""
}, },
shortUrl: { shortUrl: {
@@ -134,20 +134,20 @@ function CreateShortUrlForm() {
tagIds: formData.tags && formData.tags.length > 0 ? formData.tags : undefined tagIds: formData.tags && formData.tags.length > 0 ? formData.tags : undefined
}; };
// 调用 API 创建 shorturl 资源 // Call API to create shorturl resource
const response = await limqRequest('resource/shorturl', 'POST', requestData as unknown as Record<string, unknown>); const response = await limqRequest('resource/shorturl', 'POST', requestData as unknown as Record<string, unknown>);
console.log('创建成功:', response); console.log('Created successfully:', response);
setSuccess(true); setSuccess(true);
// 2秒后跳转到链接列表页面 // Redirect to links list page after 2 seconds
setTimeout(() => { setTimeout(() => {
router.push('/links'); router.push('/links');
}, 2000); }, 2000);
} catch (err) { } catch (err) {
console.error('创建短链接失败:', err); console.error('Failed to create short URL:', err);
setError(err instanceof Error ? err.message : '创建短链接失败,请稍后重试'); setError(err instanceof Error ? err.message : 'Failed to create short URL, please try again later');
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@@ -188,7 +188,7 @@ function CreateShortUrlForm() {
</div> </div>
<div className="ml-3"> <div className="ml-3">
<p className="text-sm text-green-700"> <p className="text-sm text-green-700">
... Short URL created successfully! Redirecting...
</p> </p>
</div> </div>
</div> </div>
@@ -196,10 +196,10 @@ function CreateShortUrlForm() {
)} )}
<form onSubmit={handleSubmit} className="p-6 space-y-6"> <form onSubmit={handleSubmit} className="p-6 space-y-6">
{/* 标题 */} {/* Title */}
<div> <div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700"> <label htmlFor="title" className="block text-sm font-medium text-gray-700">
<span className="text-red-500">*</span> Title <span className="text-red-500">*</span>
</label> </label>
<input <input
type="text" type="text"
@@ -207,16 +207,16 @@ function CreateShortUrlForm() {
name="title" name="title"
value={formData.title} value={formData.title}
onChange={handleChange} onChange={handleChange}
placeholder="例如:产品发布活动" placeholder="e.g., Product Launch Campaign"
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required required
/> />
</div> </div>
{/* 原始 URL */} {/* Original URL */}
<div> <div>
<label htmlFor="originalUrl" className="block text-sm font-medium text-gray-700"> <label htmlFor="originalUrl" className="block text-sm font-medium text-gray-700">
URL <span className="text-red-500">*</span> Original URL <span className="text-red-500">*</span>
</label> </label>
<input <input
type="url" type="url"
@@ -230,10 +230,10 @@ function CreateShortUrlForm() {
/> />
</div> </div>
{/* 自定义短链接 */} {/* Custom Short Link */}
<div> <div>
<label htmlFor="customSlug" className="block text-sm font-medium text-gray-700"> <label htmlFor="customSlug" className="block text-sm font-medium text-gray-700">
<span className="text-gray-500">()</span> Custom Short Link <span className="text-gray-500">(Optional)</span>
</label> </label>
<div className="flex mt-1 rounded-md shadow-sm"> <div className="flex mt-1 rounded-md shadow-sm">
<span className="inline-flex items-center px-3 py-2 text-sm text-gray-500 border border-r-0 border-gray-300 rounded-l-md bg-gray-50"> <span className="inline-flex items-center px-3 py-2 text-sm text-gray-500 border border-r-0 border-gray-300 rounded-l-md bg-gray-50">
@@ -250,14 +250,14 @@ function CreateShortUrlForm() {
/> />
</div> </div>
<p className="mt-1 text-xs text-gray-500"> <p className="mt-1 text-xs text-gray-500">
Leave blank to generate a random short link
</p> </p>
</div> </div>
{/* 域名 */} {/* Domain */}
<div> <div>
<label htmlFor="domain" className="block text-sm font-medium text-gray-700"> <label htmlFor="domain" className="block text-sm font-medium text-gray-700">
<span className="text-red-500">*</span> Domain <span className="text-red-500">*</span>
</label> </label>
<input <input
type="text" type="text"
@@ -265,16 +265,16 @@ function CreateShortUrlForm() {
name="domain" name="domain"
value={formData.domain} value={formData.domain}
onChange={handleChange} onChange={handleChange}
placeholder="例如:googleads.link" placeholder="e.g., googleads.link"
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required required
/> />
</div> </div>
{/* 团队选择 */} {/* Team Selection */}
<div> <div>
<label htmlFor="teamId" className="block text-sm font-medium text-gray-700"> <label htmlFor="teamId" className="block text-sm font-medium text-gray-700">
<span className="text-red-500">*</span> Team <span className="text-red-500">*</span>
</label> </label>
<div className="mt-1"> <div className="mt-1">
<TeamSelector <TeamSelector
@@ -283,7 +283,7 @@ function CreateShortUrlForm() {
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
teamId: teamId as string, teamId: teamId as string,
// 当团队变更时清除已选项目 // Clear selected project when team changes
projectId: '' projectId: ''
})); }));
}} }}
@@ -291,10 +291,10 @@ function CreateShortUrlForm() {
</div> </div>
</div> </div>
{/* 项目选择 */} {/* Project Selection */}
<div> <div>
<label htmlFor="projectId" className="block text-sm font-medium text-gray-700"> <label htmlFor="projectId" className="block text-sm font-medium text-gray-700">
<span className="text-red-500">*</span> Project <span className="text-red-500">*</span>
</label> </label>
<div className="mt-1"> <div className="mt-1">
<ProjectSelector <ProjectSelector
@@ -310,10 +310,10 @@ function CreateShortUrlForm() {
</div> </div>
</div> </div>
{/* 描述 */} {/* Description */}
<div> <div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700"> <label htmlFor="description" className="block text-sm font-medium text-gray-700">
<span className="text-gray-500">()</span> Description <span className="text-gray-500">(Optional)</span>
</label> </label>
<textarea <textarea
id="description" id="description"
@@ -321,15 +321,15 @@ function CreateShortUrlForm() {
value={formData.description} value={formData.description}
onChange={handleChange} onChange={handleChange}
rows={3} rows={3}
placeholder="对此链接的简短描述" placeholder="A brief description of this link"
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/> />
</div> </div>
{/* 标签 */} {/* Tags */}
<div> <div>
<label htmlFor="tagInput" className="block text-sm font-medium text-gray-700"> <label htmlFor="tagInput" className="block text-sm font-medium text-gray-700">
<span className="text-gray-500">()</span> Tags <span className="text-gray-500">(Optional)</span>
</label> </label>
<div className="flex mt-1 rounded-md shadow-sm"> <div className="flex mt-1 rounded-md shadow-sm">
<input <input
@@ -338,7 +338,7 @@ function CreateShortUrlForm() {
value={tagInput} value={tagInput}
onChange={(e) => setTagInput(e.target.value)} onChange={(e) => setTagInput(e.target.value)}
onKeyDown={handleTagKeyDown} onKeyDown={handleTagKeyDown}
placeholder="添加标签并按 Enter" placeholder="Add a tag and press Enter"
className="flex-1 block w-full min-w-0 px-3 py-2 border border-gray-300 rounded-l-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" className="flex-1 block w-full min-w-0 px-3 py-2 border border-gray-300 rounded-l-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/> />
<button <button
@@ -346,7 +346,7 @@ function CreateShortUrlForm() {
onClick={addTag} onClick={addTag}
className="inline-flex items-center px-3 py-2 text-sm font-medium text-white border border-transparent rounded-r-md shadow-sm bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" className="inline-flex items-center px-3 py-2 text-sm font-medium text-white border border-transparent rounded-r-md shadow-sm bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
> >
Add
</button> </button>
</div> </div>
@@ -360,7 +360,7 @@ function CreateShortUrlForm() {
onClick={() => removeTag(tag)} onClick={() => removeTag(tag)}
className="flex-shrink-0 ml-1 text-blue-500 rounded-full hover:text-blue-700 focus:outline-none" className="flex-shrink-0 ml-1 text-blue-500 rounded-full hover:text-blue-700 focus:outline-none"
> >
<span className="sr-only"> {tag}</span> <span className="sr-only">Remove tag {tag}</span>
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> <svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" /> <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg> </svg>
@@ -371,14 +371,14 @@ function CreateShortUrlForm() {
)} )}
</div> </div>
{/* 提交按钮 */} {/* Submit Button */}
<div className="flex justify-end pt-4 border-t border-gray-200"> <div className="flex justify-end pt-4 border-t border-gray-200">
<button <button
type="button" type="button"
onClick={() => router.back()} onClick={() => router.back()}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 mr-3" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 mr-3"
> >
Cancel
</button> </button>
<button <button
type="submit" type="submit"
@@ -391,9 +391,9 @@ function CreateShortUrlForm() {
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg> </svg>
... Processing...
</> </>
) : '创建短链接'} ) : 'Create Short URL'}
</button> </button>
</div> </div>
</form> </form>

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).*)',
],
};