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