feat(backend): T1.1 认证与权限 — 注册/登录/刷新/获取用户
- 后端:auth 路由(register/login/refresh/me)+ JWT + bcrypt 密码哈希 - 依赖注入:get_current_user + require_role 角色权限校验 - 跨数据库兼容:JSONBType(PG 用 JSONB,SQLite 用 JSON) - 测试:11 个认证测试 + 4 个健康检查测试 = 15 passed
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
"""认证路由:登录 / 注册 / 刷新 token / 获取当前用户。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_user
|
||||
from app.core.security import (
|
||||
create_access_token,
|
||||
create_refresh_token,
|
||||
decode_token,
|
||||
hash_password,
|
||||
verify_password,
|
||||
)
|
||||
from app.models.tenant import Tenant
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import (
|
||||
LoginRequest,
|
||||
RefreshRequest,
|
||||
RegisterRequest,
|
||||
TokenResponse,
|
||||
UserInfo,
|
||||
)
|
||||
from app.schemas.common import ApiResponse, success
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/register", response_model=ApiResponse[TokenResponse])
|
||||
async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""用户注册(创建新租户 + 首个用户)。"""
|
||||
# 检查邮箱是否已存在
|
||||
existing = await db.execute(select(User).where(User.email == req.email))
|
||||
if existing.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="邮箱已注册",
|
||||
)
|
||||
|
||||
# 创建租户
|
||||
tenant = Tenant(name=req.tenant_name, type="vc")
|
||||
db.add(tenant)
|
||||
await db.flush()
|
||||
|
||||
# 创建用户
|
||||
user = User(
|
||||
tenant_id=tenant.id,
|
||||
email=req.email,
|
||||
password_hash=hash_password(req.password),
|
||||
name=req.name,
|
||||
role=req.role,
|
||||
)
|
||||
db.add(user)
|
||||
await db.flush()
|
||||
|
||||
access_token = create_access_token(
|
||||
subject=user.id,
|
||||
extra_claims={"role": user.role, "tenant_id": user.tenant_id},
|
||||
)
|
||||
refresh_token = create_refresh_token(subject=user.id)
|
||||
|
||||
return success(
|
||||
data=TokenResponse(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
expires_in=settings.jwt_access_token_ttl_minutes * 60,
|
||||
),
|
||||
message="注册成功",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/login", response_model=ApiResponse[TokenResponse])
|
||||
async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""用户登录。"""
|
||||
result = await db.execute(select(User).where(User.email == req.email))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user or not verify_password(req.password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="邮箱或密码错误",
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="账户已禁用",
|
||||
)
|
||||
|
||||
access_token = create_access_token(
|
||||
subject=user.id,
|
||||
extra_claims={"role": user.role, "tenant_id": user.tenant_id},
|
||||
)
|
||||
refresh_token = create_refresh_token(subject=user.id)
|
||||
|
||||
return success(
|
||||
data=TokenResponse(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
expires_in=settings.jwt_access_token_ttl_minutes * 60,
|
||||
),
|
||||
message="登录成功",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=ApiResponse[TokenResponse])
|
||||
async def refresh_token(req: RefreshRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""刷新 access token。"""
|
||||
try:
|
||||
payload = decode_token(req.refresh_token)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的 refresh token",
|
||||
)
|
||||
|
||||
if payload.get("type") != "refresh":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的 token 类型",
|
||||
)
|
||||
|
||||
user_id = payload.get("sub")
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户不存在或已禁用",
|
||||
)
|
||||
|
||||
access_token = create_access_token(
|
||||
subject=user.id,
|
||||
extra_claims={"role": user.role, "tenant_id": user.tenant_id},
|
||||
)
|
||||
new_refresh_token = create_refresh_token(subject=user.id)
|
||||
|
||||
return success(
|
||||
data=TokenResponse(
|
||||
access_token=access_token,
|
||||
refresh_token=new_refresh_token,
|
||||
expires_in=settings.jwt_access_token_ttl_minutes * 60,
|
||||
),
|
||||
message="刷新成功",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/me", response_model=ApiResponse[UserInfo])
|
||||
async def get_me(user: User = Depends(get_current_user)):
|
||||
"""获取当前用户信息。"""
|
||||
return success(
|
||||
data=UserInfo(
|
||||
id=user.id,
|
||||
email=user.email,
|
||||
name=user.name,
|
||||
role=user.role,
|
||||
tenant_id=user.tenant_id,
|
||||
is_active=user.is_active,
|
||||
),
|
||||
)
|
||||
Reference in New Issue
Block a user