diff --git a/frontend/components/auth/login-button.tsx b/frontend/components/auth/login-button.tsx index 3b80b60a..c090036e 100644 --- a/frontend/components/auth/login-button.tsx +++ b/frontend/components/auth/login-button.tsx @@ -3,6 +3,7 @@ */ "use client"; +import React from "react"; import { useAuth } from "@/contexts/auth-context"; import { Button } from "@/components/ui/button"; import { @@ -40,11 +41,22 @@ function GoogleIcon({ className }: { className?: string }) { export function LoginButton() { const { user, isLoading, isAuthenticated, login, logout } = useAuth(); + const [loggingOut, setLoggingOut] = React.useState(false); - if (isLoading) { + const handleLogout = async () => { + setLoggingOut(true); + try { + await logout(); + } finally { + setLoggingOut(false); + } + }; + + if (isLoading || loggingOut) { return ( ); } @@ -84,7 +96,7 @@ export function LoginButton() { 雲端同步已啟用 - + 登出 diff --git a/frontend/contexts/auth-context.tsx b/frontend/contexts/auth-context.tsx index 3bced31d..88f8cdd2 100644 --- a/frontend/contexts/auth-context.tsx +++ b/frontend/contexts/auth-context.tsx @@ -5,6 +5,9 @@ "use client"; import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from "react"; +import { clearApiSettings, saveApiSettingsAsync } from "@/lib/storage"; +import { clearAllReports, saveReport } from "@/lib/reports-db"; +import { getCloudSettings, getCloudReports } from "@/lib/user-api"; // User interface export interface User { @@ -21,8 +24,8 @@ interface AuthContextType { isLoading: boolean; isAuthenticated: boolean; login: () => void; - logout: () => void; - setAuthFromCallback: (token: string) => void; + logout: () => Promise; + setAuthFromCallback: (token: string) => Promise; } // Create context @@ -107,6 +110,37 @@ export function AuthProvider({ children }: { children: ReactNode }) { initAuth(); }, [parseToken, isTokenExpired]); + // Auto-clear local data when unauthenticated user leaves the page + useEffect(() => { + if (typeof window === "undefined") return; + + const handleBeforeUnload = () => { + // Only clear data if user is not authenticated + const currentToken = localStorage.getItem(TOKEN_KEY); + if (!currentToken) { + // Clear API settings (synchronous) + clearApiSettings(); + + // Clear reports - use synchronous approach for beforeunload + // IndexedDB operations are async, but we can at least attempt it + clearAllReports().catch(() => { + // Ignore errors during unload + }); + + console.log("Cleared local data for unauthenticated user on page leave"); + } + }; + + // Use both beforeunload and pagehide for better browser compatibility + window.addEventListener("beforeunload", handleBeforeUnload); + window.addEventListener("pagehide", handleBeforeUnload); + + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + window.removeEventListener("pagehide", handleBeforeUnload); + }; + }, []); // No dependencies - we check localStorage directly + // Login - redirect to Google OAuth const login = useCallback(() => { if (!googleClientId) { @@ -131,22 +165,70 @@ export function AuthProvider({ children }: { children: ReactNode }) { window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`; }, [googleClientId]); - // Logout - const logout = useCallback(() => { + // Logout - clear auth and all local data + const logout = useCallback(async () => { + // Clear auth token localStorage.removeItem(TOKEN_KEY); setToken(null); setUser(null); + + // Clear all local data + clearApiSettings(); + await clearAllReports(); + + console.log("Logged out and cleared all local data"); + }, []); + + // Restore cloud data to local storage + const restoreCloudData = useCallback(async (authToken: string) => { + try { + // Temporarily set token for API calls + localStorage.setItem(TOKEN_KEY, authToken); + + // Fetch and restore cloud settings + const cloudSettings = await getCloudSettings(); + if (cloudSettings) { + await saveApiSettingsAsync(cloudSettings); + console.log("Restored API settings from cloud"); + } + + // Fetch and restore cloud reports + const cloudReports = await getCloudReports(); + if (cloudReports && cloudReports.length > 0) { + // Clear existing local reports first + await clearAllReports(); + + // Save each cloud report to local IndexedDB + for (const report of cloudReports) { + await saveReport( + report.ticker, + report.market_type, + report.analysis_date, + report.result, + (report as any).task_id + ); + } + console.log(`Restored ${cloudReports.length} reports from cloud`); + } + } catch (error) { + console.error("Failed to restore cloud data:", error); + } }, []); // Set auth from callback (after OAuth redirect) - const setAuthFromCallback = useCallback((newToken: string) => { + const setAuthFromCallback = useCallback(async (newToken: string) => { const userData = parseToken(newToken); if (userData) { + // First restore cloud data, then set the auth state + await restoreCloudData(newToken); + localStorage.setItem(TOKEN_KEY, newToken); setToken(newToken); setUser(userData); + + console.log("Login complete, cloud data restored"); } - }, [parseToken]); + }, [parseToken, restoreCloudData]); const value: AuthContextType = { user, diff --git a/frontend/lib/reports-db.ts b/frontend/lib/reports-db.ts index d508276a..7e3cee67 100644 --- a/frontend/lib/reports-db.ts +++ b/frontend/lib/reports-db.ts @@ -3,18 +3,18 @@ * Uses Dexie.js for a cleaner API */ -import Dexie, { type Table } from 'dexie'; -import type { AnalysisResponse } from './types'; +import Dexie, { type Table } from "dexie"; +import type { AnalysisResponse } from "./types"; // Saved report interface export interface SavedReport { - id?: number; // Auto-generated primary key - ticker: string; // Stock ticker symbol - market_type: "us" | "twse" | "tpex"; // Market type - analysis_date: string; // Analysis date (YYYY-MM-DD) - saved_at: Date; // Save timestamp - task_id?: string; // Original task ID - result: AnalysisResponse; // Full analysis result + id?: number; // Auto-generated primary key + ticker: string; // Stock ticker symbol + market_type: "us" | "twse" | "tpex"; // Market type + analysis_date: string; // Analysis date (YYYY-MM-DD) + saved_at: Date; // Save timestamp + task_id?: string; // Original task ID + result: AnalysisResponse; // Full analysis result } // Database class extending Dexie @@ -22,10 +22,10 @@ class ReportsDatabase extends Dexie { reports!: Table; constructor() { - super('TradingAgentsReports'); + super("TradingAgentsReports"); this.version(1).stores({ // Define indexes: ++id = auto-increment, others are indexed fields - reports: '++id, ticker, market_type, analysis_date, saved_at' + reports: "++id, ticker, market_type, analysis_date, saved_at", }); } } @@ -51,7 +51,7 @@ export async function saveReport( task_id, result, }; - + return await db.reports.add(report); } @@ -62,26 +62,25 @@ export async function getReportsByMarketType( market_type: "us" | "twse" | "tpex" ): Promise { return await db.reports - .where('market_type') + .where("market_type") .equals(market_type) .reverse() - .sortBy('saved_at'); + .sortBy("saved_at"); } /** * Get all saved reports, sorted by saved_at descending */ export async function getAllReports(): Promise { - return await db.reports - .orderBy('saved_at') - .reverse() - .toArray(); + return await db.reports.orderBy("saved_at").reverse().toArray(); } /** * Get a single report by ID */ -export async function getReportById(id: number): Promise { +export async function getReportById( + id: number +): Promise { return await db.reports.get(id); } @@ -108,11 +107,11 @@ export async function getReportCountByMarketType(): Promise<{ tpex: number; }> { const [us, twse, tpex] = await Promise.all([ - db.reports.where('market_type').equals('us').count(), - db.reports.where('market_type').equals('twse').count(), - db.reports.where('market_type').equals('tpex').count(), + db.reports.where("market_type").equals("us").count(), + db.reports.where("market_type").equals("twse").count(), + db.reports.where("market_type").equals("tpex").count(), ]); - + return { us, twse, tpex }; } @@ -124,11 +123,18 @@ export async function checkDuplicateReport( analysis_date: string ): Promise { return await db.reports - .where('ticker') + .where("ticker") .equals(ticker) - .and(report => report.analysis_date === analysis_date) + .and((report) => report.analysis_date === analysis_date) .first(); } +/** + * Clear all reports from the database (for logout) + */ +export async function clearAllReports(): Promise { + await db.reports.clear(); +} + // Export the db instance for advanced usage export { db };