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

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