cap nhat auth he thong de nhung voi chebichat

This commit is contained in:
quangdn-ght 2025-07-07 21:37:16 +07:00
parent 972f957633
commit b42c5154e3
25 changed files with 2970 additions and 28 deletions

259
AUTH_SYSTEM.md Normal file
View File

@ -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 <div>Loading...</div>;
}
if (!authenticated) {
return (
<div>
<button onClick={() => login("ACCESS_TOKEN", "REFRESH_TOKEN")}>
Login
</button>
</div>
);
}
return (
<div>
<p>Welcome, {user?.email}!</p>
<button onClick={logout}>Logout</button>
</div>
);
}
```
### Protecting Routes with HOC
```tsx
import { withAuth } from "../hooks/useAuth";
function ProtectedPage({ user }) {
return (
<div>
<h1>Protected Content</h1>
<p>User ID: {user.id}</p>
</div>
);
}
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.

View File

@ -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 },
);
}
}

View File

@ -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 },
);
}
}

View File

@ -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;
}

View File

@ -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 },
);
}
}

View File

@ -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();
};
}

102
app/api/user/route.ts Normal file
View File

@ -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 },
);
}
}

220
app/auth-demo/page.tsx Normal file
View File

@ -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 (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-500 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading authentication status...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<div className="text-center">
<h1 className="text-3xl font-extrabold text-gray-900 mb-8">
🔐 Authentication System Demo
</h1>
</div>
<div className="grid gap-6 lg:grid-cols-2">
{/* Authentication Status Card */}
<div className="bg-white shadow rounded-lg p-6">
<h2 className="text-lg font-medium text-gray-900 mb-4">
Authentication Status
</h2>
<div className="space-y-3">
<div className="flex items-center">
<span className="text-sm font-medium text-gray-700 w-24">
Status:
</span>
<span
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
authenticated
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{authenticated ? "✅ Authenticated" : "❌ Not Authenticated"}
</span>
</div>
{user?.id && (
<div className="flex items-center">
<span className="text-sm font-medium text-gray-700 w-24">
User ID:
</span>
<span className="text-sm text-gray-900 font-mono">
{user.id}
</span>
</div>
)}
{user?.email && (
<div className="flex items-center">
<span className="text-sm font-medium text-gray-700 w-24">
Email:
</span>
<span className="text-sm text-gray-900">{user.email}</span>
</div>
)}
</div>
{authenticated && (
<div className="mt-6">
<button
onClick={logout}
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
Logout
</button>
</div>
)}
</div>
{/* Quick Actions Card */}
<div className="bg-white shadow rounded-lg p-6">
<h2 className="text-lg font-medium text-gray-900 mb-4">
Quick Actions
</h2>
<div className="space-y-3">
<a
href="/login"
className="block w-full text-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
🔑 Login Page
</a>
<a
href="/profile"
className="block w-full text-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
👤 Profile Page (Protected)
</a>
<a
href="/api/auth/check"
className="block w-full text-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Check Auth API
</a>
</div>
</div>
{/* Authentication Flow Card */}
<div className="bg-white shadow rounded-lg p-6 lg:col-span-2">
<h2 className="text-lg font-medium text-gray-900 mb-4">
🔄 Authentication Flow
</h2>
<div className="space-y-4">
<div className="bg-gray-50 rounded-md p-4">
<h3 className="text-sm font-medium text-gray-900 mb-2">
1. Token Authentication via URL
</h3>
<code className="text-xs text-gray-600 bg-white p-2 rounded block">
GET
/api/auth/callback?token=YOUR_SUPABASE_TOKEN&refresh_token=REFRESH_TOKEN&redirect_to=/profile
</code>
</div>
<div className="bg-gray-50 rounded-md p-4">
<h3 className="text-sm font-medium text-gray-900 mb-2">
2. Programmatic Login via POST
</h3>
<code className="text-xs text-gray-600 bg-white p-2 rounded block">
POST /api/auth/callback
<br />
{`{"access_token": "token", "refresh_token": "refresh"}`}
</code>
</div>
<div className="bg-gray-50 rounded-md p-4">
<h3 className="text-sm font-medium text-gray-900 mb-2">
3. Manual Token Entry
</h3>
<p className="text-xs text-gray-600">
Visit the{" "}
<a
href="/login"
className="text-indigo-600 hover:text-indigo-500"
>
login page
</a>{" "}
to enter your Supabase tokens manually.
</p>
</div>
</div>
</div>
{/* API Endpoints Card */}
<div className="bg-white shadow rounded-lg p-6 lg:col-span-2">
<h2 className="text-lg font-medium text-gray-900 mb-4">
🚀 Available API Endpoints
</h2>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-900">
Authentication
</h3>
<ul className="text-xs text-gray-600 space-y-1">
<li>
<code>GET /api/auth/check</code> - Check auth status
</li>
<li>
<code>POST /api/auth/callback</code> - Login with tokens
</li>
<li>
<code>POST /api/auth/refresh</code> - Refresh access token
</li>
<li>
<code>POST /api/auth/logout</code> - Logout and clear
cookies
</li>
</ul>
</div>
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-900">
Protected Routes
</h3>
<ul className="text-xs text-gray-600 space-y-1">
<li>
<code>GET /api/user</code> - Get user information
</li>
<li>
<code>GET /profile</code> - User profile page
</li>
<li>
<code>GET /chat</code> - Chat interface (if implemented)
</li>
<li>
<code>GET /settings</code> - Settings page (if implemented)
</li>
</ul>
</div>
</div>
</div>
</div>
<div className="mt-8 text-center">
<p className="text-sm text-gray-500">
📚 See <code>AUTH_SYSTEM.md</code> for detailed documentation
</p>
</div>
</div>
</div>
);
}

