diff --git a/AUTH_SYSTEM.md b/AUTH_SYSTEM.md
new file mode 100644
index 000000000..6d4b4b373
--- /dev/null
+++ b/AUTH_SYSTEM.md
@@ -0,0 +1,259 @@
+# Authentication System Documentation
+
+This authentication system provides a complete solution for handling Supabase authentication tokens via cookies and query strings.
+
+## Features
+
+- **Token-based Authentication**: Accepts authentication tokens via query strings or POST requests
+- **Automatic Cookie Management**: Stores tokens securely as HTTP-only cookies
+- **Token Refresh**: Automatically refreshes expired tokens using refresh tokens
+- **Route Protection**: Middleware to protect routes requiring authentication
+- **Client-side Hooks**: React hooks for managing authentication state
+- **Session Management**: Proper login/logout functionality
+
+## API Endpoints
+
+### Authentication Callback
+Handle authentication tokens from external services or redirects.
+
+#### GET `/api/auth/callback`
+```
+GET /api/auth/callback?token=ACCESS_TOKEN&refresh_token=REFRESH_TOKEN&redirect_to=/dashboard
+```
+
+Parameters:
+- `token` or `access_token` (required): The Supabase access token
+- `refresh_token` (optional): The refresh token for automatic token renewal
+- `redirect_to` (optional): URL to redirect to after successful authentication (default: "/")
+
+#### POST `/api/auth/callback`
+```json
+POST /api/auth/callback
+Content-Type: application/json
+
+{
+ "access_token": "ACCESS_TOKEN",
+ "refresh_token": "REFRESH_TOKEN",
+ "redirect_to": "/dashboard"
+}
+```
+
+### Authentication Check
+Verify current authentication status.
+
+#### GET `/api/auth/check`
+```
+GET /api/auth/check
+```
+
+Returns:
+```json
+{
+ "authenticated": true,
+ "user": {
+ "id": "user_id",
+ "email": "user@example.com",
+ "user_metadata": {}
+ },
+ "userInfo": {
+ "id": "user_id",
+ "email": "user@example.com",
+ "user_metadata": {}
+ }
+}
+```
+
+### Token Refresh
+Manually refresh an expired access token.
+
+#### POST `/api/auth/refresh`
+```
+POST /api/auth/refresh
+```
+
+Uses the refresh token stored in cookies to get a new access token.
+
+### Logout
+Clear authentication cookies and end the session.
+
+#### POST `/api/auth/logout`
+```json
+POST /api/auth/logout
+```
+
+#### GET `/api/auth/logout`
+```
+GET /api/auth/logout?redirect_to=/login
+```
+
+### User Information
+Get detailed user information (protected route).
+
+#### GET `/api/user`
+```
+GET /api/user
+```
+
+Returns detailed user information including metadata.
+
+## Client-side Usage
+
+### Using the useAuth Hook
+
+```tsx
+import { useAuth } from "../hooks/useAuth";
+
+function MyComponent() {
+ const { user, authenticated, loading, login, logout, checkAuth } = useAuth();
+
+ if (loading) {
+ return
Loading...
;
+ }
+
+ if (!authenticated) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
Welcome, {user?.email}!
+
+
+ );
+}
+```
+
+### Protecting Routes with HOC
+
+```tsx
+import { withAuth } from "../hooks/useAuth";
+
+function ProtectedPage({ user }) {
+ return (
+
+
Protected Content
+
User ID: {user.id}
+
+ );
+}
+
+export default withAuth(ProtectedPage);
+```
+
+## Server-side Route Protection
+
+The middleware automatically protects routes based on the configuration in `middleware.ts`.
+
+### Protected Routes (require authentication):
+- `/chat`
+- `/settings`
+- `/profile`
+- `/api/chat`
+- `/api/user`
+
+### Public Routes (no authentication required):
+- `/`
+- `/login`
+- `/signup`
+- `/api/auth/*`
+
+## Environment Variables
+
+Ensure these environment variables are set:
+
+```env
+SUPABASE_URL=your_supabase_project_url
+SUPABASE_ANON_KEY=your_supabase_anon_key
+```
+
+## Authentication Flow Examples
+
+### 1. External Service Redirect
+```
+https://your-app.com/api/auth/callback?token=supabase_access_token&refresh_token=supabase_refresh_token&redirect_to=/dashboard
+```
+
+### 2. Programmatic Login
+```javascript
+// Client-side login
+const { login } = useAuth();
+await login("supabase_access_token", "supabase_refresh_token");
+```
+
+### 3. Form-based Login
+```javascript
+// Using the login page
+// Navigate to /login and enter token manually
+```
+
+## Cookie Configuration
+
+The system uses secure HTTP-only cookies with the following settings:
+
+- **Name**: `sb-access-token`, `sb-refresh-token`, `sb-user-info`
+- **HttpOnly**: `true` (except for `sb-user-info`)
+- **Secure**: `true` (in production)
+- **SameSite**: `lax`
+- **MaxAge**: 7 days
+- **Path**: `/`
+
+## Error Handling
+
+The system handles various error scenarios:
+
+- **No token provided**: Redirects to login with error parameter
+- **Invalid token**: Validates token with Supabase before setting cookies
+- **Expired token**: Automatically attempts refresh using refresh token
+- **Network errors**: Graceful fallback and error messaging
+
+## Integration with Existing Code
+
+The authentication system is designed to work alongside existing authentication mechanisms. It enhances the current system by providing:
+
+1. **Cookie-based session management**
+2. **Automatic token refresh**
+3. **Client-side authentication state**
+4. **Route protection middleware**
+
+## Testing the Implementation
+
+1. **Start the application**:
+ ```bash
+ npm run dev
+ ```
+
+2. **Test authentication callback**:
+ ```
+ http://localhost:3000/api/auth/callback?token=YOUR_SUPABASE_TOKEN
+ ```
+
+3. **Visit protected routes**:
+ ```
+ http://localhost:3000/profile
+ ```
+
+4. **Test the login page**:
+ ```
+ http://localhost:3000/login
+ ```
+
+5. **Check authentication status**:
+ ```
+ curl -b cookies.txt http://localhost:3000/api/auth/check
+ ```
+
+## Security Considerations
+
+1. **HTTP-only cookies**: Prevents XSS attacks
+2. **Secure flag**: Ensures cookies are only sent over HTTPS in production
+3. **Token validation**: All tokens are validated with Supabase before use
+4. **Automatic expiration**: Cookies expire after 7 days
+5. **Path restriction**: Cookies are scoped to the application path
+
+This authentication system provides a robust foundation for managing user sessions while maintaining security best practices.
diff --git a/app/api/auth/callback/route.ts b/app/api/auth/callback/route.ts
new file mode 100644
index 000000000..208837bf1
--- /dev/null
+++ b/app/api/auth/callback/route.ts
@@ -0,0 +1,174 @@
+import { NextRequest, NextResponse } from "next/server";
+import { createClient } from "@supabase/supabase-js";
+
+const SUPABASE_URL = process.env.SUPABASE_URL!;
+const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY!;
+
+export async function GET(req: NextRequest) {
+ const url = new URL(req.url);
+ const authToken = url.searchParams.get("token");
+ const access_token = url.searchParams.get("access_token");
+ const refresh_token = url.searchParams.get("refresh_token");
+ const redirectTo = url.searchParams.get("redirect_to") || "/";
+
+ console.log("[Auth Callback] Processing authentication callback");
+ console.log("[Auth Callback] authToken:", authToken);
+ console.log("[Auth Callback] access_token:", access_token);
+ console.log("[Auth Callback] refresh_token:", refresh_token);
+
+ // Use either authToken or access_token (flexible for different auth flows)
+ const token = authToken || access_token;
+
+ if (!token) {
+ console.log(
+ "[Auth Callback] No authentication token found in query string",
+ );
+ return NextResponse.redirect(new URL("/login?error=no_token", req.url));
+ }
+
+ try {
+ // Validate the token with Supabase
+ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
+ global: {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ });
+
+ const { data, error } = await supabase.auth.getUser();
+
+ if (error || !data?.user) {
+ console.error("[Auth Callback] Invalid token:", error);
+ return NextResponse.redirect(
+ new URL("/login?error=invalid_token", req.url),
+ );
+ }
+
+ console.log(
+ "[Auth Callback] Token validated successfully for user:",
+ data.user.id,
+ );
+
+ // Create response with redirect
+ const response = NextResponse.redirect(new URL(redirectTo, req.url));
+
+ // Set authentication cookies
+ const cookieOptions = {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax" as const,
+ maxAge: 60 * 60 * 24 * 7, // 7 days
+ path: "/",
+ };
+
+ // Set access token cookie
+ response.cookies.set("sb-access-token", token, cookieOptions);
+
+ // Set refresh token if available
+ if (refresh_token) {
+ response.cookies.set("sb-refresh-token", refresh_token, cookieOptions);
+ }
+
+ // Set user info cookie (optional, for quick access)
+ response.cookies.set(
+ "sb-user-info",
+ JSON.stringify({
+ id: data.user.id,
+ email: data.user.email,
+ user_metadata: data.user.user_metadata,
+ }),
+ {
+ ...cookieOptions,
+ httpOnly: false, // Allow client-side access for user info
+ },
+ );
+
+ console.log("[Auth Callback] Authentication cookies set successfully");
+
+ return response;
+ } catch (err) {
+ console.error("[Auth Callback] Error processing authentication:", err);
+ return NextResponse.redirect(new URL("/login?error=auth_failed", req.url));
+ }
+}
+
+export async function POST(req: NextRequest) {
+ // Handle POST requests for programmatic token setting
+ try {
+ const body = await req.json();
+ const { access_token, refresh_token, redirect_to = "/" } = body;
+
+ if (!access_token) {
+ return NextResponse.json(
+ { error: "access_token is required" },
+ { status: 400 },
+ );
+ }
+
+ // Validate the token
+ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
+ global: {
+ headers: {
+ Authorization: `Bearer ${access_token}`,
+ },
+ },
+ });
+
+ const { data, error } = await supabase.auth.getUser();
+
+ if (error || !data?.user) {
+ return NextResponse.json(
+ { error: "Invalid token", details: error },
+ { status: 401 },
+ );
+ }
+
+ // Create response
+ const response = NextResponse.json({
+ success: true,
+ user: {
+ id: data.user.id,
+ email: data.user.email,
+ user_metadata: data.user.user_metadata,
+ },
+ redirect_to,
+ });
+
+ // Set authentication cookies
+ const cookieOptions = {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax" as const,
+ maxAge: 60 * 60 * 24 * 7, // 7 days
+ path: "/",
+ };
+
+ response.cookies.set("sb-access-token", access_token, cookieOptions);
+
+ if (refresh_token) {
+ response.cookies.set("sb-refresh-token", refresh_token, cookieOptions);
+ }
+
+ response.cookies.set(
+ "sb-user-info",
+ JSON.stringify({
+ id: data.user.id,
+ email: data.user.email,
+ user_metadata: data.user.user_metadata,
+ }),
+ {
+ ...cookieOptions,
+ httpOnly: false,
+ },
+ );
+
+ return response;
+ } catch (err) {
+ console.error("[Auth Callback POST] Error:", err);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 },
+ );
+ }
+}
diff --git a/app/api/auth/check/route.ts b/app/api/auth/check/route.ts
index 63fbfa9bc..bc8cfeec3 100644
--- a/app/api/auth/check/route.ts
+++ b/app/api/auth/check/route.ts
@@ -1,21 +1,54 @@
// /app/api/auth/check/route.ts
-import { NextRequest } from "next/server";
-import { checkAuth } from "../../supabase";
+import { NextRequest, NextResponse } from "next/server";
+import { checkAuthWithRefresh, getUserInfoFromCookie } from "../../supabase";
export async function GET(req: NextRequest) {
- const user = await checkAuth(req);
+ try {
+ const authResult = await checkAuthWithRefresh(req);
+ const userInfo = getUserInfoFromCookie(req);
- console.log("[Auth] user ", user);
+ console.log("[Auth Check] user:", authResult.user?.email || "none");
- if (!user) {
- return new Response(JSON.stringify({ authenticated: false }), {
- status: 401,
- headers: { "Content-Type": "application/json" },
+ if (!authResult.user) {
+ return NextResponse.json(
+ {
+ authenticated: false,
+ error: "No valid authentication found",
+ },
+ { status: 401 },
+ );
+ }
+
+ // Create response
+ const response = NextResponse.json({
+ authenticated: true,
+ user: authResult.user,
+ userInfo: userInfo, // Include cached user info for quick access
});
- }
- return new Response(JSON.stringify({ authenticated: true, user }), {
- status: 200,
- headers: { "Content-Type": "application/json" },
- });
+ // If token was refreshed, merge the refreshed cookies
+ if (authResult.needsRefresh && authResult.response) {
+ // Copy cookies from refresh response
+ authResult.response.cookies.getAll().forEach((cookie) => {
+ response.cookies.set(cookie.name, cookie.value, {
+ httpOnly: cookie.httpOnly,
+ secure: cookie.secure,
+ sameSite: cookie.sameSite,
+ maxAge: cookie.maxAge,
+ path: cookie.path,
+ });
+ });
+ }
+
+ return response;
+ } catch (error) {
+ console.error("[Auth Check] Error:", error);
+ return NextResponse.json(
+ {
+ authenticated: false,
+ error: "Authentication check failed",
+ },
+ { status: 500 },
+ );
+ }
}
diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts
new file mode 100644
index 000000000..d15ee26a3
--- /dev/null
+++ b/app/api/auth/logout/route.ts
@@ -0,0 +1,68 @@
+import { NextRequest, NextResponse } from "next/server";
+
+export async function POST(req: NextRequest) {
+ console.log("[Auth Logout] Processing logout request");
+
+ const redirectTo =
+ new URL(req.url).searchParams.get("redirect_to") || "/login";
+
+ // Create response
+ const response = NextResponse.json({
+ success: true,
+ message: "Logged out successfully",
+ });
+
+ // Clear authentication cookies
+ const cookieOptions = {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax" as const,
+ maxAge: 0, // Expire immediately
+ path: "/",
+ };
+
+ response.cookies.set("sb-access-token", "", cookieOptions);
+ response.cookies.set("sb-refresh-token", "", cookieOptions);
+ response.cookies.set("sb-user-info", "", {
+ ...cookieOptions,
+ httpOnly: false,
+ });
+
+ console.log("[Auth Logout] Authentication cookies cleared");
+
+ return response;
+}
+
+export async function GET(req: NextRequest) {
+ // Handle GET requests with redirect
+ const url = new URL(req.url);
+ const redirectTo = url.searchParams.get("redirect_to") || "/login";
+
+ console.log("[Auth Logout] Processing logout request with redirect");
+
+ // Create redirect response
+ const response = NextResponse.redirect(new URL(redirectTo, req.url));
+
+ // Clear authentication cookies
+ const cookieOptions = {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax" as const,
+ maxAge: 0, // Expire immediately
+ path: "/",
+ };
+
+ response.cookies.set("sb-access-token", "", cookieOptions);
+ response.cookies.set("sb-refresh-token", "", cookieOptions);
+ response.cookies.set("sb-user-info", "", {
+ ...cookieOptions,
+ httpOnly: false,
+ });
+
+ console.log(
+ "[Auth Logout] Authentication cookies cleared, redirecting to:",
+ redirectTo,
+ );
+
+ return response;
+}
diff --git a/app/api/auth/refresh/route.ts b/app/api/auth/refresh/route.ts
new file mode 100644
index 000000000..bee106cad
--- /dev/null
+++ b/app/api/auth/refresh/route.ts
@@ -0,0 +1,99 @@
+import { NextRequest, NextResponse } from "next/server";
+import { createClient } from "@supabase/supabase-js";
+
+const SUPABASE_URL = process.env.SUPABASE_URL!;
+const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY!;
+
+export async function POST(req: NextRequest) {
+ console.log("[Auth Refresh] Processing token refresh request");
+
+ const refreshToken = req.cookies.get("sb-refresh-token")?.value;
+
+ if (!refreshToken) {
+ console.log("[Auth Refresh] No refresh token found");
+ return NextResponse.json(
+ { error: "No refresh token found" },
+ { status: 401 },
+ );
+ }
+
+ try {
+ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
+
+ const { data, error } = await supabase.auth.refreshSession({
+ refresh_token: refreshToken,
+ });
+
+ if (error || !data?.session) {
+ console.error("[Auth Refresh] Token refresh failed:", error);
+ return NextResponse.json(
+ { error: "Token refresh failed", details: error },
+ { status: 401 },
+ );
+ }
+
+ console.log(
+ "[Auth Refresh] Token refreshed successfully for user:",
+ data.session.user.id,
+ );
+
+ // Create response
+ const response = NextResponse.json({
+ success: true,
+ user: {
+ id: data.session.user.id,
+ email: data.session.user.email,
+ user_metadata: data.session.user.user_metadata,
+ },
+ session: {
+ access_token: data.session.access_token,
+ expires_at: data.session.expires_at,
+ },
+ });
+
+ // Update authentication cookies
+ const cookieOptions = {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax" as const,
+ maxAge: 60 * 60 * 24 * 7, // 7 days
+ path: "/",
+ };
+
+ response.cookies.set(
+ "sb-access-token",
+ data.session.access_token,
+ cookieOptions,
+ );
+
+ if (data.session.refresh_token) {
+ response.cookies.set(
+ "sb-refresh-token",
+ data.session.refresh_token,
+ cookieOptions,
+ );
+ }
+
+ // Update user info cookie
+ response.cookies.set(
+ "sb-user-info",
+ JSON.stringify({
+ id: data.session.user.id,
+ email: data.session.user.email,
+ user_metadata: data.session.user.user_metadata,
+ }),
+ {
+ ...cookieOptions,
+ httpOnly: false,
+ },
+ );
+
+ return response;
+ } catch (err) {
+ console.error("[Auth Refresh] Error:", err);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 },
+ );
+ }
+}
diff --git a/app/api/supabase.ts b/app/api/supabase.ts
index 5dbc05885..170921d39 100644
--- a/app/api/supabase.ts
+++ b/app/api/supabase.ts
@@ -1,5 +1,5 @@
import { createClient } from "@supabase/supabase-js";
-import { NextRequest } from "next/server";
+import { NextRequest, NextResponse } from "next/server";
const SUPABASE_URL = process.env.SUPABASE_URL!;
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY!;
@@ -42,3 +42,155 @@ export async function checkAuth(req: NextRequest) {
return null;
}
}
+
+// Enhanced auth check with token refresh capability
+export async function checkAuthWithRefresh(req: NextRequest): Promise<{
+ user: any | null;
+ response?: NextResponse;
+ needsRefresh?: boolean;
+}> {
+ const authToken = req.cookies.get("sb-access-token")?.value;
+ const refreshToken = req.cookies.get("sb-refresh-token")?.value;
+
+ if (!authToken) {
+ console.log("[Supabase] No auth token found");
+ return { user: null };
+ }
+
+ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
+ global: {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ },
+ },
+ });
+
+ try {
+ const { data, error } = await supabase.auth.getUser();
+
+ if (error || !data?.user) {
+ // Token might be expired, try to refresh if refresh token is available
+ if (refreshToken && error?.message?.includes("JWT expired")) {
+ console.log("[Supabase] Access token expired, attempting refresh");
+
+ try {
+ const { data: refreshData, error: refreshError } =
+ await supabase.auth.refreshSession({
+ refresh_token: refreshToken,
+ });
+
+ if (refreshError || !refreshData?.session) {
+ console.error("[Supabase] Token refresh failed:", refreshError);
+ return { user: null };
+ }
+
+ console.log("[Supabase] Token refreshed successfully");
+
+ // Create response with updated cookies
+ const response = new NextResponse();
+ const cookieOptions = {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax" as const,
+ maxAge: 60 * 60 * 24 * 7, // 7 days
+ path: "/",
+ };
+
+ response.cookies.set(
+ "sb-access-token",
+ refreshData.session.access_token,
+ cookieOptions,
+ );
+
+ if (refreshData.session.refresh_token) {
+ response.cookies.set(
+ "sb-refresh-token",
+ refreshData.session.refresh_token,
+ cookieOptions,
+ );
+ }
+
+ // Update user info cookie
+ response.cookies.set(
+ "sb-user-info",
+ JSON.stringify({
+ id: refreshData.session.user.id,
+ email: refreshData.session.user.email,
+ user_metadata: refreshData.session.user.user_metadata,
+ }),
+ {
+ ...cookieOptions,
+ httpOnly: false,
+ },
+ );
+
+ return {
+ user: refreshData.session.user,
+ response,
+ needsRefresh: true,
+ };
+ } catch (refreshErr) {
+ console.error("[Supabase] Error during token refresh:", refreshErr);
+ return { user: null };
+ }
+ }
+
+ console.error("[Supabase] Error getting user:", error);
+ return { user: null };
+ }
+
+ console.log("[Supabase] Authenticated user:", data.user);
+ return { user: data.user };
+ } catch (err) {
+ console.error("[Supabase] Error fetching user data:", err);
+ return { user: null };
+ }
+}
+
+// Utility function to get user info from cookie (client-side accessible)
+export function getUserInfoFromCookie(req: NextRequest) {
+ const userInfoCookie = req.cookies.get("sb-user-info")?.value;
+
+ if (!userInfoCookie) {
+ return null;
+ }
+
+ try {
+ return JSON.parse(userInfoCookie);
+ } catch (err) {
+ console.error("[Supabase] Error parsing user info cookie:", err);
+ return null;
+ }
+}
+
+// Middleware helper to protect routes
+export function createAuthMiddleware(protectedPaths: string[] = []) {
+ return async function authMiddleware(req: NextRequest) {
+ const { pathname } = req.nextUrl;
+
+ // Check if this path requires authentication
+ const isProtectedPath = protectedPaths.some(
+ (path) => pathname.startsWith(path) || pathname === path,
+ );
+
+ if (!isProtectedPath) {
+ return NextResponse.next();
+ }
+
+ const authResult = await checkAuthWithRefresh(req);
+
+ if (!authResult.user) {
+ // Redirect to login if not authenticated
+ const loginUrl = new URL("/login", req.url);
+ loginUrl.searchParams.set("redirect_to", pathname);
+ return NextResponse.redirect(loginUrl);
+ }
+
+ // If token was refreshed, return the response with updated cookies
+ if (authResult.needsRefresh && authResult.response) {
+ return authResult.response;
+ }
+
+ return NextResponse.next();
+ };
+}
diff --git a/app/api/user/route.ts b/app/api/user/route.ts
new file mode 100644
index 000000000..37a201307
--- /dev/null
+++ b/app/api/user/route.ts
@@ -0,0 +1,102 @@
+import { NextRequest, NextResponse } from "next/server";
+import { checkAuthWithRefresh } from "../supabase";
+
+export async function GET(req: NextRequest) {
+ console.log("[User API] Processing user info request");
+
+ try {
+ const authResult = await checkAuthWithRefresh(req);
+
+ if (!authResult.user) {
+ return NextResponse.json(
+ { error: "Authentication required" },
+ { status: 401 },
+ );
+ }
+
+ console.log("[User API] Returning user info for:", authResult.user.email);
+
+ // Create response
+ const response = NextResponse.json({
+ user: {
+ id: authResult.user.id,
+ email: authResult.user.email,
+ user_metadata: authResult.user.user_metadata,
+ created_at: authResult.user.created_at,
+ updated_at: authResult.user.updated_at,
+ last_sign_in_at: authResult.user.last_sign_in_at,
+ },
+ });
+
+ // If token was refreshed, merge the refreshed cookies
+ if (authResult.needsRefresh && authResult.response) {
+ authResult.response.cookies.getAll().forEach((cookie: any) => {
+ response.cookies.set(cookie.name, cookie.value, {
+ httpOnly: cookie.httpOnly,
+ secure: cookie.secure,
+ sameSite: cookie.sameSite,
+ maxAge: cookie.maxAge,
+ path: cookie.path,
+ });
+ });
+ }
+
+ return response;
+ } catch (error) {
+ console.error("[User API] Error:", error);
+ return NextResponse.json(
+ { error: "Failed to fetch user information" },
+ { status: 500 },
+ );
+ }
+}
+
+export async function PUT(req: NextRequest) {
+ console.log("[User API] Processing user update request");
+
+ try {
+ const authResult = await checkAuthWithRefresh(req);
+
+ if (!authResult.user) {
+ return NextResponse.json(
+ { error: "Authentication required" },
+ { status: 401 },
+ );
+ }
+
+ const body = await req.json();
+ const { user_metadata } = body;
+
+ // Note: In a real application, you would update the user in Supabase here
+ // For now, we'll just return the current user data
+ console.log("[User API] User update requested for:", authResult.user.email);
+ console.log("[User API] Update data:", user_metadata);
+
+ // Create response
+ const response = NextResponse.json({
+ user: authResult.user,
+ message: "User update functionality would be implemented here",
+ });
+
+ // If token was refreshed, merge the refreshed cookies
+ if (authResult.needsRefresh && authResult.response) {
+ authResult.response.cookies.getAll().forEach((cookie: any) => {
+ response.cookies.set(cookie.name, cookie.value, {
+ httpOnly: cookie.httpOnly,
+ secure: cookie.secure,
+ sameSite: cookie.sameSite,
+ maxAge: cookie.maxAge,
+ path: cookie.path,
+ });
+ });
+ }
+
+ return response;
+ } catch (error) {
+ console.error("[User API] Error:", error);
+ return NextResponse.json(
+ { error: "Failed to update user information" },
+ { status: 500 },
+ );
+ }
+}
diff --git a/app/auth-demo/page.tsx b/app/auth-demo/page.tsx
new file mode 100644
index 000000000..f575f3bf1
--- /dev/null
+++ b/app/auth-demo/page.tsx
@@ -0,0 +1,220 @@
+"use client";
+
+import React from "react";
+import { useAuth } from "../hooks/useAuth";
+
+export default function AuthDemoPage() {
+ const { user, authenticated, loading, logout } = useAuth();
+
+ if (loading) {
+ return (
+
+
+
+
Loading authentication status...
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ 🔐 Authentication System Demo
+
+
+
+
+ {/* Authentication Status Card */}
+
+
+ Authentication Status
+
+
+
+
+
+ Status:
+
+
+ {authenticated ? "✅ Authenticated" : "❌ Not Authenticated"}
+
+
+
+ {user?.id && (
+
+
+ User ID:
+
+
+ {user.id}
+
+
+ )}
+
+ {user?.email && (
+
+
+ Email:
+
+ {user.email}
+
+ )}
+
+
+ {authenticated && (
+
+
+
+ )}
+
+
+ {/* Quick Actions Card */}
+
+
+ Quick Actions
+
+
+
+
+
+ {/* Authentication Flow Card */}
+
+
+ 🔄 Authentication Flow
+
+
+
+
+
+ 1. Token Authentication via URL
+
+
+ GET
+ /api/auth/callback?token=YOUR_SUPABASE_TOKEN&refresh_token=REFRESH_TOKEN&redirect_to=/profile
+
+
+
+
+
+ 2. Programmatic Login via POST
+
+
+ POST /api/auth/callback
+
+ {`{"access_token": "token", "refresh_token": "refresh"}`}
+
+
+
+
+
+ 3. Manual Token Entry
+
+
+ Visit the{" "}
+
+ login page
+ {" "}
+ to enter your Supabase tokens manually.
+
+
+
+
+
+ {/* API Endpoints Card */}
+
+
+ 🚀 Available API Endpoints
+
+
+
+
+
+ Authentication
+
+
+ -
+
GET /api/auth/check
- Check auth status
+
+ -
+
POST /api/auth/callback
- Login with tokens
+
+ -
+
POST /api/auth/refresh
- Refresh access token
+
+ -
+
POST /api/auth/logout
- Logout and clear
+ cookies
+
+
+
+
+
+
+ Protected Routes
+
+
+ -
+
GET /api/user
- Get user information
+
+ -
+
GET /profile
- User profile page
+
+ -
+
GET /chat
- Chat interface (if implemented)
+
+ -
+
GET /settings
- Settings page (if implemented)
+
+
+
+
+
+
+
+
+
+ 📚 See AUTH_SYSTEM.md
for detailed documentation
+
+
+
+
+ );
+}
diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index 4712b03f5..e1bf0ead4 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -1458,7 +1458,7 @@ function _Chat() {
const autoFocus = isIOS() ? false : !isMobileScreen; // wont auto focus on mobile screen
- console.log("tu dong focus:", autoFocus);
+ // console.log("tu dong focus:", autoFocus);
const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
diff --git a/app/components/storage-initializer.tsx b/app/components/storage-initializer.tsx
new file mode 100644
index 000000000..b65f34f05
--- /dev/null
+++ b/app/components/storage-initializer.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import { useEffect } from "react";
+
+export default function StorageInitializer() {
+ useEffect(() => {
+ // Initialize storage migration and debugging in development
+ if (process.env.NODE_ENV === "development") {
+ // Import and initialize storage utilities
+ Promise.all([
+ import("../utils/storage-debug"),
+ import("../utils/storage-migration"),
+ ])
+ .then(([storageDebug, storageMigration]) => {
+ console.log("🔧 Storage utilities loaded");
+
+ // Run a quick health check
+ if ((window as any).debugStorage?.checkStorageHealth) {
+ (window as any).debugStorage.checkStorageHealth();
+ }
+ })
+ .catch((error) => {
+ console.error("Failed to load storage utilities:", error);
+ });
+ }
+ }, []);
+
+ return null; // This component doesn't render anything
+}
diff --git a/app/constant.ts b/app/constant.ts
index 53bd011bc..c746124c5 100644
--- a/app/constant.ts
+++ b/app/constant.ts
@@ -102,6 +102,7 @@ export enum StoreKey {
Sync = "sync",
SdList = "sd-list",
Mcp = "mcp-store",
+ Auth = "auth-store",
}
export const DEFAULT_SIDEBAR_WIDTH = 300;
diff --git a/app/hooks/useAuth.ts b/app/hooks/useAuth.ts
new file mode 100644
index 000000000..17a6240ce
--- /dev/null
+++ b/app/hooks/useAuth.ts
@@ -0,0 +1,227 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+
+export interface User {
+ id: string;
+ email?: string;
+ user_metadata?: any;
+}
+
+export interface AuthState {
+ user: User | null;
+ loading: boolean;
+ authenticated: boolean;
+}
+
+export function useAuth() {
+ const [authState, setAuthState] = useState({
+ user: null,
+ loading: true,
+ authenticated: false,
+ });
+
+ const checkAuth = async () => {
+ try {
+ const response = await fetch("/api/auth/check", {
+ method: "GET",
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data.authenticated) {
+ setAuthState({
+ user: data.user,
+ loading: false,
+ authenticated: true,
+ });
+ return data.user;
+ }
+ }
+
+ setAuthState({
+ user: null,
+ loading: false,
+ authenticated: false,
+ });
+ return null;
+ } catch (error) {
+ console.error("Auth check failed:", error);
+ setAuthState({
+ user: null,
+ loading: false,
+ authenticated: false,
+ });
+ return null;
+ }
+ };
+
+ const login = async (accessToken: string, refreshToken?: string) => {
+ try {
+ const response = await fetch("/api/auth/callback", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ credentials: "include",
+ body: JSON.stringify({
+ access_token: accessToken,
+ refresh_token: refreshToken,
+ }),
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setAuthState({
+ user: data.user,
+ loading: false,
+ authenticated: true,
+ });
+ return data.user;
+ } else {
+ throw new Error("Login failed");
+ }
+ } catch (error) {
+ console.error("Login failed:", error);
+ setAuthState({
+ user: null,
+ loading: false,
+ authenticated: false,
+ });
+ throw error;
+ }
+ };
+
+ const logout = async () => {
+ try {
+ await fetch("/api/auth/logout", {
+ method: "POST",
+ credentials: "include",
+ });
+ } catch (error) {
+ console.error("Logout request failed:", error);
+ } finally {
+ setAuthState({
+ user: null,
+ loading: false,
+ authenticated: false,
+ });
+ // Redirect to login page
+ window.location.href = "/login";
+ }
+ };
+
+ const refreshToken = async () => {
+ try {
+ const response = await fetch("/api/auth/refresh", {
+ method: "POST",
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setAuthState({
+ user: data.user,
+ loading: false,
+ authenticated: true,
+ });
+ return data.user;
+ } else {
+ // Refresh failed, user needs to login again
+ setAuthState({
+ user: null,
+ loading: false,
+ authenticated: false,
+ });
+ return null;
+ }
+ } catch (error) {
+ console.error("Token refresh failed:", error);
+ setAuthState({
+ user: null,
+ loading: false,
+ authenticated: false,
+ });
+ return null;
+ }
+ };
+
+ const getUserFromCookie = () => {
+ if (typeof window === "undefined") return null;
+
+ try {
+ const userInfoCookie = document.cookie
+ .split("; ")
+ .find((row) => row.startsWith("sb-user-info="));
+
+ if (userInfoCookie) {
+ const userInfo = decodeURIComponent(userInfoCookie.split("=")[1]);
+ return JSON.parse(userInfo);
+ }
+ } catch (error) {
+ console.error("Error reading user cookie:", error);
+ }
+
+ return null;
+ };
+
+ useEffect(() => {
+ // Try to get user from cookie first for faster initial load
+ const cachedUser = getUserFromCookie();
+ if (cachedUser) {
+ setAuthState({
+ user: cachedUser,
+ loading: true, // Still check with server
+ authenticated: true,
+ });
+ }
+
+ // Then verify with server
+ checkAuth();
+ }, []);
+
+ return {
+ ...authState,
+ checkAuth,
+ login,
+ logout,
+ refreshToken,
+ getUserFromCookie,
+ };
+}
+
+// Higher-order component for protecting routes
+export function withAuth>(
+ Component: React.ComponentType,
+) {
+ const AuthenticatedComponent = (props: T) => {
+ const { authenticated, loading } = useAuth();
+
+ useEffect(() => {
+ if (!loading && !authenticated) {
+ // Redirect to login if not authenticated
+ const currentPath = window.location.pathname;
+ window.location.href = `/login?redirect_to=${encodeURIComponent(
+ currentPath,
+ )}`;
+ }
+ }, [authenticated, loading]);
+
+ if (loading) {
+ return React.createElement("div", null, "Loading...");
+ }
+
+ if (!authenticated) {
+ return React.createElement("div", null, "Redirecting to login...");
+ }
+
+ return React.createElement(Component, props);
+ };
+
+ AuthenticatedComponent.displayName = `withAuth(${
+ Component.displayName || Component.name
+ })`;
+
+ return AuthenticatedComponent;
+}
diff --git a/app/hooks/useAuth.tsx b/app/hooks/useAuth.tsx
new file mode 100644
index 000000000..eb4023462
--- /dev/null
+++ b/app/hooks/useAuth.tsx
@@ -0,0 +1,217 @@
+import React, { useState, useEffect } from "react";
+
+export interface User {
+ id: string;
+ email?: string;
+ user_metadata?: any;
+}
+
+export interface AuthState {
+ user: User | null;
+ loading: boolean;
+ authenticated: boolean;
+}
+
+export function useAuth() {
+ const [authState, setAuthState] = useState({
+ user: null,
+ loading: true,
+ authenticated: false,
+ });
+
+ const checkAuth = async () => {
+ try {
+ const response = await fetch("/api/auth/check", {
+ method: "GET",
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data.authenticated) {
+ setAuthState({
+ user: data.user,
+ loading: false,
+ authenticated: true,
+ });
+ return data.user;
+ }
+ }
+
+ setAuthState({
+ user: null,
+ loading: false,
+ authenticated: false,
+ });
+ return null;
+ } catch (error) {
+ console.error("Auth check failed:", error);
+ setAuthState({
+ user: null,
+ loading: false,
+ authenticated: false,
+ });
+ return null;
+ }
+ };
+
+ const login = async (accessToken: string, refreshToken?: string) => {
+ try {
+ const response = await fetch("/api/auth/callback", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ credentials: "include",
+ body: JSON.stringify({
+ access_token: accessToken,
+ refresh_token: refreshToken,
+ }),
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setAuthState({
+ user: data.user,
+ loading: false,
+ authenticated: true,
+ });
+ return data.user;
+ } else {
+ throw new Error("Login failed");
+ }
+ } catch (error) {
+ console.error("Login failed:", error);
+ setAuthState({
+ user: null,
+ loading: false,
+ authenticated: false,
+ });
+ throw error;
+ }
+ };
+
+ const logout = async () => {
+ try {
+ await fetch("/api/auth/logout", {
+ method: "POST",
+ credentials: "include",
+ });
+ } catch (error) {
+ console.error("Logout request failed:", error);
+ } finally {
+ setAuthState({
+ user: null,
+ loading: false,
+ authenticated: false,
+ });
+ // Redirect to login page
+ window.location.href = "/login";
+ }
+ };
+
+ const refreshToken = async () => {
+ try {
+ const response = await fetch("/api/auth/refresh", {
+ method: "POST",
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setAuthState({
+ user: data.user,
+ loading: false,
+ authenticated: true,
+ });
+ return data.user;
+ } else {
+ // Refresh failed, user needs to login again
+ setAuthState({
+ user: null,
+ loading: false,
+ authenticated: false,
+ });
+ return null;
+ }
+ } catch (error) {
+ console.error("Token refresh failed:", error);
+ setAuthState({
+ user: null,
+ loading: false,
+ authenticated: false,
+ });
+ return null;
+ }
+ };
+
+ const getUserFromCookie = () => {
+ if (typeof window === "undefined") return null;
+
+ try {
+ const userInfoCookie = document.cookie
+ .split("; ")
+ .find((row) => row.startsWith("sb-user-info="));
+
+ if (userInfoCookie) {
+ const userInfo = decodeURIComponent(userInfoCookie.split("=")[1]);
+ return JSON.parse(userInfo);
+ }
+ } catch (error) {
+ console.error("Error reading user cookie:", error);
+ }
+
+ return null;
+ };
+
+ useEffect(() => {
+ // Try to get user from cookie first for faster initial load
+ const cachedUser = getUserFromCookie();
+ if (cachedUser) {
+ setAuthState({
+ user: cachedUser,
+ loading: true, // Still check with server
+ authenticated: true,
+ });
+ }
+
+ // Then verify with server
+ checkAuth();
+ }, []);
+
+ return {
+ ...authState,
+ checkAuth,
+ login,
+ logout,
+ refreshToken,
+ getUserFromCookie,
+ };
+}
+
+// Higher-order component for protecting routes
+export function withAuth(Component: React.ComponentType) {
+ return function AuthenticatedComponent(props: T) {
+ const { authenticated, loading, user } = useAuth();
+
+ useEffect(() => {
+ if (!loading && !authenticated) {
+ // Redirect to login if not authenticated
+ const currentPath = window.location.pathname;
+ window.location.href = `/login?redirect_to=${encodeURIComponent(
+ currentPath,
+ )}`;
+ }
+ }, [authenticated, loading]);
+
+ if (loading) {
+ return Loading...
;
+ }
+
+ if (!authenticated) {
+ return Redirecting to login...
;
+ }
+
+ return ;
+ };
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index 000b497e8..f6a2e05c9 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -8,6 +8,7 @@ import { SpeedInsights } from "@vercel/speed-insights/next";
import { GoogleTagManager, GoogleAnalytics } from "@next/third-parties/google";
import { getServerSideConfig } from "./config/server";
import SyncOnFirstLoad from "./SyncOnFirstLoad";
+import StorageInitializer from "./components/storage-initializer";
const TITLE = "Chebi Chat - Trợ lý AI học tiếng Trung";
export const metadata: Metadata = {
@@ -58,6 +59,9 @@ export default function RootLayout({
{children}
+ {/* Storage System Initialization */}
+
+
{serverConfig?.isVercel && (
<>
diff --git a/app/login/page.tsx b/app/login/page.tsx
new file mode 100644
index 000000000..8f45aef9a
--- /dev/null
+++ b/app/login/page.tsx
@@ -0,0 +1,150 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { useAuth } from "../hooks/useAuth";
+
+export default function LoginPage() {
+ const [token, setToken] = useState("");
+ const [refreshToken, setRefreshToken] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState("");
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const { login, authenticated } = useAuth();
+
+ const redirectTo = searchParams.get("redirect_to") || "/";
+ const errorParam = searchParams.get("error");
+
+ useEffect(() => {
+ if (authenticated) {
+ router.push(redirectTo);
+ }
+ }, [authenticated, redirectTo, router]);
+
+ useEffect(() => {
+ if (errorParam) {
+ switch (errorParam) {
+ case "no_token":
+ setError("No authentication token provided");
+ break;
+ case "invalid_token":
+ setError("Invalid authentication token");
+ break;
+ case "auth_failed":
+ setError("Authentication failed");
+ break;
+ case "auth_check_failed":
+ setError("Authentication check failed");
+ break;
+ default:
+ setError("An error occurred during authentication");
+ }
+ }
+ }, [errorParam]);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!token.trim()) {
+ setError("Please enter an access token");
+ return;
+ }
+
+ setLoading(true);
+ setError("");
+
+ try {
+ await login(token.trim(), refreshToken.trim() || undefined);
+ router.push(redirectTo);
+ } catch (err) {
+ setError("Login failed. Please check your token and try again.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (authenticated) {
+ return Redirecting...
;
+ }
+
+ return (
+
+
+
+
+ Sign in to your account
+
+
+ Enter your Supabase authentication token
+
+
+
+
+
+
+
+
+ You can also authenticate by visiting:{" "}
+
+ /api/auth/callback?token=YOUR_TOKEN
+
+
+
+
+
+
+ );
+}
diff --git a/app/profile/page.tsx b/app/profile/page.tsx
new file mode 100644
index 000000000..6c8beb40a
--- /dev/null
+++ b/app/profile/page.tsx
@@ -0,0 +1,74 @@
+"use client";
+
+import React from "react";
+import { useAuth, withAuth } from "../hooks/useAuth";
+
+function ProfilePage() {
+ const { user, logout } = useAuth();
+
+ const handleLogout = () => {
+ logout();
+ };
+
+ return (
+
+
+
+
+
+ Profile Information
+
+
+
+
+
+
{user?.id}
+
+
+ {user?.email && (
+
+
+
{user.email}
+
+ )}
+
+ {user?.user_metadata &&
+ Object.keys(user.user_metadata).length > 0 && (
+
+
+
+ {JSON.stringify(user.user_metadata, null, 2)}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default withAuth(ProfilePage);
diff --git a/app/utils/indexedDB-storage.ts b/app/utils/indexedDB-storage.ts
index 51417e9f3..b24a27ca2 100644
--- a/app/utils/indexedDB-storage.ts
+++ b/app/utils/indexedDB-storage.ts
@@ -5,41 +5,213 @@ import { safeLocalStorage } from "@/app/utils";
const localStorage = safeLocalStorage();
class IndexedDBStorage implements StateStorage {
+ private isIndexedDBAvailable = true;
+
public async getItem(name: string): Promise {
try {
- const value = (await get(name)) || localStorage.getItem(name);
- return value;
+ // First try IndexedDB
+ if (this.isIndexedDBAvailable) {
+ const value = await get(name);
+ if (value !== undefined) {
+ return value;
+ }
+ }
+
+ // Fallback to localStorage
+ const localValue = localStorage.getItem(name);
+ console.log(`[IndexedDB] Retrieved from localStorage for key: ${name}`);
+ return localValue;
} catch (error) {
+ console.error(`[IndexedDB] Error getting item ${name}:`, error);
+ this.isIndexedDBAvailable = false;
return localStorage.getItem(name);
}
}
public async setItem(name: string, value: string): Promise {
try {
- const _value = JSON.parse(value);
- if (!_value?.state?._hasHydrated) {
- console.warn("skip setItem", name);
+ // Validate JSON structure
+ let parsedValue;
+ try {
+ parsedValue = JSON.parse(value);
+ } catch (parseError) {
+ console.error(`[IndexedDB] Invalid JSON for key ${name}:`, parseError);
+ // Still try to store the raw value
+ parsedValue = null;
+ }
+
+ // Check if this is a Zustand store with hydration state
+ const isZustandStore =
+ parsedValue &&
+ typeof parsedValue === "object" &&
+ parsedValue.state &&
+ typeof parsedValue.state === "object";
+
+ // For Zustand stores, check hydration status
+ if (isZustandStore) {
+ const hasHydrated = parsedValue.state._hasHydrated;
+
+ // Allow storage if:
+ // 1. Already hydrated, OR
+ // 2. Initial state (not hydrated but has meaningful data)
+ const shouldStore =
+ hasHydrated ||
+ (parsedValue.state.sessions &&
+ parsedValue.state.sessions.length > 0) ||
+ Object.keys(parsedValue.state).length > 2; // More than just _hasHydrated and version
+
+ if (!shouldStore) {
+ console.log(
+ `[IndexedDB] Skipping storage for ${name} - not hydrated and no meaningful data`,
+ );
+ return;
+ }
+ }
+
+ // Try IndexedDB first
+ if (this.isIndexedDBAvailable) {
+ await set(name, value);
+ console.log(`[IndexedDB] Successfully stored ${name} in IndexedDB`);
+
+ // Also store in localStorage as backup for critical stores
+ if (
+ name.includes("chat") ||
+ name.includes("config") ||
+ name.includes("access")
+ ) {
+ localStorage.setItem(name, value);
+ }
return;
}
- await set(name, value);
- } catch (error) {
+
+ // Fallback to localStorage
localStorage.setItem(name, value);
+ console.log(
+ `[IndexedDB] Stored ${name} in localStorage (IndexedDB unavailable)`,
+ );
+ } catch (error) {
+ console.error(`[IndexedDB] Error setting item ${name}:`, error);
+ this.isIndexedDBAvailable = false;
+
+ // Always fallback to localStorage on error
+ try {
+ localStorage.setItem(name, value);
+ console.log(`[IndexedDB] Fallback: stored ${name} in localStorage`);
+ } catch (localError) {
+ console.error(
+ `[IndexedDB] Failed to store ${name} in localStorage:`,
+ localError,
+ );
+ }
}
}
public async removeItem(name: string): Promise {
try {
- await del(name);
+ // Remove from both storages to ensure cleanup
+ if (this.isIndexedDBAvailable) {
+ await del(name);
+ }
+ localStorage.removeItem(name);
+ console.log(`[IndexedDB] Removed ${name} from both storages`);
} catch (error) {
+ console.error(`[IndexedDB] Error removing item ${name}:`, error);
+ this.isIndexedDBAvailable = false;
localStorage.removeItem(name);
}
}
public async clear(): Promise {
try {
- await clear();
- } catch (error) {
+ // Clear both storages
+ if (this.isIndexedDBAvailable) {
+ await clear();
+ }
localStorage.clear();
+ console.log(`[IndexedDB] Cleared both storages`);
+ } catch (error) {
+ console.error(`[IndexedDB] Error clearing storage:`, error);
+ this.isIndexedDBAvailable = false;
+ localStorage.clear();
+ }
+ }
+
+ // Utility method to check storage health
+ public async checkHealth(): Promise<{
+ indexedDB: boolean;
+ localStorage: boolean;
+ }> {
+ const health = {
+ indexedDB: false,
+ localStorage: false,
+ };
+
+ // Test IndexedDB
+ try {
+ await set("health-check", "test");
+ await get("health-check");
+ await del("health-check");
+ health.indexedDB = true;
+ this.isIndexedDBAvailable = true;
+ } catch (error) {
+ console.warn("[IndexedDB] Health check failed:", error);
+ this.isIndexedDBAvailable = false;
+ }
+
+ // Test localStorage
+ try {
+ localStorage.setItem("health-check", "test");
+ localStorage.getItem("health-check");
+ localStorage.removeItem("health-check");
+ health.localStorage = true;
+ } catch (error) {
+ console.warn("[IndexedDB] localStorage health check failed:", error);
+ }
+
+ return health;
+ }
+
+ // Method to migrate data from localStorage to IndexedDB
+ public async migrateFromLocalStorage(): Promise {
+ if (!this.isIndexedDBAvailable) {
+ console.warn("[IndexedDB] Cannot migrate - IndexedDB unavailable");
+ return;
+ }
+
+ try {
+ // Get all store keys from constants
+ const storeKeys = [
+ "chat-next-web-store",
+ "chat-next-web-plugin",
+ "access-control",
+ "app-config",
+ "mask-store",
+ "prompt-store",
+ "chat-update",
+ "sync",
+ "sd-list",
+ "mcp-store",
+ ];
+
+ for (const key of storeKeys) {
+ const localValue = localStorage.getItem(key);
+ if (localValue) {
+ try {
+ // Check if already exists in IndexedDB
+ const existingValue = await get(key);
+ if (!existingValue) {
+ await set(key, localValue);
+ console.log(
+ `[IndexedDB] Migrated ${key} from localStorage to IndexedDB`,
+ );
+ }
+ } catch (error) {
+ console.error(`[IndexedDB] Failed to migrate ${key}:`, error);
+ }
+ }
+ }
+ } catch (error) {
+ console.error("[IndexedDB] Migration failed:", error);
}
}
}
diff --git a/app/utils/storage-debug.ts b/app/utils/storage-debug.ts
new file mode 100644
index 000000000..1ceaf0a0d
--- /dev/null
+++ b/app/utils/storage-debug.ts
@@ -0,0 +1,236 @@
+import { indexedDBStorage } from "./indexedDB-storage";
+import { StoreKey } from "../constant";
+
+// Storage debugging utilities
+export class StorageDebugger {
+ static async checkStorageHealth() {
+ console.log("🔍 Checking storage health...");
+ const health = await indexedDBStorage.checkHealth();
+
+ console.log("📊 Storage Health Report:");
+ console.log(
+ ` - IndexedDB: ${health.indexedDB ? "✅ Available" : "❌ Unavailable"}`,
+ );
+ console.log(
+ ` - localStorage: ${
+ health.localStorage ? "✅ Available" : "❌ Unavailable"
+ }`,
+ );
+
+ return health;
+ }
+
+ static async listAllStoreData() {
+ console.log("📋 Listing all store data...");
+
+ const storeKeys = Object.values(StoreKey);
+ const storeData: Record = {};
+
+ for (const key of storeKeys) {
+ try {
+ const data = await indexedDBStorage.getItem(key);
+ if (data) {
+ const parsed = JSON.parse(data);
+ storeData[key] = {
+ size: data.length,
+ hasState: !!parsed.state,
+ hasHydrated: parsed.state?._hasHydrated || false,
+ lastUpdateTime: parsed.state?.lastUpdateTime || 0,
+ keys: Object.keys(parsed.state || {}),
+ };
+
+ // Special handling for chat store
+ if (key === StoreKey.Chat && parsed.state?.sessions) {
+ storeData[key].sessionCount = parsed.state.sessions.length;
+ storeData[key].currentSessionIndex =
+ parsed.state.currentSessionIndex;
+ }
+ } else {
+ storeData[key] = null;
+ }
+ } catch (error) {
+ console.error(`Error reading store ${key}:`, error);
+ storeData[key] = {
+ error: error instanceof Error ? error.message : String(error),
+ };
+ }
+ }
+
+ console.table(storeData);
+ return storeData;
+ }
+
+ static async migrateData() {
+ console.log("🔄 Starting data migration...");
+ await indexedDBStorage.migrateFromLocalStorage();
+ console.log("✅ Migration completed");
+ }
+
+ static async clearStore(storeKey: StoreKey) {
+ console.log(`🗑️ Clearing store: ${storeKey}`);
+ try {
+ await indexedDBStorage.removeItem(storeKey);
+ console.log(`✅ Store ${storeKey} cleared successfully`);
+ } catch (error) {
+ console.error(`❌ Failed to clear store ${storeKey}:`, error);
+ }
+ }
+
+ static async clearAllStores() {
+ console.log("🗑️ Clearing all stores...");
+ try {
+ await indexedDBStorage.clear();
+ console.log("✅ All stores cleared successfully");
+ } catch (error) {
+ console.error("❌ Failed to clear all stores:", error);
+ }
+ }
+
+ static async backupStores() {
+ console.log("💾 Creating backup of all stores...");
+
+ const backup: Record = {
+ timestamp: new Date().toISOString(),
+ stores: {},
+ };
+
+ const storeKeys = Object.values(StoreKey);
+
+ for (const key of storeKeys) {
+ try {
+ const data = await indexedDBStorage.getItem(key);
+ if (data) {
+ backup.stores[key] = data;
+ }
+ } catch (error) {
+ console.error(`Error backing up store ${key}:`, error);
+ backup.stores[key] = {
+ error: error instanceof Error ? error.message : String(error),
+ };
+ }
+ }
+
+ // Save backup to localStorage as well
+ try {
+ const backupString = JSON.stringify(backup);
+ localStorage.setItem("store-backup", backupString);
+ console.log("💾 Backup saved to localStorage");
+
+ // Also download as file
+ const blob = new Blob([backupString], { type: "application/json" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `store-backup-${
+ new Date().toISOString().split("T")[0]
+ }.json`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+
+ console.log("💾 Backup downloaded as file");
+ } catch (error) {
+ console.error("❌ Failed to save backup:", error);
+ }
+
+ return backup;
+ }
+
+ static async restoreFromBackup(backupData: any) {
+ console.log("♻️ Restoring from backup...");
+
+ if (!backupData || !backupData.stores) {
+ throw new Error("Invalid backup data");
+ }
+
+ for (const [key, data] of Object.entries(backupData.stores)) {
+ if (typeof data === "string") {
+ try {
+ await indexedDBStorage.setItem(key, data);
+ console.log(`✅ Restored store: ${key}`);
+ } catch (error) {
+ console.error(`❌ Failed to restore store ${key}:`, error);
+ }
+ }
+ }
+
+ console.log("♻️ Restore completed");
+ }
+
+ static async validateStoreIntegrity() {
+ console.log("🔍 Validating store integrity...");
+
+ const issues: string[] = [];
+ const storeKeys = Object.values(StoreKey);
+
+ for (const key of storeKeys) {
+ try {
+ const data = await indexedDBStorage.getItem(key);
+ if (data) {
+ const parsed = JSON.parse(data);
+
+ // Basic structure validation
+ if (!parsed.state) {
+ issues.push(`${key}: Missing state object`);
+ }
+
+ if (
+ parsed.state &&
+ typeof parsed.state._hasHydrated === "undefined"
+ ) {
+ issues.push(`${key}: Missing _hasHydrated flag`);
+ }
+
+ // Store-specific validation
+ if (key === StoreKey.Chat) {
+ if (
+ !parsed.state?.sessions ||
+ !Array.isArray(parsed.state.sessions)
+ ) {
+ issues.push(`${key}: Invalid or missing sessions array`);
+ }
+
+ if (typeof parsed.state?.currentSessionIndex !== "number") {
+ issues.push(`${key}: Invalid currentSessionIndex`);
+ }
+ }
+ }
+ } catch (error) {
+ issues.push(
+ `${key}: JSON parse error - ${
+ error instanceof Error ? error.message : String(error)
+ }`,
+ );
+ }
+ }
+
+ if (issues.length === 0) {
+ console.log("✅ All stores have valid integrity");
+ } else {
+ console.warn("⚠️ Store integrity issues found:");
+ issues.forEach((issue) => console.warn(` - ${issue}`));
+ }
+
+ return issues;
+ }
+}
+
+// Global debugging functions for console use
+if (typeof window !== "undefined") {
+ (window as any).debugStorage = StorageDebugger;
+
+ // Add helpful console messages
+ console.log(`
+🔧 Storage Debug Utils Available:
+- debugStorage.checkStorageHealth()
+- debugStorage.listAllStoreData()
+- debugStorage.migrateData()
+- debugStorage.clearStore(StoreKey.Chat)
+- debugStorage.clearAllStores()
+- debugStorage.backupStores()
+- debugStorage.validateStoreIntegrity()
+
+Example: debugStorage.listAllStoreData()
+ `);
+}
diff --git a/app/utils/storage-migration.ts b/app/utils/storage-migration.ts
new file mode 100644
index 000000000..b345a0f6b
--- /dev/null
+++ b/app/utils/storage-migration.ts
@@ -0,0 +1,319 @@
+import { StoreKey } from "../constant";
+import { indexedDBStorage } from "./indexedDB-storage";
+import { safeLocalStorage } from "../utils";
+
+const localStorage = safeLocalStorage();
+
+export class StorageMigration {
+ /**
+ * Initialize storage system and perform necessary migrations
+ */
+ static async initialize(): Promise {
+ console.log("[StorageMigration] Initializing storage system...");
+
+ try {
+ // Check storage health
+ const health = await indexedDBStorage.checkHealth();
+
+ if (health.indexedDB) {
+ console.log("[StorageMigration] IndexedDB available");
+
+ // Attempt to migrate data from localStorage if needed
+ await this.migrateFromLocalStorageIfNeeded();
+ } else if (health.localStorage) {
+ console.log(
+ "[StorageMigration] IndexedDB unavailable, using localStorage",
+ );
+ } else {
+ console.error("[StorageMigration] No storage available!");
+ throw new Error("No storage mechanism available");
+ }
+
+ // Validate existing data
+ await this.validateAndRepairStores();
+ } catch (error) {
+ console.error("[StorageMigration] Initialization failed:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Migrate data from localStorage to IndexedDB if needed
+ */
+ private static async migrateFromLocalStorageIfNeeded(): Promise {
+ const storeKeys = Object.values(StoreKey);
+ let migratedCount = 0;
+
+ for (const key of storeKeys) {
+ try {
+ // Check if data exists in localStorage but not in IndexedDB
+ const localData = localStorage.getItem(key);
+ const indexedData = await indexedDBStorage.getItem(key);
+
+ if (localData && !indexedData) {
+ // Validate the data before migration
+ try {
+ const parsed = JSON.parse(localData);
+ if (parsed && typeof parsed === "object") {
+ await indexedDBStorage.setItem(key, localData);
+ migratedCount++;
+ console.log(
+ `[StorageMigration] Migrated ${key} from localStorage to IndexedDB`,
+ );
+ }
+ } catch (parseError) {
+ console.warn(
+ `[StorageMigration] Skipping invalid data for ${key}:`,
+ parseError,
+ );
+ }
+ }
+ } catch (error) {
+ console.error(`[StorageMigration] Failed to migrate ${key}:`, error);
+ }
+ }
+
+ if (migratedCount > 0) {
+ console.log(
+ `[StorageMigration] Successfully migrated ${migratedCount} stores`,
+ );
+ }
+ }
+
+ /**
+ * Validate and repair store data if necessary
+ */
+ private static async validateAndRepairStores(): Promise {
+ const storeKeys = Object.values(StoreKey);
+
+ for (const key of storeKeys) {
+ try {
+ const data = await indexedDBStorage.getItem(key);
+ if (data) {
+ const repaired = await this.repairStoreData(key, data);
+ if (repaired && repaired !== data) {
+ await indexedDBStorage.setItem(key, repaired);
+ console.log(`[StorageMigration] Repaired data for ${key}`);
+ }
+ }
+ } catch (error) {
+ console.error(`[StorageMigration] Failed to validate ${key}:`, error);
+ }
+ }
+ }
+
+ /**
+ * Repair store data if it has known issues
+ */
+ private static async repairStoreData(
+ storeKey: string,
+ data: string,
+ ): Promise {
+ try {
+ const parsed = JSON.parse(data);
+ let modified = false;
+
+ // Ensure basic structure exists
+ if (!parsed.state) {
+ parsed.state = {};
+ modified = true;
+ }
+
+ // Ensure _hasHydrated flag exists
+ if (typeof parsed.state._hasHydrated === "undefined") {
+ parsed.state._hasHydrated = false;
+ modified = true;
+ }
+
+ // Ensure lastUpdateTime exists
+ if (typeof parsed.state.lastUpdateTime === "undefined") {
+ parsed.state.lastUpdateTime = Date.now();
+ modified = true;
+ }
+
+ // Store-specific repairs
+ switch (storeKey) {
+ case StoreKey.Chat:
+ if (this.repairChatStore(parsed.state)) {
+ modified = true;
+ }
+ break;
+
+ case StoreKey.Config:
+ if (this.repairConfigStore(parsed.state)) {
+ modified = true;
+ }
+ break;
+
+ case StoreKey.Access:
+ if (this.repairAccessStore(parsed.state)) {
+ modified = true;
+ }
+ break;
+ }
+
+ return modified ? JSON.stringify(parsed) : null;
+ } catch (error) {
+ console.error(`[StorageMigration] Failed to repair ${storeKey}:`, error);
+ return null;
+ }
+ }
+
+ /**
+ * Repair chat store specific issues
+ */
+ private static repairChatStore(state: any): boolean {
+ let modified = false;
+
+ // Ensure sessions array exists
+ if (!Array.isArray(state.sessions)) {
+ state.sessions = [];
+ modified = true;
+ }
+
+ // Ensure currentSessionIndex is valid
+ if (
+ typeof state.currentSessionIndex !== "number" ||
+ state.currentSessionIndex < 0 ||
+ state.currentSessionIndex >= state.sessions.length
+ ) {
+ state.currentSessionIndex = 0;
+ modified = true;
+ }
+
+ // Ensure each session has required properties
+ state.sessions.forEach((session: any, index: number) => {
+ if (!session.id) {
+ session.id = `session-${Date.now()}-${index}`;
+ modified = true;
+ }
+
+ if (!Array.isArray(session.messages)) {
+ session.messages = [];
+ modified = true;
+ }
+
+ if (!session.mask) {
+ session.mask = { modelConfig: {} };
+ modified = true;
+ }
+
+ if (typeof session.lastUpdate !== "number") {
+ session.lastUpdate = Date.now();
+ modified = true;
+ }
+ });
+
+ return modified;
+ }
+
+ /**
+ * Repair config store specific issues
+ */
+ private static repairConfigStore(state: any): boolean {
+ let modified = false;
+
+ // Ensure basic config properties exist
+ if (!state.theme) {
+ state.theme = "auto";
+ modified = true;
+ }
+
+ if (!state.models || !Array.isArray(state.models)) {
+ state.models = [];
+ modified = true;
+ }
+
+ return modified;
+ }
+
+ /**
+ * Repair access store specific issues
+ */
+ private static repairAccessStore(state: any): boolean {
+ let modified = false;
+
+ // Ensure access code exists
+ if (typeof state.accessCode !== "string") {
+ state.accessCode = "";
+ modified = true;
+ }
+
+ return modified;
+ }
+
+ /**
+ * Clear corrupted stores and reset to defaults
+ */
+ static async clearCorruptedStores(): Promise {
+ console.log("[StorageMigration] Clearing corrupted stores...");
+
+ const storeKeys = Object.values(StoreKey);
+
+ for (const key of storeKeys) {
+ try {
+ const data = await indexedDBStorage.getItem(key);
+ if (data) {
+ try {
+ JSON.parse(data);
+ } catch (parseError) {
+ console.warn(`[StorageMigration] Clearing corrupted store ${key}`);
+ await indexedDBStorage.removeItem(key);
+ }
+ }
+ } catch (error) {
+ console.error(`[StorageMigration] Error checking ${key}:`, error);
+ }
+ }
+ }
+
+ /**
+ * Create a backup before performing migrations
+ */
+ static async createPreMigrationBackup(): Promise {
+ console.log("[StorageMigration] Creating pre-migration backup...");
+
+ const backup: Record = {
+ timestamp: new Date().toISOString(),
+ type: "pre-migration",
+ stores: {},
+ };
+
+ const storeKeys = Object.values(StoreKey);
+
+ for (const key of storeKeys) {
+ try {
+ const localData = localStorage.getItem(key);
+ const indexedData = await indexedDBStorage.getItem(key);
+
+ backup.stores[key] = {
+ localStorage: localData,
+ indexedDB: indexedData,
+ };
+ } catch (error) {
+ console.error(`[StorageMigration] Error backing up ${key}:`, error);
+ backup.stores[key] = {
+ error: error instanceof Error ? error.message : String(error),
+ };
+ }
+ }
+
+ const backupString = JSON.stringify(backup);
+ localStorage.setItem("pre-migration-backup", backupString);
+
+ console.log("[StorageMigration] Backup created successfully");
+ return backupString;
+ }
+}
+
+// Auto-initialize when module is loaded (client-side only)
+if (typeof window !== "undefined") {
+ // Wait for the page to load before initializing
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", () => {
+ StorageMigration.initialize().catch(console.error);
+ });
+ } else {
+ StorageMigration.initialize().catch(console.error);
+ }
+}
diff --git a/app/utils/store.ts b/app/utils/store.ts
index 04a1d6fcf..ca7e42f7a 100644
--- a/app/utils/store.ts
+++ b/app/utils/store.ts
@@ -47,7 +47,35 @@ export function createPersistStore(
// Gán lại hàm onRehydrateStorage để đánh dấu đã hydrate khi khôi phục dữ liệu
persistOptions.onRehydrateStorage = (state) => {
oldOonRehydrateStorage?.(state);
- return () => state.setHasHydrated(true);
+ return async (state, error) => {
+ if (error) {
+ console.error(
+ `[Store] Hydration failed for ${persistOptions.name}:`,
+ error,
+ );
+ } else {
+ console.log(`[Store] Successfully hydrated ${persistOptions.name}`);
+
+ // Check IndexedDB health on first hydration
+ if (typeof window !== "undefined") {
+ try {
+ const { indexedDBStorage } = await import("./indexedDB-storage");
+ const health = await indexedDBStorage.checkHealth();
+ if (!health.indexedDB) {
+ console.warn(
+ `[Store] IndexedDB unavailable for ${persistOptions.name}, using localStorage fallback`,
+ );
+ }
+ } catch (err) {
+ console.warn(`[Store] Storage health check failed:`, err);
+ }
+ }
+ }
+
+ if (state) {
+ state.setHasHydrated(true);
+ }
+ };
};
// Tạo store với zustand, kết hợp các middleware và phương thức bổ sung
diff --git a/git.sh b/git.sh
index 0ffa7ba54..fa98bc62c 100644
--- a/git.sh
+++ b/git.sh
@@ -2,7 +2,7 @@
# git config --global user.name "quangdn-ght"
git add .
-git commit -m "loi he thong"
+git commit -m "cap nhat auth he thong de nhung voi chebichat"
git push
# mdZddHXcuzsB0Akk
\ No newline at end of file
diff --git a/middleware.ts b/middleware.ts
new file mode 100644
index 000000000..90a173523
--- /dev/null
+++ b/middleware.ts
@@ -0,0 +1,104 @@
+import { NextRequest, NextResponse } from "next/server";
+import { checkAuthWithRefresh } from "./app/api/supabase";
+
+// Define protected routes that require authentication
+const PROTECTED_PATHS = [
+ "/chat",
+ "/settings",
+ "/profile",
+ "/api/chat",
+ "/api/user",
+ // Add more protected paths as needed
+];
+
+// Define public routes that don't require authentication
+const PUBLIC_PATHS = [
+ "/",
+ "/login",
+ "/signup",
+ "/api/auth/callback",
+ "/api/auth/logout",
+ "/api/auth/check",
+ // Add more public paths as needed
+];
+
+export async function middleware(req: NextRequest) {
+ const { pathname } = req.nextUrl;
+
+ console.log("[Middleware] Processing request for:", pathname);
+
+ // Skip middleware for static files and Next.js internals
+ if (
+ pathname.startsWith("/_next/") ||
+ pathname.startsWith("/favicon") ||
+ pathname.startsWith("/public/") ||
+ pathname.includes(".")
+ ) {
+ return NextResponse.next();
+ }
+
+ // Check if path is explicitly public
+ const isPublicPath = PUBLIC_PATHS.some(path =>
+ pathname === path || pathname.startsWith(path)
+ );
+
+ if (isPublicPath) {
+ console.log("[Middleware] Public path, allowing access");
+ return NextResponse.next();
+ }
+
+ // Check if path requires authentication
+ const isProtectedPath = PROTECTED_PATHS.some(path =>
+ pathname.startsWith(path) || pathname === path
+ );
+
+ if (isProtectedPath) {
+ console.log("[Middleware] Protected path, checking authentication");
+
+ try {
+ const authResult = await checkAuthWithRefresh(req);
+
+ if (!authResult.user) {
+ console.log("[Middleware] User not authenticated, redirecting to login");
+ const loginUrl = new URL("/login", req.url);
+ loginUrl.searchParams.set("redirect_to", pathname);
+ return NextResponse.redirect(loginUrl);
+ }
+
+ console.log("[Middleware] User authenticated:", authResult.user.email);
+
+ // If token was refreshed, return the response with updated cookies
+ if (authResult.needsRefresh && authResult.response) {
+ console.log("[Middleware] Returning response with refreshed tokens");
+ // Continue to the original destination
+ authResult.response.headers.set("x-middleware-rewrite", req.url);
+ return authResult.response;
+ }
+
+ return NextResponse.next();
+ } catch (error) {
+ console.error("[Middleware] Auth check failed:", error);
+ const loginUrl = new URL("/login", req.url);
+ loginUrl.searchParams.set("redirect_to", pathname);
+ loginUrl.searchParams.set("error", "auth_check_failed");
+ return NextResponse.redirect(loginUrl);
+ }
+ }
+
+ // For all other paths, allow access without authentication
+ console.log("[Middleware] Unprotected path, allowing access");
+ return NextResponse.next();
+}
+
+export const config = {
+ matcher: [
+ /*
+ * Match all request paths except for the ones starting with:
+ * - api/auth (auth routes)
+ * - _next/static (static files)
+ * - _next/image (image optimization files)
+ * - favicon.ico (favicon file)
+ */
+ "/((?!_next/static|_next/image|favicon.ico).*)",
+ ],
+};
diff --git a/run.sh b/run.sh
index 5a57bb434..1483c9677 100644
--- a/run.sh
+++ b/run.sh
@@ -1,2 +1,2 @@
# yarn build && pm2 start "yarn start" --name "chebi-nextjs" -i max
-pm2 start "yarn start" --name "chebi-nextjs" -i max
\ No newline at end of file
+pm2 start "yarn start" --name "chebi-nextjs"
\ No newline at end of file
diff --git a/test_auth.sh b/test_auth.sh
new file mode 100755
index 000000000..35bd516ce
--- /dev/null
+++ b/test_auth.sh
@@ -0,0 +1,98 @@
+#!/bin/bash
+
+# Test Script for Authentication System
+# This script demonstrates how to test the authentication endpoints
+
+echo "🔐 Authentication System Test Script"
+echo "====================================="
+echo ""
+
+# Base URL (adjust as needed)
+BASE_URL="http://localhost:3000"
+
+# Test token (replace with a real Supabase token for actual testing)
+TEST_TOKEN="eyJhbGciOiJIUzI1NiIsImtpZCI6InhXR2xGVUY3KytFRmdqN2siLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3p6Z2t5bHNiZGd3b29oY2JvbXBpLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiIyZjg4NzZlMy01NGYxLTQ3ODUtODFlMC0zYzcyMmYyM2E4YTkiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzUyNDkyMTU2LCJpYXQiOjE3NTE4ODczNTYsImVtYWlsIjoicXVhbmdkbkBnaWFodW5ndGVjaC5jb20udm4iLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6Imdvb2dsZSIsInByb3ZpZGVycyI6WyJnb29nbGUiXX0sInVzZXJfbWV0YWRhdGEiOnsiYXZhdGFyX3VybCI6Imh0dHBzOi8venpna3lsc2JkZ3dvb2hjYm9tcGkuc3VwYWJhc2UuY28vc3RvcmFnZS92MS9vYmplY3QvcHVibGljL2F2YXRhcnMvMmY4ODc2ZTMtNTRmMS00Nzg1LTgxZTAtM2M3MjJmMjNhOGE5L2F2YXRhci5qcGciLCJjdXN0b21fY2xhaW1zIjp7ImhkIjoiZ2lhaHVuZ3RlY2guY29tLnZuIn0sImVtYWlsIjoicXVhbmdkbkBnaWFodW5ndGVjaC5jb20udm4iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZnVsbF9uYW1lIjoiTmjhuq10IFF1YW5nIMSQ4buXIiwiaXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwibmFtZSI6Ik5o4bqtdCBRdWFuZyDEkOG7lyIsInBob25lX3ZlcmlmaWVkIjpmYWxzZSwicHJvdmlkZXJfaWQiOiIxMDY3ODcyNzQyMTEwMDM2Mjc3ODUiLCJzdWIiOiIxMDY3ODcyNzQyMTEwMDM2Mjc3ODUifSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJvYXV0aCIsInRpbWVzdGFtcCI6MTc1MTg4NzM1Nn1dLCJzZXNzaW9uX2lkIjoiY2ZlZjBhMzctZDdjYi00OWQwLWExODEtOTA1YjIxNzk5Y2ZhIiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.vKSX_n5HazYDNFpNa-HUjtY9wWrb7bWPsrhDtEzXcDg"
+REFRESH_TOKEN="your_supabase_refresh_token_here"
+
+echo "📋 Testing Authentication Endpoints..."
+echo ""
+
+# Test 1: Check auth status (should be unauthenticated)
+echo "1️⃣ Testing auth check (should be unauthenticated):"
+curl -s -X GET "$BASE_URL/api/auth/check" \
+ -H "Content-Type: application/json" \
+ -c cookies.txt \
+ | jq '.' || echo "Response: Not authenticated (expected)"
+echo ""
+
+# Test 2: Login via POST (requires real token)
+echo "2️⃣ Testing login via POST:"
+echo " (Replace TEST_TOKEN with real Supabase token)"
+curl -s -X POST "$BASE_URL/api/auth/callback" \
+ -H "Content-Type: application/json" \
+ -c cookies.txt \
+ -d "{
+ \"access_token\": \"$TEST_TOKEN\",
+ \"refresh_token\": \"$REFRESH_TOKEN\"
+ }" \
+ | jq '.' || echo "Response: Login attempt (requires valid token)"
+echo ""
+
+# Test 3: Check auth status after login
+echo "3️⃣ Testing auth check after login:"
+curl -s -X GET "$BASE_URL/api/auth/check" \
+ -H "Content-Type: application/json" \
+ -b cookies.txt \
+ | jq '.' || echo "Response: Auth check after login"
+echo ""
+
+# Test 4: Access protected user endpoint
+echo "4️⃣ Testing protected user endpoint:"
+curl -s -X GET "$BASE_URL/api/user" \
+ -H "Content-Type: application/json" \
+ -b cookies.txt \
+ | jq '.' || echo "Response: User info (requires authentication)"
+echo ""
+
+# Test 5: Test token refresh
+echo "5️⃣ Testing token refresh:"
+curl -s -X POST "$BASE_URL/api/auth/refresh" \
+ -H "Content-Type: application/json" \
+ -b cookies.txt \
+ | jq '.' || echo "Response: Token refresh attempt"
+echo ""
+
+# Test 6: Logout
+echo "6️⃣ Testing logout:"
+curl -s -X POST "$BASE_URL/api/auth/logout" \
+ -H "Content-Type: application/json" \
+ -b cookies.txt \
+ -c cookies.txt \
+ | jq '.' || echo "Response: Logout successful"
+echo ""
+
+# Test 7: Check auth status after logout
+echo "7️⃣ Testing auth check after logout:"
+curl -s -X GET "$BASE_URL/api/auth/check" \
+ -H "Content-Type: application/json" \
+ -b cookies.txt \
+ | jq '.' || echo "Response: Should be unauthenticated"
+echo ""
+
+echo "✅ Authentication system test completed!"
+echo ""
+echo "📝 Manual Tests:"
+echo " 1. Visit: $BASE_URL/login"
+echo " 2. Visit: $BASE_URL/profile (should redirect to login)"
+echo " 3. Login with token, then visit: $BASE_URL/profile"
+echo " 4. Test callback URL: $BASE_URL/api/auth/callback?token=YOUR_TOKEN"
+echo ""
+echo "🔧 To test with real tokens:"
+echo " 1. Replace TEST_TOKEN and REFRESH_TOKEN variables"
+echo " 2. Get tokens from your Supabase authentication flow"
+echo " 3. Run this script again"
+echo ""
+
+# Cleanup
+rm -f cookies.txt
+echo "🧹 Cleaned up temporary files"
diff --git a/test_indexeddb.sh b/test_indexeddb.sh
new file mode 100644
index 000000000..b3549d123
--- /dev/null
+++ b/test_indexeddb.sh
@@ -0,0 +1,176 @@
+#!/bin/bash
+
+# IndexedDB Storage Testing Script
+# This script provides comprehensive testing for the IndexedDB storage fixes
+
+echo "🗄️ IndexedDB Storage Testing Script"
+echo "===================================="
+echo ""
+
+# Function to print section headers
+print_section() {
+ echo ""
+ echo "📋 $1"
+ echo "----------------------------------------"
+}
+
+# Function to run a test and capture results
+run_test() {
+ local test_name="$1"
+ local test_command="$2"
+
+ echo "🧪 Testing: $test_name"
+ echo "Command: $test_command"
+
+ # Execute the test (this would typically be run in browser console)
+ echo " ⚠️ Run this in browser console: $test_command"
+ echo ""
+}
+
+print_section "Test Environment Setup"
+echo "1. Start the development server:"
+echo " npm run dev"
+echo ""
+echo "2. Open browser and go to: http://localhost:3000"
+echo ""
+echo "3. Open browser developer tools (F12)"
+echo ""
+echo "4. Go to Console tab"
+echo ""
+
+print_section "IndexedDB Health Check Tests"
+run_test "Check Storage Health" "await debugStorage.checkStorageHealth()"
+run_test "List All Store Data" "await debugStorage.listAllStoreData()"
+run_test "Validate Store Integrity" "await debugStorage.validateStoreIntegrity()"
+
+print_section "Chat Store Specific Tests"
+echo "🧪 Chat Store Tests (run in console):"
+echo ""
+echo "// Test 1: Check if chat store exists and is properly structured"
+echo "const chatData = await debugStorage.listAllStoreData();"
+echo "console.log('Chat Store:', chatData['chat-next-web-store']);"
+echo ""
+echo "// Test 2: Create a new chat session"
+echo "const { useChatStore } = await import('/app/store/chat');"
+echo "const chatStore = useChatStore.getState();"
+echo "console.log('Current sessions:', chatStore.sessions.length);"
+echo "console.log('Current session index:', chatStore.currentSessionIndex);"
+echo ""
+echo "// Test 3: Add a message and verify persistence"
+echo "chatStore.onUserInput('Test message for IndexedDB');"
+echo "await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for persistence"
+echo "await debugStorage.listAllStoreData(); // Check if saved"
+echo ""
+
+print_section "Storage Migration Tests"
+run_test "Migrate Data" "await debugStorage.migrateData()"
+run_test "Create Backup" "await debugStorage.backupStores()"
+
+print_section "Error Handling Tests"
+echo "🧪 Error Handling Tests (run in console):"
+echo ""
+echo "// Test 1: Simulate IndexedDB failure"
+echo "// (This requires manually disabling IndexedDB in browser settings)"
+echo ""
+echo "// Test 2: Test with corrupted data"
+echo "localStorage.setItem('test-corrupt', '{invalid json}');"
+echo "const testStorage = await import('/app/utils/indexedDB-storage');"
+echo "try {"
+echo " await testStorage.indexedDBStorage.getItem('test-corrupt');"
+echo "} catch (error) {"
+echo " console.log('Handled corrupted data:', error);"
+echo "}"
+echo ""
+
+print_section "Store Key Consistency Tests"
+echo "🧪 Store Key Tests (run in console):"
+echo ""
+echo "// Test 1: Verify all store keys are accessible"
+echo "const { StoreKey } = await import('/app/constant');"
+echo "console.log('Available Store Keys:', Object.values(StoreKey));"
+echo ""
+echo "// Test 2: Check each store exists"
+echo "for (const key of Object.values(StoreKey)) {"
+echo " const data = await debugStorage.indexedDBStorage.getItem(key);"
+echo " console.log(\`Store \${key}: \${data ? 'EXISTS' : 'MISSING'}\`);"
+echo "}"
+echo ""
+
+print_section "Performance Tests"
+echo "🧪 Performance Tests (run in console):"
+echo ""
+echo "// Test 1: Large data storage performance"
+echo "const largeData = { state: { messages: new Array(1000).fill({id: 'test', content: 'test message', date: new Date().toISOString()}), _hasHydrated: true } };"
+echo "console.time('Large data storage');"
+echo "await debugStorage.indexedDBStorage.setItem('performance-test', JSON.stringify(largeData));"
+echo "console.timeEnd('Large data storage');"
+echo ""
+echo "// Test 2: Retrieval performance"
+echo "console.time('Large data retrieval');"
+echo "const retrieved = await debugStorage.indexedDBStorage.getItem('performance-test');"
+echo "console.timeEnd('Large data retrieval');"
+echo "console.log('Retrieved data size:', retrieved?.length || 0, 'characters');"
+echo ""
+
+print_section "Cleanup Tests"
+run_test "Clear Test Data" "await debugStorage.indexedDBStorage.removeItem('performance-test')"
+run_test "Clear All Stores (CAREFUL!)" "await debugStorage.clearAllStores()"
+
+print_section "Manual Testing Checklist"
+echo "✅ Manual Tests to Perform:"
+echo ""
+echo "1. Chat Functionality:"
+echo " - Create a new chat session"
+echo " - Send multiple messages"
+echo " - Refresh the page"
+echo " - Verify messages persist"
+echo ""
+echo "2. Settings Persistence:"
+echo " - Change app settings"
+echo " - Refresh the page"
+echo " - Verify settings persist"
+echo ""
+echo "3. Cross-tab Sync:"
+echo " - Open app in multiple tabs"
+echo " - Make changes in one tab"
+echo " - Verify changes appear in other tabs"
+echo ""
+echo "4. Storage Fallback:"
+echo " - Disable IndexedDB in browser settings"
+echo " - Refresh the page"
+echo " - Verify app still works with localStorage"
+echo ""
+
+print_section "Expected Results"
+echo "✅ Success Indicators:"
+echo " - No console errors related to storage"
+echo " - Chat messages persist across page refreshes"
+echo " - IndexedDB health check returns { indexedDB: true, localStorage: true }"
+echo " - Store integrity validation returns empty issues array"
+echo " - All store keys have valid data structure"
+echo ""
+echo "❌ Failure Indicators:"
+echo " - 'skip setItem' warnings in console"
+echo " - Messages lost after page refresh"
+echo " - Health check shows IndexedDB: false"
+echo " - Store integrity issues found"
+echo " - JSON parse errors in console"
+echo ""
+
+print_section "Troubleshooting"
+echo "🔧 Common Issues & Solutions:"
+echo ""
+echo "Issue: 'skip setItem' warnings"
+echo "Solution: Fixed with improved hydration logic"
+echo ""
+echo "Issue: Data not persisting"
+echo "Solution: Check _hasHydrated flag and storage availability"
+echo ""
+echo "Issue: IndexedDB unavailable"
+echo "Solution: App should fallback to localStorage automatically"
+echo ""
+echo "Issue: Corrupted data"
+echo "Solution: Use debugStorage.clearStore(StoreKey.Chat) to reset"
+echo ""
+
+echo "🎯 Test completed! Review results in browser console."