Add services, videos, AI assistant, map/weather, gigs post, notifications settings, static legal pages, Google OAuth, payment router, real-user matching, newsletter, and content submission — all using nomadweb UI patterns. Co-authored-by: Cursor <cursoragent@cursor.com>
160 lines
5.1 KiB
Python
160 lines
5.1 KiB
Python
from fastapi import APIRouter, Header, HTTPException
|
|
|
|
import httpx
|
|
|
|
from app.schemas import (
|
|
AuthLogin, AuthRegister, AuthResponse, Destination, GoogleLoginRequest,
|
|
FavoriteRequest, FavoriteResponse, ProfileStats, UserProfile,
|
|
UserPlanPayload,
|
|
)
|
|
from app.services.auth import (
|
|
get_favorites, get_plan, get_user_by_token,
|
|
login_demo, login_google, login_user, register_user, save_plan, toggle_favorite,
|
|
)
|
|
from app.services.pocketbase import pb
|
|
|
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
|
|
|
|
@router.post("/register", response_model=AuthResponse)
|
|
async def register(body: AuthRegister):
|
|
result = register_user(body.email, body.password, body.name)
|
|
if not result:
|
|
raise HTTPException(400, "该邮箱已注册")
|
|
return AuthResponse(**result)
|
|
|
|
|
|
@router.post("/login", response_model=AuthResponse)
|
|
async def login(body: AuthLogin):
|
|
result = login_user(body.email, body.password)
|
|
if not result:
|
|
raise HTTPException(401, "邮箱或密码错误")
|
|
return AuthResponse(**result)
|
|
|
|
|
|
@router.get("/me", response_model=UserProfile)
|
|
async def me(authorization: str | None = Header(None)):
|
|
token = _extract_token(authorization)
|
|
user = get_user_by_token(token)
|
|
if not user:
|
|
raise HTTPException(401, "未登录")
|
|
return UserProfile(**user)
|
|
|
|
|
|
@router.get("/demo", response_model=AuthResponse)
|
|
async def demo_login():
|
|
"""一键体验演示账号"""
|
|
result = login_demo()
|
|
if not result:
|
|
raise HTTPException(500, "演示账号不可用")
|
|
return AuthResponse(**result)
|
|
|
|
|
|
@router.post("/google", response_model=AuthResponse)
|
|
async def google_login(body: GoogleLoginRequest):
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
res = await client.get(
|
|
"https://oauth2.googleapis.com/tokeninfo",
|
|
params={"id_token": body.id_token},
|
|
)
|
|
res.raise_for_status()
|
|
data = res.json()
|
|
except httpx.HTTPError:
|
|
raise HTTPException(401, "Google 登录验证失败")
|
|
email = data.get("email")
|
|
verified = data.get("email_verified")
|
|
if not email or verified not in (True, "true"):
|
|
raise HTTPException(401, "无效的 Google 账号")
|
|
name = data.get("name") or email.split("@")[0]
|
|
result = login_google(email, name)
|
|
if not result:
|
|
raise HTTPException(500, "登录失败")
|
|
return AuthResponse(**result)
|
|
|
|
|
|
@router.get("/favorites", response_model=FavoriteResponse)
|
|
async def list_favorites(authorization: str | None = Header(None)):
|
|
token = _extract_token(authorization)
|
|
if not get_user_by_token(token):
|
|
raise HTTPException(401, "未登录")
|
|
return FavoriteResponse(slugs=get_favorites(token))
|
|
|
|
|
|
@router.get("/favorites/detail", response_model=list[Destination])
|
|
async def favorite_details(authorization: str | None = Header(None)):
|
|
token = _extract_token(authorization)
|
|
user = get_user_by_token(token)
|
|
if not user:
|
|
raise HTTPException(401, "未登录")
|
|
slugs = get_favorites(token)
|
|
all_dests = await pb.get_destinations()
|
|
return [Destination(**d) for d in all_dests if d["slug"] in slugs]
|
|
|
|
|
|
@router.get("/profile/stats", response_model=ProfileStats)
|
|
async def profile_stats(authorization: str | None = Header(None)):
|
|
token = _extract_token(authorization)
|
|
user = get_user_by_token(token)
|
|
if not user:
|
|
raise HTTPException(401, "未登录")
|
|
favs = get_favorites(token)
|
|
plan = get_plan(token) or {}
|
|
cities = len(plan.get("items") or [])
|
|
levels = [(0, "🌱 新手游民"), (1, "🎒 背包客"), (2, "✈️ 飞行游民"), (3, "🌍 环球游民")]
|
|
explored = max(len(favs), cities)
|
|
level = levels[min(explored, 3)][1]
|
|
return ProfileStats(
|
|
favorites_count=len(favs),
|
|
destinations_explored=explored,
|
|
member_since="2026",
|
|
nomad_level=level,
|
|
)
|
|
|
|
|
|
@router.post("/favorites", response_model=FavoriteResponse)
|
|
async def add_favorite(body: FavoriteRequest, authorization: str | None = Header(None)):
|
|
token = _extract_token(authorization)
|
|
if not get_user_by_token(token):
|
|
raise HTTPException(401, "未登录")
|
|
dest = await pb.get_destination(body.destination_slug)
|
|
if not dest:
|
|
raise HTTPException(404, "目的地不存在")
|
|
slugs = toggle_favorite(token, body.destination_slug)
|
|
return FavoriteResponse(slugs=slugs)
|
|
|
|
|
|
@router.get("/plan", response_model=UserPlanPayload)
|
|
async def read_plan(authorization: str | None = Header(None)):
|
|
token = _extract_token(authorization)
|
|
if not get_user_by_token(token):
|
|
raise HTTPException(401, "未登录")
|
|
plan = get_plan(token)
|
|
if not plan:
|
|
raise HTTPException(401, "未登录")
|
|
return UserPlanPayload(**plan)
|
|
|
|
|
|
@router.put("/plan", response_model=UserPlanPayload)
|
|
async def write_plan(body: UserPlanPayload, authorization: str | None = Header(None)):
|
|
token = _extract_token(authorization)
|
|
if not get_user_by_token(token):
|
|
raise HTTPException(401, "未登录")
|
|
saved = save_plan(
|
|
token,
|
|
[item.model_dump() for item in body.items],
|
|
body.meta.model_dump(),
|
|
body.updated_at or None,
|
|
)
|
|
if not saved:
|
|
raise HTTPException(401, "未登录")
|
|
return UserPlanPayload(**saved)
|
|
|
|
|
|
def _extract_token(authorization: str | None) -> str:
|
|
if not authorization:
|
|
return ""
|
|
if authorization.startswith("Bearer "):
|
|
return authorization[7:]
|
|
return authorization
|