From 1b4e0bafc776b464f1a47365098f4143afe02adc Mon Sep 17 00:00:00 2001 From: William Tso Date: Wed, 23 Apr 2025 17:36:54 +0800 Subject: [PATCH] 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. --- app/(app)/page.tsx | 126 ++++++++++++++++-------------- app/analytics/page.tsx | 17 ++-- app/api/activities/readme.md | 126 ++++++++++++++++++++++++++++++ app/api/activities/route.ts | 8 +- app/auth/callback/route.ts | 32 ++++++++ app/components/ProtectedRoute.tsx | 42 ++++++++++ app/create-shorturl/page.tsx | 2 +- app/debug/page.tsx | 89 +++++++++++++++++++++ app/links/page.tsx | 12 +++ app/login/page.tsx | 75 ++++++++++++++++-- app/page.tsx | 31 +++++++- lib/auth.tsx | 16 +++- middleware.ts | 69 ++++++++++++++++ next.config.ts | 9 ++- pnpm-lock.yaml | 28 +++++++ 15 files changed, 597 insertions(+), 85 deletions(-) create mode 100644 app/api/activities/readme.md create mode 100644 app/auth/callback/route.ts create mode 100644 app/components/ProtectedRoute.tsx create mode 100644 app/debug/page.tsx create mode 100644 middleware.ts diff --git a/app/(app)/page.tsx b/app/(app)/page.tsx index 8f1146f..fcc8b20 100644 --- a/app/(app)/page.tsx +++ b/app/(app)/page.tsx @@ -1,65 +1,71 @@ +'use client'; + +import ProtectedRoute from '@/app/components/ProtectedRoute'; + export default function HomePage() { return ( -
-
-

- Welcome to ShortURL Analytics -

+ +
+
+

+ Welcome to ShortURL Analytics +

+
+ +
- -
- -

- Dashboard -

- -

- Get an overview of all your short URL analytics data. -

-
- - -

- Event Tracking -

- -

- View detailed events for all your short URLs. -

-
- - -

- URL Analysis -

- -

- Analyze performance of specific short URLs. -

-
- - -

- Account Settings -

- -

- Manage your account and team settings. -

-
-
-
+ ); } \ No newline at end of file diff --git a/app/analytics/page.tsx b/app/analytics/page.tsx index c290ed4..590a251 100644 --- a/app/analytics/page.tsx +++ b/app/analytics/page.tsx @@ -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 ( - -
-
- }> - -
+ + +
+
+ }> + +
+
); } \ No newline at end of file diff --git a/app/api/activities/readme.md b/app/api/activities/readme.md new file mode 100644 index 0000000..a1b106b --- /dev/null +++ b/app/api/activities/readme.md @@ -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: 服务器内部错误 \ No newline at end of file diff --git a/app/api/activities/route.ts b/app/api/activities/route.ts index e9e4fce..d0ce1dc 100644 --- a/app/api/activities/route.ts +++ b/app/api/activities/route.ts @@ -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' } }); } diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts new file mode 100644 index 0000000..5ca070e --- /dev/null +++ b/app/auth/callback/route.ts @@ -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) + ); + } +} \ No newline at end of file diff --git a/app/components/ProtectedRoute.tsx b/app/components/ProtectedRoute.tsx new file mode 100644 index 0000000..da5314e --- /dev/null +++ b/app/components/ProtectedRoute.tsx @@ -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 ( +
+
+
+

加载中...

+
+
+ ); + } + + // 如果用户未登录,不渲染任何内容(等待重定向) + if (!user) { + console.log('ProtectedRoute: 用户未登录,等待重定向'); + return null; + } + + // 用户已登录,渲染子组件 + console.log('ProtectedRoute: 用户已登录,渲染内容'); + return <>{children}; +} \ No newline at end of file diff --git a/app/create-shorturl/page.tsx b/app/create-shorturl/page.tsx index 70f8496..0e0a1c2 100644 --- a/app/create-shorturl/page.tsx +++ b/app/create-shorturl/page.tsx @@ -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'; diff --git a/app/debug/page.tsx b/app/debug/page.tsx new file mode 100644 index 0000000..cee06ba --- /dev/null +++ b/app/debug/page.tsx @@ -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>({}); + 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); + + 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 ( +
+

