This commit is contained in:
parent
c9f6e6a8d1
commit
bddcca3ebb
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
Loading…
Reference in New Issue