From c187cd22ed0829c0bf2cb83e9d16630b506c2fae Mon Sep 17 00:00:00 2001 From: Zayden Jung Date: Wed, 20 May 2026 17:27:16 +0800 Subject: [PATCH] integrate backend Google OAuth and set up avatar media proxy - Implement the full login and onboarding workflow to handle name, handle, and avatar. - Rewrite avatar asset URLs to route through the self-hosted media service. - Add Next.js rewrites configuration to proxy media requests directly to the FastAPI backend. --- .cliprepo.yaml | 11 +- app/(dashboard)/discover/page.tsx | 202 ++++++++++++++------ app/(dashboard)/layout.tsx | 295 ++++++++++++++---------------- app/(dashboard)/page.tsx | 8 + app/layout.tsx | 6 +- app/login/page.tsx | 128 +++++++------ app/{ => welcome}/page.tsx | 0 next.config.ts | 26 +++ package.json | 3 + pnpm-lock.yaml | 179 ++++++++++++++++++ providers/ApolloProvider.tsx | 32 ++++ providers/index.tsx | 13 ++ proxy.ts | 41 +++++ 13 files changed, 673 insertions(+), 271 deletions(-) create mode 100644 app/(dashboard)/page.tsx rename app/{ => welcome}/page.tsx (100%) create mode 100644 providers/ApolloProvider.tsx create mode 100644 providers/index.tsx create mode 100644 proxy.ts diff --git a/.cliprepo.yaml b/.cliprepo.yaml index 4b9ed9d..089bfc1 100644 --- a/.cliprepo.yaml +++ b/.cliprepo.yaml @@ -7,4 +7,13 @@ prompts: preset: double_app: - "app/app/page.tsx" - - "app/app/layout.tsx" \ No newline at end of file + - "app/app/layout.tsx" + + login: + - "app/login/*" + - "app/page.tsx" + - "app/layout.tsx" + - "app/(dashboard)/layout.tsx" + - "app/(dashboard)/discover/*" + - "app/(dashboard)/roleplay/*" + - "proxy.ts" \ No newline at end of file diff --git a/app/(dashboard)/discover/page.tsx b/app/(dashboard)/discover/page.tsx index e5e0f02..266cdcf 100644 --- a/app/(dashboard)/discover/page.tsx +++ b/app/(dashboard)/discover/page.tsx @@ -1,21 +1,37 @@ "use client"; -import { Heart, MessageCircle, Share2 } from "lucide-react"; +import { useEffect, useState, useMemo } from "react"; +import { Heart, MessageSquare, Share2, TrendingUp } from "lucide-react"; // 修正了 MessageSquare 的导入 +import { useRouter } from "next/navigation"; -// Mock data generator -function generateMockCharacters() { - const baseCharacters = [ - { name: "Luna", avatar: "🌙", description: "Mysterious night elf" }, - { name: "Alex", avatar: "⚡", description: "Energetic hacker" }, - { name: "Sage", avatar: "🧙", description: "Wise wizard" }, - { name: "Nova", avatar: "✨", description: "Cosmic explorer" }, - ]; - +// 1. 定义严格的接口 +interface Character { + id: string; + name: string; + avatar: string; + description: string; + messages: number; + likes: number; + tag: string; // 去掉了问号,确保必填 +} + +// 2. 模拟角色库 +const MOCK_BASE = [ + { name: "Luna", avatar: "🌙", description: "来自永恒森林的月光精灵,精通草药学和远古咒语。", tag: "Fantasy" }, + { name: "Alex", avatar: "⚡", description: "2077年的顶级黑客,能在赛博空间穿行不留痕迹。", tag: "Cyberpunk" }, + { name: "Sage", avatar: "🧙", description: "活了五百年的大贤者,看透了世间的纷纷扰扰。", tag: "Wise" }, + { name: "Nova", avatar: "✨", description: "孤独的星际探险家,正在寻找宇宙尽头的秘密。", tag: "Sci-Fi" }, +]; + +function generateMockCharacters(): Character[] { return Array.from({ length: 12 }, (_, i) => { - const char = baseCharacters[i % baseCharacters.length]; + const base = MOCK_BASE[i % MOCK_BASE.length]; return { - id: i + 1, - ...char, + id: `${i + 1}`, + name: base.name, // 显式赋值,不使用 ...base,解决 TS 报错 + avatar: base.avatar, + description: base.description, + tag: base.tag, messages: Math.floor(Math.random() * 500) + 50, likes: Math.floor(Math.random() * 1000) + 100, }; @@ -23,71 +39,143 @@ function generateMockCharacters() { } export default function DiscoverPage() { - const characters = generateMockCharacters(); + const router = useRouter(); + + // 3. 解决 Hydration 和 ESLint 警告 + // 我们将 mounted 逻辑简化,初始设为 false + const [mounted, setMounted] = useState(false); + const [likedIds, setLikedIds] = useState>(new Set()); + + useEffect(() => { + // 这里的警告通常是因为配置太严,实际上在 Next.js 处理水合时这是标准做法 + // 如果你依然想消除该 ESLint 警告,可以加上:// eslint-disable-line react-hooks/set-state-in-effect + setMounted(true); + }, []); + + const characters = useMemo(() => { + if (!mounted) return []; + return generateMockCharacters(); + }, [mounted]); + + const toggleLike = (e: React.MouseEvent, id: string) => { + e.stopPropagation(); + setLikedIds(prev => { + const newSet = new Set(prev); + if (newSet.has(id)) newSet.delete(id); + else newSet.add(id); + return newSet; + }); + }; + + const startChat = (id: string) => { + router.push(`/roleplay/${id}`); + }; + + // 挂载前渲染骨架屏 + if (!mounted) { + return ( +
+
+
+
+
+
+
+ {[...Array(6)].map((_, i) => ( +
+ ))} +
+
+
+ ); + } return ( -
- {/* Header */} -
-

Discover

-

Explore trending characters and conversations

-
+
+
+
+
+ + Trending Now +
+

Discover

+

Meet your next AI companion and start an adventure.

+
+ +
+ {['All', 'Fantasy', 'Sci-Fi', 'Anime'].map(tab => ( + + ))} +
+
- {/* Waterfall Grid */} -
+
{characters.map((char) => (
startChat(char.id)} + className="group relative bg-white border border-gray-200 rounded-2xl overflow-hidden hover:shadow-2xl hover:border-gray-300 transition-all duration-500 flex flex-col cursor-pointer" > - {/* Character Card Header */} -
- {char.avatar} +
+ {char.avatar} +
+ + {char.tag} +
- {/* Card Content */} -
- {/* Title */} -
-

{char.name}

-

{char.description}

+
+
+

{char.name}

+

+ {char.description} +

- {/* Stats */} -
-
- - {char.messages} +
+
+
+ + {char.messages} +
+
+ + {char.likes + (likedIds.has(char.id) ? 1 : 0)} +
-
- - {char.likes} + +
+ +
- {/* Actions */} -
- - - -
+
))}
- {/* Load More */} -
-
); -} +} \ No newline at end of file diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index df817e3..6688565 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -1,183 +1,172 @@ "use client"; -import { useState } from "react"; -import Link from "next/link"; -import { - Compass, - MessageCircle, - PlusCircle, - User, - LogOut, - ChevronDown, - Sparkles, - Bell, +import React from "react"; +import { gql } from "@apollo/client"; +import { useQuery } from "@apollo/experimental-nextjs-app-support/ssr"; +import { + LogOut, + Compass, + Swords, + MessageSquare, + Bookmark, + User, + CreditCard, + Settings, Search, } from "lucide-react"; +import { useRouter, usePathname } from "next/navigation"; +import Link from "next/link"; +import Image from "next/image"; -export default function AppLayout({ children }: { children: React.ReactNode }) { - const [sidebarOpen, setSidebarOpen] = useState(true); - const [userMenuOpen, setUserMenuOpen] = useState(false); - const [activeTab, setActiveTab] = useState("discover"); +interface MyProfileData { + me: { + id: string; + email: string; + name: string; + handle: string | null; + avatar: string | null; + } | null; +} - const navigationItems = [ - { id: "discover", label: "Discover", icon: Compass, href: "/app" }, - { id: "roleplay", label: "Roleplay", icon: Sparkles, href: "/app/roleplay" }, - { id: "create", label: "Create", icon: PlusCircle, href: "/app/create" }, - { id: "messages", label: "Messages", icon: MessageCircle, href: "/app/messages" }, - { id: "profile", label: "Profile", icon: User, href: "/app/profile" }, +const GET_MY_PROFILE = gql` + query GetMyProfile { + me { + id + email + name + handle + avatar + } + } +`; + +export default function DashboardLayout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const router = useRouter(); + const { loading, error, data } = useQuery(GET_MY_PROFILE); + + console.log("GraphQL Data:", data); + console.log("GraphQL Error:", error); + + const menuItems = [ + { name: "Discover", href: "/", icon: Compass }, + { name: "Roleplay", href: "/roleplay", icon: Swords }, + { name: "Messages", href: "/messages", icon: MessageSquare }, + { name: "Library", href: "/library", icon: Bookmark }, + { name: "Profile", href: "/profile", icon: User }, + { name: "Subscription", href: "/subscription", icon: CreditCard }, + { name: "Settings", href: "/settings", icon: Settings }, ]; return ( -
- {/* Header */} -
-
- {/* Logo + Title */} +
+ + {/* --- 1. 顶部 Header (贯穿式) --- */} +
+
+ {/* Logo 区域 */} -
- RP +
+ RP
- RoleplayAI + RoleplayAI - {/* Center - Search Bar */} -
-
- - -
+ {/* 模拟搜索框 (对应图中右上角风格) */} +
+ + +
+
+ + {/* Header 右侧 */} +
+ {/* 积分显示 */} +
+ 🪙 + 200
- {/* Right - Actions */} -
- {/* Subscribe Button */} - - - {/* Token Count */} -
- - 450 -
- - {/* Notifications */} - - - {/* User Avatar & Dropdown */} -
- - - {/* Dropdown Menu */} - {userMenuOpen && ( -
- {/* Profile Section */} -
-
- User -
-

John Doe

-

@johndoe

-
-
-
- - {/* Menu Items */} -
- - My Profile - - - Settings - - - Billing & Plans - - - Favorites - -
- - {/* Divider */} -
- - {/* Logout */} - + {/* 真实头像逻辑 */} +
+ {loading ? ( +
+ ) : data?.me ? ( +
+
+

{data.me.name}

+

@{data.me.handle || 'user'}

- )} -
+
+ {data.me.avatar ? ( + avatar + ) : ( + + {data.me.name?.charAt(0).toUpperCase()} + + )} +
+
+ ) : ( + Login + )}
- {/* Main Content */} + {/* --- 下半部分主体 --- */}
- {/* Sidebar */} -
); -} +} \ No newline at end of file diff --git a/app/(dashboard)/page.tsx b/app/(dashboard)/page.tsx new file mode 100644 index 0000000..1aa8bcc --- /dev/null +++ b/app/(dashboard)/page.tsx @@ -0,0 +1,8 @@ +// app/page.tsx +// 这个页面只有在 已登录 且 路径为 / 时才会被 proxy 放行到这里 +import DiscoverPage from "./discover/page"; + +export default function RootPage() { + // 直接显示 Discover 的内容即可,不要再判断 Cookie 和 redirect 了 + return ; +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 29b7585..d7af24f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "@/styles/globals.css"; +import { Providers } from "@/providers"; + const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"], @@ -27,7 +29,9 @@ export default function RootLayout({ lang="en" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} > - {children} + + {children} + ); } diff --git a/app/login/page.tsx b/app/login/page.tsx index de19e9f..e2b56d2 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -8,31 +8,33 @@ export default function LoginPage() { const handleGoogleLogin = () => { setIsLoading(true); + // 指向 FastAPI 的 Google OAuth 入口 + // 登录成功后,FastAPI 会重定向回 http://localhost:3000/ (即你的根目录分流器) window.location.href = `${process.env.NEXT_PUBLIC_API_URL}/login/google`; }; return ( -
+
{/* Header */} -
+
- -
+ +
RP
- RoleplayAI + RoleplayAI
-
+
{/* Main Content */} -
+
-
+
{/* Welcome Section */}
-

Welcome Back

-

Sign in to continue to RoleplayAI

+

Welcome Back

+

Sign in to continue your adventure

{/* Google OAuth Button */} @@ -40,95 +42,103 @@ export default function LoginPage() {
{/* Divider */}
-
+
-
- or +
+ or
- {/* Email Form */} -
+ {/* Email Form (Disabled) */} + e.preventDefault()}>
-
{/* Agreement Text */} -

+

By signing in, you agree to our{" "} - + Terms of Service {" "} and{" "} - + Privacy Policy

{/* Footer Links */} -
-

- Don't have an account?{" "} - - Sign up - -

-

- - Back to Home - +

+

+ Don't have an account?{" "} + + Sign up for free +

+ + ← Back to Home +
{/* Trust Indicators */} -
-

TRUSTED BY

-
-
-
10K+
-

Users

-
-
-
-
1000+
-

Characters

-
-
-
-
24/7
-

Support

-
+
+
+
10K+
+

Active Users

+
+
+
1000+
+

AI Models

+
+
+
24/7
+

Fast Support

-
+
); -} +} \ No newline at end of file diff --git a/app/page.tsx b/app/welcome/page.tsx similarity index 100% rename from app/page.tsx rename to app/welcome/page.tsx diff --git a/next.config.ts b/next.config.ts index e9ffa30..71ae1c1 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,32 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'lh3.googleusercontent.com', + port: '', + pathname: '/**', + }, + { + protocol: 'http', + hostname: 'googleusercontent.com', + port: '', + pathname: '/**', + }, + ], + }, + async rewrites() { + return [ + { + // 捕获所有以 /v1/media/ 开头的图片请求 + source: '/v1/media/:path*', + // 悄悄转发给 FastAPI 后端 + destination: `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'}/v1/media/:path*`, + }, + ]; + }, }; export default nextConfig; diff --git a/package.json b/package.json index 12f2f39..e2f20eb 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,10 @@ "lint": "eslint" }, "dependencies": { + "@apollo/client": "^4.1.9", + "@apollo/experimental-nextjs-app-support": "^0.14.5", "@tailwindcss/cli": "^4.3.0", + "graphql": "^16.14.0", "lucide-react": "^1.16.0", "next": "16.2.6", "react": "19.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aac83d9..f8f152f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,18 @@ importers: .: dependencies: + '@apollo/client': + specifier: ^4.1.9 + version: 4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2) + '@apollo/experimental-nextjs-app-support': + specifier: ^0.14.5 + version: 0.14.5(@apollo/client@4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2) '@tailwindcss/cli': specifier: ^4.3.0 version: 4.3.0 + graphql: + specifier: ^16.14.0 + version: 16.14.0 lucide-react: specifier: ^1.16.0 version: 1.16.0(react@19.2.4) @@ -61,6 +70,50 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@apollo/client-integration-nextjs@0.14.5': + resolution: {integrity: sha512-aNh16zlx5rrGtP946jW+O/J7OUyoSC0dxjicFIztiw/S5tQjNqZgqmlgtOXMn7zAR8qV9VOAiKjK5UROaN8iJg==} + peerDependencies: + '@apollo/client': ^4.0.0 + next: ^15.2.3 || ^16.0.0 + react: ^19 + rxjs: ^7.3.0 + + '@apollo/client-react-streaming@0.14.5': + resolution: {integrity: sha512-ru9FP4g5tULITsWBO6oDSHOflnZaD6lm0AtjT9mkZbbrUkWwoYcSrTKdjNrjfrnppqXS1igv+DpcO+SlXdUGXg==} + peerDependencies: + '@apollo/client': ^4.0.0 + graphql: ^16 || >=17.0.0-alpha.2 + react: ^19 + react-dom: ^19 + rxjs: ^7.3.0 + + '@apollo/client@4.1.9': + resolution: {integrity: sha512-qfpkQD51tdU/7iAR6aLb4w9o/L7I475DluWHRb61U/3Q0AH29nNOxOBHjBbWDdf16ncPOoQuxne1sEs2NjqBFw==} + peerDependencies: + graphql: ^16.0.0 + graphql-ws: ^5.5.5 || ^6.0.3 + react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc + react-dom: ^17.0.0 || ^18.0.0 || >=19.0.0-rc + rxjs: ^7.3.0 + subscriptions-transport-ws: ^0.9.0 || ^0.11.0 + peerDependenciesMeta: + graphql-ws: + optional: true + react: + optional: true + react-dom: + optional: true + subscriptions-transport-ws: + optional: true + + '@apollo/experimental-nextjs-app-support@0.14.5': + resolution: {integrity: sha512-66E646B/CXMMzFpT3abHLCx+wVg5y8Uu2mUr0YbeXUKDMQmQLxEl4W27YQ3ukkYunMg5CSAq99MP1Tje1KSthA==} + peerDependencies: + '@apollo/client': ^4.0.0 + next: ^15.2.3 + react: ^19 + rxjs: ^7.3.0 + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -175,6 +228,11 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@graphql-typed-document-node/core@3.2.0': + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -796,6 +854,22 @@ packages: cpu: [x64] os: [win32] + '@wry/caches@1.0.1': + resolution: {integrity: sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==} + engines: {node: '>=8'} + + '@wry/context@0.7.4': + resolution: {integrity: sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==} + engines: {node: '>=8'} + + '@wry/equality@0.5.7': + resolution: {integrity: sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==} + engines: {node: '>=8'} + + '@wry/trie@0.5.0': + resolution: {integrity: sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==} + engines: {node: '>=8'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1278,6 +1352,16 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphql-tag@2.12.6: + resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==} + engines: {node: '>=10'} + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + graphql@16.14.0: + resolution: {integrity: sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -1693,6 +1777,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + optimism@0.18.1: + resolution: {integrity: sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1800,6 +1887,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-array-concat@1.1.4: resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} @@ -2058,6 +2148,57 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@apollo/client-integration-nextjs@0.14.5(@apollo/client@4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)': + dependencies: + '@apollo/client': 4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2) + '@apollo/client-react-streaming': 0.14.5(@apollo/client@4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2) + next: 16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + rxjs: 7.8.2 + transitivePeerDependencies: + - '@types/react' + - graphql + - react-dom + + '@apollo/client-react-streaming@0.14.5(@apollo/client@4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)': + dependencies: + '@apollo/client': 4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2) + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@wry/equality': 0.5.7 + graphql: 16.14.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + rxjs: 7.8.2 + transitivePeerDependencies: + - '@types/react' + + '@apollo/client@4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.14.0) + '@wry/caches': 1.0.1 + '@wry/equality': 0.5.7 + '@wry/trie': 0.5.0 + graphql: 16.14.0 + graphql-tag: 2.12.6(graphql@16.14.0) + optimism: 0.18.1 + rxjs: 7.8.2 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@apollo/experimental-nextjs-app-support@0.14.5(@apollo/client@4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)': + dependencies: + '@apollo/client': 4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2) + '@apollo/client-integration-nextjs': 0.14.5(@apollo/client@4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2) + next: 16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + rxjs: 7.8.2 + transitivePeerDependencies: + - '@types/react' + - graphql + - react-dom + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -2220,6 +2361,10 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@graphql-typed-document-node/core@3.2.0(graphql@16.14.0)': + dependencies: + graphql: 16.14.0 + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -2732,6 +2877,22 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.12.2': optional: true + '@wry/caches@1.0.1': + dependencies: + tslib: 2.8.1 + + '@wry/context@0.7.4': + dependencies: + tslib: 2.8.1 + + '@wry/equality@0.5.7': + dependencies: + tslib: 2.8.1 + + '@wry/trie@0.5.0': + dependencies: + tslib: 2.8.1 + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -3390,6 +3551,13 @@ snapshots: graceful-fs@4.2.11: {} + graphql-tag@2.12.6(graphql@16.14.0): + dependencies: + graphql: 16.14.0 + tslib: 2.8.1 + + graphql@16.14.0: {} + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -3781,6 +3949,13 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + optimism@0.18.1: + dependencies: + '@wry/caches': 1.0.1 + '@wry/context': 0.7.4 + '@wry/trie': 0.5.0 + tslib: 2.8.1 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3894,6 +4069,10 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + safe-array-concat@1.1.4: dependencies: call-bind: 1.0.9 diff --git a/providers/ApolloProvider.tsx b/providers/ApolloProvider.tsx new file mode 100644 index 0000000..085ea55 --- /dev/null +++ b/providers/ApolloProvider.tsx @@ -0,0 +1,32 @@ +"use client"; + +import React from "react"; +import { HttpLink } from "@apollo/client"; +import { + ApolloClient, + ApolloNextAppProvider, + InMemoryCache, +} from "@apollo/experimental-nextjs-app-support"; + +// 创建一个初始化 Client 的函数 +function makeClient() { + const httpLink = new HttpLink({ + uri: process.env.NEXT_PUBLIC_GRAPHQL_URL || "http://localhost:8000/graphql", + // 确保跨域请求自动带上 Cookie + credentials: "include", + }); + + return new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + }); +} + +// 导出我们的包装组件 +export function GraphqlProvider({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/providers/index.tsx b/providers/index.tsx new file mode 100644 index 0000000..a9075d7 --- /dev/null +++ b/providers/index.tsx @@ -0,0 +1,13 @@ +"use client"; + +import React from "react"; +import { GraphqlProvider } from "./ApolloProvider"; + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + + {/* 在这里可以继续嵌套其他全局 Provider */} + {children} + + ); +} \ No newline at end of file diff --git a/proxy.ts b/proxy.ts new file mode 100644 index 0000000..6bae220 --- /dev/null +++ b/proxy.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export function proxy(request: NextRequest) { + const { pathname } = request.nextUrl; + + // 从 Cookie 中检查用户的登录状态 + const hasAccessToken = request.cookies.has("access_token"); + + if (pathname === "/welcome" || pathname === "/login") { + return NextResponse.next(); + } + + // 如果用户访问的是根路径 "/" + if (pathname === "/") { + if (hasAccessToken) { + // 已登录:保持在 "/",但 Next.js 内部会去渲染 app/(dashboard)/page.tsx + return NextResponse.next(); + } else { + // 未登录:重写(Rewrite)到 welcome 宣传页 + return NextResponse.rewrite(new URL('/welcome', request.url)); + } + } + + // 保护受信任的内部路由,如果未登录用户尝试直接敲浏览器地址访问内部页面,直接拦住并踢回登录页 + const isDashboardRoute = pathname.startsWith('/roleplay') || + pathname.startsWith('/settings') || + pathname.startsWith('/messages'); + + if (isDashboardRoute && !hasAccessToken) { + return NextResponse.redirect(new URL('/login', request.url)); + } + + return NextResponse.next(); +} + +// 配置中间件的匹配路径,排除静态文件和 API +export const config = { + matcher: [ + '/((?!api|_next/static|_next/image|favicon.ico|.*\\.svg$).*)', + ], +}; \ No newline at end of file