Compare commits
5 Commits
dafa7f53ac
...
ed01b88e04
| Author | SHA1 | Date | |
|---|---|---|---|
| ed01b88e04 | |||
| bee8369a6b | |||
| ebb1e77ecc | |||
| 1b4e0bafc7 | |||
| c56410b4de |
@@ -1,50 +0,0 @@
|
||||
# 配置 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 配置
|
||||
@@ -1,65 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import ProtectedRoute from '@/app/components/ProtectedRoute';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-8">
|
||||
Welcome to ShortURL Analytics
|
||||
</h1>
|
||||
<ProtectedRoute>
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-8">
|
||||
Welcome to ShortURL Analytics
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<a
|
||||
href="/dashboard"
|
||||
className="block p-6 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
|
||||
>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Dashboard
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-600">
|
||||
Get an overview of all your short URL analytics data.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/events"
|
||||
className="block p-6 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
|
||||
>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Event Tracking
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-600">
|
||||
View detailed events for all your short URLs.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/analytics"
|
||||
className="block p-6 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
|
||||
>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
URL Analysis
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-600">
|
||||
Analyze performance of specific short URLs.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/account"
|
||||
className="block p-6 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
|
||||
>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Account Settings
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-600">
|
||||
Manage your account and team settings.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<a
|
||||
href="/dashboard"
|
||||
className="block p-6 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
|
||||
>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Dashboard
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-600">
|
||||
Get an overview of all your short URL analytics data.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/events"
|
||||
className="block p-6 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
|
||||
>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Event Tracking
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-600">
|
||||
View detailed events for all your short URLs.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/analytics"
|
||||
className="block p-6 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
|
||||
>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
URL Analysis
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-600">
|
||||
Analyze performance of specific short URLs.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/account"
|
||||
className="block p-6 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
|
||||
>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Account Settings
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-600">
|
||||
Manage your account and team settings.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { ProjectSelector } from '@/app/components/ui/ProjectSelector';
|
||||
import { TagSelector } from '@/app/components/ui/TagSelector';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useShortUrlStore } from '@/app/utils/store';
|
||||
import ProtectedRoute from '@/app/components/ProtectedRoute';
|
||||
|
||||
// 事件类型定义
|
||||
interface Event {
|
||||
@@ -1109,12 +1110,14 @@ function AnalyticsContent() {
|
||||
// Main page component with Suspense
|
||||
export default function AnalyticsPage() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500" />
|
||||
</div>
|
||||
}>
|
||||
<AnalyticsContent />
|
||||
</Suspense>
|
||||
<ProtectedRoute>
|
||||
<Suspense fallback={
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500" />
|
||||
</div>
|
||||
}>
|
||||
<AnalyticsContent />
|
||||
</Suspense>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
126
app/api/activities/readme.md
Normal file
126
app/api/activities/readme.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# 数据分析活动接口说明
|
||||
|
||||
## 接口概述
|
||||
`/api/activities` 端点提供了访问和导出分析事件数据的功能,可用于查询短链接的点击和访问记录。
|
||||
|
||||
## 请求方式
|
||||
- HTTP 方法: **GET**
|
||||
- URL: `/api/activities`
|
||||
|
||||
## 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|-------|------|------|------|
|
||||
| slug | string | 否* | 短链接标识符 |
|
||||
| domain | string | 否* | 短链接域名 |
|
||||
| format | string | 否 | 响应格式,可选值: `csv`(纯文本显示);不传则默认返回JSON |
|
||||
| startTime | string | 否* | 起始时间,ISO格式 |
|
||||
| endTime | string | 否* | 结束时间,ISO格式 |
|
||||
| page | number | 否 | 当前页码,默认为1 |
|
||||
| pageSize | number | 否 | 每页记录数,默认为50 |
|
||||
|
||||
_*注:必须提供 (slug和domain) 或 (startTime和endTime) 中的至少一组过滤条件_
|
||||
|
||||
## 响应格式
|
||||
|
||||
### JSON格式(默认)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "事件ID",
|
||||
"type": "事件类型",
|
||||
"time": "事件时间",
|
||||
"visitor": {
|
||||
"id": "访问者ID",
|
||||
"ipAddress": "IP地址",
|
||||
"userAgent": "浏览器用户代理",
|
||||
"referrer": "来源页面"
|
||||
},
|
||||
"device": {
|
||||
"type": "设备类型",
|
||||
"browser": "浏览器",
|
||||
"os": "操作系统"
|
||||
},
|
||||
"location": {
|
||||
"country": "国家",
|
||||
"city": "城市"
|
||||
},
|
||||
"link": {
|
||||
"id": "链接ID",
|
||||
"slug": "短链标识",
|
||||
"originalUrl": "原始链接",
|
||||
"label": "链接标签",
|
||||
"tags": ["标签1", "标签2"]
|
||||
},
|
||||
"utm": {
|
||||
"source": "来源",
|
||||
"medium": "媒介",
|
||||
"campaign": "活动",
|
||||
"term": "关键词",
|
||||
"content": "内容"
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total": 总记录数,
|
||||
"page": 当前页码,
|
||||
"pageSize": 每页记录数
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CSV格式
|
||||
|
||||
当使用 `format=csv` 参数时,接口将返回以下列的CSV纯文本:
|
||||
|
||||
```
|
||||
time,activity,campaign,clientId,originPath
|
||||
2023-01-01 12:34:56,click,spring_sale,abc123,https://example.com/path?utm_campaign=spring_sale
|
||||
```
|
||||
|
||||
列说明:
|
||||
- `time`: 事件发生时间
|
||||
- `activity`: 事件类型(如点击、访问等)
|
||||
- `campaign`: 营销活动标识(从URL中的utm_campaign提取)
|
||||
- `clientId`: 访问者标识的前半部分
|
||||
- `originPath`: 原始请求路径或引荐URL
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基本查询(JSON)
|
||||
```
|
||||
GET /api/activities?slug=0326recap10&domain=example.com
|
||||
```
|
||||
|
||||
### 导出CSV
|
||||
```
|
||||
GET /api/activities?slug=0326recap10&format=csv
|
||||
```
|
||||
|
||||
### 按日期范围过滤
|
||||
```
|
||||
GET /api/activities?startTime=2023-03-01T00:00:00Z&endTime=2023-03-31T23:59:59Z
|
||||
```
|
||||
|
||||
### 分页
|
||||
```
|
||||
GET /api/activities?slug=0326recap10&page=2&pageSize=100
|
||||
```
|
||||
|
||||
## 错误响应
|
||||
|
||||
当请求参数不正确或服务器发生错误时,返回以下格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "错误描述信息"
|
||||
}
|
||||
```
|
||||
|
||||
常见错误代码:
|
||||
- 400: 参数错误,例如缺少必要的过滤条件
|
||||
- 500: 服务器内部错误
|
||||
@@ -128,16 +128,10 @@ export async function GET(request: NextRequest) {
|
||||
csvContent += `${time},${activity},${campaign},${clientId},${originPath}\n`;
|
||||
});
|
||||
|
||||
// Generate filename based on available parameters
|
||||
const filename = slug
|
||||
? `activities-${slug}.csv`
|
||||
: `activities-${new Date().toISOString().slice(0,10)}.csv`;
|
||||
|
||||
// Return CSV response
|
||||
return new NextResponse(csvContent, {
|
||||
headers: {
|
||||
'Content-Type': 'text/csv',
|
||||
'Content-Disposition': `attachment; filename="${filename}"`
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,17 +1,68 @@
|
||||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
|
||||
import { cookies } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } 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);
|
||||
console.log('Auth callback received:', { url: request.url, hasCode: !!code });
|
||||
|
||||
// If no code parameter found, redirect to login page
|
||||
if (!code) {
|
||||
console.log('No code parameter found, redirecting to login page');
|
||||
return NextResponse.redirect(new URL('/login', request.url));
|
||||
}
|
||||
|
||||
// URL to redirect to after sign in process completes
|
||||
return NextResponse.redirect(new URL('/analytics', request.url));
|
||||
try {
|
||||
// Create Supabase client
|
||||
const cookieStore = cookies();
|
||||
const supabaseRouteHandler = createRouteHandlerClient({ cookies: () => cookieStore });
|
||||
|
||||
// Exchange code for session
|
||||
console.log('Starting code exchange for session');
|
||||
const { data, error } = await supabaseRouteHandler.auth.exchangeCodeForSession(code);
|
||||
|
||||
if (error) {
|
||||
console.error('Error exchanging code for session:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log('Successfully retrieved session, user:', data.session?.user.email);
|
||||
|
||||
// Check if session was successfully created
|
||||
if (data.session) {
|
||||
console.log('Session created successfully:', {
|
||||
userId: data.session.user.id,
|
||||
email: data.session.user.email,
|
||||
expiresAt: data.session.expires_at ? new Date(data.session.expires_at * 1000).toISOString() : 'unknown'
|
||||
});
|
||||
|
||||
// Set additional cookie to ensure client can detect login status
|
||||
// Use Next.js Response to set cookie
|
||||
const response = NextResponse.redirect(new URL('/', request.url));
|
||||
response.cookies.set({
|
||||
name: 'sb-auth-token',
|
||||
value: 'true',
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||
sameSite: 'lax',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
httpOnly: false,
|
||||
});
|
||||
console.log('Set backup cookie: sb-auth-token');
|
||||
return response;
|
||||
}
|
||||
|
||||
// Redirect to home page by default
|
||||
console.log('Redirecting to home page');
|
||||
return NextResponse.redirect(new URL('/', request.url));
|
||||
} catch (error) {
|
||||
console.error('Auth callback error:', error);
|
||||
// Redirect to login page on error
|
||||
return NextResponse.redirect(
|
||||
new URL('/login?message=Authentication failed. Please try again.', request.url)
|
||||
);
|
||||
}
|
||||
}
|
||||
42
app/components/ProtectedRoute.tsx
Normal file
42
app/components/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
|
||||
export default function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
const { user, isLoading } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
// 如果非加载状态且用户未登录,重定向到登录页
|
||||
if (!isLoading && !user) {
|
||||
console.log('ProtectedRoute: 未登录,重定向到登录页');
|
||||
// 保存当前URL以便登录后可以返回
|
||||
const currentUrl = window.location.href;
|
||||
router.push(`/login?redirect=${encodeURIComponent(currentUrl)}`);
|
||||
}
|
||||
}, [isLoading, user, router]);
|
||||
|
||||
// 如果正在加载,显示加载指示器
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500 mx-auto"></div>
|
||||
<p className="mt-4 text-lg text-gray-700 dark:text-gray-300">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果用户未登录,不渲染任何内容(等待重定向)
|
||||
if (!user) {
|
||||
console.log('ProtectedRoute: 用户未登录,等待重定向');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 用户已登录,渲染子组件
|
||||
console.log('ProtectedRoute: 用户已登录,渲染内容');
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -99,32 +99,42 @@ export function ProjectSelector({
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
console.log(`开始获取项目数据,用户ID: ${userId}, 团队ID过滤: ${effectiveTeamIds?.join(', ') || '无'}`);
|
||||
|
||||
try {
|
||||
const supabase = getSupabaseClient();
|
||||
console.log('Supabase客户端已创建,准备获取项目数据');
|
||||
|
||||
if (effectiveTeamIds && effectiveTeamIds.length > 0) {
|
||||
// If team IDs are provided, get projects for those teams
|
||||
console.log(`通过团队ID获取项目: ${effectiveTeamIds.join(', ')}`);
|
||||
|
||||
const { data: projectsData, error: projectsError } = await supabase
|
||||
.from('team_projects')
|
||||
.select('project_id, projects:project_id(*), teams:team_id(name)')
|
||||
.in('team_id', effectiveTeamIds)
|
||||
.is('projects.deleted_at', null);
|
||||
|
||||
console.log(`团队项目查询结果:`, projectsData ? `找到${projectsData.length}个` : '无数据',
|
||||
projectsError ? `错误: ${projectsError.message}` : '无错误');
|
||||
|
||||
if (projectsError) throw projectsError;
|
||||
|
||||
if (!projectsData || projectsData.length === 0) {
|
||||
console.log('未找到团队项目,返回空列表');
|
||||
if (isMounted) setProjects([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract projects from response with team info
|
||||
if (isMounted) {
|
||||
console.log('处理团队项目数据');
|
||||
const projectList: Project[] = [];
|
||||
|
||||
for (const item of projectsData as ProjectWithTeam[]) {
|
||||
for (const item of projectsData as unknown as ProjectWithTeam[]) {
|
||||
if (item.projects && typeof item.projects === 'object' && 'id' in item.projects && 'name' in item.projects) {
|
||||
const project = item.projects as Project;
|
||||
if (item.teams && 'name' in item.teams) {
|
||||
if (item.teams && typeof item.teams === 'object' && 'name' in item.teams) {
|
||||
project.team_name = item.teams.name;
|
||||
}
|
||||
// Avoid duplicate projects from different teams
|
||||
@@ -134,32 +144,42 @@ export function ProjectSelector({
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`处理后的项目数据: ${projectList.length}个`);
|
||||
setProjects(projectList);
|
||||
}
|
||||
} else {
|
||||
// If no team IDs, get all user's projects
|
||||
console.log(`获取用户所有项目,用户ID: ${userId}`);
|
||||
|
||||
const { data: projectsData, error: projectsError } = await supabase
|
||||
.from('user_projects')
|
||||
.select('project_id, projects:project_id(*)')
|
||||
.eq('user_id', userId)
|
||||
.is('projects.deleted_at', null);
|
||||
|
||||
console.log(`用户项目查询结果:`, projectsData ? `找到${projectsData.length}个` : '无数据',
|
||||
projectsError ? `错误: ${projectsError.message}` : '无错误');
|
||||
|
||||
if (projectsError) throw projectsError;
|
||||
|
||||
if (!projectsData || projectsData.length === 0) {
|
||||
console.log('未找到用户项目,返回空列表');
|
||||
if (isMounted) setProjects([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch team info for these projects
|
||||
const projectIds = projectsData.map(item => item.project_id);
|
||||
console.log(`获取项目的团队信息,项目IDs: ${projectIds.join(', ')}`);
|
||||
|
||||
// Get team info for each project
|
||||
const { data: teamProjectsData, error: teamProjectsError } = await supabase
|
||||
.from('team_projects')
|
||||
.select('project_id, teams:team_id(name)')
|
||||
.in('project_id', projectIds);
|
||||
|
||||
|
||||
console.log(`项目团队关系查询结果:`, teamProjectsData ? `找到${teamProjectsData.length}个` : '无数据');
|
||||
|
||||
if (teamProjectsError) throw teamProjectsError;
|
||||
|
||||
// Create project ID to team name mapping
|
||||
@@ -174,6 +194,7 @@ export function ProjectSelector({
|
||||
|
||||
// Extract projects with team names
|
||||
if (isMounted && projectsData) {
|
||||
console.log('处理用户项目数据');
|
||||
const projectList: Project[] = [];
|
||||
|
||||
for (const item of projectsData) {
|
||||
@@ -184,12 +205,14 @@ export function ProjectSelector({
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`处理后的项目数据: ${projectList.length}个`);
|
||||
setProjects(projectList);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取项目数据出错:', err);
|
||||
if (isMounted) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load projects');
|
||||
setError(err instanceof Error ? err.message : '获取项目数据失败');
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
@@ -198,27 +221,51 @@ export function ProjectSelector({
|
||||
}
|
||||
};
|
||||
|
||||
const supabase = getSupabaseClient();
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent, session: Session | null) => {
|
||||
if (event === 'SIGNED_IN' && session?.user?.id) {
|
||||
fetchProjects(session.user.id);
|
||||
} else if (event === 'SIGNED_OUT') {
|
||||
setProjects([]);
|
||||
setError(null);
|
||||
}
|
||||
});
|
||||
// 获取Supabase客户端实例并订阅认证状态变化
|
||||
try {
|
||||
const supabase = getSupabaseClient();
|
||||
console.log('注册项目选择器认证状态变化监听器');
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent, session: Session | null) => {
|
||||
console.log(`项目选择器认证状态变化: ${event}`, session ? `用户ID: ${session.user.id}` : '无会话');
|
||||
if (event === 'SIGNED_IN' && session?.user?.id) {
|
||||
fetchProjects(session.user.id);
|
||||
} else if (event === 'SIGNED_OUT') {
|
||||
setProjects([]);
|
||||
setError(null);
|
||||
}
|
||||
});
|
||||
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
if (session?.user?.id) {
|
||||
fetchProjects(session.user.id);
|
||||
}
|
||||
});
|
||||
// 初始化时获取当前会话
|
||||
console.log('项目选择器获取当前会话状态');
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
console.log('项目选择器当前会话状态:', session ? `用户已登录,ID: ${session.user.id}` : '用户未登录');
|
||||
if (session?.user?.id) {
|
||||
fetchProjects(session.user.id);
|
||||
} else {
|
||||
// 如果没有会话但组件需要初始化,可以设置加载完成
|
||||
setLoading(false);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('项目选择器获取会话状态失败:', err);
|
||||
// 确保即使获取会话失败也停止加载状态
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
return () => {
|
||||
console.log('ProjectSelector组件卸载,清理订阅');
|
||||
isMounted = false;
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
} catch (initError) {
|
||||
console.error('初始化ProjectSelector出错:', initError);
|
||||
// 确保在初始化出错时也停止加载状态
|
||||
setLoading(false);
|
||||
setError('初始化失败,请刷新页面重试');
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}
|
||||
}, [effectiveTeamIds]);
|
||||
|
||||
const handleToggle = () => {
|
||||
|
||||
@@ -123,31 +123,48 @@ export function TagSelector({
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
console.log(`开始获取标签数据, 团队ID过滤: ${effectiveTeamIds?.join(', ') || '无'}`);
|
||||
|
||||
try {
|
||||
const supabase = getSupabaseClient();
|
||||
console.log('Supabase客户端已创建,准备获取标签数据');
|
||||
|
||||
let query = supabase.from('tags').select('*').is('deleted_at', null);
|
||||
|
||||
// Filter by team if teamId is provided
|
||||
if (effectiveTeamIds) {
|
||||
console.log(`通过团队ID过滤标签: ${effectiveTeamIds.join(', ')}`);
|
||||
query = query.in('team_id', effectiveTeamIds);
|
||||
}
|
||||
|
||||
const { data: tagsData, error: tagsError } = await query;
|
||||
|
||||
console.log(`标签查询结果:`, tagsData ? `找到${tagsData.length}个` : '无数据',
|
||||
tagsError ? `错误: ${tagsError.message}` : '无错误');
|
||||
|
||||
if (tagsError) throw tagsError;
|
||||
|
||||
if (!tagsData || tagsData.length === 0) {
|
||||
console.log('未找到标签,返回空列表');
|
||||
if (isMounted) setTags([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMounted) {
|
||||
console.log(`设置${tagsData.length}个标签数据`);
|
||||
setTags(tagsData as Tag[]);
|
||||
|
||||
// 如果已有value但tags刚加载好,重新设置selectedIds
|
||||
if (value && tagsData.length > 0) {
|
||||
const ids = nameToId(value);
|
||||
console.log(`根据名称设置选中的标签ID: ${ids.join(', ')}`);
|
||||
setSelectedIds(ids);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取标签数据出错:', err);
|
||||
if (isMounted) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load tags');
|
||||
setError(err instanceof Error ? err.message : '获取标签数据失败');
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
@@ -156,26 +173,40 @@ export function TagSelector({
|
||||
}
|
||||
};
|
||||
|
||||
const supabase = getSupabaseClient();
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent) => {
|
||||
if (event === 'SIGNED_IN') {
|
||||
fetchTags();
|
||||
} else if (event === 'SIGNED_OUT') {
|
||||
setTags([]);
|
||||
setError(null);
|
||||
}
|
||||
});
|
||||
// 获取Supabase客户端实例并订阅认证状态变化
|
||||
try {
|
||||
const supabase = getSupabaseClient();
|
||||
console.log('注册标签选择器认证状态变化监听器');
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent) => {
|
||||
console.log(`标签选择器认证状态变化: ${event}`);
|
||||
if (event === 'SIGNED_IN') {
|
||||
fetchTags();
|
||||
} else if (event === 'SIGNED_OUT') {
|
||||
setTags([]);
|
||||
setError(null);
|
||||
}
|
||||
});
|
||||
|
||||
supabase.auth.getSession().then(() => {
|
||||
// 初始化时获取标签数据
|
||||
console.log('标签选择器初始化,获取标签数据');
|
||||
fetchTags();
|
||||
});
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [effectiveTeamIds]);
|
||||
return () => {
|
||||
console.log('TagSelector组件卸载,清理订阅');
|
||||
isMounted = false;
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
} catch (initError) {
|
||||
console.error('初始化TagSelector出错:', initError);
|
||||
// 确保在初始化出错时也停止加载状态
|
||||
setLoading(false);
|
||||
setError('初始化失败,请刷新页面重试');
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}
|
||||
}, [effectiveTeamIds, nameToId, value]);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (!loading && !error && tags.length > 0) {
|
||||
|
||||
@@ -68,44 +68,98 @@ export function TeamSelector({
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
console.log(`开始获取团队数据,用户ID: ${userId}`);
|
||||
|
||||
try {
|
||||
const supabase = getSupabaseClient();
|
||||
console.log('Supabase客户端已创建,准备获取团队数据');
|
||||
|
||||
// 尝试创建默认团队和项目(如果用户还没有)
|
||||
// 先尝试直接获取团队数据,不等待create-default
|
||||
try {
|
||||
const response = await limqRequest('team/create-default', 'POST');
|
||||
console.log('Default team creation response:', response);
|
||||
} catch (teamError) {
|
||||
console.error('Error creating default team:', teamError);
|
||||
}
|
||||
|
||||
const { data: memberships, error: membershipError } = await supabase
|
||||
.from('team_membership')
|
||||
.select('team_id')
|
||||
.eq('user_id', userId);
|
||||
const { data: memberships, error: membershipError } = await supabase
|
||||
.from('team_membership')
|
||||
.select('team_id')
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (membershipError) throw membershipError;
|
||||
console.log(`团队成员关系查询结果:`, memberships ? `找到${memberships.length}个` : '无数据', membershipError ? `错误: ${membershipError.message}` : '无错误');
|
||||
|
||||
if (!memberships || memberships.length === 0) {
|
||||
if (isMounted) setTeams([]);
|
||||
return;
|
||||
}
|
||||
if (membershipError) throw membershipError;
|
||||
|
||||
const teamIds = memberships.map(m => m.team_id);
|
||||
const { data: teamsData, error: teamsError } = await supabase
|
||||
.from('teams')
|
||||
.select('*')
|
||||
.in('id', teamIds)
|
||||
.is('deleted_at', null);
|
||||
if (!memberships || memberships.length === 0) {
|
||||
console.log('未找到团队成员关系,尝试创建默认团队');
|
||||
|
||||
// 尝试创建默认团队和项目(如果用户还没有)
|
||||
try {
|
||||
const response = await limqRequest('team/create-default', 'POST');
|
||||
console.log('默认团队创建成功:', response);
|
||||
|
||||
// 创建默认团队后重新获取团队列表
|
||||
const { data: refreshedMemberships, error: refreshError } = await supabase
|
||||
.from('team_membership')
|
||||
.select('team_id')
|
||||
.eq('user_id', userId);
|
||||
|
||||
console.log(`刷新后的团队成员关系:`, refreshedMemberships ? `找到${refreshedMemberships.length}个` : '无数据');
|
||||
|
||||
if (refreshError) throw refreshError;
|
||||
|
||||
if (!refreshedMemberships || refreshedMemberships.length === 0) {
|
||||
if (isMounted) {
|
||||
console.log('创建默认团队后仍未找到团队,设置空团队列表');
|
||||
setTeams([]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const teamIds = refreshedMemberships.map(m => m.team_id);
|
||||
console.log('获取到团队IDs:', teamIds);
|
||||
|
||||
const { data: teamsData, error: teamsError } = await supabase
|
||||
.from('teams')
|
||||
.select('*')
|
||||
.in('id', teamIds)
|
||||
.is('deleted_at', null);
|
||||
|
||||
console.log(`团队数据查询结果:`, teamsData ? `找到${teamsData.length}个` : '无数据');
|
||||
|
||||
if (teamsError) throw teamsError;
|
||||
if (teamsError) throw teamsError;
|
||||
|
||||
if (isMounted && teamsData) {
|
||||
setTeams(teamsData);
|
||||
if (isMounted && teamsData) {
|
||||
setTeams(teamsData);
|
||||
}
|
||||
return;
|
||||
} catch (teamError) {
|
||||
console.error('创建默认团队失败:', teamError);
|
||||
// 创建失败也继续,返回空列表
|
||||
if (isMounted) setTeams([]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const teamIds = memberships.map(m => m.team_id);
|
||||
console.log('获取到团队IDs:', teamIds);
|
||||
|
||||
const { data: teamsData, error: teamsError } = await supabase
|
||||
.from('teams')
|
||||
.select('*')
|
||||
.in('id', teamIds)
|
||||
.is('deleted_at', null);
|
||||
|
||||
console.log(`团队数据查询结果:`, teamsData ? `找到${teamsData.length}个` : '无数据');
|
||||
|
||||
if (teamsError) throw teamsError;
|
||||
|
||||
if (isMounted && teamsData) {
|
||||
setTeams(teamsData);
|
||||
}
|
||||
} catch (dataError) {
|
||||
console.error('获取团队数据失败:', dataError);
|
||||
throw dataError;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取团队数据出错:', err);
|
||||
if (isMounted) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load teams');
|
||||
setError(err instanceof Error ? err.message : '获取团队数据失败');
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
@@ -114,27 +168,51 @@ export function TeamSelector({
|
||||
}
|
||||
};
|
||||
|
||||
const supabase = getSupabaseClient();
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent, session: Session | null) => {
|
||||
if (event === 'SIGNED_IN' && session?.user?.id) {
|
||||
fetchTeams(session.user.id);
|
||||
} else if (event === 'SIGNED_OUT') {
|
||||
setTeams([]);
|
||||
setError(null);
|
||||
}
|
||||
});
|
||||
// 获取Supabase客户端实例并订阅认证状态变化
|
||||
try {
|
||||
const supabase = getSupabaseClient();
|
||||
console.log('注册认证状态变化监听器');
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent, session: Session | null) => {
|
||||
console.log(`认证状态变化: ${event}`, session ? `用户ID: ${session.user.id}` : '无会话');
|
||||
if (event === 'SIGNED_IN' && session?.user?.id) {
|
||||
fetchTeams(session.user.id);
|
||||
} else if (event === 'SIGNED_OUT') {
|
||||
setTeams([]);
|
||||
setError(null);
|
||||
}
|
||||
});
|
||||
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
if (session?.user?.id) {
|
||||
fetchTeams(session.user.id);
|
||||
}
|
||||
});
|
||||
// 初始化时获取当前会话
|
||||
console.log('获取当前会话状态');
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
console.log('当前会话状态:', session ? `用户已登录,ID: ${session.user.id}` : '用户未登录');
|
||||
if (session?.user?.id) {
|
||||
fetchTeams(session.user.id);
|
||||
} else {
|
||||
// 如果没有会话但组件需要初始化,可以设置加载完成
|
||||
setLoading(false);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('获取会话状态失败:', err);
|
||||
// 确保即使获取会话失败也停止加载状态
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
return () => {
|
||||
console.log('TeamSelector组件卸载,清理订阅');
|
||||
isMounted = false;
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
} catch (initError) {
|
||||
console.error('初始化TeamSelector出错:', initError);
|
||||
// 确保在初始化出错时也停止加载状态
|
||||
setLoading(false);
|
||||
setError('初始化失败,请刷新页面重试');
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleToggle = () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import { ProtectedRoute } from '@/lib/auth';
|
||||
import ProtectedRoute from '@/app/components/ProtectedRoute';
|
||||
import { limqRequest } from '@/lib/api';
|
||||
import { TeamSelector } from '@/app/components/ui/TeamSelector';
|
||||
import { ProjectSelector } from '@/app/components/ui/ProjectSelector';
|
||||
@@ -47,7 +47,7 @@ function CreateShortUrlForm() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
// Use useEffect to add user information to form data on load
|
||||
// Use useEffect to add user information to form data when loading
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
console.log('Current user:', user.email);
|
||||
@@ -114,7 +114,7 @@ function CreateShortUrlForm() {
|
||||
throw new Error('Domain is required');
|
||||
}
|
||||
|
||||
// Construct request data according to API requirements
|
||||
// Build request data according to API requirements
|
||||
const requestData = {
|
||||
type: "shorturl",
|
||||
attributes: {
|
||||
@@ -137,7 +137,7 @@ function CreateShortUrlForm() {
|
||||
// Call API to create shorturl resource
|
||||
const response = await limqRequest('resource/shorturl', 'POST', requestData as unknown as Record<string, unknown>);
|
||||
|
||||
console.log('Created successfully:', response);
|
||||
console.log('Creation successful:', response);
|
||||
setSuccess(true);
|
||||
|
||||
// Redirect to links list page after 2 seconds
|
||||
@@ -207,7 +207,7 @@ function CreateShortUrlForm() {
|
||||
name="title"
|
||||
value={formData.title}
|
||||
onChange={handleChange}
|
||||
placeholder="e.g., Product Launch Campaign"
|
||||
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"
|
||||
required
|
||||
/>
|
||||
@@ -265,7 +265,7 @@ function CreateShortUrlForm() {
|
||||
name="domain"
|
||||
value={formData.domain}
|
||||
onChange={handleChange}
|
||||
placeholder="e.g., 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"
|
||||
required
|
||||
/>
|
||||
|
||||
168
app/debug/page.tsx
Normal file
168
app/debug/page.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import supabase from '@/lib/supabase';
|
||||
import { Session, User } from '@supabase/supabase-js';
|
||||
|
||||
interface SessionData {
|
||||
session: Session | null;
|
||||
user?: User | null;
|
||||
}
|
||||
|
||||
export default function DebugPage() {
|
||||
const { user, session, isLoading } = useAuth();
|
||||
const [cookies, setCookies] = useState<Record<string, string>>({});
|
||||
const [rawCookies, setRawCookies] = useState('');
|
||||
const [sessionData, setSessionData] = useState<SessionData | null>(null);
|
||||
const [redirectTarget, setRedirectTarget] = useState('/analytics');
|
||||
|
||||
useEffect(() => {
|
||||
// Get all cookies
|
||||
const allCookies = document.cookie.split(';').reduce((acc, cookie) => {
|
||||
const [key, value] = cookie.trim().split('=');
|
||||
if (key) acc[key] = value || '';
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
setCookies(allCookies);
|
||||
setRawCookies(document.cookie);
|
||||
|
||||
// Test Supabase session
|
||||
const testSession = async () => {
|
||||
try {
|
||||
console.log('Getting Supabase session');
|
||||
const { data, error } = await supabase.auth.getSession();
|
||||
console.log('Supabase session result:', { data, error });
|
||||
|
||||
if (error) {
|
||||
console.error('Session error:', error);
|
||||
} else {
|
||||
setSessionData(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error getting session:', err);
|
||||
}
|
||||
};
|
||||
|
||||
testSession();
|
||||
}, []);
|
||||
|
||||
const refreshSession = async () => {
|
||||
try {
|
||||
console.log('Manually refreshing session');
|
||||
const { data, error } = await supabase.auth.refreshSession();
|
||||
console.log('Refresh result:', { data, error });
|
||||
alert('Session refresh complete, please check console logs');
|
||||
|
||||
if (!error && data.session) {
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error refreshing session:', err);
|
||||
alert('Error refreshing session: ' + String(err));
|
||||
}
|
||||
};
|
||||
|
||||
const forceRedirect = () => {
|
||||
if (redirectTarget) {
|
||||
window.location.href = redirectTarget;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-8">
|
||||
<h1 className="text-3xl font-bold mb-6">Authentication Debug Page</h1>
|
||||
|
||||
<div className="bg-gray-100 p-6 rounded-lg mb-6">
|
||||
<h2 className="text-xl font-semibold mb-4">User Status</h2>
|
||||
<div className="space-y-2">
|
||||
<p>Loading status: {isLoading ? 'Loading...' : 'Loaded'}</p>
|
||||
<p>Logged in: {user ? 'Yes' : 'No'}</p>
|
||||
<p>User email: {user?.email || 'Not logged in'}</p>
|
||||
<p>User ID: {user?.id || 'Not logged in'}</p>
|
||||
<p>Session valid: {session ? 'Yes' : 'No'}</p>
|
||||
<p>Session expires: {session?.expires_at ? new Date(session.expires_at * 1000).toLocaleString() : 'No session'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-100 p-6 rounded-lg mb-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Supabase Session Data</h2>
|
||||
<pre className="bg-gray-200 p-4 rounded text-xs overflow-auto max-h-60">
|
||||
{sessionData ? JSON.stringify(sessionData, null, 2) : 'Loading...'}
|
||||
</pre>
|
||||
|
||||
<button
|
||||
onClick={refreshSession}
|
||||
className="mt-4 px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
|
||||
>
|
||||
Refresh Session
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-100 p-6 rounded-lg mb-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Cookie Information</h2>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm mb-2">Raw cookie string:</p>
|
||||
<pre className="bg-gray-200 p-4 rounded overflow-x-auto text-xs">
|
||||
{rawCookies || '(empty)'}
|
||||
</pre>
|
||||
|
||||
<p className="text-sm mt-4 mb-2">Parsed cookies:</p>
|
||||
<pre className="bg-gray-200 p-4 rounded overflow-x-auto text-xs">
|
||||
{JSON.stringify(cookies, null, 2) || '{}'}
|
||||
</pre>
|
||||
|
||||
<p className="text-sm mt-4 mb-2">Supabase-related cookies:</p>
|
||||
<div className="space-y-1">
|
||||
<p>sb-access-token: {cookies['sb-access-token'] ? 'Exists' : 'Not found'}</p>
|
||||
<p>sb-refresh-token: {cookies['sb-refresh-token'] ? 'Exists' : 'Not found'}</p>
|
||||
<p>supabase-auth-token: {cookies['supabase-auth-token'] ? 'Exists' : 'Not found'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-100 p-6 rounded-lg mb-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Manual Redirect</h2>
|
||||
<div className="flex space-x-2 items-center">
|
||||
<input
|
||||
type="text"
|
||||
value={redirectTarget}
|
||||
onChange={(e) => setRedirectTarget(e.target.value)}
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded"
|
||||
placeholder="/analytics"
|
||||
/>
|
||||
<button
|
||||
onClick={forceRedirect}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
Force Redirect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<button
|
||||
onClick={() => window.location.href = '/login'}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
Go to Login
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await supabase.auth.signOut();
|
||||
alert('Signed out, refreshing page...');
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
alert('Error signing out: ' + String(err));
|
||||
}
|
||||
}}
|
||||
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { Loader2, ExternalLink, Search } from 'lucide-react';
|
||||
import { TeamSelector } from '@/app/components/ui/TeamSelector';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useShortUrlStore, ShortUrlData } from '@/app/utils/store';
|
||||
import ProtectedRoute from '@/app/components/ProtectedRoute';
|
||||
|
||||
// Define attribute type to avoid using 'any'
|
||||
interface LinkAttributes {
|
||||
@@ -102,6 +103,17 @@ const convertClickHouseToShortLink = (data: Record<string, unknown>): ShortLink
|
||||
};
|
||||
|
||||
export default function LinksPage() {
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<div className="container p-6 mx-auto">
|
||||
<h1 className="mb-6 text-2xl font-bold">短链接管理</h1>
|
||||
<LinksPageContent />
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
|
||||
function LinksPageContent() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [links, setLinks] = useState<ShortLink[]>([]);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, Suspense } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
|
||||
@@ -20,20 +20,43 @@ function MessageHandler({ setMessage }: { setMessage: (message: { type: string,
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { signIn, signInWithGoogle, user } = useAuth();
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [message, setMessage] = useState({ type: '', content: '' });
|
||||
const [redirectUrl, setRedirectUrl] = useState<string | null>(null);
|
||||
|
||||
// 如果用户已登录,重定向到首页
|
||||
// Get redirect URL
|
||||
useEffect(() => {
|
||||
if (searchParams) {
|
||||
const redirect = searchParams.get('redirect');
|
||||
if (redirect) {
|
||||
setRedirectUrl(decodeURIComponent(redirect));
|
||||
}
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
// If user is logged in, redirect to original page or home page
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
router.push('/');
|
||||
console.log('User is logged in, preparing to redirect', { redirectUrl });
|
||||
|
||||
// Add a short delay to ensure state updates are complete
|
||||
setTimeout(() => {
|
||||
if (redirectUrl) {
|
||||
// Use hard redirect instead of router.push
|
||||
console.log('Redirecting to original URL:', redirectUrl);
|
||||
window.location.href = redirectUrl;
|
||||
} else {
|
||||
console.log('Redirecting to home page');
|
||||
window.location.href = '/';
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, [user, router]);
|
||||
}, [user, redirectUrl]);
|
||||
|
||||
const handleEmailSignIn = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -53,10 +76,10 @@ export default function LoginPage() {
|
||||
const { error } = await signIn(email, password);
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
throw new Error(error instanceof Error ? error.message : 'Unknown error');
|
||||
}
|
||||
|
||||
// 登录成功后,会通过 useEffect 重定向
|
||||
// After successful login, redirect via useEffect
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
setMessage({
|
||||
@@ -75,10 +98,10 @@ export default function LoginPage() {
|
||||
const { error } = await signInWithGoogle();
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
throw new Error(error instanceof Error ? error.message : 'Unknown error');
|
||||
}
|
||||
|
||||
// Google OAuth will handle the redirect
|
||||
// Google OAuth will redirect the user
|
||||
} catch (error) {
|
||||
console.error('Google login error:', error);
|
||||
setMessage({
|
||||
@@ -123,31 +146,7 @@ export default function LoginPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
<form onSubmit={handleEmailSignIn} className="mt-8 space-y-6">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
Email address
|
||||
@@ -190,11 +189,38 @@ export default function LoginPage() {
|
||||
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"
|
||||
>
|
||||
{isLoading ? 'Signing in...' : 'Sign in with Email'}
|
||||
{isLoading ? 'Signing in...' : 'Sign in'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-white text-gray-500">Or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button
|
||||
onClick={handleGoogleSignIn}
|
||||
disabled={isLoading}
|
||||
className="w-full flex items-center justify-center gap-3 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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
</svg>
|
||||
Sign in with Google
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm mt-6 text-gray-600">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/register" className="font-medium text-blue-600 hover:text-blue-500">
|
||||
|
||||
38
app/page.tsx
38
app/page.tsx
@@ -1,5 +1,39 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
|
||||
export default function Home() {
|
||||
redirect('/analytics');
|
||||
const { user, isLoading } = useAuth();
|
||||
|
||||
// Add debug logs
|
||||
console.log('Root page state:', { isLoading, userAuthenticated: !!user });
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
console.log('Preparing to redirect from root page', { isLoggedIn: !!user });
|
||||
|
||||
// Use hard redirect to ensure full page refresh
|
||||
if (user) {
|
||||
console.log('User is logged in, redirecting to analytics page');
|
||||
window.location.href = '/analytics';
|
||||
} else {
|
||||
console.log('User is not logged in, redirecting to login page');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
}, [isLoading, user]);
|
||||
|
||||
// Display loading indicator with status information
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500 mx-auto"></div>
|
||||
<p className="mt-4 text-lg text-gray-700">Loading...</p>
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
Status: {isLoading ? 'Checking login status' : (user ? 'Logged in' : 'Not logged in')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,44 +12,44 @@ export default function RegisterPage() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { signUp, signInWithGoogle } = useAuth();
|
||||
|
||||
// Handle registration form submission
|
||||
// 处理注册表单提交
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
// Validate passwords
|
||||
// 验证密码
|
||||
if (password !== confirmPassword) {
|
||||
setError('Passwords do not match');
|
||||
setError('两次输入的密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
// Password strength validation
|
||||
// 密码强度验证
|
||||
if (password.length < 6) {
|
||||
setError('Password must be at least 6 characters');
|
||||
setError('密码长度至少为6个字符');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await signUp(email, password);
|
||||
// After successful registration, redirect to login page with email verification prompt
|
||||
// 注册成功后会跳转到登录页面,提示用户验证邮箱
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
setError('Registration failed. Please try again later or use a different email');
|
||||
setError('注册失败,请稍后再试或使用其他邮箱');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle Google registration/login
|
||||
// 处理Google注册/登录
|
||||
const handleGoogleSignIn = async () => {
|
||||
setError(null);
|
||||
try {
|
||||
await signInWithGoogle();
|
||||
// Login flow will redirect to Google and then back to the application
|
||||
// 登录流程会重定向到Google,然后回到应用
|
||||
} catch (error) {
|
||||
console.error('Google sign in error:', error);
|
||||
setError('Google login failed. Please try again later');
|
||||
setError('Google登录失败,请稍后再试');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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="w-full max-w-md p-8 space-y-8 bg-white dark:bg-gray-800 rounded-lg shadow-md">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Register</h1>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">注册</h1>
|
||||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Create your account to access the analytics dashboard
|
||||
创建您的帐户以访问分析仪表板
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{/* 错误提示 */}
|
||||
{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">
|
||||
{error}
|
||||
@@ -74,7 +74,7 @@ export default function RegisterPage() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Email Address
|
||||
邮箱地址
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
@@ -90,7 +90,7 @@ export default function RegisterPage() {
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Password
|
||||
密码
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
@@ -106,7 +106,7 @@ export default function RegisterPage() {
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Confirm Password
|
||||
确认密码
|
||||
</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
@@ -128,7 +128,7 @@ export default function RegisterPage() {
|
||||
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"
|
||||
>
|
||||
{isLoading ? 'Registering...' : 'Register'}
|
||||
{isLoading ? '注册中...' : '注册'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -137,7 +137,7 @@ export default function RegisterPage() {
|
||||
<div className="w-full border-t border-gray-300 dark:border-gray-600"></div>
|
||||
</div>
|
||||
<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">or</span>
|
||||
<span className="px-2 bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400">或</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -173,19 +173,19 @@ export default function RegisterPage() {
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
Sign up with Google
|
||||
使用Google账号注册
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Already have an account?{' '}
|
||||
已有账号?{' '}
|
||||
<Link
|
||||
href="/login"
|
||||
className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
Log in
|
||||
登录
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,12 @@ import type { Database } from "@/types/supabase";
|
||||
|
||||
let supabase: SupabaseClient<Database> | null = null;
|
||||
|
||||
// 简单的存储适配器,使用localStorage
|
||||
// 增强的存储适配器,使用localStorage并添加更多错误处理
|
||||
const storageAdapter = {
|
||||
getItem: async (key: string) => {
|
||||
try {
|
||||
const item = localStorage.getItem(key);
|
||||
console.log(`Storage get for key [${key}]: ${item ? "found" : "not found"}`);
|
||||
return item;
|
||||
} catch (error) {
|
||||
console.error("Storage get error:", error);
|
||||
@@ -18,6 +19,7 @@ const storageAdapter = {
|
||||
setItem: async (key: string, value: string) => {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
console.log(`Storage set for key [${key}] successful`);
|
||||
} catch (error) {
|
||||
console.error("Storage set error:", error);
|
||||
}
|
||||
@@ -26,18 +28,42 @@ const storageAdapter = {
|
||||
removeItem: async (key: string) => {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
console.log(`Storage remove for key [${key}] successful`);
|
||||
} catch (error) {
|
||||
console.error("Storage remove error:", error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 添加一个函数来检查Supabase连接状态
|
||||
export const checkSupabaseConnection = async (): Promise<boolean> => {
|
||||
try {
|
||||
const client = getSupabaseClient();
|
||||
const { error } = await client.from('_health').select('*').limit(1);
|
||||
|
||||
if (error) {
|
||||
console.error('Supabase connection check failed:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Supabase connection check successful');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Supabase connection check exception:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getSupabaseClient = (): SupabaseClient<Database> => {
|
||||
if (!supabase) {
|
||||
if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
|
||||
console.error('Missing Supabase environment variables');
|
||||
throw new Error('Missing Supabase environment variables');
|
||||
}
|
||||
|
||||
console.log('Creating new Supabase client with URL:', process.env.NEXT_PUBLIC_SUPABASE_URL);
|
||||
|
||||
// 使用as断言来避免类型错误
|
||||
supabase = createClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
||||
@@ -47,13 +73,27 @@ export const getSupabaseClient = (): SupabaseClient<Database> => {
|
||||
storage: storageAdapter,
|
||||
persistSession: true,
|
||||
autoRefreshToken: true,
|
||||
detectSessionInUrl: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
) as SupabaseClient<Database>;
|
||||
|
||||
// 立即检查客户端创建后的会话状态
|
||||
void supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
console.log('Initial session check:', session ? 'Session exists' : 'No session');
|
||||
}).catch(err => {
|
||||
console.error('Error checking initial session:', err);
|
||||
});
|
||||
}
|
||||
|
||||
if (!supabase) {
|
||||
throw new Error('Failed to create Supabase client');
|
||||
}
|
||||
|
||||
return supabase;
|
||||
};
|
||||
|
||||
export const clearSupabaseInstance = () => {
|
||||
console.log('Clearing Supabase instance');
|
||||
supabase = null;
|
||||
};
|
||||
151
lib/api.ts
151
lib/api.ts
@@ -8,43 +8,132 @@ export interface ApiResponse<T = unknown> {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// Common function for authenticated API requests to LIMQ
|
||||
/**
|
||||
* 通用的LIMQ API请求函数,包含重试机制和错误处理
|
||||
*/
|
||||
export async function limqRequest<T = unknown>(
|
||||
endpoint: string,
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||||
data?: Record<string, unknown>
|
||||
data?: Record<string, unknown>,
|
||||
options?: {
|
||||
retryCount?: number;
|
||||
retryDelay?: number;
|
||||
timeout?: number;
|
||||
}
|
||||
): Promise<ApiResponse<T>> {
|
||||
// Get current session
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
// 默认配置
|
||||
const retryCount = options?.retryCount ?? 2; // 默认重试2次
|
||||
const retryDelay = options?.retryDelay ?? 1000; // 默认延迟1秒
|
||||
const timeout = options?.timeout ?? 10000; // 默认超时10秒
|
||||
|
||||
if (!session) {
|
||||
throw new Error('No active session. User must be authenticated.');
|
||||
let lastError: Error | null = null;
|
||||
let currentRetry = 0;
|
||||
|
||||
// 创建延迟函数
|
||||
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
// 重试循环
|
||||
while (currentRetry <= retryCount) {
|
||||
try {
|
||||
console.log(`[API] ${method} ${endpoint} 尝试 ${currentRetry + 1}/${retryCount + 1}`);
|
||||
|
||||
// 获取会话
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
// 检查会话是否存在
|
||||
if (!session) {
|
||||
console.error(`[API] 未找到活跃会话,用户需要登录`);
|
||||
|
||||
if (currentRetry < retryCount) {
|
||||
currentRetry++;
|
||||
console.log(`[API] 等待 ${retryDelay}ms 后重试获取会话...`);
|
||||
await delay(retryDelay);
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: '需要登录才能访问API'
|
||||
};
|
||||
}
|
||||
|
||||
// 获取API基础URL
|
||||
const baseUrl = process.env.NEXT_PUBLIC_LIMQ_API;
|
||||
if (!baseUrl) {
|
||||
throw new Error('API URL未配置');
|
||||
}
|
||||
|
||||
const url = `${baseUrl}${endpoint.startsWith('/') ? endpoint : '/' + endpoint}`;
|
||||
console.log(`[API] 请求URL: ${url}`);
|
||||
|
||||
// 构建请求选项
|
||||
const fetchOptions: RequestInit = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${session.access_token}`
|
||||
},
|
||||
mode: 'cors'
|
||||
};
|
||||
|
||||
if (data && (method === 'POST' || method === 'PUT')) {
|
||||
fetchOptions.body = JSON.stringify(data);
|
||||
console.log(`[API] 请求数据:`, data);
|
||||
}
|
||||
|
||||
// 添加超时控制
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
fetchOptions.signal = controller.signal;
|
||||
|
||||
// 发送请求
|
||||
const response = await fetch(url, fetchOptions);
|
||||
clearTimeout(timeoutId); // 清除超时控制
|
||||
|
||||
// 处理响应
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => null);
|
||||
console.error(`[API] 请求失败: ${response.status} ${response.statusText}`, errorData);
|
||||
|
||||
// 对于认证错误,尝试重试
|
||||
if ((response.status === 401 || response.status === 403) && currentRetry < retryCount) {
|
||||
currentRetry++;
|
||||
console.log(`[API] 认证错误,等待 ${retryDelay}ms 后重试...`);
|
||||
await delay(retryDelay);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(errorData?.error || `请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
// 成功响应
|
||||
const responseData = await response.json();
|
||||
console.log(`[API] ${method} ${endpoint} 成功`);
|
||||
return responseData;
|
||||
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
console.error(`[API] 请求出错:`, lastError);
|
||||
|
||||
// 对于超时和网络错误,尝试重试
|
||||
if (currentRetry < retryCount &&
|
||||
(error instanceof DOMException && error.name === 'AbortError' ||
|
||||
error instanceof TypeError && error.message.includes('network'))) {
|
||||
currentRetry++;
|
||||
console.log(`[API] 网络错误,等待 ${retryDelay}ms 后重试...`);
|
||||
await delay(retryDelay);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 已达到最大重试次数或不是网络错误
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_LIMQ_API;
|
||||
const url = `${baseUrl}${endpoint.startsWith('/') ? endpoint : '/' + endpoint}`;
|
||||
|
||||
const options: RequestInit = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${session.access_token}`
|
||||
},
|
||||
mode: 'cors'
|
||||
// 所有重试均失败
|
||||
console.error(`[API] ${method} ${endpoint} 失败,已重试 ${currentRetry} 次`);
|
||||
return {
|
||||
success: false,
|
||||
error: lastError?.message || '请求失败,请稍后重试'
|
||||
};
|
||||
|
||||
if (data && (method === 'POST' || method === 'PUT')) {
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
const response = await fetch(url, options);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => null);
|
||||
throw new Error(
|
||||
errorData?.error || `Request failed with status ${response.status}`
|
||||
);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
89
lib/auth.tsx
89
lib/auth.tsx
@@ -4,12 +4,11 @@ import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Session, User } from '@supabase/supabase-js';
|
||||
import supabase from './supabase';
|
||||
import { limqRequest } from './api';
|
||||
|
||||
// 定义用户类型
|
||||
// Define user type
|
||||
export type AuthUser = User | null;
|
||||
|
||||
// 定义验证上下文类型
|
||||
// Define auth context type
|
||||
export type AuthContextType = {
|
||||
user: AuthUser;
|
||||
session: Session | null;
|
||||
@@ -21,22 +20,22 @@ export type AuthContextType = {
|
||||
signOut: () => Promise<void>;
|
||||
};
|
||||
|
||||
// 创建验证上下文
|
||||
// Create auth context
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
// 验证提供者组件
|
||||
// Auth provider component
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [user, setUser] = useState<AuthUser>(null);
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const router = useRouter();
|
||||
|
||||
// 初始化验证状态
|
||||
// Initialize auth state
|
||||
useEffect(() => {
|
||||
const getSession = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// 尝试从Supabase获取会话
|
||||
// Try to get session from Supabase
|
||||
const { data: { session }, error } = await supabase.auth.getSession();
|
||||
|
||||
if (error) {
|
||||
@@ -44,6 +43,15 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
return;
|
||||
}
|
||||
|
||||
// Print session info for debugging
|
||||
console.log('Supabase session loaded:', session ? 'Found' : 'Not found');
|
||||
if (session) {
|
||||
console.log('User authenticated:', session.user.email);
|
||||
if (session.expires_at) {
|
||||
console.log('Session expires at:', new Date(session.expires_at * 1000).toLocaleString());
|
||||
}
|
||||
}
|
||||
|
||||
setSession(session);
|
||||
setUser(session?.user || null);
|
||||
} catch (error) {
|
||||
@@ -55,79 +63,94 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
getSession();
|
||||
|
||||
// 监听验证状态变化
|
||||
// Listen for auth state changes
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
console.log('Auth state changed, event:', _event);
|
||||
console.log('New session:', session ? 'Valid' : 'None');
|
||||
setSession(session);
|
||||
setUser(session?.user || null);
|
||||
});
|
||||
|
||||
// 清理函数
|
||||
// Cleanup function
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 登录函数
|
||||
// Sign in function
|
||||
const signIn = async (email: string, password: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
console.log('尝试登录:', { email });
|
||||
console.log('Attempting to sign in:', { email });
|
||||
|
||||
// 尝试通过Supabase登录
|
||||
// Try to sign in with Supabase
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('登录出错:', error);
|
||||
console.error('Sign in error:', error);
|
||||
return { error };
|
||||
}
|
||||
|
||||
// Sign in successful, set session and user info
|
||||
console.log('Sign in successful, user:', data.user?.email);
|
||||
setSession(data.session);
|
||||
setUser(data.user);
|
||||
|
||||
router.push('/analytics');
|
||||
// Use hard redirect instead of router.push to ensure full page refresh
|
||||
console.log('Preparing to redirect to analytics page');
|
||||
|
||||
// Add short delay to ensure state is updated, then redirect
|
||||
setTimeout(() => {
|
||||
window.location.href = '/analytics';
|
||||
}, 100);
|
||||
|
||||
return {};
|
||||
} catch (error) {
|
||||
console.error('登录过程出错:', error);
|
||||
console.error('Error during sign in process:', error);
|
||||
return { error };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Google登录函数
|
||||
// Google sign in function
|
||||
const signInWithGoogle = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// 尝试通过Supabase登录Google
|
||||
// Try to sign in with Google via Supabase
|
||||
const { error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'google',
|
||||
options: {
|
||||
redirectTo: `${window.location.origin}/auth/callback`,
|
||||
queryParams: {
|
||||
access_type: 'offline',
|
||||
prompt: 'consent',
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Google登录出错:', error);
|
||||
console.error('Google sign in error:', error);
|
||||
return { error };
|
||||
}
|
||||
|
||||
return {}; // Return empty object when successful
|
||||
} catch (error) {
|
||||
console.error('Google登录过程出错:', error);
|
||||
console.error('Error during Google sign in process:', error);
|
||||
return { error };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// GitHub登录函数
|
||||
// GitHub sign in function
|
||||
const signInWithGitHub = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// 尝试通过Supabase登录GitHub
|
||||
// Try to sign in with GitHub via Supabase
|
||||
const { error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'github',
|
||||
options: {
|
||||
@@ -149,11 +172,11 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
}
|
||||
};
|
||||
|
||||
// 注册函数
|
||||
// Sign up function
|
||||
const signUp = async (email: string, password: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// 尝试通过Supabase注册
|
||||
// Try to sign up via Supabase
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
@@ -163,35 +186,35 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('注册出错:', error);
|
||||
console.error('Sign up error:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 注册成功后跳转到登录页面并显示确认消息
|
||||
// After successful registration, redirect to login page with confirmation message
|
||||
router.push('/login?message=Registration successful! Please check your email to verify your account before logging in.');
|
||||
} catch (error) {
|
||||
console.error('注册过程出错:', error);
|
||||
console.error('Error during sign up process:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 登出函数
|
||||
// Sign out function
|
||||
const signOut = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// 尝试通过Supabase登出
|
||||
// Try to sign out via Supabase
|
||||
const { error } = await supabase.auth.signOut();
|
||||
if (error) {
|
||||
console.error('登出出错:', error);
|
||||
console.error('Sign out error:', error);
|
||||
throw error;
|
||||
}
|
||||
setSession(null);
|
||||
setUser(null);
|
||||
router.push('/login');
|
||||
} catch (error) {
|
||||
console.error('登出过程出错:', error);
|
||||
console.error('Error during sign out process:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -216,7 +239,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
);
|
||||
};
|
||||
|
||||
// 自定义钩子
|
||||
// Custom hook
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
@@ -225,7 +248,7 @@ export const useAuth = () => {
|
||||
return context;
|
||||
};
|
||||
|
||||
// 受保护路由组件
|
||||
// Protected route component
|
||||
export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { user, isLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
@@ -241,7 +264,7 @@ export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500 mx-auto"></div>
|
||||
<p className="mt-4 text-lg text-gray-700 dark:text-gray-300">加载中...</p>
|
||||
<p className="mt-4 text-lg text-gray-700 dark:text-gray-300">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
// 从环境变量获取Supabase配置
|
||||
// Get Supabase configuration from environment variables
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || '';
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY || '';
|
||||
|
||||
@@ -8,7 +8,7 @@ console.log('Supabase Configuration Check:', {
|
||||
urlDefined: !!supabaseUrl,
|
||||
keyDefined: !!supabaseAnonKey,
|
||||
url: supabaseUrl,
|
||||
// 打印部分key以便调试
|
||||
// Print partial key for debugging
|
||||
keyPrefix: supabaseAnonKey ? supabaseAnonKey.substring(0, 20) + '...' : 'undefined',
|
||||
keyLength: supabaseAnonKey ? supabaseAnonKey.length : 0
|
||||
});
|
||||
@@ -17,7 +17,7 @@ if (!supabaseUrl || !supabaseAnonKey) {
|
||||
console.error('Supabase URL and Anon Key are required');
|
||||
}
|
||||
|
||||
// 尝试解码JWT token并打印解码内容
|
||||
// Try to decode JWT token and print decoded content
|
||||
try {
|
||||
if (supabaseAnonKey) {
|
||||
const parts = supabaseAnonKey.split('.');
|
||||
@@ -30,19 +30,54 @@ try {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('JWT解码失败:', error);
|
||||
console.error('JWT decoding failed:', error);
|
||||
}
|
||||
|
||||
// 创建Supabase客户端
|
||||
// Create custom cookie handling logic
|
||||
const customStorage = {
|
||||
getItem: (key: string): string | null => {
|
||||
if (typeof document === 'undefined') return null;
|
||||
const cookie = document.cookie
|
||||
.split(';')
|
||||
.find((c) => c.trim().startsWith(`${key}=`));
|
||||
return cookie ? cookie.split('=')[1] : null;
|
||||
},
|
||||
setItem: (key: string, value: string): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
// Get current host and port to handle different ports on localhost
|
||||
const host = typeof window !== 'undefined' ? window.location.hostname : '';
|
||||
|
||||
// Set cookie, using generic domain for localhost
|
||||
document.cookie = `${key}=${value}; path=/; max-age=${60 * 60 * 24 * 7}; samesite=lax; domain=${host}`;
|
||||
|
||||
console.log(`Cookie ${key} has been set, domain=${host}`);
|
||||
},
|
||||
removeItem: (key: string): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
// Get current host and port to handle different ports on localhost
|
||||
const host = typeof window !== 'undefined' ? window.location.hostname : '';
|
||||
|
||||
// Remove cookie, using generic domain for localhost
|
||||
document.cookie = `${key}=; path=/; max-age=0; samesite=lax; domain=${host}`;
|
||||
|
||||
console.log(`Cookie ${key} has been removed`);
|
||||
},
|
||||
};
|
||||
|
||||
// Create Supabase client
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
persistSession: true,
|
||||
autoRefreshToken: true,
|
||||
detectSessionInUrl: true,
|
||||
storageKey: 'sb-auth-token',
|
||||
storage: customStorage
|
||||
}
|
||||
});
|
||||
|
||||
// 测试Supabase连接
|
||||
// Test Supabase connection
|
||||
supabase.auth.onAuthStateChange((event, session) => {
|
||||
console.log(`Supabase auth event: ${event}`, session ? 'Session exists' : 'No session');
|
||||
if (session) {
|
||||
@@ -50,7 +85,7 @@ supabase.auth.onAuthStateChange((event, session) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 尝试执行健康检查
|
||||
// Try to perform health check
|
||||
async function checkSupabaseHealth() {
|
||||
try {
|
||||
const { data, error } = await supabase.from('_health').select('*').limit(1);
|
||||
|
||||
@@ -1,22 +1,80 @@
|
||||
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();
|
||||
export function middleware(request: NextRequest) {
|
||||
// Get the request path
|
||||
const path = request.nextUrl.pathname;
|
||||
console.log(`[Middleware] Request path: ${path}`);
|
||||
|
||||
// Create a Supabase client configured to use cookies
|
||||
const supabase = createMiddlewareClient({ req, res });
|
||||
// Define paths that don't require authentication
|
||||
const publicPaths = ['/login', '/register', '/auth/callback'];
|
||||
|
||||
// Refresh session if expired - required for Server Components
|
||||
await supabase.auth.getSession();
|
||||
// API routes don't require authentication
|
||||
if (path.startsWith('/api/')) {
|
||||
console.log('[Middleware] API route, skipping validation');
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// Static resources don't require authentication
|
||||
if (path.includes('/_next/') || path.includes('/static/') || path.match(/\.(ico|png|jpg|jpeg|svg|css|js)$/)) {
|
||||
console.log('[Middleware] Static resource, skipping validation');
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
return res;
|
||||
// Check if it's a public path
|
||||
const isPublicPath = publicPaths.some(publicPath => path === publicPath || path.startsWith(publicPath));
|
||||
console.log(`[Middleware] Is public path: ${isPublicPath}`);
|
||||
|
||||
// Get all cookies
|
||||
const allCookies = Object.fromEntries(request.cookies.getAll().map(c => [c.name, c.value]));
|
||||
console.log('[Middleware] All cookies:', JSON.stringify(allCookies));
|
||||
|
||||
// Check each authentication cookie
|
||||
const accessToken = request.cookies.get('sb-access-token');
|
||||
const refreshToken = request.cookies.get('sb-refresh-token');
|
||||
const providerToken = request.cookies.get('sb-provider-token');
|
||||
const authToken = request.cookies.get('supabase-auth-token');
|
||||
const customAuthToken = request.cookies.get('sb-auth-token');
|
||||
|
||||
console.log('[Middleware] Auth cookie details:', {
|
||||
'sb-access-token': accessToken ? 'exists' : 'not found',
|
||||
'sb-refresh-token': refreshToken ? 'exists' : 'not found',
|
||||
'sb-provider-token': providerToken ? 'exists' : 'not found',
|
||||
'supabase-auth-token': authToken ? 'exists' : 'not found',
|
||||
'sb-auth-token': customAuthToken ? 'exists' : 'not found'
|
||||
});
|
||||
|
||||
// Check if user is logged in
|
||||
const isLoggedIn = !!(accessToken || refreshToken || providerToken || authToken || customAuthToken);
|
||||
console.log(`[Middleware] User is logged in: ${isLoggedIn}`);
|
||||
|
||||
// If it's a public path but user is logged in, redirect to home page
|
||||
if (isPublicPath && isLoggedIn) {
|
||||
console.log('[Middleware] User is logged in and accessing public path, redirecting to home page');
|
||||
return NextResponse.redirect(new URL('/', request.url));
|
||||
}
|
||||
|
||||
// If it's not a public path and user is not logged in, redirect to login page
|
||||
if (!isPublicPath && !isLoggedIn) {
|
||||
console.log('[Middleware] User is not logged in and accessing private path, redirecting to login page');
|
||||
const redirectUrl = new URL('/login', request.url);
|
||||
redirectUrl.searchParams.set('redirect', encodeURIComponent(request.url));
|
||||
return NextResponse.redirect(redirectUrl);
|
||||
}
|
||||
|
||||
console.log('[Middleware] Validation passed, allowing access');
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// Specify the paths where this middleware should run
|
||||
// Configure middleware matching paths
|
||||
export const config = {
|
||||
matcher: [
|
||||
// Match all paths, but exclude static resources
|
||||
'/((?!_next/static|_next/image|favicon.ico).*)',
|
||||
// Explicitly include important routes
|
||||
'/',
|
||||
'/analytics',
|
||||
'/links',
|
||||
'/create-shorturl',
|
||||
],
|
||||
};
|
||||
@@ -6,8 +6,13 @@ const nextConfig: NextConfig = {
|
||||
|
||||
// 配置实验性选项
|
||||
experimental: {
|
||||
// 禁用外部目录处理,避免monorepo问题
|
||||
// externalDir: true,
|
||||
// 启用边缘函数中间件
|
||||
instrumentationHook: true,
|
||||
// 配置中间件匹配
|
||||
middleware: {
|
||||
// 确保匹配所有路径
|
||||
matchAll: '/((?!_next|static|api|public).*)',
|
||||
},
|
||||
},
|
||||
|
||||
// 禁用严格模式,避免开发时重复渲染
|
||||
|
||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -237,67 +237,79 @@ packages:
|
||||
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.0.5':
|
||||
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.0.4':
|
||||
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.0.4':
|
||||
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
|
||||
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
|
||||
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.33.5':
|
||||
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.33.5':
|
||||
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.33.5':
|
||||
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.33.5':
|
||||
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.33.5':
|
||||
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.33.5':
|
||||
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.33.5':
|
||||
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
|
||||
@@ -366,24 +378,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.2.3':
|
||||
resolution: {integrity: sha512-2gAPA7P652D3HzR4cLyAuVYwYqjG0mt/3pHSWTCyKZq/N/dJcUAEoNQMyUmwTZWCJRKofB+JPuDVP2aD8w2J6Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.2.3':
|
||||
resolution: {integrity: sha512-ODSKvrdMgAJOVU4qElflYy1KSZRM3M45JVbeZu42TINCMG3anp7YCBn80RkISV6bhzKwcUqLBAmOiWkaGtBA9w==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-musl@15.2.3':
|
||||
resolution: {integrity: sha512-ZR9kLwCWrlYxwEoytqPi1jhPd1TlsSJWAc+H/CJHmHkf2nD92MQpSRIURR1iNgA/kuFSdxB8xIPt4p/T78kwsg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.2.3':
|
||||
resolution: {integrity: sha512-+G2FrDcfm2YDbhDiObDU/qPriWeiz/9cRR0yMWJeTLGGX6/x8oryO3tt7HhodA1vZ8r2ddJPCjtLcpaVl7TE2Q==}
|
||||
@@ -1164,24 +1180,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.0.15':
|
||||
resolution: {integrity: sha512-342GVnhH/6PkVgKtEzvNVuQ4D+Q7B7qplvuH20Cfz9qEtydG6IQczTZ5IT4JPlh931MG1NUCVxg+CIorr1WJyw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.0.15':
|
||||
resolution: {integrity: sha512-g76GxlKH124RuGqacCEFc2nbzRl7bBrlC8qDQMiUABkiifDRHOIUjgKbLNG4RuR9hQAD/MKsqZ7A8L08zsoBrw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.0.15':
|
||||
resolution: {integrity: sha512-Gg/Y1XrKEvKpq6WeNt2h8rMIKOBj/W3mNa5NMvkQgMC7iO0+UNLrYmt6zgZufht66HozNpn+tJMbbkZ5a3LczA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-win32-arm64-msvc@4.0.15':
|
||||
resolution: {integrity: sha512-7QtSSJwYZ7ZK1phVgcNZpuf7c7gaCj8Wb0xjliligT5qCGCp79OV2n3SJummVZdw4fbTNKUOYMO7m1GinppZyA==}
|
||||
@@ -1350,21 +1370,25 @@ packages:
|
||||
resolution: {integrity: sha512-fp4Azi8kHz6TX8SFmKfyScZrMLfp++uRm2srpqRjsRZIIBzH74NtSkdEUHImR4G7f7XJ+sVZjCc6KDDK04YEpQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/rspack-resolver-binding-linux-arm64-musl@1.2.2':
|
||||
resolution: {integrity: sha512-gMiG3DCFioJxdGBzhlL86KcFgt9HGz0iDhw0YVYPsShItpN5pqIkNrI+L/Q/0gfDiGrfcE0X3VANSYIPmqEAlQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/rspack-resolver-binding-linux-x64-gnu@1.2.2':
|
||||
resolution: {integrity: sha512-n/4n2CxaUF9tcaJxEaZm+lqvaw2gflfWQ1R9I7WQgYkKEKbRKbpG/R3hopYdUmLSRI4xaW1Cy0Bz40eS2Yi4Sw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/rspack-resolver-binding-linux-x64-musl@1.2.2':
|
||||
resolution: {integrity: sha512-cHyhAr6rlYYbon1L2Ag449YCj3p6XMfcYTP0AQX+KkQo025d1y/VFtPWvjMhuEsE2lLvtHm7GdJozj6BOMtzVg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/rspack-resolver-binding-wasm32-wasi@1.2.2':
|
||||
resolution: {integrity: sha512-eogDKuICghDLGc32FtP+WniG38IB1RcGOGz0G3z8406dUdjJvxfHGuGs/dSlM9YEp/v0lEqhJ4mBu6X2nL9pog==}
|
||||
@@ -2301,24 +2325,28 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.29.2:
|
||||
resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.29.2:
|
||||
resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.29.2:
|
||||
resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.29.2:
|
||||
resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==}
|
||||
|
||||
@@ -125,7 +125,7 @@ export async function main(
|
||||
user: pgConfig.user,
|
||||
password: pgConfig.password,
|
||||
database: pgConfig.dbname || 'postgres'
|
||||
}, 3);
|
||||
}, 1);
|
||||
|
||||
console.log("PostgreSQL连接池创建完成,尝试连接...");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user