This commit is contained in:
MarkLo 2025-12-13 16:19:46 +08:00
parent c9f6e6a8d1
commit bddcca3ebb
3 changed files with 133 additions and 33 deletions

View File

@ -3,6 +3,7 @@
*/ */
"use client"; "use client";
import React from "react";
import { useAuth } from "@/contexts/auth-context"; import { useAuth } from "@/contexts/auth-context";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@ -40,11 +41,22 @@ function GoogleIcon({ className }: { className?: string }) {
export function LoginButton() { export function LoginButton() {
const { user, isLoading, isAuthenticated, login, logout } = useAuth(); 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 ( return (
<Button variant="outline" size="sm" disabled> <Button variant="outline" size="sm" disabled>
<div className="w-4 h-4 border-2 border-gray-300 border-t-gray-600 rounded-full animate-spin" /> <div className="w-4 h-4 border-2 border-gray-300 border-t-gray-600 rounded-full animate-spin" />
{loggingOut && <span className="ml-2 hidden sm:inline">...</span>}
</Button> </Button>
); );
} }
@ -84,7 +96,7 @@ export function LoginButton() {
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={logout} className="text-red-600"> <DropdownMenuItem onClick={handleLogout} className="text-red-600">
<LogOut className="w-4 h-4 mr-2" /> <LogOut className="w-4 h-4 mr-2" />
</DropdownMenuItem> </DropdownMenuItem>

View File

@ -5,6 +5,9 @@
"use client"; "use client";
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from "react"; 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 // User interface
export interface User { export interface User {
@ -21,8 +24,8 @@ interface AuthContextType {
isLoading: boolean; isLoading: boolean;
isAuthenticated: boolean; isAuthenticated: boolean;
login: () => void; login: () => void;
logout: () => void; logout: () => Promise<void>;
setAuthFromCallback: (token: string) => void; setAuthFromCallback: (token: string) => Promise<void>;
} }
// Create context // Create context
@ -107,6 +110,37 @@ export function AuthProvider({ children }: { children: ReactNode }) {
initAuth(); initAuth();
}, [parseToken, isTokenExpired]); }, [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 // Login - redirect to Google OAuth
const login = useCallback(() => { const login = useCallback(() => {
if (!googleClientId) { 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()}`; window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
}, [googleClientId]); }, [googleClientId]);
// Logout // Logout - clear auth and all local data
const logout = useCallback(() => { const logout = useCallback(async () => {
// Clear auth token
localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(TOKEN_KEY);
setToken(null); setToken(null);
setUser(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) // Set auth from callback (after OAuth redirect)
const setAuthFromCallback = useCallback((newToken: string) => { const setAuthFromCallback = useCallback(async (newToken: string) => {
const userData = parseToken(newToken); const userData = parseToken(newToken);
if (userData) { if (userData) {
// First restore cloud data, then set the auth state
await restoreCloudData(newToken);
localStorage.setItem(TOKEN_KEY, newToken); localStorage.setItem(TOKEN_KEY, newToken);
setToken(newToken); setToken(newToken);
setUser(userData); setUser(userData);
console.log("Login complete, cloud data restored");
} }
}, [parseToken]); }, [parseToken, restoreCloudData]);
const value: AuthContextType = { const value: AuthContextType = {
user, user,

View File

@ -3,8 +3,8 @@
* Uses Dexie.js for a cleaner API * Uses Dexie.js for a cleaner API
*/ */
import Dexie, { type Table } from 'dexie'; import Dexie, { type Table } from "dexie";
import type { AnalysisResponse } from './types'; import type { AnalysisResponse } from "./types";
// Saved report interface // Saved report interface
export interface SavedReport { export interface SavedReport {
@ -22,10 +22,10 @@ class ReportsDatabase extends Dexie {
reports!: Table<SavedReport>; reports!: Table<SavedReport>;
constructor() { constructor() {
super('TradingAgentsReports'); super("TradingAgentsReports");
this.version(1).stores({ this.version(1).stores({
// Define indexes: ++id = auto-increment, others are indexed fields // 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",
}); });
} }
} }
@ -62,26 +62,25 @@ export async function getReportsByMarketType(
market_type: "us" | "twse" | "tpex" market_type: "us" | "twse" | "tpex"
): Promise<SavedReport[]> { ): Promise<SavedReport[]> {
return await db.reports return await db.reports
.where('market_type') .where("market_type")
.equals(market_type) .equals(market_type)
.reverse() .reverse()
.sortBy('saved_at'); .sortBy("saved_at");
} }
/** /**
* Get all saved reports, sorted by saved_at descending * Get all saved reports, sorted by saved_at descending
*/ */
export async function getAllReports(): Promise<SavedReport[]> { export async function getAllReports(): Promise<SavedReport[]> {
return await db.reports return await db.reports.orderBy("saved_at").reverse().toArray();
.orderBy('saved_at')
.reverse()
.toArray();
} }
/** /**
* Get a single report by ID * Get a single report by ID
*/ */
export async function getReportById(id: number): Promise<SavedReport | undefined> { export async function getReportById(
id: number
): Promise<SavedReport | undefined> {
return await db.reports.get(id); return await db.reports.get(id);
} }
@ -108,9 +107,9 @@ export async function getReportCountByMarketType(): Promise<{
tpex: number; tpex: number;
}> { }> {
const [us, twse, tpex] = await Promise.all([ const [us, twse, tpex] = await Promise.all([
db.reports.where('market_type').equals('us').count(), db.reports.where("market_type").equals("us").count(),
db.reports.where('market_type').equals('twse').count(), db.reports.where("market_type").equals("twse").count(),
db.reports.where('market_type').equals('tpex').count(), db.reports.where("market_type").equals("tpex").count(),
]); ]);
return { us, twse, tpex }; return { us, twse, tpex };
@ -124,11 +123,18 @@ export async function checkDuplicateReport(
analysis_date: string analysis_date: string
): Promise<SavedReport | undefined> { ): Promise<SavedReport | undefined> {
return await db.reports return await db.reports
.where('ticker') .where("ticker")
.equals(ticker) .equals(ticker)
.and(report => report.analysis_date === analysis_date) .and((report) => report.analysis_date === analysis_date)
.first(); .first();
} }
/**
* Clear all reports from the database (for logout)
*/
export async function clearAllReports(): Promise<void> {
await db.reports.clear();
}
// Export the db instance for advanced usage // Export the db instance for advanced usage
export { db }; export { db };