This commit is contained in:
parent
4eed994fcb
commit
2a65c7f44d
|
|
@ -15,10 +15,15 @@ from backend.app.services.auth_utils import create_access_token
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/auth", tags=["Authentication"])
|
router = APIRouter(prefix="/api/auth", tags=["Authentication"])
|
||||||
|
|
||||||
# Google OAuth Configuration
|
# Google OAuth Configuration - read at request time for dynamic updates
|
||||||
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID", "")
|
def get_google_client_id():
|
||||||
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET", "")
|
return os.getenv("GOOGLE_CLIENT_ID", "")
|
||||||
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:3000")
|
|
||||||
|
def get_google_client_secret():
|
||||||
|
return os.getenv("GOOGLE_CLIENT_SECRET", "")
|
||||||
|
|
||||||
|
def get_frontend_url():
|
||||||
|
return os.getenv("FRONTEND_URL", "http://localhost:3000")
|
||||||
|
|
||||||
# Google OAuth URLs
|
# Google OAuth URLs
|
||||||
GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
|
GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||||
|
|
@ -31,17 +36,20 @@ async def google_login():
|
||||||
"""
|
"""
|
||||||
Redirect to Google OAuth login page
|
Redirect to Google OAuth login page
|
||||||
"""
|
"""
|
||||||
if not GOOGLE_CLIENT_ID:
|
client_id = get_google_client_id()
|
||||||
|
frontend_url = get_frontend_url()
|
||||||
|
|
||||||
|
if not client_id:
|
||||||
raise HTTPException(status_code=500, detail="Google OAuth not configured")
|
raise HTTPException(status_code=500, detail="Google OAuth not configured")
|
||||||
|
|
||||||
# Build the authorization URL
|
# Build the authorization URL
|
||||||
redirect_uri = f"{FRONTEND_URL}/api/auth/callback/google"
|
redirect_uri = f"{frontend_url}/api/auth/callback/google"
|
||||||
|
|
||||||
# For backend-handled callback (alternative):
|
# For backend-handled callback (alternative):
|
||||||
# redirect_uri = f"{os.getenv('BACKEND_URL', 'http://localhost:8000')}/api/auth/google/callback"
|
# redirect_uri = f"{os.getenv('BACKEND_URL', 'http://localhost:8000')}/api/auth/google/callback"
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"client_id": GOOGLE_CLIENT_ID,
|
"client_id": client_id,
|
||||||
"redirect_uri": redirect_uri,
|
"redirect_uri": redirect_uri,
|
||||||
"response_type": "code",
|
"response_type": "code",
|
||||||
"scope": "openid email profile",
|
"scope": "openid email profile",
|
||||||
|
|
@ -64,7 +72,11 @@ async def google_callback(
|
||||||
Handle Google OAuth callback (backend-handled flow)
|
Handle Google OAuth callback (backend-handled flow)
|
||||||
Exchange authorization code for tokens and create/update user
|
Exchange authorization code for tokens and create/update user
|
||||||
"""
|
"""
|
||||||
if not GOOGLE_CLIENT_ID or not GOOGLE_CLIENT_SECRET:
|
client_id = get_google_client_id()
|
||||||
|
client_secret = get_google_client_secret()
|
||||||
|
frontend_url = get_frontend_url()
|
||||||
|
|
||||||
|
if not client_id or not client_secret:
|
||||||
raise HTTPException(status_code=500, detail="Google OAuth not configured")
|
raise HTTPException(status_code=500, detail="Google OAuth not configured")
|
||||||
|
|
||||||
# Determine redirect URI (must match what was used in the login request)
|
# Determine redirect URI (must match what was used in the login request)
|
||||||
|
|
@ -76,8 +88,8 @@ async def google_callback(
|
||||||
GOOGLE_TOKEN_URL,
|
GOOGLE_TOKEN_URL,
|
||||||
data={
|
data={
|
||||||
"code": code,
|
"code": code,
|
||||||
"client_id": GOOGLE_CLIENT_ID,
|
"client_id": client_id,
|
||||||
"client_secret": GOOGLE_CLIENT_SECRET,
|
"client_secret": client_secret,
|
||||||
"redirect_uri": redirect_uri,
|
"redirect_uri": redirect_uri,
|
||||||
"grant_type": "authorization_code",
|
"grant_type": "authorization_code",
|
||||||
}
|
}
|
||||||
|
|
@ -143,7 +155,7 @@ async def google_callback(
|
||||||
})
|
})
|
||||||
|
|
||||||
# Redirect to frontend with token
|
# Redirect to frontend with token
|
||||||
redirect_url = f"{FRONTEND_URL}/auth/callback?token={jwt_token}"
|
redirect_url = f"{frontend_url}/auth/callback?token={jwt_token}"
|
||||||
return RedirectResponse(url=redirect_url)
|
return RedirectResponse(url=redirect_url)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -157,7 +169,10 @@ async def exchange_google_token(
|
||||||
Exchange Google authorization code for JWT token (frontend-handled flow)
|
Exchange Google authorization code for JWT token (frontend-handled flow)
|
||||||
This is called by the frontend after receiving the code from Google
|
This is called by the frontend after receiving the code from Google
|
||||||
"""
|
"""
|
||||||
if not GOOGLE_CLIENT_ID or not GOOGLE_CLIENT_SECRET:
|
client_id = get_google_client_id()
|
||||||
|
client_secret = get_google_client_secret()
|
||||||
|
|
||||||
|
if not client_id or not client_secret:
|
||||||
raise HTTPException(status_code=500, detail="Google OAuth not configured")
|
raise HTTPException(status_code=500, detail="Google OAuth not configured")
|
||||||
|
|
||||||
# Exchange code for tokens
|
# Exchange code for tokens
|
||||||
|
|
@ -166,8 +181,8 @@ async def exchange_google_token(
|
||||||
GOOGLE_TOKEN_URL,
|
GOOGLE_TOKEN_URL,
|
||||||
data={
|
data={
|
||||||
"code": code,
|
"code": code,
|
||||||
"client_id": GOOGLE_CLIENT_ID,
|
"client_id": client_id,
|
||||||
"client_secret": GOOGLE_CLIENT_SECRET,
|
"client_secret": client_secret,
|
||||||
"redirect_uri": redirect_uri,
|
"redirect_uri": redirect_uri,
|
||||||
"grant_type": "authorization_code",
|
"grant_type": "authorization_code",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,48 @@
|
||||||
/**
|
/**
|
||||||
* Header component
|
* Header component with mobile-responsive design
|
||||||
*/
|
*/
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { Menu, X } from "lucide-react";
|
||||||
import { ThemeToggle } from "@/components/theme/ThemeToggle";
|
import { ThemeToggle } from "@/components/theme/ThemeToggle";
|
||||||
import { ApiSettingsDialog } from "@/components/settings/ApiSettingsDialog";
|
import { ApiSettingsDialog } from "@/components/settings/ApiSettingsDialog";
|
||||||
import { LoginButton } from "@/components/auth/login-button";
|
import { LoginButton } from "@/components/auth/login-button";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="border-b bg-gradient-to-r from-blue-500 to-pink-500 dark:from-blue-600 dark:to-purple-600 text-white">
|
<header className="border-b bg-gradient-to-r from-blue-500 to-pink-500 dark:from-blue-600 dark:to-purple-600 text-white">
|
||||||
<div className="container mx-auto px-4 py-6">
|
<div className="container mx-auto px-4 py-4 md:py-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Link href="/" className="flex items-center gap-2">
|
{/* Logo */}
|
||||||
<div className="text-3xl font-bold">TradingAgentsX</div>
|
<Link href="/" className="flex items-center gap-2 shrink-0">
|
||||||
<div className="hidden md:block text-sm font-light opacity-90">
|
<div className="text-xl md:text-3xl font-bold">TradingAgentsX</div>
|
||||||
|
<div className="hidden lg:block text-sm font-light opacity-90">
|
||||||
多代理 LLM 金融交易框架
|
多代理 LLM 金融交易框架
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="flex gap-6 items-center">
|
|
||||||
|
{/* Desktop Navigation */}
|
||||||
|
<nav className="hidden md:flex gap-4 lg:gap-6 items-center">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="hover:opacity-80 transition-opacity font-medium"
|
className="hover:opacity-80 transition-opacity font-medium text-sm lg:text-base"
|
||||||
>
|
>
|
||||||
首頁
|
首頁
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/analysis"
|
href="/analysis"
|
||||||
className="hover:opacity-80 transition-opacity font-medium"
|
className="hover:opacity-80 transition-opacity font-medium text-sm lg:text-base"
|
||||||
>
|
>
|
||||||
分析
|
分析
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/history"
|
href="/history"
|
||||||
className="hover:opacity-80 transition-opacity font-medium"
|
className="hover:opacity-80 transition-opacity font-medium text-sm lg:text-base"
|
||||||
>
|
>
|
||||||
歷史報告
|
歷史報告
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -42,7 +50,51 @@ export function Header() {
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
<LoginButton />
|
<LoginButton />
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{/* Mobile Menu Button */}
|
||||||
|
<div className="flex md:hidden items-center gap-2">
|
||||||
|
<ThemeToggle />
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-white hover:bg-white/20"
|
||||||
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
|
>
|
||||||
|
{mobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Navigation */}
|
||||||
|
{mobileMenuOpen && (
|
||||||
|
<nav className="md:hidden mt-4 pt-4 border-t border-white/20 space-y-3">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="block py-2 hover:opacity-80 transition-opacity font-medium"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
>
|
||||||
|
首頁
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/analysis"
|
||||||
|
className="block py-2 hover:opacity-80 transition-opacity font-medium"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
>
|
||||||
|
分析
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/history"
|
||||||
|
className="block py-2 hover:opacity-80 transition-opacity font-medium"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
>
|
||||||
|
歷史報告
|
||||||
|
</Link>
|
||||||
|
<div className="flex items-center gap-3 pt-2">
|
||||||
|
<ApiSettingsDialog />
|
||||||
|
<LoginButton />
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue