diff --git a/app/router/private/__init__.py b/app/router/private/__init__.py index f577a77..27d3eb3 100644 --- a/app/router/private/__init__.py +++ b/app/router/private/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from . import avatar # noqa: F401 +from . import avatar, oauth, username # noqa: F401 from .router import router as private_router __all__ = [ diff --git a/app/router/private/avatar.py b/app/router/private/avatar.py index 3f2116f..308253c 100644 --- a/app/router/private/avatar.py +++ b/app/router/private/avatar.py @@ -16,13 +16,28 @@ from PIL import Image from sqlmodel.ext.asyncio.session import AsyncSession -@router.post("/avatar/upload", tags=["avatar"]) +@router.post( + "/avatar/upload", + name="上传头像", +) async def upload_avatar( - file: str = Body(...), - user_id: int = Body(...), + file: str = Body(..., description="Base64 编码的图片数据"), + user_id: int = Body(..., description="用户 ID"), storage: StorageService = Depends(get_storage_service), session: AsyncSession = Depends(get_db), ): + """上传用户头像 + + 接收 Base64 编码的图片数据,验证图片格式和大小后存储到头像目录,并更新用户的头像 URL + + 限制条件: + - 支持的图片格式: PNG、JPEG、GIF + - 最大文件大小: 5MB + - 最大图片尺寸: 256x256 像素 + + 返回: + - 头像 URL 和文件哈希值 + """ content = base64.b64decode(file) user = await session.get(User, user_id) diff --git a/app/router/private/oauth.py b/app/router/private/oauth.py index 858371f..68142fd 100644 --- a/app/router/private/oauth.py +++ b/app/router/private/oauth.py @@ -13,12 +13,16 @@ from sqlmodel import select, text from sqlmodel.ext.asyncio.session import AsyncSession -@router.post("/oauth-app/create", tags=["OAuth"]) +@router.post( + "/oauth-app/create", + name="创建 OAuth 应用", + description="创建一个新的 OAuth 应用程序,并生成客户端 ID 和密钥", +) async def create_oauth_app( - name: str = Body(..., max_length=100), - description: str = Body(""), - redirect_uris: list[str] = Body(...), - owner_id: int = Body(...), + name: str = Body(..., max_length=100, description="应用程序名称"), + description: str = Body("", description="应用程序描述"), + redirect_uris: list[str] = Body(..., description="允许的重定向 URI 列表"), + owner_id: int = Body(..., description="应用程序所有者的用户 ID"), session: AsyncSession = Depends(get_db), ): result = await session.execute( # pyright: ignore[reportDeprecated] @@ -48,8 +52,15 @@ async def create_oauth_app( } -@router.get("/oauth-apps/{client_id}", tags=["OAuth"]) -async def get_oauth_app(client_id: int, session: AsyncSession = Depends(get_db)): +@router.get( + "/oauth-apps/{client_id}", + name="获取 OAuth 应用信息", + description="通过客户端 ID 获取 OAuth 应用的详细信息", +) +async def get_oauth_app( + client_id: int, + session: AsyncSession = Depends(get_db), +): oauth_app = await session.get(OAuthClient, client_id) if not oauth_app: raise HTTPException(status_code=404, detail="OAuth app not found") @@ -61,8 +72,15 @@ async def get_oauth_app(client_id: int, session: AsyncSession = Depends(get_db)) } -@router.get("/oauth-apps/user/{owner_id}", tags=["OAuth"]) -async def get_user_oauth_apps(owner_id: int, session: AsyncSession = Depends(get_db)): +@router.get( + "/oauth-apps/user/{owner_id}", + name="获取用户的 OAuth 应用列表", + description="获取指定用户创建的所有 OAuth 应用程序", +) +async def get_user_oauth_apps( + owner_id: int, + session: AsyncSession = Depends(get_db), +): oauth_apps = await session.exec( select(OAuthClient).where(OAuthClient.owner_id == owner_id) ) @@ -77,7 +95,12 @@ async def get_user_oauth_apps(owner_id: int, session: AsyncSession = Depends(get ] -@router.delete("/oauth-app/{client_id}", tags=["OAuth"], status_code=204) +@router.delete( + "/oauth-app/{client_id}", + status_code=204, + name="删除 OAuth 应用", + description="删除指定的 OAuth 应用程序及其关联的所有令牌", +) async def delete_oauth_app( client_id: int, session: AsyncSession = Depends(get_db), @@ -96,12 +119,16 @@ async def delete_oauth_app( await session.commit() -@router.patch("/oauth-app/{client_id}", tags=["OAuth"]) +@router.patch( + "/oauth-app/{client_id}", + name="更新 OAuth 应用", + description="更新指定 OAuth 应用的名称、描述和重定向 URI", +) async def update_oauth_app( client_id: int, - name: str = Body(..., max_length=100), - description: str = Body(""), - redirect_uris: list[str] = Body(...), + name: str = Body(..., max_length=100, description="应用程序新名称"), + description: str = Body("", description="应用程序新描述"), + redirect_uris: list[str] = Body(..., description="新的重定向 URI 列表"), session: AsyncSession = Depends(get_db), ): oauth_client = await session.get(OAuthClient, client_id) @@ -122,7 +149,11 @@ async def update_oauth_app( } -@router.post("/oauth-app/{client_id}/refresh", tags=["OAuth"]) +@router.post( + "/oauth-app/{client_id}/refresh", + name="刷新 OAuth 密钥", + description="为指定的 OAuth 应用生成新的客户端密钥,并使所有现有的令牌失效", +) async def refresh_secret( client_id: int, session: AsyncSession = Depends(get_db), @@ -148,12 +179,16 @@ async def refresh_secret( } -@router.post("/oauth-app/{client_id}/code") +@router.post( + "/oauth-app/{client_id}/code", + name="生成 OAuth 授权码", + description="为特定用户和 OAuth 应用生成授权码,用于授权码授权流程", +) async def generate_oauth_code( client_id: int, - user_id: int = Body(...), - redirect_uri: str = Body(...), - scopes: list[str] = Body(...), + user_id: int = Body(..., description="授权用户的 ID"), + redirect_uri: str = Body(..., description="授权后重定向的 URI"), + scopes: list[str] = Body(..., description="请求的权限范围列表"), session: AsyncSession = Depends(get_db), redis: Redis = Depends(get_redis), ): @@ -172,3 +207,9 @@ async def generate_oauth_code( mapping={"user_id": user_id, "scopes": ",".join(scopes)}, ) await redis.expire(f"oauth:code:{client_id}:{code}", 300) + + return { + "code": code, + "redirect_uri": redirect_uri, + "expires_in": 300, + } diff --git a/app/router/private/router.py b/app/router/private/router.py index 1d3a6e4..da6944b 100644 --- a/app/router/private/router.py +++ b/app/router/private/router.py @@ -35,5 +35,6 @@ async def verify_signature( router = APIRouter( prefix="/api/private", dependencies=[Depends(verify_signature)], - include_in_schema=False, + include_in_schema=settings.debug, + tags=["私有 API"], ) diff --git a/app/router/private/username.py b/app/router/private/username.py index 27da481..8140e3b 100644 --- a/app/router/private/username.py +++ b/app/router/private/username.py @@ -10,13 +10,27 @@ from sqlmodel import select from sqlmodel.ext.asyncio.session import AsyncSession -@router.post("/rename", tags=["rename"]) +@router.post( + "/rename", + name="修改用户名", +) async def user_rename( - user_id: int = Body(...), - new_name: str = Body(...), + user_id: int = Body(..., description="要修改名称的用户 ID"), + new_name: str = Body(..., description="新的用户名"), session: AsyncSession = Depends(get_db), # currentUser: User = Depends(get_current_user) ): + """修改用户名 + + 为指定用户修改用户名,并将原用户名添加到历史用户名列表中 + + 错误情况: + - 404: 找不到指定用户 + - 409: 新用户名已被占用 + + 返回: + - 成功: None + """ current_user = (await session.exec(select(User).where(User.id == user_id))).first() if current_user is None: raise HTTPException(404, "User not found") diff --git a/main.py b/main.py index d9a9192..d279318 100644 --- a/main.py +++ b/main.py @@ -39,11 +39,18 @@ async def lifespan(app: FastAPI): await redis_client.aclose() +desc = ( + "osu! API 模拟服务器,支持 osu! API v2 和 osu!lazer 的绝大部分功能。\n\n" + "官方文档:[osu!web 文档](https://osu.ppy.sh/docs/index.html)" +) +if settings.debug: + desc += "\n\n私有 API 签名机制:[GitHub](https://github.com/GooGuTeam/osu_lazer_api/wiki/%E7%A7%81%E6%9C%89-API-%E7%AD%BE%E5%90%8D%E9%AA%8C%E8%AF%81%E6%9C%BA%E5%88%B6)" + app = FastAPI( title="osu! API 模拟服务器", version="1.0.0", lifespan=lifespan, - summary="osu! API 模拟服务器,支持 osu! API v2 和 osu!lazer 的绝大部分功能。官方文档:https://osu.ppy.sh/docs/index.html", + description=desc, ) app.include_router(api_v2_router)