Compare commits

5 Commits

24 changed files with 1212 additions and 366 deletions

View File

@@ -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 配置

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View 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: 服务器内部错误

View File

@@ -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'
}
});
}

View File

@@ -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)
);
}
}

View 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}</>;
}

View File

@@ -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 = () => {

View File

@@ -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) {

View File

@@ -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 = () => {

View File

@@ -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
View 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>
);
}

View File

@@ -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[]>([]);

View File

@@ -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&apos;t have an account?{' '}
<Link href="/register" className="font-medium text-blue-600 hover:text-blue-500">

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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;
};

View File

@@ -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();
}

View File

@@ -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>
);

View File

@@ -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);

View File

@@ -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',
],
};

View File

@@ -6,8 +6,13 @@ const nextConfig: NextConfig = {
// 配置实验性选项
experimental: {
// 禁用外部目录处理避免monorepo问题
// externalDir: true,
// 启用边缘函数中间件
instrumentationHook: true,
// 配置中间件匹配
middleware: {
// 确保匹配所有路径
matchAll: '/((?!_next|static|api|public).*)',
},
},
// 禁用严格模式,避免开发时重复渲染

28
pnpm-lock.yaml generated
View File

@@ -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==}

View File

@@ -125,7 +125,7 @@ export async function main(
user: pgConfig.user,
password: pgConfig.password,
database: pgConfig.dbname || 'postgres'
}, 3);
}, 1);
console.log("PostgreSQL连接池创建完成尝试连接...");