Enhance authentication flow by implementing ProtectedRoute component across various pages, ensuring users are redirected based on their authentication status. Update login page to support Google sign-in and handle redirect URLs after login. Modify analytics and links pages to include loading indicators and protected access. Update next.config.ts to enable middleware for edge functions.
This commit is contained in:
@@ -1,65 +1,71 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import ProtectedRoute from '@/app/components/ProtectedRoute';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<ProtectedRoute>
|
||||||
<div className="text-center">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-8">
|
<div className="text-center">
|
||||||
Welcome to ShortURL Analytics
|
<h1 className="text-4xl font-bold text-gray-900 mb-8">
|
||||||
</h1>
|
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>
|
||||||
|
</ProtectedRoute>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ import { ProjectSelector } from '@/app/components/ui/ProjectSelector';
|
|||||||
import { TagSelector } from '@/app/components/ui/TagSelector';
|
import { TagSelector } from '@/app/components/ui/TagSelector';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { useShortUrlStore } from '@/app/utils/store';
|
import { useShortUrlStore } from '@/app/utils/store';
|
||||||
|
import ProtectedRoute from '@/app/components/ProtectedRoute';
|
||||||
|
|
||||||
// 事件类型定义
|
// 事件类型定义
|
||||||
interface Event {
|
interface Event {
|
||||||
@@ -1109,12 +1110,14 @@ function AnalyticsContent() {
|
|||||||
// Main page component with Suspense
|
// Main page component with Suspense
|
||||||
export default function AnalyticsPage() {
|
export default function AnalyticsPage() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={
|
<ProtectedRoute>
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<Suspense fallback={
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500" />
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
</div>
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500" />
|
||||||
}>
|
</div>
|
||||||
<AnalyticsContent />
|
}>
|
||||||
</Suspense>
|
<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`;
|
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 CSV response
|
||||||
return new NextResponse(csvContent, {
|
return new NextResponse(csvContent, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/csv',
|
'Content-Type': 'text/plain'
|
||||||
'Content-Disposition': `attachment; filename="${filename}"`
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
32
app/auth/callback/route.ts
Normal file
32
app/auth/callback/route.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 如果没有code参数,则重定向到登录页面
|
||||||
|
if (!code) {
|
||||||
|
return NextResponse.redirect(new URL('/login', request.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 创建supabase客户端
|
||||||
|
const cookieStore = cookies();
|
||||||
|
const supabaseRouteHandler = createRouteHandlerClient({ cookies: () => cookieStore });
|
||||||
|
|
||||||
|
// 交换code获取会话
|
||||||
|
await supabaseRouteHandler.auth.exchangeCodeForSession(code);
|
||||||
|
|
||||||
|
// 直接重定向到首页,避免中间跳转
|
||||||
|
return NextResponse.redirect(new URL('/', request.url));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auth callback error:', 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}</>;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useAuth } from '@/lib/auth';
|
import { useAuth } from '@/lib/auth';
|
||||||
import { ProtectedRoute } from '@/lib/auth';
|
import ProtectedRoute from '@/app/components/ProtectedRoute';
|
||||||
import { limqRequest } from '@/lib/api';
|
import { limqRequest } from '@/lib/api';
|
||||||
import { TeamSelector } from '@/app/components/ui/TeamSelector';
|
import { TeamSelector } from '@/app/components/ui/TeamSelector';
|
||||||
import { ProjectSelector } from '@/app/components/ui/ProjectSelector';
|
import { ProjectSelector } from '@/app/components/ui/ProjectSelector';
|
||||||
|
|||||||
89
app/debug/page.tsx
Normal file
89
app/debug/page.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useAuth } from '@/lib/auth';
|
||||||
|
import supabase from '@/lib/supabase';
|
||||||
|
|
||||||
|
export default function DebugPage() {
|
||||||
|
const { user, session, isLoading } = useAuth();
|
||||||
|
const [cookies, setCookies] = useState<Record<string, string>>({});
|
||||||
|
const [rawCookies, setRawCookies] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 获取所有cookie
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 测试supabase会话
|
||||||
|
const testSession = async () => {
|
||||||
|
const { data, error } = await supabase.auth.getSession();
|
||||||
|
console.log('Debug page - Supabase session:', data);
|
||||||
|
if (error) console.error('Debug page - Session error:', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
testSession();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-8">
|
||||||
|
<h1 className="text-3xl font-bold mb-6">认证调试页面</h1>
|
||||||
|
|
||||||
|
<div className="bg-gray-100 p-6 rounded-lg mb-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">用户状态</h2>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>加载状态: {isLoading ? '加载中...' : '已加载'}</p>
|
||||||
|
<p>已登录: {user ? '是' : '否'}</p>
|
||||||
|
<p>用户邮箱: {user?.email || '未登录'}</p>
|
||||||
|
<p>用户ID: {user?.id || '未登录'}</p>
|
||||||
|
<p>会话有效: {session ? '是' : '否'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-100 p-6 rounded-lg mb-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Cookies 信息</h2>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm mb-2">原始Cookie字符串:</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">解析后的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相关Cookies:</p>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p>sb-access-token: {cookies['sb-access-token'] ? '存在' : '不存在'}</p>
|
||||||
|
<p>sb-refresh-token: {cookies['sb-refresh-token'] ? '存在' : '不存在'}</p>
|
||||||
|
<p>supabase-auth-token: {cookies['supabase-auth-token'] ? '存在' : '不存在'}</p>
|
||||||
|
</div>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
去登录页
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
await supabase.auth.signOut();
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
|
||||||
|
>
|
||||||
|
登出
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { Loader2, ExternalLink, Search } from 'lucide-react';
|
|||||||
import { TeamSelector } from '@/app/components/ui/TeamSelector';
|
import { TeamSelector } from '@/app/components/ui/TeamSelector';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useShortUrlStore, ShortUrlData } from '@/app/utils/store';
|
import { useShortUrlStore, ShortUrlData } from '@/app/utils/store';
|
||||||
|
import ProtectedRoute from '@/app/components/ProtectedRoute';
|
||||||
|
|
||||||
// Define attribute type to avoid using 'any'
|
// Define attribute type to avoid using 'any'
|
||||||
interface LinkAttributes {
|
interface LinkAttributes {
|
||||||
@@ -102,6 +103,17 @@ const convertClickHouseToShortLink = (data: Record<string, unknown>): ShortLink
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function LinksPage() {
|
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 [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [links, setLinks] = useState<ShortLink[]>([]);
|
const [links, setLinks] = useState<ShortLink[]>([]);
|
||||||
|
|||||||
@@ -21,19 +21,35 @@ function MessageHandler({ setMessage }: { setMessage: (message: { type: string,
|
|||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { signIn, user } = useAuth();
|
const searchParams = useSearchParams();
|
||||||
|
const { signIn, signInWithGoogle, user } = useAuth();
|
||||||
|
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [message, setMessage] = useState({ type: '', content: '' });
|
const [message, setMessage] = useState({ type: '', content: '' });
|
||||||
|
const [redirectUrl, setRedirectUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
// 如果用户已登录,重定向到首页
|
// 获取重定向URL
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchParams) {
|
||||||
|
const redirect = searchParams.get('redirect');
|
||||||
|
if (redirect) {
|
||||||
|
setRedirectUrl(decodeURIComponent(redirect));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
// 如果用户已登录,重定向到原始页面或首页
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
router.push('/');
|
if (redirectUrl) {
|
||||||
|
router.push(redirectUrl);
|
||||||
|
} else {
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [user, router]);
|
}, [user, router, redirectUrl]);
|
||||||
|
|
||||||
const handleEmailSignIn = async (e: React.FormEvent) => {
|
const handleEmailSignIn = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -53,7 +69,7 @@ export default function LoginPage() {
|
|||||||
const { error } = await signIn(email, password);
|
const { error } = await signIn(email, password);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw new Error(error.message);
|
throw new Error(error instanceof Error ? error.message : 'Unknown error');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录成功后,会通过 useEffect 重定向
|
// 登录成功后,会通过 useEffect 重定向
|
||||||
@@ -67,6 +83,28 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleGoogleSignIn = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setMessage({ type: '', content: '' });
|
||||||
|
|
||||||
|
const { error } = await signInWithGoogle();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error instanceof Error ? error.message : 'Unknown error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google OAuth will redirect the user
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Google login error:', error);
|
||||||
|
setMessage({
|
||||||
|
type: 'error',
|
||||||
|
content: error instanceof Error ? error.message : 'Failed to sign in with Google'
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen bg-gray-100">
|
<div className="flex items-center justify-center min-h-screen bg-gray-100">
|
||||||
{/* Wrap the component using useSearchParams in Suspense */}
|
{/* Wrap the component using useSearchParams in Suspense */}
|
||||||
@@ -149,6 +187,33 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</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">
|
<p className="text-sm mt-6 text-gray-600">
|
||||||
Don't have an account?{' '}
|
Don't have an account?{' '}
|
||||||
<Link href="/register" className="font-medium text-blue-600 hover:text-blue-500">
|
<Link href="/register" className="font-medium text-blue-600 hover:text-blue-500">
|
||||||
|
|||||||
31
app/page.tsx
31
app/page.tsx
@@ -1,5 +1,32 @@
|
|||||||
import { redirect } from 'next/navigation';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useAuth } from '@/lib/auth';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
redirect('/analytics');
|
const router = useRouter();
|
||||||
|
const { user, isLoading } = useAuth();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading) {
|
||||||
|
if (user) {
|
||||||
|
// 已登录用户重定向到分析页面
|
||||||
|
router.push('/analytics');
|
||||||
|
} else {
|
||||||
|
// 未登录用户重定向到登录页面
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [user, isLoading, router]);
|
||||||
|
|
||||||
|
// 显示加载指示器
|
||||||
|
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">正在加载...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
16
lib/auth.tsx
16
lib/auth.tsx
@@ -4,7 +4,6 @@ import React, { createContext, useContext, useEffect, useState } from 'react';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { Session, User } from '@supabase/supabase-js';
|
import { Session, User } from '@supabase/supabase-js';
|
||||||
import supabase from './supabase';
|
import supabase from './supabase';
|
||||||
import { limqRequest } from './api';
|
|
||||||
|
|
||||||
// 定义用户类型
|
// 定义用户类型
|
||||||
export type AuthUser = User | null;
|
export type AuthUser = User | null;
|
||||||
@@ -44,6 +43,15 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打印会话信息,帮助调试
|
||||||
|
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);
|
setSession(session);
|
||||||
setUser(session?.user || null);
|
setUser(session?.user || null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -57,6 +65,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
|
|
||||||
// 监听验证状态变化
|
// 监听验证状态变化
|
||||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||||
|
console.log('Auth state changed, event:', _event);
|
||||||
|
console.log('New session:', session ? 'Valid' : 'None');
|
||||||
setSession(session);
|
setSession(session);
|
||||||
setUser(session?.user || null);
|
setUser(session?.user || null);
|
||||||
});
|
});
|
||||||
@@ -106,6 +116,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
provider: 'google',
|
provider: 'google',
|
||||||
options: {
|
options: {
|
||||||
redirectTo: `${window.location.origin}/auth/callback`,
|
redirectTo: `${window.location.origin}/auth/callback`,
|
||||||
|
queryParams: {
|
||||||
|
access_type: 'offline',
|
||||||
|
prompt: 'consent',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
69
middleware.ts
Normal file
69
middleware.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
export function middleware(request: NextRequest) {
|
||||||
|
// 获取请求的路径
|
||||||
|
const path = request.nextUrl.pathname;
|
||||||
|
console.log(`[Middleware] 请求路径: ${path}`);
|
||||||
|
|
||||||
|
// 定义不需要验证的路径
|
||||||
|
const publicPaths = ['/login', '/register', '/auth/callback'];
|
||||||
|
|
||||||
|
// API 路由不需要验证
|
||||||
|
if (path.startsWith('/api/')) {
|
||||||
|
console.log('[Middleware] API路由,跳过验证');
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 静态资源不需要验证
|
||||||
|
if (path.includes('/_next/') || path.includes('/static/') || path.match(/\.(ico|png|jpg|jpeg|svg|css|js)$/)) {
|
||||||
|
console.log('[Middleware] 静态资源,跳过验证');
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是公开路径
|
||||||
|
const isPublicPath = publicPaths.some(publicPath => path === publicPath || path.startsWith(publicPath));
|
||||||
|
console.log(`[Middleware] 是公开路径: ${isPublicPath}`);
|
||||||
|
|
||||||
|
// 获取所有 cookie
|
||||||
|
const allCookies = Object.fromEntries(request.cookies.getAll().map(c => [c.name, c.value]));
|
||||||
|
console.log('[Middleware] 所有Cookie:', JSON.stringify(allCookies));
|
||||||
|
|
||||||
|
// 检查用户是否登录
|
||||||
|
const supabaseCookie = request.cookies.get('sb-access-token') ||
|
||||||
|
request.cookies.get('sb-refresh-token') ||
|
||||||
|
request.cookies.get('sb-provider-token') ||
|
||||||
|
request.cookies.get('supabase-auth-token');
|
||||||
|
const isLoggedIn = !!supabaseCookie;
|
||||||
|
console.log(`[Middleware] 用户是否登录: ${isLoggedIn}`);
|
||||||
|
|
||||||
|
// 如果是公开路径但已登录,重定向到首页
|
||||||
|
if (isPublicPath && isLoggedIn) {
|
||||||
|
console.log('[Middleware] 已登录用户访问公开路径,重定向到首页');
|
||||||
|
return NextResponse.redirect(new URL('/', request.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不是公开路径且未登录,重定向到登录页
|
||||||
|
if (!isPublicPath && !isLoggedIn) {
|
||||||
|
console.log('[Middleware] 未登录用户访问私有路径,重定向到登录页');
|
||||||
|
const redirectUrl = new URL('/login', request.url);
|
||||||
|
redirectUrl.searchParams.set('redirect', encodeURIComponent(request.url));
|
||||||
|
return NextResponse.redirect(redirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Middleware] 通过验证,允许访问');
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置中间件匹配的路径
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
// 匹配所有路径,但排除静态资源
|
||||||
|
'/((?!_next/static|_next/image|favicon.ico).*)',
|
||||||
|
// 明确包括重要的路由
|
||||||
|
'/',
|
||||||
|
'/analytics',
|
||||||
|
'/links',
|
||||||
|
'/create-shorturl',
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -6,8 +6,13 @@ const nextConfig: NextConfig = {
|
|||||||
|
|
||||||
// 配置实验性选项
|
// 配置实验性选项
|
||||||
experimental: {
|
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==}
|
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-arm@1.0.5':
|
'@img/sharp-libvips-linux-arm@1.0.5':
|
||||||
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
|
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-s390x@1.0.4':
|
'@img/sharp-libvips-linux-s390x@1.0.4':
|
||||||
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
|
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-x64@1.0.4':
|
'@img/sharp-libvips-linux-x64@1.0.4':
|
||||||
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
|
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
|
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
|
||||||
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
|
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
|
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
|
||||||
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
|
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-linux-arm64@0.33.5':
|
'@img/sharp-linux-arm64@0.33.5':
|
||||||
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
|
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-arm@0.33.5':
|
'@img/sharp-linux-arm@0.33.5':
|
||||||
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
|
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-s390x@0.33.5':
|
'@img/sharp-linux-s390x@0.33.5':
|
||||||
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
|
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-x64@0.33.5':
|
'@img/sharp-linux-x64@0.33.5':
|
||||||
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
|
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-arm64@0.33.5':
|
'@img/sharp-linuxmusl-arm64@0.33.5':
|
||||||
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
|
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-x64@0.33.5':
|
'@img/sharp-linuxmusl-x64@0.33.5':
|
||||||
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
|
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-wasm32@0.33.5':
|
'@img/sharp-wasm32@0.33.5':
|
||||||
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
|
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
|
||||||
@@ -366,24 +378,28 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@next/swc-linux-arm64-musl@15.2.3':
|
'@next/swc-linux-arm64-musl@15.2.3':
|
||||||
resolution: {integrity: sha512-2gAPA7P652D3HzR4cLyAuVYwYqjG0mt/3pHSWTCyKZq/N/dJcUAEoNQMyUmwTZWCJRKofB+JPuDVP2aD8w2J6Q==}
|
resolution: {integrity: sha512-2gAPA7P652D3HzR4cLyAuVYwYqjG0mt/3pHSWTCyKZq/N/dJcUAEoNQMyUmwTZWCJRKofB+JPuDVP2aD8w2J6Q==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@next/swc-linux-x64-gnu@15.2.3':
|
'@next/swc-linux-x64-gnu@15.2.3':
|
||||||
resolution: {integrity: sha512-ODSKvrdMgAJOVU4qElflYy1KSZRM3M45JVbeZu42TINCMG3anp7YCBn80RkISV6bhzKwcUqLBAmOiWkaGtBA9w==}
|
resolution: {integrity: sha512-ODSKvrdMgAJOVU4qElflYy1KSZRM3M45JVbeZu42TINCMG3anp7YCBn80RkISV6bhzKwcUqLBAmOiWkaGtBA9w==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@next/swc-linux-x64-musl@15.2.3':
|
'@next/swc-linux-x64-musl@15.2.3':
|
||||||
resolution: {integrity: sha512-ZR9kLwCWrlYxwEoytqPi1jhPd1TlsSJWAc+H/CJHmHkf2nD92MQpSRIURR1iNgA/kuFSdxB8xIPt4p/T78kwsg==}
|
resolution: {integrity: sha512-ZR9kLwCWrlYxwEoytqPi1jhPd1TlsSJWAc+H/CJHmHkf2nD92MQpSRIURR1iNgA/kuFSdxB8xIPt4p/T78kwsg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@next/swc-win32-arm64-msvc@15.2.3':
|
'@next/swc-win32-arm64-msvc@15.2.3':
|
||||||
resolution: {integrity: sha512-+G2FrDcfm2YDbhDiObDU/qPriWeiz/9cRR0yMWJeTLGGX6/x8oryO3tt7HhodA1vZ8r2ddJPCjtLcpaVl7TE2Q==}
|
resolution: {integrity: sha512-+G2FrDcfm2YDbhDiObDU/qPriWeiz/9cRR0yMWJeTLGGX6/x8oryO3tt7HhodA1vZ8r2ddJPCjtLcpaVl7TE2Q==}
|
||||||
@@ -1164,24 +1180,28 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-musl@4.0.15':
|
'@tailwindcss/oxide-linux-arm64-musl@4.0.15':
|
||||||
resolution: {integrity: sha512-342GVnhH/6PkVgKtEzvNVuQ4D+Q7B7qplvuH20Cfz9qEtydG6IQczTZ5IT4JPlh931MG1NUCVxg+CIorr1WJyw==}
|
resolution: {integrity: sha512-342GVnhH/6PkVgKtEzvNVuQ4D+Q7B7qplvuH20Cfz9qEtydG6IQczTZ5IT4JPlh931MG1NUCVxg+CIorr1WJyw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-gnu@4.0.15':
|
'@tailwindcss/oxide-linux-x64-gnu@4.0.15':
|
||||||
resolution: {integrity: sha512-g76GxlKH124RuGqacCEFc2nbzRl7bBrlC8qDQMiUABkiifDRHOIUjgKbLNG4RuR9hQAD/MKsqZ7A8L08zsoBrw==}
|
resolution: {integrity: sha512-g76GxlKH124RuGqacCEFc2nbzRl7bBrlC8qDQMiUABkiifDRHOIUjgKbLNG4RuR9hQAD/MKsqZ7A8L08zsoBrw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-musl@4.0.15':
|
'@tailwindcss/oxide-linux-x64-musl@4.0.15':
|
||||||
resolution: {integrity: sha512-Gg/Y1XrKEvKpq6WeNt2h8rMIKOBj/W3mNa5NMvkQgMC7iO0+UNLrYmt6zgZufht66HozNpn+tJMbbkZ5a3LczA==}
|
resolution: {integrity: sha512-Gg/Y1XrKEvKpq6WeNt2h8rMIKOBj/W3mNa5NMvkQgMC7iO0+UNLrYmt6zgZufht66HozNpn+tJMbbkZ5a3LczA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@tailwindcss/oxide-win32-arm64-msvc@4.0.15':
|
'@tailwindcss/oxide-win32-arm64-msvc@4.0.15':
|
||||||
resolution: {integrity: sha512-7QtSSJwYZ7ZK1phVgcNZpuf7c7gaCj8Wb0xjliligT5qCGCp79OV2n3SJummVZdw4fbTNKUOYMO7m1GinppZyA==}
|
resolution: {integrity: sha512-7QtSSJwYZ7ZK1phVgcNZpuf7c7gaCj8Wb0xjliligT5qCGCp79OV2n3SJummVZdw4fbTNKUOYMO7m1GinppZyA==}
|
||||||
@@ -1350,21 +1370,25 @@ packages:
|
|||||||
resolution: {integrity: sha512-fp4Azi8kHz6TX8SFmKfyScZrMLfp++uRm2srpqRjsRZIIBzH74NtSkdEUHImR4G7f7XJ+sVZjCc6KDDK04YEpQ==}
|
resolution: {integrity: sha512-fp4Azi8kHz6TX8SFmKfyScZrMLfp++uRm2srpqRjsRZIIBzH74NtSkdEUHImR4G7f7XJ+sVZjCc6KDDK04YEpQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@unrs/rspack-resolver-binding-linux-arm64-musl@1.2.2':
|
'@unrs/rspack-resolver-binding-linux-arm64-musl@1.2.2':
|
||||||
resolution: {integrity: sha512-gMiG3DCFioJxdGBzhlL86KcFgt9HGz0iDhw0YVYPsShItpN5pqIkNrI+L/Q/0gfDiGrfcE0X3VANSYIPmqEAlQ==}
|
resolution: {integrity: sha512-gMiG3DCFioJxdGBzhlL86KcFgt9HGz0iDhw0YVYPsShItpN5pqIkNrI+L/Q/0gfDiGrfcE0X3VANSYIPmqEAlQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@unrs/rspack-resolver-binding-linux-x64-gnu@1.2.2':
|
'@unrs/rspack-resolver-binding-linux-x64-gnu@1.2.2':
|
||||||
resolution: {integrity: sha512-n/4n2CxaUF9tcaJxEaZm+lqvaw2gflfWQ1R9I7WQgYkKEKbRKbpG/R3hopYdUmLSRI4xaW1Cy0Bz40eS2Yi4Sw==}
|
resolution: {integrity: sha512-n/4n2CxaUF9tcaJxEaZm+lqvaw2gflfWQ1R9I7WQgYkKEKbRKbpG/R3hopYdUmLSRI4xaW1Cy0Bz40eS2Yi4Sw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@unrs/rspack-resolver-binding-linux-x64-musl@1.2.2':
|
'@unrs/rspack-resolver-binding-linux-x64-musl@1.2.2':
|
||||||
resolution: {integrity: sha512-cHyhAr6rlYYbon1L2Ag449YCj3p6XMfcYTP0AQX+KkQo025d1y/VFtPWvjMhuEsE2lLvtHm7GdJozj6BOMtzVg==}
|
resolution: {integrity: sha512-cHyhAr6rlYYbon1L2Ag449YCj3p6XMfcYTP0AQX+KkQo025d1y/VFtPWvjMhuEsE2lLvtHm7GdJozj6BOMtzVg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@unrs/rspack-resolver-binding-wasm32-wasi@1.2.2':
|
'@unrs/rspack-resolver-binding-wasm32-wasi@1.2.2':
|
||||||
resolution: {integrity: sha512-eogDKuICghDLGc32FtP+WniG38IB1RcGOGz0G3z8406dUdjJvxfHGuGs/dSlM9YEp/v0lEqhJ4mBu6X2nL9pog==}
|
resolution: {integrity: sha512-eogDKuICghDLGc32FtP+WniG38IB1RcGOGz0G3z8406dUdjJvxfHGuGs/dSlM9YEp/v0lEqhJ4mBu6X2nL9pog==}
|
||||||
@@ -2301,24 +2325,28 @@ packages:
|
|||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
lightningcss-linux-arm64-musl@1.29.2:
|
lightningcss-linux-arm64-musl@1.29.2:
|
||||||
resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==}
|
resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
lightningcss-linux-x64-gnu@1.29.2:
|
lightningcss-linux-x64-gnu@1.29.2:
|
||||||
resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==}
|
resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
lightningcss-linux-x64-musl@1.29.2:
|
lightningcss-linux-x64-musl@1.29.2:
|
||||||
resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==}
|
resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
lightningcss-win32-arm64-msvc@1.29.2:
|
lightningcss-win32-arm64-msvc@1.29.2:
|
||||||
resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==}
|
resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==}
|
||||||
|
|||||||
Reference in New Issue
Block a user