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