View File

@ -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;

View File

@ -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
}

View File

@ -102,6 +102,7 @@ export enum StoreKey {
Sync = "sync",
SdList = "sd-list",
Mcp = "mcp-store",
Auth = "auth-store",
}
export const DEFAULT_SIDEBAR_WIDTH = 300;

227
app/hooks/useAuth.ts Normal file
View File

@ -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<AuthState>({
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<T extends Record<string, any>>(
Component: React.ComponentType<T>,
) {
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;
}

217
app/hooks/useAuth.tsx Normal file
View File

@ -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<AuthState>({
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<T extends object>(Component: React.ComponentType<T>) {
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 <div>Loading...</div>;
}
if (!authenticated) {
return <div>Redirecting to login...</div>;
}
return <Component {...props} user={user} />;
};
}

View File

@ -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({
<body>
{children}
{/* Storage System Initialization */}
<StorageInitializer />
{serverConfig?.isVercel && (
<>
<SpeedInsights />

150
app/login/page.tsx Normal file
View File

@ -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 <div>Redirecting...</div>;
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign in to your account
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Enter your Supabase authentication token
</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}
<div>
<label
htmlFor="token"
className="block text-sm font-medium text-gray-700"
>
Access Token *
</label>
<input
id="token"
name="token"
type="text"
required
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Enter your access token"
value={token}
onChange={(e) => setToken(e.target.value)}
/>
</div>
<div>
<label
htmlFor="refreshToken"
className="block text-sm font-medium text-gray-700"
>
Refresh Token (optional)
</label>
<input
id="refreshToken"
name="refreshToken"
type="text"
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Enter your refresh token (optional)"
value={refreshToken}
onChange={(e) => setRefreshToken(e.target.value)}
/>
</div>
<div>
<button
type="submit"
disabled={loading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? "Signing in..." : "Sign in"}
</button>
</div>
</form>
<div className="mt-6">
<div className="text-center">
<p className="text-sm text-gray-600">
You can also authenticate by visiting:{" "}
<code className="bg-gray-100 px-2 py-1 rounded text-xs">
/api/auth/callback?token=YOUR_TOKEN
</code>
</p>
</div>
</div>
</div>
</div>
);
}

74
app/profile/page.tsx Normal file
View File

@ -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 (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-2xl mx-auto">
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h2 className="text-lg font-medium text-gray-900 mb-4">
Profile Information
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">
User ID
</label>
<p className="mt-1 text-sm text-gray-900">{user?.id}</p>
</div>
{user?.email && (
<div>
<label className="block text-sm font-medium text-gray-700">
Email
</label>
<p className="mt-1 text-sm text-gray-900">{user.email}</p>
</div>
)}
{user?.user_metadata &&
Object.keys(user.user_metadata).length > 0 && (
<div>
<label className="block text-sm font-medium text-gray-700">
User Metadata
</label>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded">
{JSON.stringify(user.user_metadata, null, 2)}
</pre>
</div>
)}
</div>
<div className="mt-6 flex space-x-3">
<button
onClick={() => (window.location.href = "/")}
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Home
</button>
<button
onClick={handleLogout}
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
Logout
</button>
</div>
</div>
</div>
</div>
</div>
);
}
export default withAuth(ProfilePage);

View File

@ -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<string | null> {
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<void> {
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<void> {
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<void> {
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<void> {
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);
}
}
}

236
app/utils/storage-debug.ts Normal file
View File

@ -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<string, any> = {};
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<string, any> = {
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()
`);
}

View File

@ -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<void> {
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<void> {
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<void> {
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<string | null> {
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<void> {
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<string> {
console.log("[StorageMigration] Creating pre-migration backup...");
const backup: Record<string, any> = {
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);
}
}

View File

@ -47,7 +47,35 @@ export function createPersistStore<T extends object, M>(
// 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

2
git.sh
View File

@ -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

104
middleware.ts Normal file
View File

@ -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).*)",
],
};

2
run.sh
View File

@ -1,2 +1,2 @@
# yarn build && pm2 start "yarn start" --name "chebi-nextjs" -i max
pm2 start "yarn start" --name "chebi-nextjs" -i max
pm2 start "yarn start" --name "chebi-nextjs"

98
test_auth.sh Executable file
View File

@ -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"

176
test_indexeddb.sh Normal file
View File

@ -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."