Files
Neo-ZQYY/apps/backend/app/routers/auth.py
Neo 6f8f12314f feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更:
- backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔
- admin-web: ETL 状态页、任务管理、调度配置、登录优化
- miniprogram: 看板页面、聊天集成、UI 组件、导航更新
- etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强
- tenant-admin: 项目初始化
- db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8)
- packages/shared: 枚举和工具函数更新
- tools: 数据库工具、报表生成、健康检查
- docs: PRD/架构/部署/合约文档更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 00:03:48 +08:00

112 lines
3.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
认证路由:登录与令牌刷新。
- POST /api/auth/login — 验证用户名密码,返回 JWT 令牌对
- POST /api/auth/refresh — 用刷新令牌换取新的访问令牌
"""
import logging
from fastapi import APIRouter, HTTPException, status
from jose import JWTError
from app.auth.jwt import (
create_access_token,
create_token_pair,
decode_refresh_token,
verify_password,
)
from app.database import get_connection
from app.schemas.auth import LoginRequest, RefreshRequest, TokenResponse
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/auth", tags=["认证"])
@router.post("/login", response_model=TokenResponse)
async def login(body: LoginRequest):
"""
用户登录。
查询 admin_users 表验证用户名密码,成功后返回 JWT 令牌对。
- 用户不存在或密码错误401
- 账号已禁用is_active=false401
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"SELECT id, password_hash, site_id, is_active, roles "
"FROM admin_users WHERE username = %s",
(body.username,),
)
row = cur.fetchone()
finally:
conn.close()
if row is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
)
user_id, password_hash, site_id, is_active, roles = row
if not is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="账号已被禁用",
)
if not verify_password(body.password, password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
)
tokens = create_token_pair(user_id, site_id, roles=roles or [])
return TokenResponse(**tokens)
@router.post("/refresh", response_model=TokenResponse)
async def refresh(body: RefreshRequest):
"""
刷新访问令牌。
验证 refresh_token 有效性,成功后仅返回新的 access_token
refresh_token 保持不变,由客户端继续持有)。
"""
try:
payload = decode_refresh_token(body.refresh_token)
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的刷新令牌",
)
user_id = int(payload["sub"])
site_id = payload["site_id"]
# CHANGE 2026-03-24 | Prompt: 修复 refresh 丢失 roles | 刷新前查询数据库获取最新 roles
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"SELECT roles FROM admin_users WHERE id = %s",
(user_id,),
)
row = cur.fetchone()
finally:
conn.close()
roles = row[0] if row else []
# 生成新的 access_tokenrefresh_token 原样返回
new_access = create_access_token(user_id, site_id, roles=roles or [])
return TokenResponse(
access_token=new_access,
refresh_token=body.refresh_token,
token_type="bearer",
)