Files
2026-08-21 16:00:13 +08:00

208 lines
6.5 KiB
Python

"""认证 API。"""
from fastapi import APIRouter, Depends
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.v1.deps import get_current_user
from app.core.config import settings
from app.db.session import get_session
from app.models.rbac import Role, UserRole
from app.schemas.auth import (
ChangePasswordRequest,
CurrentUser,
LoginRequest,
LoginResponse,
RefreshRequest,
RegisterRequest,
UpdateProfileRequest,
UserResponse,
)
from app.services import auth as auth_service
from app.services.auth import get_user_permissions
router = APIRouter(prefix="/auth", tags=["auth"])
def _jwt_enabled() -> None:
if not settings.jwt_secret:
from fastapi import HTTPException
raise HTTPException(
status_code=503,
detail={
"error": {
"code": "service_unavailable",
"message": "认证服务未配置(JWT_SECRET 为空)。",
}
},
)
@router.post("/register", response_model=UserResponse)
async def register(
payload: RegisterRequest,
db: AsyncSession = Depends(get_session),
) -> UserResponse:
_jwt_enabled()
user = await auth_service.create_user(
db,
username=payload.username,
email=payload.email,
password=payload.password,
)
# 为新用户创建默认角色和默认人物
from app.services.characters import ensure_default_character_for_owner
await ensure_default_character_for_owner(db, user.id)
# 分配普通用户角色
role_result = await db.execute(select(Role).where(Role.name == "user"))
user_role = role_result.scalar_one_or_none()
if user_role:
db.add(UserRole(user_id=user.id, role_id=user_role.id))
await db.commit()
await db.refresh(user)
return UserResponse(
id=user.id,
username=user.username,
email=user.email,
is_admin=user.is_admin,
created_at=user.created_at.isoformat(),
)
@router.post("/login", response_model=LoginResponse)
async def login(
payload: LoginRequest,
db: AsyncSession = Depends(get_session),
) -> LoginResponse:
_jwt_enabled()
user = await auth_service.authenticate_user(db, payload.username, payload.password)
access_token = auth_service.create_access_token(
subject=user.id,
extra={"owner_scope": user.id},
)
refresh_token = auth_service.create_refresh_token(subject=user.id)
perms = await get_user_permissions(db, user.id)
return LoginResponse(
access_token=access_token,
refresh_token=refresh_token,
token_type="bearer",
expires_in=settings.access_token_ttl,
user=UserResponse(
id=user.id,
username=user.username,
email=user.email,
is_admin=user.is_admin,
permissions=perms,
created_at=user.created_at.isoformat(),
),
)
@router.post("/refresh", response_model=LoginResponse)
async def refresh(
payload: RefreshRequest,
db: AsyncSession = Depends(get_session),
) -> LoginResponse:
_jwt_enabled()
data = auth_service.decode_token(payload.refresh_token)
if data.get("type") != "refresh":
from fastapi import HTTPException
raise HTTPException(
status_code=401,
detail={"error": {"code": "unauthorized", "message": "Token 类型不匹配。"}},
)
user = await auth_service.get_user_by_id(db, data["sub"])
if user is None or not user.is_active:
from fastapi import HTTPException
raise HTTPException(
status_code=401,
detail={"error": {"code": "unauthorized", "message": "用户不存在或已禁用。"}},
)
new_access = auth_service.create_access_token(
subject=user.id,
extra={"owner_scope": user.id},
)
new_refresh = auth_service.create_refresh_token(subject=user.id)
perms = await get_user_permissions(db, user.id)
return LoginResponse(
access_token=new_access,
refresh_token=new_refresh,
token_type="bearer",
expires_in=settings.access_token_ttl,
user=UserResponse(
id=user.id,
username=user.username,
email=user.email,
is_admin=user.is_admin,
permissions=perms,
created_at=user.created_at.isoformat(),
),
)
@router.get("/me", response_model=UserResponse)
async def get_me(
current_user: CurrentUser = Depends(get_current_user),
db: AsyncSession = Depends(get_session),
) -> UserResponse:
perms = await get_user_permissions(db, current_user.user_id)
return UserResponse(
id=current_user.user_id,
username=current_user.username,
email=current_user.email,
is_admin=current_user.is_admin,
permissions=perms,
created_at="",
)
@router.patch("/me", response_model=UserResponse)
async def update_profile(
payload: UpdateProfileRequest,
current_user: CurrentUser = Depends(get_current_user),
db: AsyncSession = Depends(get_session),
) -> UserResponse:
"""更新个人资料(当前仅支持邮箱)。"""
if payload.email is not None:
from sqlalchemy import select as _select
from app.models.user import User
result = await db.execute(_select(User).where(User.email == payload.email))
existing = result.scalar_one_or_none()
if existing is not None and existing.id != current_user.user_id:
from app.core.errors import AppError
raise AppError("conflict", "该邮箱已被其他用户使用。")
user = await db.get(User, current_user.user_id)
if user is not None:
user.email = payload.email
await db.commit()
return await get_me(current_user, db)
@router.post("/me/change-password", status_code=204)
async def change_password(
payload: ChangePasswordRequest,
current_user: CurrentUser = Depends(get_current_user),
db: AsyncSession = Depends(get_session),
) -> None:
"""修改当前用户密码。"""
from app.models.user import User
user = await db.get(User, current_user.user_id)
if user is None:
from app.core.errors import AppError
raise AppError("not_found", "用户不存在。")
if not auth_service.verify_password(payload.old_password, user.password_hash):
from app.core.errors import AppError
raise AppError("unauthorized", "原密码错误。")
user.password_hash = auth_service.hash_password(payload.new_password)
await db.commit()