初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,207 @@
|
||||
"""认证 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()
|
||||
Reference in new issue
Block a user