From dffae10f5647b1e879e672d86d40892018794bdb Mon Sep 17 00:00:00 2001 From: Kerwin Zheng <1747269691@qq.com> Date: Tue, 7 Apr 2026 06:06:59 +0800 Subject: [PATCH 01/15] feat(auth): implement neon authentication middleware system Split monolithic auth module into separate client and server boundaries Add catch-all auth api route handler Initialize neon database and cloudflare r2 infrastructure clients Update root middleware route matching patterns Add @neondatabase/auth beta dependency --- app/api/auth/[...path]/route.ts | 3 +++ infra/{orm => }/drizzle.ts | 0 infra/{neon/neon.ts => jwks.ts} | 0 infra/neon.ts | 0 infra/{r2 => }/r2.ts | 0 lib/auth.ts | 15 --------------- lib/auth/client.ts | 5 +++++ middleware.ts | 7 +------ middlewares/auth.ts | 0 package.json | 1 + pnpm-lock.yaml | 5 ++++- 11 files changed, 14 insertions(+), 22 deletions(-) create mode 100644 app/api/auth/[...path]/route.ts rename infra/{orm => }/drizzle.ts (100%) rename infra/{neon/neon.ts => jwks.ts} (100%) create mode 100644 infra/neon.ts rename infra/{r2 => }/r2.ts (100%) delete mode 100644 lib/auth.ts create mode 100644 lib/auth/client.ts create mode 100644 middlewares/auth.ts diff --git a/app/api/auth/[...path]/route.ts b/app/api/auth/[...path]/route.ts new file mode 100644 index 0000000..16ed1c4 --- /dev/null +++ b/app/api/auth/[...path]/route.ts @@ -0,0 +1,3 @@ +import { auth } from "@/lib/auth/server"; + +export const { GET, POST } = auth.handler(); diff --git a/infra/orm/drizzle.ts b/infra/drizzle.ts similarity index 100% rename from infra/orm/drizzle.ts rename to infra/drizzle.ts diff --git a/infra/neon/neon.ts b/infra/jwks.ts similarity index 100% rename from infra/neon/neon.ts rename to infra/jwks.ts diff --git a/infra/neon.ts b/infra/neon.ts new file mode 100644 index 0000000..e69de29 diff --git a/infra/r2/r2.ts b/infra/r2.ts similarity index 100% rename from infra/r2/r2.ts rename to infra/r2.ts diff --git a/lib/auth.ts b/lib/auth.ts deleted file mode 100644 index 24225f8..0000000 --- a/lib/auth.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createClient } from "@neondatabase/neon-js"; - -const neonAuthUrl = process.env.PUBLIC_NEON_AUTH_URL ?? ""; -const neonDataApiUrl = process.env.NEON_DATA_PUBLIC_API_URL ?? ""; - -// Docs: https://neon.com/docs/reference/javascript-sdk -// Auth & database query -export const client = createClient({ - auth: { - url: neonAuthUrl, - }, - dataApi: { - url: neonDataApiUrl, - }, -}); diff --git a/lib/auth/client.ts b/lib/auth/client.ts new file mode 100644 index 0000000..170ef65 --- /dev/null +++ b/lib/auth/client.ts @@ -0,0 +1,5 @@ +"use client"; + +import { createAuthClient } from "@neondatabase/auth/next"; + +export const authClient = createAuthClient(); diff --git a/middleware.ts b/middleware.ts index 84b28e2..2c58762 100644 --- a/middleware.ts +++ b/middleware.ts @@ -6,10 +6,5 @@ export async function middleware(_request: NextRequest) { } export const config = { - matcher: [ - "/settings/:path*", - "/mycreations/:path*", - "/myfavourite/:path*", - "/morecredits/:path*", - ], + matcher: ["/settings/:path*", "/chat:path"], }; diff --git a/middlewares/auth.ts b/middlewares/auth.ts new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json index 1662911..24ff151 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@aws-sdk/client-s3": "^3.1024.0", "@e2b/code-interpreter": "^2.4.0", "@e2b/desktop": "^2.2.2", + "@neondatabase/auth": "0.2.0-beta.1", "@neondatabase/neon-js": "0.2.0-beta.1", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4849ced..dc3d7c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@e2b/desktop': specifier: ^2.2.2 version: 2.2.2 + '@neondatabase/auth': + specifier: 0.2.0-beta.1 + version: 0.2.0-beta.1(85ca8aaaf0dc70c8971f70acc0d3d7de) '@neondatabase/neon-js': specifier: 0.2.0-beta.1 version: 0.2.0-beta.1(85ca8aaaf0dc70c8971f70acc0d3d7de) @@ -10599,7 +10602,7 @@ snapshots: better-call@1.1.5(zod@4.3.6): dependencies: '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.18 + '@better-fetch/fetch': 1.1.21 rou3: 0.7.12 set-cookie-parser: 2.7.2 optionalDependencies: From 225e912ee0cd84db132d01b7fbefb9bd6e3261e3 Mon Sep 17 00:00:00 2001 From: Kerwin Zheng <1747269691@qq.com> Date: Tue, 7 Apr 2026 06:10:16 +0800 Subject: [PATCH 02/15] refactor: migrate auth from client-side localStorage to Next.js cookie-based session - Replace @neondatabase/neon-js client with @neondatabase/auth/next createAuthClient - Use authClient.useSession() hook in providers for reactive session sync - Rewrite services/user.ts to use authClient for OTP and OAuth - Rename middleware.ts to proxy.ts (Next.js 16 convention) with auth.middleware() - Remove localStorage token management from api-client.ts - Session now stored in HTTP-only cookies, enabling server-side middleware auth --- app/providers.tsx | 21 ++++++++-- lib/auth/server.ts | 11 ++++++ middleware.ts | 10 ----- proxy.ts | 9 +++++ services/api-client.ts | 27 +------------ services/user.ts | 88 +++++++++--------------------------------- 6 files changed, 58 insertions(+), 108 deletions(-) create mode 100644 lib/auth/server.ts delete mode 100644 middleware.ts create mode 100644 proxy.ts diff --git a/app/providers.tsx b/app/providers.tsx index f93295d..0012d97 100644 --- a/app/providers.tsx +++ b/app/providers.tsx @@ -1,16 +1,31 @@ "use client"; +import jotaiStore, { userAtom } from "@/atoms"; import { Toaster } from "@/components/ui/sonner"; -import { getUserInfo } from "@/services/user"; +import { authClient } from "@/lib/auth/client"; +import type { User } from "@/types"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useEffect } from "react"; const queryClient = new QueryClient(); const AppProviders = ({ children }: { children: React.ReactNode }) => { + const { data: session } = authClient.useSession(); + useEffect(() => { - getUserInfo(); - }, []); + if (session?.user) { + const { id, name, email, image, createdAt, updatedAt } = session.user; + const user: User = { + id, + userName: name, + email, + avatar: image || null, + createdAt: new Date(createdAt).toISOString(), + updatedAt: new Date(updatedAt).toISOString(), + }; + jotaiStore.set(userAtom, user); + } + }, [session]); return ( diff --git a/lib/auth/server.ts b/lib/auth/server.ts new file mode 100644 index 0000000..f5b655b --- /dev/null +++ b/lib/auth/server.ts @@ -0,0 +1,11 @@ +import { createNeonAuth } from "@neondatabase/auth/next/server"; + +const neonAuthUrl = process.env.NEON_AUTH_URL ?? ""; +const neonDataApiUrl = process.env.NEON_AUTH_COOKIE_SECRET ?? ""; + +export const auth = createNeonAuth({ + baseUrl: neonAuthUrl, + cookies: { + secret: neonDataApiUrl, + }, +}); diff --git a/middleware.ts b/middleware.ts deleted file mode 100644 index 2c58762..0000000 --- a/middleware.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NextResponse } from "next/server"; -import type { NextRequest } from "next/server"; - -export async function middleware(_request: NextRequest) { - return NextResponse.next(); -} - -export const config = { - matcher: ["/settings/:path*", "/chat:path"], -}; diff --git a/proxy.ts b/proxy.ts new file mode 100644 index 0000000..bf50867 --- /dev/null +++ b/proxy.ts @@ -0,0 +1,9 @@ +import { auth } from "@/lib/auth/server"; + +export default auth.middleware({ + loginUrl: "/", +}); + +export const config = { + matcher: ["/settings/:path*", "/chat/:path*"], +}; diff --git a/services/api-client.ts b/services/api-client.ts index 89b34f9..f44cf2c 100644 --- a/services/api-client.ts +++ b/services/api-client.ts @@ -18,38 +18,14 @@ const apiClient = axios.create({ withCredentials: true, }); -// Define the type of business status code const resultEnum: Record = { success: 0, unauthorized: 401, sensitive: 105, }; -// 添加请求拦截器 apiClient.interceptors.request.use( (config: InternalAxiosRequestConfig) => { - if (typeof window !== "undefined") { - const token = localStorage.getItem("token"); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } else { - // const cookies = document.cookie.split(";"); - // for (const cookie of cookies) { - // const eqIndex = cookie.indexOf("="); - // if (eqIndex === -1) continue; - // const name = cookie.substring(0, eqIndex).trim(); - // const value = cookie.substring(eqIndex + 1); - // if ( - // name.toLowerCase().includes("neon") || - // name.toLowerCase().includes("auth") || - // name.toLowerCase().includes("session") - // ) { - // config.headers.Authorization = `Bearer ${value}`; - // break; - // } - // } - } - } return config; }, (error) => Promise.reject(error), @@ -60,7 +36,7 @@ apiClient.interceptors.response.use( const { code, success, data, message } = response.data || {}; if (code === resultEnum.success && success) { - return data as unknown as AxiosResponse; // A mandatory declaration is AxiosResponse + return data as unknown as AxiosResponse; } const validMsg = message || "Internal unknown error"; @@ -80,7 +56,6 @@ apiClient.interceptors.response.use( if (status === 401) { if (typeof window !== "undefined") { - // Trigger login dialog to show jotaiStore.set(loginDialogAtom, true); } } diff --git a/services/user.ts b/services/user.ts index 093303c..943cc93 100644 --- a/services/user.ts +++ b/services/user.ts @@ -1,99 +1,49 @@ -import jotaiStore, { logoutAtom, userAtom } from "@/atoms"; -import { handleError } from "@/lib/error-handler"; -import { client } from "@/lib/neon-auth"; -import type { User } from "@/types"; +import jotaiStore, { logoutAtom } from "@/atoms"; +import { authClient } from "@/lib/auth/client"; -type provider = "google" | "github" | "vercel"; - -const getUserInfo = async () => { - const { data, error } = await client.auth.getSession(); - if (error) { - handleError(error, "Failed to get user info"); - } - - if (data?.session) { - localStorage.setItem("token", data?.session.token); - } - - if (data?.user) { - const { id, name, email, image, createdAt, updatedAt, banned } = data.user; - - const user: User = { - id: id, - userName: name, - email: email, - avatar: image || null, - createdAt: createdAt.toISOString(), - updatedAt: updatedAt.toISOString(), - banned: banned, - }; - - jotaiStore.set(userAtom, user); - } else { - console.error("No active session"); - } -}; +type Provider = "google" | "github" | "vercel"; const sendSignInOtp = async (email: string) => { - const { error } = await client.auth.emailOtp.sendVerificationOtp({ - email: email, + const { error } = await authClient.emailOtp.sendVerificationOtp({ + email, type: "sign-in", }); - if (error) { - handleError(error); - } + if (error) throw error; }; const signInWithOtp = async (email: string, otpCode: string) => { - const { data, error } = await client.auth.signIn.emailOtp({ - email: email, + const { error } = await authClient.signIn.emailOtp({ + email, otp: otpCode, }); - - if (error) { - handleError(error, "Login faild"); - } - - console.log("Successfully login:", data); + if (error) throw error; }; -const handleOauthSignIn = async (provider: provider) => { - const callbackURL = process.env.DEV ? "http://localhost:3000" : ""; - - try { - await client.auth.signIn.social({ - provider: provider, - callbackURL: callbackURL || window.location.origin, - }); - } catch (error) { - handleError(error); - } +const handleOAuthSignIn = async (provider: Provider) => { + await authClient.signIn.social({ + provider, + callbackURL: window.location.origin, + }); }; const signInGoogle = async () => { - await handleOauthSignIn("google"); + await handleOAuthSignIn("google"); }; const signInGithub = async () => { - await handleOauthSignIn("github"); + await handleOAuthSignIn("github"); }; const signInVercel = async () => { - await handleOauthSignIn("vercel"); + await handleOAuthSignIn("vercel"); }; const signOut = async () => { - try { - await client.auth.signOut(); - jotaiStore.set(logoutAtom); - localStorage.removeItem("token"); - } catch (error) { - handleError(error); - } + await authClient.signOut(); + jotaiStore.set(logoutAtom); }; export { - getUserInfo, signOut, signInGithub, sendSignInOtp, From bed9b7d6dc95ff330c54135bb2e9984e1bd63961 Mon Sep 17 00:00:00 2001 From: Kerwin Zheng <1747269691@qq.com> Date: Tue, 7 Apr 2026 06:39:37 +0800 Subject: [PATCH 03/15] refactor(auth): replace legacy proxy with native next middleware Remove deprecated client-side neon auth client implementation Delete unused proxy auth handler Standardise neon auth environment variable names Add explicit session validation flow for protected routes --- lib/auth/server.ts | 2 +- lib/neon-auth.ts | 15 --------------- middleware.ts | 23 +++++++++++++++++++++++ proxy.ts | 9 --------- 4 files changed, 24 insertions(+), 25 deletions(-) delete mode 100644 lib/neon-auth.ts create mode 100644 middleware.ts delete mode 100644 proxy.ts diff --git a/lib/auth/server.ts b/lib/auth/server.ts index f5b655b..a1943d8 100644 --- a/lib/auth/server.ts +++ b/lib/auth/server.ts @@ -1,6 +1,6 @@ import { createNeonAuth } from "@neondatabase/auth/next/server"; -const neonAuthUrl = process.env.NEON_AUTH_URL ?? ""; +const neonAuthUrl = process.env.NEON_AUTH_BASE_URL ?? ""; const neonDataApiUrl = process.env.NEON_AUTH_COOKIE_SECRET ?? ""; export const auth = createNeonAuth({ diff --git a/lib/neon-auth.ts b/lib/neon-auth.ts deleted file mode 100644 index 54e6160..0000000 --- a/lib/neon-auth.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createClient } from "@neondatabase/neon-js"; - -const neonAuthUrl = process.env.NEXT_PUBLIC_NEON_AUTH_URL ?? ""; -const neonDataApiUrl = process.env.NEXT_PUBLIC_NEON_DATA_PUBLIC_API_URL ?? ""; - -// Docs: https://neon.com/docs/reference/javascript-sdk -// Auth & database query -export const client = createClient({ - auth: { - url: neonAuthUrl, - }, - dataApi: { - url: neonDataApiUrl, - }, -}); diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..3d04ae2 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,23 @@ +import { auth } from "@/lib/auth/server"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +const protectedPaths = ["/settings", "/chat"]; + +export async function middleware(request: NextRequest) { + const pathname = request.nextUrl.pathname; + + const isProtected = protectedPaths.some((p) => pathname.startsWith(p)); + if (!isProtected) return NextResponse.next(); + + const { data: session } = await auth.getSession(); + if (!session?.user) { + return NextResponse.redirect(new URL("/", request.url)); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/settings/:path*", "/chat/:path*"], +}; diff --git a/proxy.ts b/proxy.ts deleted file mode 100644 index bf50867..0000000 --- a/proxy.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { auth } from "@/lib/auth/server"; - -export default auth.middleware({ - loginUrl: "/", -}); - -export const config = { - matcher: ["/settings/:path*", "/chat/:path*"], -}; From aa6f0edfe364a380be50c06764ea30c6b6fb31b0 Mon Sep 17 00:00:00 2001 From: Kerwin Zheng <1747269691@qq.com> Date: Tue, 7 Apr 2026 06:46:46 +0800 Subject: [PATCH 04/15] refactor: restructure middleware with chain pattern - Move auth check to middlewares/auth.ts as standalone middleware - Use middleware chain pattern in middleware.ts for extensibility - Check session via auth.getSession() and redirect unauthenticated users --- middleware.ts | 17 +++++++---------- middlewares/auth.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/middleware.ts b/middleware.ts index 3d04ae2..7843270 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,18 +1,15 @@ -import { auth } from "@/lib/auth/server"; +import authMiddleware from "@/middlewares/auth"; import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; -const protectedPaths = ["/settings", "/chat"]; +const middlewares = [authMiddleware]; export async function middleware(request: NextRequest) { - const pathname = request.nextUrl.pathname; - - const isProtected = protectedPaths.some((p) => pathname.startsWith(p)); - if (!isProtected) return NextResponse.next(); - - const { data: session } = await auth.getSession(); - if (!session?.user) { - return NextResponse.redirect(new URL("/", request.url)); + for (const mw of middlewares) { + const response = await mw(request); + if (response && response !== NextResponse.next()) { + return response; + } } return NextResponse.next(); diff --git a/middlewares/auth.ts b/middlewares/auth.ts index e69de29..679d221 100644 --- a/middlewares/auth.ts +++ b/middlewares/auth.ts @@ -0,0 +1,28 @@ +import { auth } from "@/lib/auth/server"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +const authMiddleware = async (request: NextRequest) => { + const { data: session } = await auth.getSession(); + + if ( + !session?.user && + !request.nextUrl.pathname.startsWith("/login") && + !request.nextUrl.pathname.startsWith("/auth") + ) { + if (request.nextUrl.pathname.startsWith("/api")) { + return NextResponse.json( + { success: false, message: "Unauthorized request" }, + { status: 401 }, + ); + } + + const url = request.nextUrl.clone(); + url.pathname = "/"; + return NextResponse.redirect(url); + } + + return NextResponse.next(); +}; + +export default authMiddleware; From 25bde14b7559e72cf0995ebf0031e74727eda838 Mon Sep 17 00:00:00 2001 From: Kerwin Zheng <1747269691@qq.com> Date: Tue, 7 Apr 2026 06:48:15 +0800 Subject: [PATCH 05/15] refactor: use ApiResponse type in auth middleware --- middlewares/auth.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/middlewares/auth.ts b/middlewares/auth.ts index 679d221..c837104 100644 --- a/middlewares/auth.ts +++ b/middlewares/auth.ts @@ -1,4 +1,5 @@ import { auth } from "@/lib/auth/server"; +import type { ApiResponse } from "@/types/api"; import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; @@ -11,10 +12,12 @@ const authMiddleware = async (request: NextRequest) => { !request.nextUrl.pathname.startsWith("/auth") ) { if (request.nextUrl.pathname.startsWith("/api")) { - return NextResponse.json( - { success: false, message: "Unauthorized request" }, - { status: 401 }, - ); + const body: ApiResponse = { + code: 401, + success: false, + message: "Unauthorized request", + }; + return NextResponse.json(body, { status: 401 }); } const url = request.nextUrl.clone(); From ae1ab8baf4008a153d76b8b620a2b3ab3a54e136 Mon Sep 17 00:00:00 2001 From: Kerwin Zheng <1747269691@qq.com> Date: Tue, 7 Apr 2026 07:03:54 +0800 Subject: [PATCH 06/15] fix: open login dialog for ai chat 401 --- hooks/use-chat.test.ts | 33 ++++++++++++++++++++++++++++++++- hooks/use-chat.tsx | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/hooks/use-chat.test.ts b/hooks/use-chat.test.ts index da3d99a..2a8f336 100644 --- a/hooks/use-chat.test.ts +++ b/hooks/use-chat.test.ts @@ -1,4 +1,9 @@ -import { extractToolEventsFromMessages } from "@/hooks/use-chat"; +import jotaiStore from "@/atoms"; +import loginDialogAtom from "@/atoms/login-dialog"; +import { + createAuthAwareChatFetch, + extractToolEventsFromMessages, +} from "@/hooks/use-chat"; type SimplePart = { type: string; @@ -157,6 +162,32 @@ describe("extractToolEventsFromMessages", () => { }); }); +describe("createAuthAwareChatFetch", () => { + beforeEach(() => { + jotaiStore.set(loginDialogAtom, false); + }); + + it("opens the login dialog when chat requests return 401", async () => { + const authAwareFetch = createAuthAwareChatFetch(async () => { + return { status: 401 } as Response; + }); + + await authAwareFetch("/api/chat"); + + expect(jotaiStore.get(loginDialogAtom)).toBe(true); + }); + + it("keeps the login dialog closed for non-401 responses", async () => { + const authAwareFetch = createAuthAwareChatFetch(async () => { + return { status: 200 } as Response; + }); + + await authAwareFetch("/api/chat"); + + expect(jotaiStore.get(loginDialogAtom)).toBe(false); + }); +}); + describe("formatRelativeDate", () => { const { formatRelativeDate } = require("@/lib/utils"); diff --git a/hooks/use-chat.tsx b/hooks/use-chat.tsx index 25a99e8..072487f 100644 --- a/hooks/use-chat.tsx +++ b/hooks/use-chat.tsx @@ -7,9 +7,10 @@ import { dispatchToolEventAtom, vncUrlAtom, } from "@/atoms/chat"; +import loginDialogAtom from "@/atoms/login-dialog"; import type { ToolCallEvent } from "@/types/chat"; import { useChat } from "@ai-sdk/react"; -import type { UIMessage } from "ai"; +import { DefaultChatTransport, type UIMessage } from "ai"; import { useCallback, useEffect, useRef, useState } from "react"; type ChatStatus = "submitted" | "streaming" | "ready" | "error"; @@ -39,6 +40,20 @@ type UseChatReturn = { stop: () => void; }; +const createAuthAwareChatFetch = ( + fetchImpl: typeof fetch = fetch, +): typeof fetch => { + return async (input, init) => { + const response = await fetchImpl(input, init); + + if (response.status === 401 && typeof window !== "undefined") { + jotaiStore.set(loginDialogAtom, true); + } + + return response; + }; +}; + const extractToolEventsFromMessages = ( messages: UIMessage[], existingMap: Map, @@ -112,7 +127,7 @@ const extractToolEventsFromMessages = ( }; export type { UseChatReturn, ChatStatus }; -export { extractToolEventsFromMessages }; +export { createAuthAwareChatFetch, extractToolEventsFromMessages }; const useAgentChat = (options: UseChatOptions = {}): UseChatReturn => { const { @@ -127,10 +142,21 @@ const useAgentChat = (options: UseChatOptions = {}): UseChatReturn => { const [thinkingTime, setThinkingTime] = useState(null); const reasoningStartTimeRef = useRef(null); const processedToolCallsRef = useRef(new Map()); + const transportApiRef = useRef(api); + const transportRef = useRef | null>(null); + + if (!transportRef.current || transportApiRef.current !== api) { + transportRef.current = new DefaultChatTransport({ + api, + fetch: createAuthAwareChatFetch(), + }); + transportApiRef.current = api; + } const chat = useChat({ id: sessionId, messages: initialMessages, + transport: transportRef.current, }); const messages = chat.messages; @@ -199,9 +225,9 @@ const useAgentChat = (options: UseChatOptions = {}): UseChatReturn => { jotaiStore.set(clearToolEventsAtom); jotaiStore.set(agentStatusAtom, "thinking"); - await chat.sendMessage({ text }, { body: { ...opts?.body, api } }); + await chat.sendMessage({ text }, { body: opts?.body }); }, - [chat, api], + [chat], ); const reload = useCallback(async () => { From b5efb8da35e85bc2960901a1f2d605711df92bf9 Mon Sep 17 00:00:00 2001 From: Kerwin Zheng <1747269691@qq.com> Date: Tue, 7 Apr 2026 07:08:22 +0800 Subject: [PATCH 07/15] fix: open login dialog before chat redirect --- components/share/header.tsx | 30 ++++++++++++++++++++---------- components/share/input-field.tsx | 10 +++++++++- components/share/login-dialog.tsx | 14 +++++++++----- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/components/share/header.tsx b/components/share/header.tsx index 7d0a62d..8946939 100644 --- a/components/share/header.tsx +++ b/components/share/header.tsx @@ -35,6 +35,14 @@ const Header = () => { return ( <> + {!user?.id && ( + + )}
{/* Sidebar toggle button */} )}
@@ -122,11 +131,12 @@ const Header = () => { ) : (
- +
)} diff --git a/components/share/input-field.tsx b/components/share/input-field.tsx index 17301d8..c78c8c9 100644 --- a/components/share/input-field.tsx +++ b/components/share/input-field.tsx @@ -1,6 +1,7 @@ "use client"; -import { firstUserInputAtom } from "@/atoms"; +import { firstUserInputAtom, userAtom } from "@/atoms"; +import loginDialogAtom from "@/atoms/login-dialog"; import { InputGroup, InputGroupAddon, @@ -78,6 +79,8 @@ const InputField = ({ const router = useRouter(); const [firstUserInput, setFirstUserInput] = useAtom(firstUserInputAtom); + const [user] = useAtom(userAtom); + const [, setIsLoginDialogOpen] = useAtom(loginDialogAtom); const [attachments, setAttachments] = useState([]); const [uploadedFiles, setUploadedFiles] = useState([]); @@ -210,6 +213,11 @@ const InputField = ({ let newInput = ""; if (firstUserInput && isHome) { + if (!user?.id) { + setIsLoginDialogOpen(true); + return; + } + const sessionID = generateId(); router.push(`/chat/${sessionID}`); return; diff --git a/components/share/login-dialog.tsx b/components/share/login-dialog.tsx index 89bccb2..c66255b 100644 --- a/components/share/login-dialog.tsx +++ b/components/share/login-dialog.tsx @@ -48,10 +48,12 @@ const LoginDialog = ({ open, onOpenChange, loginText, + showTrigger = true, }: { open?: boolean; onOpenChange?: (open: boolean) => void; loginText?: string; + showTrigger?: boolean; }) => { const t = useTranslations("login"); const [isLoading, setIsLoading] = useState(false); @@ -142,11 +144,13 @@ const LoginDialog = ({ onOpenChange?.(isOpen); }} > - - - + {showTrigger && ( + + + + )} Date: Tue, 7 Apr 2026 07:14:45 +0800 Subject: [PATCH 08/15] fix: replace div with button in DropdownMenuTrigger to resolve hydration mismatch --- app/globals.css | 4 ++++ components/share/avatar.tsx | 8 +++++--- middleware.ts | 2 +- middlewares/auth.ts | 4 ++++ services/api-client.ts | 2 +- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/globals.css b/app/globals.css index 5d6b788..075b18c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -124,6 +124,10 @@ body { @apply bg-background text-foreground; } + + button { + cursor: pointer; + } } /* Scroll animation for testimonials */ diff --git a/components/share/avatar.tsx b/components/share/avatar.tsx index e782119..1dd0e98 100644 --- a/components/share/avatar.tsx +++ b/components/share/avatar.tsx @@ -58,14 +58,16 @@ const Avatar = ({ mode = "default", className = "" }: AvatarProps) => { return ( - {/* Avatar */} -
+
+
{t("myAccount")} diff --git a/middleware.ts b/middleware.ts index 7843270..99f4ecb 100644 --- a/middleware.ts +++ b/middleware.ts @@ -16,5 +16,5 @@ export async function middleware(request: NextRequest) { } export const config = { - matcher: ["/settings/:path*", "/chat/:path*"], + matcher: ["/settings/:path*", "/chat/:path*", "/api/:path*"], }; diff --git a/middlewares/auth.ts b/middlewares/auth.ts index c837104..b2235ef 100644 --- a/middlewares/auth.ts +++ b/middlewares/auth.ts @@ -4,6 +4,10 @@ import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; const authMiddleware = async (request: NextRequest) => { + if (request.nextUrl.pathname.startsWith("/api/auth")) { + return NextResponse.next(); + } + const { data: session } = await auth.getSession(); if ( diff --git a/services/api-client.ts b/services/api-client.ts index f44cf2c..ff285ed 100644 --- a/services/api-client.ts +++ b/services/api-client.ts @@ -11,7 +11,7 @@ import { toast } from "sonner"; const baseURL = process.env.NODE_ENV === "development" ? "http://localhost:3000/api/v1" - : "https://agent-dashboard/api/v1"; + : "https://fire-wave/api/"; const apiClient = axios.create({ baseURL: baseURL, From 1301cf46f83f8760050465e7b5e1c7f97982fc4a Mon Sep 17 00:00:00 2001 From: Kerwin Zheng <1747269691@qq.com> Date: Tue, 7 Apr 2026 07:20:10 +0800 Subject: [PATCH 09/15] fix: prevent debug panel overflow in message area --- app/chat/components/debug-panel.tsx | 14 +++++++------- components/ui/scroll-area.tsx | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/chat/components/debug-panel.tsx b/app/chat/components/debug-panel.tsx index 21fe74e..257a950 100644 --- a/app/chat/components/debug-panel.tsx +++ b/app/chat/components/debug-panel.tsx @@ -36,10 +36,10 @@ const ToolEventItem = memo(({ event }: ToolEventItemProps) => { : "..."; return ( -
+
{isExpanded && ( -
+

{t("arguments")}

-
+						
 							{JSON.stringify(event.args, null, 2)}
 						
@@ -80,7 +80,7 @@ const ToolEventItem = memo(({ event }: ToolEventItemProps) => {

{t("result")}

-
+							
 								{JSON.stringify(event.result, null, 2)}
 							
@@ -99,7 +99,7 @@ const DebugPanel = memo(() => { const t = useTranslations("debug"); return ( -
+
{isOpen && ( - + {toolEvents.length === 0 ? (

{t("noEvents")} diff --git a/components/ui/scroll-area.tsx b/components/ui/scroll-area.tsx index bec4f11..ce68dfe 100644 --- a/components/ui/scroll-area.tsx +++ b/components/ui/scroll-area.tsx @@ -13,7 +13,7 @@ function ScrollArea({ return ( Date: Tue, 7 Apr 2026 07:21:46 +0800 Subject: [PATCH 10/15] fix: wrap long assistant message content --- app/chat/components/message-item.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/chat/components/message-item.tsx b/app/chat/components/message-item.tsx index 8d9f5ff..6309cc6 100644 --- a/app/chat/components/message-item.tsx +++ b/app/chat/components/message-item.tsx @@ -31,7 +31,7 @@ const ReasoningBlock = memo(({ text }: { text: string }) => { {t("thinking")}

-

+

{text}

@@ -167,7 +167,7 @@ const MessageItem = memo(
@@ -195,7 +195,9 @@ const MessageItem = memo( isUser ? "bg-primary text-primary-foreground" : "bg-muted", )} > -

{textPart.text}

+

+ {textPart.text} +

); })} From 5649d2d4e9bcc7cae15532373445a08634fa7cd2 Mon Sep 17 00:00:00 2001 From: Kerwin Zheng <1747269691@qq.com> Date: Tue, 7 Apr 2026 07:24:03 +0800 Subject: [PATCH 11/15] fix: constrain debug panel height in chat layout --- app/chat/[id]/page.tsx | 16 ++++++++++------ app/chat/components/debug-panel.tsx | 7 +++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/chat/[id]/page.tsx b/app/chat/[id]/page.tsx index a2c4604..0f7ef35 100644 --- a/app/chat/[id]/page.tsx +++ b/app/chat/[id]/page.tsx @@ -129,15 +129,15 @@ const ChatPage = ({ params }: ChatPageProps) => {
{isMobile ? ( -
+
-
+
{ className="flex flex-1 overflow-hidden" > -
- +
+ -
+
{ const t = useTranslations("debug"); return ( -
+
{isOpen && ( - +
{toolEvents.length === 0 ? (

{t("noEvents")} @@ -132,7 +131,7 @@ const DebugPanel = memo(() => { )) )} - +

)}
); From 83f02ea84863a25f6fc90691a8f7296c865e5659 Mon Sep 17 00:00:00 2001 From: Kerwin Zheng <1747269691@qq.com> Date: Tue, 7 Apr 2026 07:33:17 +0800 Subject: [PATCH 12/15] refactor: scope local chat storage by user id --- app/providers.tsx | 5 ++- jest.setup.ts | 13 +++++++ lib/indexeddb/chat.test.ts | 48 ++++++++++++++++++-------- lib/indexeddb/chat.ts | 70 ++++++++++++++++++++++++++++++-------- services/chat.ts | 61 +++++++++++++++++++++++++-------- 5 files changed, 151 insertions(+), 46 deletions(-) diff --git a/app/providers.tsx b/app/providers.tsx index 0012d97..71ea84f 100644 --- a/app/providers.tsx +++ b/app/providers.tsx @@ -1,6 +1,6 @@ "use client"; -import jotaiStore, { userAtom } from "@/atoms"; +import jotaiStore, { logoutAtom, userAtom } from "@/atoms"; import { Toaster } from "@/components/ui/sonner"; import { authClient } from "@/lib/auth/client"; import type { User } from "@/types"; @@ -24,7 +24,10 @@ const AppProviders = ({ children }: { children: React.ReactNode }) => { updatedAt: new Date(updatedAt).toISOString(), }; jotaiStore.set(userAtom, user); + return; } + + jotaiStore.set(logoutAtom); }, [session]); return ( diff --git a/jest.setup.ts b/jest.setup.ts index f0ec82a..b6cc22d 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -76,3 +76,16 @@ if (typeof globalThis.WritableStream === "undefined") { writable: true, }); } + +if (typeof globalThis.fetch === "undefined") { + Object.defineProperty(globalThis, "fetch", { + value: jest.fn(async () => ({ + ok: true, + status: 200, + headers: new Headers(), + json: async () => ({}), + text: async () => "", + })), + writable: true, + }); +} diff --git a/lib/indexeddb/chat.test.ts b/lib/indexeddb/chat.test.ts index a82208e..0d51034 100644 --- a/lib/indexeddb/chat.test.ts +++ b/lib/indexeddb/chat.test.ts @@ -1,7 +1,7 @@ import type { UIMessage } from "ai"; import { buildMessageRows } from "./chat"; -const makeMessage = ( +const makeTextMessage = ( id: string, role: UIMessage["role"], text: string, @@ -11,23 +11,41 @@ const makeMessage = ( parts: [{ type: "text", text }], }); -describe("buildMessageRows", () => { - it("keeps message order stable within the same save batch", () => { +describe("chat indexeddb storage", () => { + it("builds message rows with the owning user id", () => { + const sessionId = `session-${crypto.randomUUID()}`; + const userId = `user-${crypto.randomUUID()}`; const messages = [ - makeMessage("user-1", "user", "first question"), - makeMessage("assistant-1", "assistant", "first answer"), - makeMessage("user-2", "user", "follow up"), + makeTextMessage(`message-${crypto.randomUUID()}`, "user", "first user"), ]; - const rows = buildMessageRows(messages, "session-1", 1_710_000_000_000); + const rows = buildMessageRows(messages, sessionId, userId); - expect(rows.map((row) => row.id)).toEqual([ - "user-1", - "assistant-1", - "user-2", - ]); - expect(rows.map((row) => row.createdAt)).toEqual([ - 1_710_000_000_000_000, 1_710_000_000_000_001, 1_710_000_000_000_002, - ]); + expect(rows).toHaveLength(1); + expect(rows[0]).toMatchObject({ + id: messages[0]?.id, + userId, + sessionId, + role: "user", + }); + }); + + it("keeps batched message ordering stable", () => { + const sessionId = `session-${crypto.randomUUID()}`; + const userId = `user-${crypto.randomUUID()}`; + const batchTimestamp = Date.now(); + const messages = [ + makeTextMessage(`message-${crypto.randomUUID()}`, "user", "first user"), + makeTextMessage( + `message-${crypto.randomUUID()}`, + "assistant", + "second user", + ), + ]; + + const rows = buildMessageRows(messages, sessionId, userId, batchTimestamp); + + expect(rows[0]?.createdAt).toBe(batchTimestamp * 1000); + expect(rows[1]?.createdAt).toBe(batchTimestamp * 1000 + 1); }); }); diff --git a/lib/indexeddb/chat.ts b/lib/indexeddb/chat.ts index 8949f31..769913f 100644 --- a/lib/indexeddb/chat.ts +++ b/lib/indexeddb/chat.ts @@ -3,6 +3,7 @@ import Dexie, { type EntityTable } from "dexie"; type SessionRow = { id: string; + userId: string; title: string; createdAt: number; updatedAt: number; @@ -10,6 +11,7 @@ type SessionRow = { type MessageRow = { id: string; + userId: string; sessionId: string; role: string; parts: UIMessage["parts"]; @@ -28,40 +30,66 @@ db.version(1).stores({ messages: "id, sessionId, createdAt", }); -const createSession = async (id: string, title: string) => { +db.version(2).stores({ + sessions: "id, userId, updatedAt, [userId+updatedAt]", + messages: "id, userId, sessionId, createdAt, [userId+sessionId]", +}); + +const createSession = async (id: string, title: string, userId: string) => { const now = Date.now(); - await db.sessions.add({ id, title, createdAt: now, updatedAt: now }); - return { id, title, createdAt: now, updatedAt: now }; + await db.sessions.add({ id, userId, title, createdAt: now, updatedAt: now }); + return { id, userId, title, createdAt: now, updatedAt: now }; }; -const getAllSessions = async (): Promise => { - return db.sessions.orderBy("updatedAt").reverse().toArray(); +const getAllSessions = async (userId: string): Promise => { + const sessions = await db.sessions + .where("userId") + .equals(userId) + .sortBy("updatedAt"); + return sessions.reverse(); }; -const getSession = async (id: string) => { - return db.sessions.get(id); +const getSession = async (id: string, userId: string) => { + const session = await db.sessions.get(id); + return session?.userId === userId ? session : undefined; }; -const updateSessionTitle = async (id: string, title: string) => { +const updateSessionTitle = async ( + id: string, + title: string, + userId: string, +) => { + const session = await getSession(id, userId); + if (!session) { + return; + } + await db.sessions.update(id, { title, updatedAt: Date.now() }); }; -const deleteSession = async (id: string) => { +const deleteSession = async (id: string, userId: string) => { await db.transaction("rw", [db.sessions, db.messages], async () => { + const session = await getSession(id, userId); + if (!session) { + return; + } + await db.sessions.delete(id); - await db.messages.where("sessionId").equals(id).delete(); + await db.messages.where("[userId+sessionId]").equals([userId, id]).delete(); }); }; const buildMessageRows = ( messages: UIMessage[], sessionId: string, + userId: string, batchTimestamp = Date.now(), ): MessageRow[] => { const batchBase = batchTimestamp * MESSAGE_BATCH_PRECISION; return messages.map((msg, index) => ({ id: msg.id, + userId, sessionId, role: msg.role, parts: msg.parts, @@ -69,8 +97,17 @@ const buildMessageRows = ( })); }; -const saveMessages = async (messages: UIMessage[], sessionId: string) => { - const rows = buildMessageRows(messages, sessionId); +const saveMessages = async ( + messages: UIMessage[], + sessionId: string, + userId: string, +) => { + const session = await getSession(sessionId, userId); + if (!session) { + return; + } + + const rows = buildMessageRows(messages, sessionId, userId); await db.transaction("rw", [db.messages, db.sessions], async () => { await db.messages.bulkPut(rows); @@ -78,10 +115,13 @@ const saveMessages = async (messages: UIMessage[], sessionId: string) => { }); }; -const getMessages = async (sessionId: string): Promise => { +const getMessages = async ( + sessionId: string, + userId: string, +): Promise => { const rows = await db.messages - .where("sessionId") - .equals(sessionId) + .where("[userId+sessionId]") + .equals([userId, sessionId]) .sortBy("createdAt"); return rows.map((row) => ({ diff --git a/services/chat.ts b/services/chat.ts index e614770..45aa9c1 100644 --- a/services/chat.ts +++ b/services/chat.ts @@ -1,3 +1,4 @@ +import { userAtom } from "@/atoms"; import { type SessionRow, createSession, @@ -15,11 +16,28 @@ import { useQueryClient, } from "@tanstack/react-query"; import type { UIMessage } from "ai"; +import { useAtomValue } from "jotai"; + +const useChatStorageUserId = () => { + const { id } = useAtomValue(userAtom); + return id; +}; + +const getRequiredUserId = (userId: string) => { + if (!userId) { + throw new Error("User is not authenticated"); + } + + return userId; +}; const useAllSessions = (): UseQueryResult => { + const userId = useChatStorageUserId(); + return useQuery({ - queryKey: ["allSessions"], - queryFn: getAllSessions, + queryKey: ["allSessions", userId], + queryFn: () => getAllSessions(userId), + enabled: !!userId, staleTime: 1000, }); }; @@ -27,10 +45,12 @@ const useAllSessions = (): UseQueryResult => { const useChatHistory = ( sessionId: string, ): UseQueryResult => { + const userId = useChatStorageUserId(); + return useQuery({ - queryKey: ["chatHistory", sessionId], - queryFn: () => getMessages(sessionId), - enabled: !!sessionId, + queryKey: ["chatHistory", userId, sessionId], + queryFn: () => getMessages(sessionId, userId), + enabled: !!sessionId && !!userId, staleTime: 1000, }); }; @@ -40,11 +60,13 @@ const useCreateSession = (): UseMutationResult< Error, { id: string; title: string } > => { + const userId = useChatStorageUserId(); const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ id, title }) => createSession(id, title), + mutationFn: ({ id, title }) => + createSession(id, title, getRequiredUserId(userId)), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["allSessions"] }); + queryClient.invalidateQueries({ queryKey: ["allSessions", userId] }); }, }); }; @@ -54,21 +76,28 @@ const useUpdateSessionTitle = (): UseMutationResult< Error, { sessionId: string; title: string } > => { + const userId = useChatStorageUserId(); const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ sessionId, title }) => updateSessionTitle(sessionId, title), + mutationFn: ({ sessionId, title }) => + updateSessionTitle(sessionId, title, getRequiredUserId(userId)), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["allSessions"] }); + queryClient.invalidateQueries({ queryKey: ["allSessions", userId] }); }, }); }; const useDeleteSession = (): UseMutationResult => { + const userId = useChatStorageUserId(); const queryClient = useQueryClient(); return useMutation({ - mutationFn: deleteSession, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["allSessions"] }); + mutationFn: (sessionId) => + deleteSession(sessionId, getRequiredUserId(userId)), + onSuccess: (_data, sessionId) => { + queryClient.invalidateQueries({ queryKey: ["allSessions", userId] }); + queryClient.removeQueries({ + queryKey: ["chatHistory", userId, sessionId], + }); }, }); }; @@ -78,14 +107,16 @@ const useSaveMessages = (): UseMutationResult< Error, { messages: UIMessage[]; sessionId: string } > => { + const userId = useChatStorageUserId(); const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ messages, sessionId }) => saveMessages(messages, sessionId), + mutationFn: ({ messages, sessionId }) => + saveMessages(messages, sessionId, getRequiredUserId(userId)), onSuccess: (_data, variables) => { queryClient.invalidateQueries({ - queryKey: ["chatHistory", variables.sessionId], + queryKey: ["chatHistory", userId, variables.sessionId], }); - queryClient.invalidateQueries({ queryKey: ["allSessions"] }); + queryClient.invalidateQueries({ queryKey: ["allSessions", userId] }); }, }); }; From 9e0fb957ae33c672aae14d5cffeae147463406b7 Mon Sep 17 00:00:00 2001 From: Kerwin Zheng <1747269691@qq.com> Date: Tue, 7 Apr 2026 07:36:50 +0800 Subject: [PATCH 13/15] refactor: remove legacy chat db schema --- lib/indexeddb/chat.ts | 8 ++------ tests/e2e/chat-flow.spec.ts | 6 +++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/indexeddb/chat.ts b/lib/indexeddb/chat.ts index 769913f..616a860 100644 --- a/lib/indexeddb/chat.ts +++ b/lib/indexeddb/chat.ts @@ -19,18 +19,14 @@ type MessageRow = { }; const MESSAGE_BATCH_PRECISION = 1000; +const CHAT_DB_NAME = "firewave-agent-v2"; -const db = new Dexie("firewave-agent") as Dexie & { +const db = new Dexie(CHAT_DB_NAME) as Dexie & { sessions: EntityTable; messages: EntityTable; }; db.version(1).stores({ - sessions: "id, updatedAt", - messages: "id, sessionId, createdAt", -}); - -db.version(2).stores({ sessions: "id, userId, updatedAt, [userId+updatedAt]", messages: "id, userId, sessionId, createdAt, [userId+sessionId]", }); diff --git a/tests/e2e/chat-flow.spec.ts b/tests/e2e/chat-flow.spec.ts index 8282af6..1b33907 100644 --- a/tests/e2e/chat-flow.spec.ts +++ b/tests/e2e/chat-flow.spec.ts @@ -68,7 +68,7 @@ test.describe("Chat History Persistence", () => { const hasSession = await page.evaluate(() => { return new Promise((resolve) => { - const request = indexedDB.open("firewave-agent"); + const request = indexedDB.open("firewave-agent-v2"); request.onsuccess = () => { const db = request.result; const tx = db.transaction("sessions", "readonly"); @@ -98,7 +98,7 @@ test.describe("Chat History Persistence", () => { const title = await page.evaluate(() => { return new Promise((resolve) => { - const request = indexedDB.open("firewave-agent"); + const request = indexedDB.open("firewave-agent-v2"); request.onsuccess = () => { const db = request.result; const tx = db.transaction("sessions", "readonly"); @@ -147,7 +147,7 @@ test.describe("Chat History Persistence", () => { const messageCount = await page.evaluate(() => { return new Promise((resolve) => { - const request = indexedDB.open("firewave-agent"); + const request = indexedDB.open("firewave-agent-v2"); request.onsuccess = () => { const db = request.result; const tx = db.transaction("messages", "readonly"); From c76f540cee3e74f4b17a289438d66082977185b9 Mon Sep 17 00:00:00 2001 From: Kerwin Zheng <1747269691@qq.com> Date: Tue, 7 Apr 2026 07:44:22 +0800 Subject: [PATCH 14/15] fix: lazy-init neon auth to fix build without env vars --- lib/auth/server.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/auth/server.ts b/lib/auth/server.ts index a1943d8..de9db90 100644 --- a/lib/auth/server.ts +++ b/lib/auth/server.ts @@ -1,11 +1,15 @@ import { createNeonAuth } from "@neondatabase/auth/next/server"; -const neonAuthUrl = process.env.NEON_AUTH_BASE_URL ?? ""; -const neonDataApiUrl = process.env.NEON_AUTH_COOKIE_SECRET ?? ""; +const getAuth = () => + createNeonAuth({ + baseUrl: process.env.NEON_AUTH_BASE_URL ?? "", + cookies: { + secret: process.env.NEON_AUTH_COOKIE_SECRET ?? "", + }, + }); -export const auth = createNeonAuth({ - baseUrl: neonAuthUrl, - cookies: { - secret: neonDataApiUrl, +export const auth = new Proxy({} as ReturnType, { + get(_target, prop: string | symbol) { + return Reflect.get(getAuth(), prop); }, }); From 1cbc03ddb24f83a70196bc97a56145520538f20f Mon Sep 17 00:00:00 2001 From: Kerwin Zheng <1747269691@qq.com> Date: Tue, 7 Apr 2026 07:47:35 +0800 Subject: [PATCH 15/15] docs(readme): update neon auth environment variable definitions Correct authentication environment variable names in both English and Chinese documentation. Replace deprecated configuration keys with the new required NEON_AUTH_BASE_URL and NEON_AUTH_COOKIE_SECRET values for project setup instructions. --- README.md | 4 ++-- README_ZH.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1ae2b11..a9d5781 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,8 @@ E2B_API_KEY=your_e2b_api_key GLM_MODLE=glm-4-flash # Neon Auth (Required for authentication) -PUBLIC_NEON_AUTH_URL=your_neon_auth_url -NEON_DATA_PUBLIC_API_URL=your_neon_api_url +NEON_AUTH_BASE_URL=your_neon_auth_url +NEON_AUTH_COOKIE_SECRET=your_cookie_secret # Cloudflare R2 (Required for file upload) CLOUDFLARE_ACCOUNT_ID=your_account_id diff --git a/README_ZH.md b/README_ZH.md index e6cb69b..cdd14dd 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -63,8 +63,8 @@ E2B_API_KEY=your_e2b_api_key GLM_MODLE=glm-4-flash # Neon Auth(认证功能必填) -PUBLIC_NEON_AUTH_URL=your_neon_auth_url -NEON_DATA_PUBLIC_API_URL=your_neon_api_url +NEON_AUTH_BASE_URL=your_neon_auth_url +NEON_AUTH_COOKIE_SECRET=your_cookie_secret # Cloudflare R2(文件上传功能必填) CLOUDFLARE_ACCOUNT_ID=your_account_id