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";
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 (
<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" />
{loggingOut && <span className="ml-2 hidden sm:inline">...</span>}
</Button>
);
}
@ -84,7 +96,7 @@ export function LoginButton() {
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={logout} className="text-red-600">
<DropdownMenuItem onClick={handleLogout} className="text-red-600">
<LogOut className="w-4 h-4 mr-2" />
</DropdownMenuItem>

View File

@ -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<void>;
setAuthFromCallback: (token: string) => Promise<void>;
}
// 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,

View File

@ -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<SavedReport>;
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<SavedReport[]> {
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<SavedReport[]> {
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<SavedReport | undefined> {
export async function getReportById(
id: number
): Promise<SavedReport | undefined> {
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<SavedReport | undefined> {
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<void> {
await db.reports.clear();
}
// Export the db instance for advanced usage
export { db };