认证调试页面

+ +
+

用户状态

+
+

加载状态: {isLoading ? '加载中...' : '已加载'}

+

已登录: {user ? '是' : '否'}

+

用户邮箱: {user?.email || '未登录'}

+

用户ID: {user?.id || '未登录'}

+

会话有效: {session ? '是' : '否'}

+
+
+ +
+

Cookies 信息

+
+

原始Cookie字符串:

+
+            {rawCookies || '(empty)'}
+          
+ +

解析后的Cookies:

+
+            {JSON.stringify(cookies, null, 2) || '{}'}
+          
+ +

Supabase相关Cookies:

+
+

sb-access-token: {cookies['sb-access-token'] ? '存在' : '不存在'}

+

sb-refresh-token: {cookies['sb-refresh-token'] ? '存在' : '不存在'}

+

supabase-auth-token: {cookies['supabase-auth-token'] ? '存在' : '不存在'}

+
+
+
+ +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/app/links/page.tsx b/app/links/page.tsx index dcd25cb..d681c11 100644 --- a/app/links/page.tsx +++ b/app/links/page.tsx @@ -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): ShortLink }; export default function LinksPage() { + return ( + +
+

短链接管理

+ +
+
+ ); +} + +function LinksPageContent() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [links, setLinks] = useState([]); diff --git a/app/login/page.tsx b/app/login/page.tsx index 6403930..9b8108c 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -21,19 +21,35 @@ function MessageHandler({ setMessage }: { setMessage: (message: { type: string, export default function LoginPage() { const router = useRouter(); - const { signIn, user } = useAuth(); + 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(null); - // 如果用户已登录,重定向到首页 + // 获取重定向URL + useEffect(() => { + if (searchParams) { + const redirect = searchParams.get('redirect'); + if (redirect) { + setRedirectUrl(decodeURIComponent(redirect)); + } + } + }, [searchParams]); + + // 如果用户已登录,重定向到原始页面或首页 useEffect(() => { if (user) { - router.push('/'); + if (redirectUrl) { + router.push(redirectUrl); + } else { + router.push('/'); + } } - }, [user, router]); + }, [user, router, redirectUrl]); const handleEmailSignIn = async (e: React.FormEvent) => { e.preventDefault(); @@ -53,7 +69,7 @@ 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 重定向 @@ -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 (
{/* Wrap the component using useSearchParams in Suspense */} @@ -149,6 +187,33 @@ export default function LoginPage() {
+
+
+
+
+
+
+ Or continue with +
+
+ +
+ +
+
+

Don't have an account?{' '} diff --git a/app/page.tsx b/app/page.tsx index 87264c6..7b1fbde 100644 --- a/app/page.tsx +++ b/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() { - 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 ( +

+
+
+

正在加载...

+
+
+ ); } \ No newline at end of file diff --git a/lib/auth.tsx b/lib/auth.tsx index 266b8d8..e7d2fb4 100644 --- a/lib/auth.tsx +++ b/lib/auth.tsx @@ -4,7 +4,6 @@ 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'; // 定义用户类型 export type AuthUser = User | null; @@ -44,6 +43,15 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children 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); setUser(session?.user || null); } catch (error) { @@ -57,6 +65,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children // 监听验证状态变化 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); }); @@ -106,6 +116,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children provider: 'google', options: { redirectTo: `${window.location.origin}/auth/callback`, + queryParams: { + access_type: 'offline', + prompt: 'consent', + } }, }); diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..fc0dd3e --- /dev/null +++ b/middleware.ts @@ -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', + ], +}; \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index dcc3f4a..77f2f86 100644 --- a/next.config.ts +++ b/next.config.ts @@ -6,8 +6,13 @@ const nextConfig: NextConfig = { // 配置实验性选项 experimental: { - // 禁用外部目录处理,避免monorepo问题 - // externalDir: true, + // 启用边缘函数中间件 + instrumentationHook: true, + // 配置中间件匹配 + middleware: { + // 确保匹配所有路径 + matchAll: '/((?!_next|static|api|public).*)', + }, }, // 禁用严格模式,避免开发时重复渲染 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 42a9428..2d2ef16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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==}