feat: Implement API routers for user, tag, vas, webdav, and slave functionalities

- Added user authentication and registration endpoints with JWT support.
- Created tag management routes for creating and deleting tags.
- Implemented value-added service (VAS) endpoints for managing storage packs and orders.
- Developed WebDAV account management routes for creating, updating, and deleting accounts.
- Introduced slave router for handling file uploads, downloads, and aria2 task management.
- Enhanced JWT utility functions for token creation and secret key management.
- Established lifespan management for FastAPI application startup and shutdown processes.
- Integrated password handling utilities with Argon2 hashing and two-factor authentication support.
This commit is contained in:
2025-12-19 18:04:34 +08:00
parent 11b67bde6d
commit 51b6de921b
30 changed files with 223 additions and 534 deletions

View File

@@ -0,0 +1,516 @@
from fastapi import APIRouter, Depends
from loguru import logger
from middleware.auth import AdminRequired
from middleware.dependencies import SessionDep
from models import User
from models.user import UserPublic
from models.response import ResponseBase
# 管理员根目录 /api/admin
admin_router = APIRouter(
prefix="/admin",
tags=["admin"],
)
# 用户组 /api/admin/group
admin_group_router = APIRouter(
prefix="/admin/group",
tags=["admin", "admin_group"],
)
# 用户 /api/admin/user
admin_user_router = APIRouter(
prefix="/admin/user",
tags=["admin", "admin_user"],
)
# 文件 /api/admin/file
admin_file_router = APIRouter(
prefix="/admin/file",
tags=["admin", "admin_file"],
)
# 离线下载 /api/admin/aria2
admin_aria2_router = APIRouter(
prefix='/admin/aria2',
tags=['admin', 'admin_aria2']
)
# 存储策略管理 /api/admin/policy
admin_policy_router = APIRouter(
prefix='/admin/policy',
tags=['admin', 'admin_policy']
)
# 分享 /api/admin/share
admin_share_router = APIRouter(
prefix='/admin/share',
tags=['admin', 'admin_share']
)
# 任务 /api/admin/task
admin_task_router = APIRouter(
prefix='/admin/task',
tags=['admin', 'admin_task']
)
# 增值服务 /api/admin/vas
admin_vas_router = APIRouter(
prefix='/admin/vas',
tags=['admin', 'admin_vas']
)
@admin_router.get(
path='/summary',
summary='获取站点概况',
description='Get site summary information',
dependencies=[Depends(AdminRequired)],
)
def router_admin_get_summary() -> ResponseBase:
"""
获取站点概况信息,包括用户数、分享数、文件数等。
Returns:
ResponseModel: 包含站点概况信息的响应模型。
"""
pass
@admin_router.get(
path='/news',
summary='获取社区新闻',
description='Get community news',
dependencies=[Depends(AdminRequired)],
)
def router_admin_get_news() -> ResponseBase:
"""
获取社区新闻信息,包括最新的动态和公告。
Returns:
ResponseModel: 包含社区新闻信息的响应模型。
"""
pass
@admin_router.patch(
path='/settings',
summary='更新设置',
description='Update settings',
dependencies=[Depends(AdminRequired)],
)
def router_admin_update_settings() -> ResponseBase:
"""
更新站点设置,包括站点名称、描述等。
Returns:
ResponseModel: 包含更新结果的响应模型。
"""
pass
@admin_router.get(
path='/settings',
summary='获取设置',
description='Get settings',
dependencies=[Depends(AdminRequired)],
)
def router_admin_get_settings() -> ResponseBase:
"""
获取站点设置,包括站点名称、描述等。
Returns:
ResponseModel: 包含站点设置的响应模型。
"""
pass
@admin_group_router.get(
path='/',
summary='获取用户组列表',
description='Get user group list',
dependencies=[Depends(AdminRequired)],
)
def router_admin_get_groups() -> ResponseBase:
"""
获取用户组列表,包括每个用户组的名称和权限信息。
Returns:
ResponseModel: 包含用户组列表的响应模型。
"""
pass
@admin_group_router.get(
path='/{group_id}',
summary='获取用户组信息',
description='Get user group information by ID',
dependencies=[Depends(AdminRequired)],
)
def router_admin_get_group(group_id: int) -> ResponseBase:
"""
根据用户组ID获取用户组信息包括名称、权限等。
Args:
group_id (int): 用户组ID。
Returns:
ResponseModel: 包含用户组信息的响应模型。
"""
pass
@admin_group_router.get(
path='/list/{group_id}',
summary='获取用户组成员列表',
description='Get user group member list by group ID',
dependencies=[Depends(AdminRequired)],
)
def router_admin_get_group_members(
group_id: int,
page: int = 1,
page_size: int = 20
) -> ResponseBase:
"""
根据用户组ID获取用户组成员列表。
Args:
group_id (int): 用户组ID。
page (int): 页码默认为1。
page_size (int, optional): 每页显示的成员数量默认为20。
Returns:
ResponseModel: 包含用户组成员列表的响应模型。
"""
pass
@admin_group_router.post(
path='/',
summary='创建用户组',
description='Create a new user group',
dependencies=[Depends(AdminRequired)],
)
def router_admin_create_group() -> ResponseBase:
"""
创建一个新的用户组,设置名称和权限等信息。
Returns:
ResponseModel: 包含创建结果的响应模型。
"""
pass
@admin_group_router.patch(
path='/{group_id}',
summary='更新用户组信息',
description='Update user group information by ID',
dependencies=[Depends(AdminRequired)],
)
def router_admin_update_group(group_id: int) -> ResponseBase:
"""
根据用户组ID更新用户组信息包括名称、权限等。
Args:
group_id (int): 用户组ID。
Returns:
ResponseModel: 包含更新结果的响应模型。
"""
pass
@admin_group_router.delete(
path='/{group_id}',
summary='删除用户组',
description='Delete user group by ID',
dependencies=[Depends(AdminRequired)],
)
def router_admin_delete_group(group_id: int) -> ResponseBase:
"""
根据用户组ID删除用户组。
Args:
group_id (int): 用户组ID。
Returns:
ResponseModel: 包含删除结果的响应模型。
"""
pass
@admin_user_router.get(
path='/info/{user_id}',
summary='获取用户信息',
description='Get user information by ID',
dependencies=[Depends(AdminRequired)],
)
async def router_admin_get_user(session: SessionDep, user_id: int) -> ResponseBase:
"""
根据用户ID获取用户信息包括用户名、邮箱、注册时间等。
Args:
session(SessionDep): 数据库会话依赖项。
user_id (int): 用户ID。
Returns:
ResponseModel: 包含用户信息的响应模型。
"""
user = await User.get_exist_one(session, user_id)
return ResponseBase(data=user.to_public().model_dump())
@admin_user_router.get(
path='/list',
summary='获取用户列表',
description='Get user list',
dependencies=[Depends(AdminRequired)],
)
async def router_admin_get_users(
session: SessionDep,
page: int = 1,
page_size: int = 20
) -> ResponseBase:
"""
获取用户列表,支持分页。
Args:
session: 数据库会话依赖项。
page (int): 页码默认为1。
page_size (int): 每页显示的用户数量默认为20。
Returns:
ResponseModel: 包含用户列表的响应模型。
"""
offset = (page - 1) * page_size
users: list[User] = await User.get(
session,
None,
fetch_mode="all",
offset=offset,
limit=page_size
)
return ResponseBase(
data=[user.to_public().model_dump() for user in users]
)
@admin_user_router.post(
path='/create',
summary='创建用户',
description='Create a new user',
dependencies=[Depends(AdminRequired)],
)
async def router_admin_create_user(
session: SessionDep,
user: User,
) -> ResponseBase:
"""
创建一个新的用户,设置用户名、密码等信息。
Returns:
ResponseModel: 包含创建结果的响应模型。
"""
existing_user = await User.get(session, User.username == user.username)
if existing_user:
return ResponseBase(
code=400,
msg="User with this username already exists."
)
user = await user.save(session)
return ResponseBase(data=user.to_public().model_dump())
@admin_user_router.patch(
path='/{user_id}',
summary='更新用户信息',
description='Update user information by ID',
dependencies=[Depends(AdminRequired)],
)
def router_admin_update_user(user_id: int) -> ResponseBase:
"""
根据用户ID更新用户信息包括用户名、邮箱等。
Args:
user_id (int): 用户ID。
Returns:
ResponseModel: 包含更新结果的响应模型。
"""
pass
@admin_user_router.delete(
path='/{user_id}',
summary='删除用户',
description='Delete user by ID',
dependencies=[Depends(AdminRequired)],
)
def router_admin_delete_user(user_id: int) -> ResponseBase:
"""
根据用户ID删除用户。
Args:
user_id (int): 用户ID。
Returns:
ResponseModel: 包含删除结果的响应模型。
"""
pass
@admin_user_router.post(
path='/calibrate/{user_id}',
summary='校准用户存储容量',
description='Calibrate the user storage.',
dependencies=[Depends(AdminRequired)]
)
def router_admin_calibrate_storage():
pass
@admin_file_router.get(
path='/list',
summary='获取文件',
description='Get file list',
dependencies=[Depends(AdminRequired)],
)
def router_admin_get_file_list() -> ResponseBase:
"""
获取文件列表,包括文件名称、大小、上传时间等。
Returns:
ResponseModel: 包含文件列表的响应模型。
"""
pass
@admin_file_router.get(
path='/preview/{file_id}',
summary='预览文件',
description='Preview file by ID',
dependencies=[Depends(AdminRequired)],
)
def router_admin_preview_file(file_id: int) -> ResponseBase:
"""
根据文件ID预览文件内容。
Args:
file_id (int): 文件ID。
Returns:
ResponseModel: 包含文件预览内容的响应模型。
"""
pass
@admin_file_router.patch(
path='/ban/{file_id}',
summary='封禁文件',
description='Ban the file, user can\'t open, copy, move, download or share this file if administrator ban.',
dependencies=[Depends(AdminRequired)],
)
def router_admin_ban_file(file_id: int) -> ResponseBase:
"""
根据文件ID封禁文件。
如果管理员封禁了某个文件,用户将无法打开、复制或移动、下载或分享此文件。
Args:
file_id (int): 文件ID。
Returns:
ResponseModel: 包含删除结果的响应模型。
"""
pass
@admin_file_router.delete(
path='/{file_id}',
summary='删除文件',
description='Delete file by ID',
dependencies=[Depends(AdminRequired)],
)
def router_admin_delete_file(file_id: int) -> ResponseBase:
"""
根据文件ID删除文件。
Args:
file_id (int): 文件ID。
Returns:
ResponseModel: 包含删除结果的响应模型。
"""
pass
@admin_aria2_router.post(
path='/test',
summary='测试连接配置',
description='',
dependencies=[Depends(AdminRequired)]
)
def router_admin_aira2_test() -> ResponseBase:
pass
@admin_policy_router.get(
path='/list',
summary='列出存储策略',
description='',
dependencies=[Depends(AdminRequired)]
)
def router_policy_list() -> ResponseBase:
pass
@admin_policy_router.post(
path='/test/path',
summary='测试本地路径可用性',
description='',
dependencies=[Depends(AdminRequired)]
)
def router_policy_test_path() -> ResponseBase:
pass
@admin_policy_router.post(
path='/test/slave',
summary='测试从机通信',
description='',
dependencies=[Depends(AdminRequired)]
)
def router_policy_test_slave() -> ResponseBase:
pass
@admin_policy_router.post(
path='/',
summary='创建存储策略',
description='',
dependencies=[Depends(AdminRequired)]
)
def router_policy_add_policy() -> ResponseBase:
pass
@admin_policy_router.post(
path='/cors',
summary='创建跨域策略',
description='',
dependencies=[Depends(AdminRequired)]
)
def router_policy_add_cors() -> ResponseBase:
pass
@admin_policy_router.post(
path='/scf',
summary='创建COS回调函数',
description='',
dependencies=[Depends(AdminRequired)]
)
def router_policy_add_scf() -> ResponseBase:
pass
@admin_policy_router.get(
path='/{id}/oauth',
summary='获取 OneDrive OAuth URL',
description='',
dependencies=[Depends(AdminRequired)]
)
def router_policy_onddrive_oauth() -> ResponseBase:
pass
@admin_policy_router.get(
path='/{id}',
summary='获取存储策略',
description='',
dependencies=[Depends(AdminRequired)]
)
def router_policy_get_policy() -> ResponseBase:
pass
@admin_policy_router.delete(
path='/{id}',
summary='删除存储策略',
description='',
dependencies=[Depends(AdminRequired)]
)
def router_policy_delete_policy() -> ResponseBase:
pass

View File

@@ -0,0 +1,298 @@
from fastapi import APIRouter, Depends, Query
from fastapi.responses import PlainTextResponse, RedirectResponse
from middleware.auth import SignRequired
from models.response import ResponseBase
import service.oauth
callback_router = APIRouter(
prefix='/callback',
tags=["callback"],
)
oauth_router = APIRouter(
prefix='/callback/oauth',
tags=["callback", "oauth"],
)
pay_router = APIRouter(
prefix='/callback/pay',
tags=["callback", "pay"],
)
upload_router = APIRouter(
prefix='/callback/upload',
tags=["callback", "upload"],
)
callback_router.include_router(oauth_router)
callback_router.include_router(pay_router)
callback_router.include_router(upload_router)
@oauth_router.post(
path='/qq',
summary='QQ互联回调',
description='Handle QQ OAuth callback and return user information.',
)
def router_callback_qq() -> ResponseBase:
"""
Handle QQ OAuth callback and return user information.
Returns:
ResponseModel: A model containing the response data for the QQ OAuth callback.
"""
pass
@oauth_router.get(
path='/github',
summary='GitHub OAuth 回调',
description='Handle GitHub OAuth callback and return user information.',
)
async def router_callback_github(
code: str = Query(description="The token received from GitHub for authentication.")) -> PlainTextResponse:
"""
GitHub OAuth 回调处理
- 错误响应示例:
- {
'error': 'bad_verification_code',
'error_description': 'The code passed is incorrect or expired.',
'error_uri': 'https://docs.github.com/apps/managing-oauth-apps/troubleshooting-oauth-app-access-token-request-errors/#bad-verification-code'
}
Returns:
PlainTextResponse: A response containing the user information from GitHub.
"""
try:
access_token = await service.oauth.github.get_access_token(code)
# [TODO] 把access_token写数据库里
if not access_token:
return PlainTextResponse("Failed to retrieve access token from GitHub.", status_code=400)
user_data = await service.oauth.github.get_user_info(access_token.access_token)
# [TODO] 把user_data写数据库里
return PlainTextResponse(f"User information processed successfully, code: {code}, user_data: {user_data.json_dump()}", status_code=200)
except Exception as e:
return PlainTextResponse(f"An error occurred: {str(e)}", status_code=500)
@pay_router.post(
path='/alipay',
summary='支付宝支付回调',
description='Handle Alipay payment callback and return payment status.',
)
def router_callback_alipay() -> ResponseBase:
"""
Handle Alipay payment callback and return payment status.
Returns:
ResponseModel: A model containing the response data for the Alipay payment callback.
"""
pass
@pay_router.post(
path='/wechat',
summary='微信支付回调',
description='Handle WeChat Pay payment callback and return payment status.',
)
def router_callback_wechat() -> ResponseBase:
"""
Handle WeChat Pay payment callback and return payment status.
Returns:
ResponseModel: A model containing the response data for the WeChat Pay payment callback.
"""
pass
@pay_router.post(
path='/stripe',
summary='Stripe支付回调',
description='Handle Stripe payment callback and return payment status.',
)
def router_callback_stripe() -> ResponseBase:
"""
Handle Stripe payment callback and return payment status.
Returns:
ResponseModel: A model containing the response data for the Stripe payment callback.
"""
pass
@pay_router.get(
path='/easypay',
summary='易支付回调',
description='Handle EasyPay payment callback and return payment status.',
)
def router_callback_easypay() -> PlainTextResponse:
"""
Handle EasyPay payment callback and return payment status.
Returns:
PlainTextResponse: A response containing the payment status for the EasyPay payment callback.
"""
pass
# return PlainTextResponse("success", status_code=200)
@pay_router.get(
path='/custom/{order_no}/{id}',
summary='自定义支付回调',
description='Handle custom payment callback and return payment status.',
)
def router_callback_custom(order_no: str, id: str) -> ResponseBase:
"""
Handle custom payment callback and return payment status.
Args:
order_no (str): The order number for the payment.
id (str): The ID associated with the payment.
Returns:
ResponseModel: A model containing the response data for the custom payment callback.
"""
pass
@upload_router.post(
path='/remote/{session_id}/{key}',
summary='远程上传回调',
description='Handle remote upload callback and return upload status.',
)
def router_callback_remote(session_id: str, key: str) -> ResponseBase:
"""
Handle remote upload callback and return upload status.
Args:
session_id (str): The session ID for the upload.
key (str): The key for the uploaded file.
Returns:
ResponseModel: A model containing the response data for the remote upload callback.
"""
pass
@upload_router.post(
path='/qiniu/{session_id}',
summary='七牛云上传回调',
description='Handle Qiniu Cloud upload callback and return upload status.',
)
def router_callback_qiniu(session_id: str) -> ResponseBase:
"""
Handle Qiniu Cloud upload callback and return upload status.
Args:
session_id (str): The session ID for the upload.
Returns:
ResponseModel: A model containing the response data for the Qiniu Cloud upload callback.
"""
pass
@upload_router.post(
path='/tencent/{session_id}',
summary='腾讯云上传回调',
description='Handle Tencent Cloud upload callback and return upload status.',
)
def router_callback_tencent(session_id: str) -> ResponseBase:
"""
Handle Tencent Cloud upload callback and return upload status.
Args:
session_id (str): The session ID for the upload.
Returns:
ResponseModel: A model containing the response data for the Tencent Cloud upload callback.
"""
pass
@upload_router.post(
path='/aliyun/{session_id}',
summary='阿里云上传回调',
description='Handle Aliyun upload callback and return upload status.',
)
def router_callback_aliyun(session_id: str) -> ResponseBase:
"""
Handle Aliyun upload callback and return upload status.
Args:
session_id (str): The session ID for the upload.
Returns:
ResponseModel: A model containing the response data for the Aliyun upload callback.
"""
pass
@upload_router.post(
path='/upyun/{session_id}',
summary='又拍云上传回调',
description='Handle Upyun upload callback and return upload status.',
)
def router_callback_upyun(session_id: str) -> ResponseBase:
"""
Handle Upyun upload callback and return upload status.
Args:
session_id (str): The session ID for the upload.
Returns:
ResponseModel: A model containing the response data for the Upyun upload callback.
"""
pass
@upload_router.post(
path='/aws/{session_id}',
summary='AWS S3上传回调',
description='Handle AWS S3 upload callback and return upload status.',
)
def router_callback_aws(session_id: str) -> ResponseBase:
"""
Handle AWS S3 upload callback and return upload status.
Args:
session_id (str): The session ID for the upload.
Returns:
ResponseModel: A model containing the response data for the AWS S3 upload callback.
"""
pass
@upload_router.post(
path='/onedrive/finish/{session_id}',
summary='OneDrive上传完成回调',
description='Handle OneDrive upload completion callback and return upload status.',
)
def router_callback_onedrive_finish(session_id: str) -> ResponseBase:
"""
Handle OneDrive upload completion callback and return upload status.
Args:
session_id (str): The session ID for the upload.
Returns:
ResponseModel: A model containing the response data for the OneDrive upload completion callback.
"""
pass
@upload_router.get(
path='/ondrive/auth',
summary='OneDrive授权回调',
description='Handle OneDrive authorization callback and return authorization status.',
)
def router_callback_onedrive_auth() -> ResponseBase:
"""
Handle OneDrive authorization callback and return authorization status.
Returns:
ResponseModel: A model containing the response data for the OneDrive authorization callback.
"""
pass
@upload_router.get(
path='/google/auth',
summary='Google OAuth 完成',
description='Handle Google OAuth completion callback and return authorization status.',
)
def router_callback_google_auth() -> ResponseBase:
"""
Handle Google OAuth completion callback and return authorization status.
Returns:
ResponseModel: A model containing the response data for the Google OAuth completion callback.
"""
pass

View File

@@ -0,0 +1,155 @@
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
from middleware.auth import AuthRequired
from middleware.dependencies import SessionDep
from models import (
DirectoryCreateRequest,
DirectoryResponse,
Object,
ObjectResponse,
ObjectType,
PolicyResponse,
User,
response,
)
directory_router = APIRouter(
prefix="/directory",
tags=["directory"]
)
@directory_router.get(
path="/{path:path}",
summary="获取目录内容",
)
async def router_directory_get(
session: SessionDep,
user: Annotated[User, Depends(AuthRequired)],
path: str
) -> DirectoryResponse:
"""
获取目录内容
路径必须以用户名开头,如 /api/directory/admin 或 /api/directory/admin/docs
:param session: 数据库会话
:param user: 当前登录用户
:param path: 目录路径(必须以用户名开头)
:return: 目录内容
"""
# 路径必须以用户名开头
path = path.strip("/")
if not path:
raise HTTPException(status_code=400, detail="路径不能为空,请使用 /{username} 格式")
path_parts = path.split("/")
if path_parts[0] != user.username:
raise HTTPException(status_code=403, detail="无权访问其他用户的目录")
folder = await Object.get_by_path(session, user.id, "/" + path, user.username)
if not folder:
raise HTTPException(status_code=404, detail="目录不存在")
if not folder.is_folder:
raise HTTPException(status_code=400, detail="指定路径不是目录")
children = await Object.get_children(session, user.id, folder.id)
policy = await folder.awaitable_attrs.policy
objects = [
ObjectResponse(
id=child.id,
name=child.name,
path=f"/{child.name}", # TODO: 完整路径
thumb=False,
size=child.size,
type=ObjectType.FOLDER if child.is_folder else ObjectType.FILE,
date=child.updated_at,
create_date=child.created_at,
source_enabled=False,
)
for child in children
]
policy_response = PolicyResponse(
id=policy.id,
name=policy.name,
type=policy.type.value,
max_size=policy.max_size,
)
return DirectoryResponse(
id=folder.id,
parent=folder.parent_id,
objects=objects,
policy=policy_response,
)
@directory_router.put(
path="/",
summary="创建目录",
)
async def router_directory_create(
session: SessionDep,
user: Annotated[User, Depends(AuthRequired)],
request: DirectoryCreateRequest
) -> response.ResponseBase:
"""
创建目录
:param session: 数据库会话
:param user: 当前登录用户
:param request: 创建请求(包含 parent_id UUID 和 name
:return: 创建结果
"""
# 验证目录名称
name = request.name.strip()
if not name:
raise HTTPException(status_code=400, detail="目录名称不能为空")
if "/" in name or "\\" in name:
raise HTTPException(status_code=400, detail="目录名称不能包含斜杠")
# 通过 UUID 获取父目录
parent = await Object.get(session, Object.id == request.parent_id)
if not parent or parent.owner_id != user.id:
raise HTTPException(status_code=404, detail="父目录不存在")
if not parent.is_folder:
raise HTTPException(status_code=400, detail="父路径不是目录")
# 检查是否已存在同名对象
existing = await Object.get(
session,
(Object.owner_id == user.id) &
(Object.parent_id == parent.id) &
(Object.name == name)
)
if existing:
raise HTTPException(status_code=409, detail="同名文件或目录已存在")
policy_id = request.policy_id if request.policy_id else parent.policy_id
parent_id = parent.id # 在 save 前保存
new_folder = Object(
name=name,
type=ObjectType.FOLDER,
owner_id=user.id,
parent_id=parent_id,
policy_id=policy_id,
)
new_folder_id = new_folder.id # 在 save 前保存 UUID
new_folder_name = new_folder.name
await new_folder.save(session)
return response.ResponseBase(
data={
"id": new_folder_id,
"name": new_folder_name,
"parent_id": parent_id,
}
)

View File

@@ -0,0 +1,107 @@
from fastapi import APIRouter, Depends
from middleware.auth import SignRequired
from models.response import ResponseBase
aria2_router = APIRouter(
prefix="/aria2",
tags=["aria2"]
)
@aria2_router.post(
path='/url',
summary='创建URL下载任务',
description='Create a URL download task endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_aria2_url() -> ResponseBase:
"""
Create a URL download task endpoint.
Returns:
ResponseModel: A model containing the response data for the URL download task.
"""
pass
@aria2_router.post(
path='/torrent/{id}',
summary='创建种子下载任务',
description='Create a torrent download task endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_aria2_torrent(id: str) -> ResponseBase:
"""
Create a torrent download task endpoint.
Args:
id (str): The ID of the torrent to download.
Returns:
ResponseModel: A model containing the response data for the torrent download task.
"""
pass
@aria2_router.put(
path='/select/{gid}',
summary='重新选择要下载的文件',
description='Re-select files to download endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_aria2_select(gid: str) -> ResponseBase:
"""
Re-select files to download endpoint.
Args:
gid (str): The GID of the download task.
Returns:
ResponseModel: A model containing the response data for the re-selection of files.
"""
pass
@aria2_router.delete(
path='/task/{gid}',
summary='取消或删除下载任务',
description='Delete a download task endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_aria2_delete(gid: str) -> ResponseBase:
"""
Delete a download task endpoint.
Args:
gid (str): The GID of the download task to delete.
Returns:
ResponseModel: A model containing the response data for the deletion of the download task.
"""
pass
@aria2_router.get(
'/downloading',
summary='获取正在下载中的任务',
description='Get currently downloading tasks endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_aria2_downloading() -> ResponseBase:
"""
Get currently downloading tasks endpoint.
Returns:
ResponseModel: A model containing the response data for currently downloading tasks.
"""
pass
@aria2_router.get(
path='/finished',
summary='获取已完成的任务',
description='Get finished tasks endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_aria2_finished() -> ResponseBase:
"""
Get finished tasks endpoint.
Returns:
ResponseModel: A model containing the response data for finished tasks.
"""
pass

View File

@@ -0,0 +1,382 @@
from fastapi import APIRouter, Depends, UploadFile
from fastapi.responses import FileResponse
from middleware.auth import SignRequired
from models.response import ResponseBase
file_router = APIRouter(
prefix="/file",
tags=["file"]
)
file_upload_router = APIRouter(
prefix="/file/upload",
tags=["file"]
)
@file_router.get(
path='/get/{id}/{name}',
summary='文件外链(直接输出文件数据)',
description='Get file external link endpoint.',
)
def router_file_get(id: str, name: str) -> FileResponse:
"""
Get file external link endpoint.
Args:
id (str): The ID of the file.
name (str): The name of the file.
Returns:
FileResponse: A response containing the file data.
"""
pass
@file_router.get(
path='/source/{id}/{name}',
summary='文件外链(301跳转)',
description='Get file external link with 301 redirect endpoint.',
)
def router_file_source(id: str, name: str) -> ResponseBase:
"""
Get file external link with 301 redirect endpoint.
Args:
id (str): The ID of the file.
name (str): The name of the file.
Returns:
ResponseModel: A model containing the response data for the file with a redirect.
"""
pass
@file_upload_router.get(
path='/download/{id}',
summary='下载文件',
description='Download file endpoint.',
)
def router_file_download(id: str) -> ResponseBase:
"""
Download file endpoint.
Args:
id (str): The ID of the file to download.
Returns:
ResponseModel: A model containing the response data for the file download.
"""
pass
@file_upload_router.get(
path='/archive/{sessionID}/archive.zip',
summary='打包并下载文件',
description='Archive and download files endpoint.',
)
def router_file_archive_download(sessionID: str) -> ResponseBase:
"""
Archive and download files endpoint.
Args:
sessionID (str): The session ID for the archive.
Returns:
ResponseModel: A model containing the response data for the archived files download.
"""
pass
@file_upload_router.post(
path='/{sessionID}/{index}',
summary='文件上传',
description='File upload endpoint.',
)
def router_file_upload(sessionID: str, index: int, file: UploadFile) -> ResponseBase:
"""
File upload endpoint.
Args:
sessionID (str): The session ID for the upload.
index (int): The index of the file being uploaded.
Returns:
ResponseModel: A model containing the response data.
"""
pass
@file_upload_router.put(
path='/',
summary='创建上传会话',
description='Create an upload session endpoint.',
dependencies=[Depends(SignRequired)],
)
def router_file_upload_session() -> ResponseBase:
"""
Create an upload session endpoint.
Returns:
ResponseModel: A model containing the response data for the upload session.
"""
pass
@file_upload_router.delete(
path='/{sessionID}',
summary='删除上传会话',
description='Delete an upload session endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_file_upload_session_delete(sessionID: str) -> ResponseBase:
"""
Delete an upload session endpoint.
Args:
sessionID (str): The session ID to delete.
Returns:
ResponseModel: A model containing the response data for the deletion.
"""
pass
@file_upload_router.delete(
path='/',
summary='清除所有上传会话',
description='Clear all upload sessions endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_file_upload_session_clear() -> ResponseBase:
"""
Clear all upload sessions endpoint.
Returns:
ResponseModel: A model containing the response data for clearing all sessions.
"""
pass
@file_router.put(
path='/update/{id}',
summary='更新文件',
description='Update file information endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_file_update(id: str) -> ResponseBase:
"""
Update file information endpoint.
Args:
id (str): The ID of the file to update.
Returns:
ResponseModel: A model containing the response data for the file update.
"""
pass
@file_router.post(
path='/create',
summary='创建空白文件',
description='Create a blank file endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_file_create() -> ResponseBase:
"""
Create a blank file endpoint.
Returns:
ResponseModel: A model containing the response data for the file creation.
"""
pass
@file_router.put(
path='/download/{id}',
summary='创建文件下载会话',
description='Create a file download session endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_file_download(id: str) -> ResponseBase:
"""
Create a file download session endpoint.
Args:
id (str): The ID of the file to download.
Returns:
ResponseModel: A model containing the response data for the file download session.
"""
pass
@file_router.get(
path='/preview/{id}',
summary='预览文件',
description='Preview file endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_file_preview(id: str) -> ResponseBase:
"""
Preview file endpoint.
Args:
id (str): The ID of the file to preview.
Returns:
ResponseModel: A model containing the response data for the file preview.
"""
pass
@file_router.get(
path='/content/{id}',
summary='获取文本文件内容',
description='Get text file content endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_file_content(id: str) -> ResponseBase:
"""
Get text file content endpoint.
Args:
id (str): The ID of the text file.
Returns:
ResponseModel: A model containing the response data for the text file content.
"""
pass
@file_router.get(
path='/doc/{id}',
summary='获取Office文档预览地址',
description='Get Office document preview URL endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_file_doc(id: str) -> ResponseBase:
"""
Get Office document preview URL endpoint.
Args:
id (str): The ID of the Office document.
Returns:
ResponseModel: A model containing the response data for the Office document preview URL.
"""
pass
@file_router.get(
path='/thumb/{id}',
summary='获取文件缩略图',
description='Get file thumbnail endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_file_thumb(id: str) -> ResponseBase:
"""
Get file thumbnail endpoint.
Args:
id (str): The ID of the file to get the thumbnail for.
Returns:
ResponseModel: A model containing the response data for the file thumbnail.
"""
pass
@file_router.post(
path='/source/{id}',
summary='取得文件外链',
description='Get file external link endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_file_source(id: str) -> ResponseBase:
"""
Get file external link endpoint.
Args:
id (str): The ID of the file to get the external link for.
Returns:
ResponseModel: A model containing the response data for the file external link.
"""
pass
@file_router.post(
path='/archive',
summary='打包要下载的文件',
description='Archive files for download endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_file_archive(id: str) -> ResponseBase:
"""
Archive files for download endpoint.
Args:
id (str): The ID of the file to archive.
Returns:
ResponseModel: A model containing the response data for the archived files.
"""
pass
@file_router.post(
path='/compress',
summary='创建文件压缩任务',
description='Create file compression task endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_file_compress(id: str) -> ResponseBase:
"""
Create file compression task endpoint.
Args:
id (str): The ID of the file to compress.
Returns:
ResponseModel: A model containing the response data for the file compression task.
"""
pass
@file_router.post(
path='/decompress',
summary='创建文件解压任务',
description='Create file extraction task endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_file_decompress(id: str) -> ResponseBase:
"""
Create file extraction task endpoint.
Args:
id (str): The ID of the file to decompress.
Returns:
ResponseModel: A model containing the response data for the file extraction task.
"""
pass
@file_router.post(
path='/relocate',
summary='创建文件转移任务',
description='Create file relocation task endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_file_relocate(id: str) -> ResponseBase:
"""
Create file relocation task endpoint.
Args:
id (str): The ID of the file to relocate.
Returns:
ResponseModel: A model containing the response data for the file relocation task.
"""
pass
@file_router.get(
path='/search/{type}/{keyword}',
summary='搜索文件',
description='Search files by keyword endpoint.',
dependencies=[Depends(SignRequired)]
)
def router_file_search(type: str, keyword: str) -> ResponseBase:
"""
Search files by keyword endpoint.
Args:
type (str): The type of search (e.g., 'name', 'content').
keyword (str): The keyword to search for.
Returns:
ResponseModel: A model containing the response data for the file search.
"""
pass

View File

@@ -0,0 +1,156 @@
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
from middleware.auth import AuthRequired
from middleware.dependencies import SessionDep
from models import Object, ObjectDeleteRequest, ObjectMoveRequest, User
from models.response import ResponseBase
object_router = APIRouter(
prefix="/object",
tags=["object"]
)
@object_router.delete(
path='/',
summary='删除对象',
description='删除一个或多个对象(文件或目录)',
)
async def router_object_delete(
session: SessionDep,
user: Annotated[User, Depends(AuthRequired)],
request: ObjectDeleteRequest,
) -> ResponseBase:
"""
删除对象端点
:param session: 数据库会话
:param user: 当前登录用户
:param request: 删除请求包含待删除对象的UUID列表
:return: 删除结果
"""
deleted_count = 0
for obj_id in request.ids:
obj = await Object.get(session, Object.id == obj_id)
if obj and obj.owner_id == user.id:
# TODO: 递归删除子对象(如果是目录)
# TODO: 更新用户存储空间
await obj.delete(session)
deleted_count += 1
return ResponseBase(
data={
"deleted": deleted_count,
"total": len(request.ids),
}
)
@object_router.patch(
path='/',
summary='移动对象',
description='移动一个或多个对象到目标目录',
)
async def router_object_move(
session: SessionDep,
user: Annotated[User, Depends(AuthRequired)],
request: ObjectMoveRequest,
) -> ResponseBase:
"""
移动对象端点
:param session: 数据库会话
:param user: 当前登录用户
:param request: 移动请求包含源对象UUID列表和目标目录UUID
:return: 移动结果
"""
# 验证目标目录
dst = await Object.get(session, Object.id == request.dst_id)
if not dst or dst.owner_id != user.id:
raise HTTPException(status_code=404, detail="目标目录不存在")
if not dst.is_folder:
raise HTTPException(status_code=400, detail="目标不是有效文件夹")
moved_count = 0
for src_id in request.src_ids:
src = await Object.get(session, Object.id == src_id)
if not src or src.owner_id != user.id:
continue
# 检查是否移动到自身或子目录(防止循环引用)
if src.id == dst.id:
continue
# 检查目标目录下是否存在同名对象
existing = await Object.get(
session,
(Object.owner_id == user.id) &
(Object.parent_id == dst.id) &
(Object.name == src.name)
)
if existing:
continue # 跳过重名对象
src.parent_id = dst.id
await src.save(session)
moved_count += 1
return ResponseBase(
data={
"moved": moved_count,
"total": len(request.src_ids),
}
)
@object_router.post(
path='/copy',
summary='复制对象',
description='Copy an object endpoint.',
dependencies=[Depends(AuthRequired)]
)
def router_object_copy() -> ResponseBase:
"""
Copy an object endpoint.
Returns:
ResponseModel: A model containing the response data for the object copy.
"""
pass
@object_router.post(
path='/rename',
summary='重命名对象',
description='Rename an object endpoint.',
dependencies=[Depends(AuthRequired)]
)
def router_object_rename() -> ResponseBase:
"""
Rename an object endpoint.
Returns:
ResponseModel: A model containing the response data for the object rename.
"""
pass
@object_router.get(
path='/property/{id}',
summary='获取对象属性',
description='Get object properties endpoint.',
dependencies=[Depends(AuthRequired)]
)
def router_object_property(id: str) -> ResponseBase:
"""
Get object properties endpoint.
Args:
id (str): The ID of the object to retrieve properties for.
Returns:
ResponseModel: A model containing the response data for the object properties.
"""
pass

View File

@@ -0,0 +1,105 @@
from fastapi import APIRouter
from sqlalchemy import and_
import json
from middleware.dependencies import SessionDep
from models.response import ResponseBase
from models.setting import Setting
site_router = APIRouter(
prefix="/site",
tags=["site"],
)
async def _get_setting(session: SessionDep, type_: str, name: str) -> str | None:
"""获取设置值"""
setting = await Setting.get(session, and_(Setting.type == type_, Setting.name == name))
return setting.value if setting else None
async def _get_setting_bool(session: SessionDep, type_: str, name: str) -> bool:
"""获取布尔类型设置值"""
value = await _get_setting(session, type_, name)
return value == "1" if value else False
async def _get_setting_json(session: SessionDep, type_: str, name: str) -> dict | list | None:
"""获取 JSON 类型设置值"""
value = await _get_setting(session, type_, name)
return json.loads(value) if value else None
@site_router.get(
path="/ping",
summary="测试用路由",
description="A simple endpoint to check if the site is up and running.",
response_model=ResponseBase,
)
def router_site_ping():
"""
Ping the site to check if it is up and running.
Returns:
str: A message indicating the site is running.
"""
from utils.conf.appmeta import BackendVersion
return ResponseBase(data=BackendVersion)
@site_router.get(
path='/captcha',
summary='验证码',
description='Get a Base64 captcha image.',
response_model=ResponseBase,
)
def router_site_captcha():
"""
Get a Base64 captcha image.
Returns:
str: A Base64 encoded string of the captcha image.
"""
pass
@site_router.get(
path='/config',
summary='站点全局配置',
description='Get the configuration file.',
response_model=ResponseBase,
)
async def router_site_config(session: SessionDep):
"""
Get the configuration file.
Returns:
dict: The site configuration.
"""
return ResponseBase(
data={
"title": await _get_setting(session, "basic", "siteName"),
"loginCaptcha": await _get_setting_bool(session, "login", "login_captcha"),
"regCaptcha": await _get_setting_bool(session, "login", "reg_captcha"),
"forgetCaptcha": await _get_setting_bool(session, "login", "forget_captcha"),
"emailActive": await _get_setting_bool(session, "login", "email_active"),
"QQLogin": None,
"themes": await _get_setting_json(session, "basic", "themes"),
"defaultTheme": await _get_setting(session, "basic", "defaultTheme"),
"score_enabled": None,
"share_score_rate": None,
"home_view_method": await _get_setting(session, "view", "home_view_method"),
"share_view_method": await _get_setting(session, "view", "share_view_method"),
"authn": await _get_setting_bool(session, "authn", "authn_enabled"),
"user": {},
"captcha_type": None,
"captcha_ReCaptchaKey": await _get_setting(session, "captcha", "captcha_ReCaptchaKey"),
"captcha_CloudflareKey": await _get_setting(session, "captcha", "captcha_CloudflareKey"),
"captcha_tcaptcha_appid": None,
"site_notice": None,
"registerEnabled": await _get_setting_bool(session, "register", "register_enabled"),
"app_promotion": None,
"wopi_exts": None,
"app_feedback": None,
"app_forum": None,
}
)

View File

@@ -0,0 +1,225 @@
from fastapi import APIRouter, Depends
from fastapi.responses import FileResponse
from middleware.auth import SignRequired
from models.response import ResponseBase
slave_router = APIRouter(
prefix="/slave",
tags=["slave"],
)
slave_aria2_router = APIRouter(
prefix="/aria2",
tags=["slave_aria2"],
)
@slave_router.get(
path='/ping',
summary='测试用路由',
description='Test route for checking connectivity.',
)
def router_slave_ping() -> ResponseBase:
"""
Test route for checking connectivity.
Returns:
ResponseModel: A response model indicating success.
"""
from utils.conf.appmeta import BackendVersion
return ResponseBase(data=BackendVersion)
@slave_router.post(
path='/post',
summary='上传',
description='Upload data to the server.',
dependencies=[Depends(SignRequired)],
)
def router_slave_post(data: str) -> ResponseBase:
"""
Upload data to the server.
Args:
data (str): The data to be uploaded.
Returns:
ResponseModel: A response model indicating success.
"""
pass
@slave_router.get(
path='/get/{speed}/{path}/{name}',
summary='获取下载',
)
def router_slave_download(speed: int, path: str, name: str) -> ResponseBase:
"""
Get download information.
Args:
speed (int): The speed of the download.
path (str): The path where the file is located.
name (str): The name of the file to be downloaded.
Returns:
ResponseModel: A response model containing download information.
"""
pass
@slave_router.get(
path='/download/{sign}',
summary='根据签名下载文件',
description='Download a file based on its signature.',
dependencies=[Depends(SignRequired)],
)
def router_slave_download_by_sign(sign: str) -> FileResponse:
"""
Download a file based on its signature.
Args:
sign (str): The signature of the file to be downloaded.
Returns:
FileResponse: A response containing the file to be downloaded.
"""
pass
@slave_router.get(
path='/source/{speed}/{path}/{name}',
summary='获取文件外链',
description='Get the external link for a file based on its signature.',
dependencies=[Depends(SignRequired)],
)
def router_slave_source(speed: int, path: str, name: str) -> ResponseBase:
"""
Get the external link for a file based on its signature.
Args:
speed (int): The speed of the download.
path (str): The path where the file is located.
name (str): The name of the file to be linked.
Returns:
ResponseModel: A response model containing the external link for the file.
"""
pass
@slave_router.get(
path='/source/{sign}',
summary='根据签名获取文件',
description='Get a file based on its signature.',
dependencies=[Depends(SignRequired)],
)
def router_slave_source_by_sign(sign: str) -> FileResponse:
"""
Get a file based on its signature.
Args:
sign (str): The signature of the file to be retrieved.
Returns:
FileResponse: A response containing the file to be retrieved.
"""
pass
@slave_router.get(
path='/thumb/{id}',
summary='获取缩略图',
description='Get a thumbnail image based on its ID.',
dependencies=[Depends(SignRequired)],
)
def router_slave_thumb(id: str) -> ResponseBase:
"""
Get a thumbnail image based on its ID.
Args:
id (str): The ID of the thumbnail image.
Returns:
ResponseModel: A response model containing the Base64 encoded thumbnail image.
"""
pass
@slave_router.delete(
path='/delete',
summary='删除文件',
description='Delete a file from the server.',
dependencies=[Depends(SignRequired)],
)
def router_slave_delete(path: str) -> ResponseBase:
"""
Delete a file from the server.
Args:
path (str): The path of the file to be deleted.
Returns:
ResponseModel: A response model indicating success or failure of the deletion.
"""
pass
@slave_aria2_router.post(
path='/test',
summary='测试从机连接Aria2服务',
description='Test the connection to the Aria2 service from the slave.',
dependencies=[Depends(SignRequired)],
)
def router_slave_aria2_test() -> ResponseBase:
"""
Test the connection to the Aria2 service from the slave.
"""
pass
@slave_aria2_router.get(
path='/get/{gid}',
summary='获取Aria2任务信息',
description='Get information about an Aria2 task by its GID.',
dependencies=[Depends(SignRequired)],
)
def router_slave_aria2_get(gid: str = None) -> ResponseBase:
"""
Get information about an Aria2 task by its GID.
Args:
gid (str): The GID of the Aria2 task.
Returns:
ResponseModel: A response model containing the task information.
"""
pass
@slave_aria2_router.post(
path='/add',
summary='添加Aria2任务',
description='Add a new Aria2 task.',
dependencies=[Depends(SignRequired)],
)
def router_slave_aria2_add(gid: str, url: str, options: dict = None) -> ResponseBase:
"""
Add a new Aria2 task.
Args:
gid (str): The GID for the new task.
url (str): The URL of the file to be downloaded.
options (dict, optional): Additional options for the task.
Returns:
ResponseModel: A response model indicating success or failure of the task addition.
"""
pass
@slave_aria2_router.delete(
path='/remove/{gid}',
summary='删除Aria2任务',
description='Remove an Aria2 task by its GID.',
dependencies=[Depends(SignRequired)],
)
def router_slave_aria2_remove(gid: str) -> ResponseBase:
"""
Remove an Aria2 task by its GID.
Args:
gid (str): The GID of the Aria2 task to be removed.
Returns:
ResponseModel: A response model indicating success or failure of the task removal.
"""
pass

View File

@@ -0,0 +1,56 @@
from fastapi import APIRouter, Depends
from middleware.auth import SignRequired
from models.response import ResponseBase
tag_router = APIRouter(
prefix='/tag',
tags=["tag"],
)
@tag_router.post(
path='/filter',
summary='创建文件分类标签',
description='Create a file classification tag.',
dependencies=[Depends(SignRequired)],
)
def router_tag_create_filter() -> ResponseBase:
"""
Create a file classification tag.
Returns:
ResponseModel: A model containing the response data for the created tag.
"""
pass
@tag_router.post(
path='/link',
summary='创建目录快捷方式标签',
description='Create a directory shortcut tag.',
dependencies=[Depends(SignRequired)],
)
def router_tag_create_link() -> ResponseBase:
"""
Create a directory shortcut tag.
Returns:
ResponseModel: A model containing the response data for the created tag.
"""
pass
@tag_router.delete(
path='/{id}',
summary='删除标签',
description='Delete a tag by its ID.',
dependencies=[Depends(SignRequired)],
)
def router_tag_delete(id: str) -> ResponseBase:
"""
Delete a tag by its ID.
Args:
id (str): The ID of the tag to be deleted.
Returns:
ResponseModel: A model containing the response data for the deletion operation.
"""
pass

View File

@@ -0,0 +1,564 @@
from typing import Annotated, Literal
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy import and_
from webauthn import generate_registration_options
from webauthn.helpers import options_to_json_dict
from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired
import models
import service
from middleware.auth import AuthRequired
from middleware.dependencies import SessionDep
from utils.JWT.JWT import SECRET_KEY
from utils import Password
user_router = APIRouter(
prefix="/user",
tags=["user"],
)
user_settings_router = APIRouter(
prefix='/user/settings',
tags=["user", "user_settings"],
dependencies=[Depends(AuthRequired)],
)
@user_router.post(
path='/session',
summary='用户登录',
description='User login endpoint. 当用户启用两步验证时,需要传入 otp 参数。',
)
async def router_user_session(
session: SessionDep,
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
) -> models.TokenResponse:
"""
用户登录端点。
根据 OAuth2.1 规范,使用 password grant type 进行登录。
当用户启用两步验证时,需要在表单中传入 otp 参数(通过 scopes 字段传递)。
OAuth2 scopes 字段格式: "otp:123456" 或直接传入验证码
:raises HTTPException 401: 用户名或密码错误
:raises HTTPException 403: 用户账号被封禁或未完成注册
:raises HTTPException 428: 需要两步验证但未提供验证码
:raises HTTPException 400: 两步验证码无效
"""
username = form_data.username
password = form_data.password
# 从 scopes 中提取 OTP 验证码OAuth2.1 扩展方式)
# scopes 格式可以是 ["otp:123456"] 或 ["123456"]
otp_code: str | None = None
for scope in form_data.scopes:
if scope.startswith("otp:"):
otp_code = scope[4:]
break
elif scope.isdigit() and len(scope) == 6:
otp_code = scope
break
result = await service.user.Login(
session,
models.LoginRequest(
username=username,
password=password,
two_fa_code=otp_code,
),
)
if isinstance(result, models.TokenResponse):
return result
elif result is None:
raise HTTPException(status_code=401, detail="Invalid username or password")
elif result is False:
raise HTTPException(status_code=403, detail="User account is banned or not fully registered")
elif result == "2fa_required":
raise HTTPException(
status_code=428,
detail="Two-factor authentication required",
headers={"X-2FA-Required": "true"},
)
elif result == "2fa_invalid":
raise HTTPException(status_code=400, detail="Invalid two-factor authentication code")
else:
raise HTTPException(status_code=500, detail="Internal server error during login")
@user_router.post(
path='/',
summary='用户注册',
description='User registration endpoint.',
)
async def router_user_register(
session: SessionDep,
request: models.RegisterRequest,
) -> models.response.ResponseBase:
"""
用户注册端点
流程:
1. 验证用户名唯一性
2. 获取默认用户组
3. 创建用户记录
4. 创建以用户名命名的根目录
:param session: 数据库会话
:param request: 注册请求
:return: 注册结果
:raises HTTPException 400: 用户名已存在
:raises HTTPException 500: 默认用户组或存储策略不存在
"""
# 1. 验证用户名唯一性
existing_user = await models.User.get(
session,
models.User.username == request.username
)
if existing_user:
raise HTTPException(status_code=400, detail="用户名已存在")
# 2. 获取默认用户组(从设置中读取 UUID
default_group_setting: models.Setting | None = await models.Setting.get(
session,
and_(models.Setting.type == models.SettingsType.REGISTER, models.Setting.name == "default_group")
)
if default_group_setting is None or not default_group_setting.value:
raise HTTPException(status_code=500, detail="默认用户组设置不存在")
default_group_id = UUID(default_group_setting.value)
default_group = await models.Group.get(session, models.Group.id == default_group_id)
if not default_group:
raise HTTPException(status_code=500, detail="默认用户组不存在")
# 3. 创建用户
hashed_password = Password.hash(request.password)
new_user = models.User(
username=request.username,
password=hashed_password,
group_id=default_group.id,
)
new_user_id = new_user.id # 在 save 前保存 UUID
new_user_username = new_user.username
await new_user.save(session)
# 4. 创建以用户名命名的根目录
default_policy = await models.Policy.get(session, models.Policy.name == "本地存储")
if not default_policy:
raise HTTPException(status_code=500, detail="默认存储策略不存在")
await models.Object(
name=new_user_username,
type=models.ObjectType.FOLDER,
owner_id=new_user_id,
parent_id=None,
policy_id=default_policy.id,
).save(session)
return models.response.ResponseBase(
data={
"user_id": new_user_id,
"username": new_user_username,
},
msg="注册成功",
)
@user_router.post(
path='/code',
summary='发送验证码邮件',
description='Send a verification code email.',
)
def router_user_email_code(
reason: Literal['register', 'reset'] = 'register',
) -> models.response.ResponseBase:
"""
Send a verification code email.
Returns:
dict: A dictionary containing information about the password reset email.
"""
pass
@user_router.get(
path='/qq',
summary='初始化QQ登录',
description='Initialize QQ login for a user.',
)
def router_user_qq() -> models.response.ResponseBase:
"""
Initialize QQ login for a user.
Returns:
dict: A dictionary containing QQ login initialization information.
"""
pass
@user_router.get(
path='authn/{username}',
summary='WebAuthn登录初始化',
description='Initialize WebAuthn login for a user.',
)
async def router_user_authn(username: str) -> models.response.ResponseBase:
pass
@user_router.post(
path='authn/finish/{username}',
summary='WebAuthn登录',
description='Finish WebAuthn login for a user.',
)
def router_user_authn_finish(username: str) -> models.response.ResponseBase:
"""
Finish WebAuthn login for a user.
Args:
username (str): The username of the user.
Returns:
dict: A dictionary containing WebAuthn login information.
"""
pass
@user_router.get(
path='/profile/{id}',
summary='获取用户主页展示用分享',
description='Get user profile for display.',
)
def router_user_profile(id: str) -> models.response.ResponseBase:
"""
Get user profile for display.
Args:
id (str): The user ID.
Returns:
dict: A dictionary containing user profile information.
"""
pass
@user_router.get(
path='/avatar/{id}/{size}',
summary='获取用户头像',
description='Get user avatar by ID and size.',
)
def router_user_avatar(id: str, size: int = 128) -> models.response.ResponseBase:
"""
Get user avatar by ID and size.
Args:
id (str): The user ID.
size (int): The size of the avatar image.
Returns:
str: A Base64 encoded string of the user avatar image.
"""
pass
#####################
# 需要登录的接口
#####################
@user_router.get(
path='/me',
summary='获取用户信息',
description='Get user information.',
dependencies=[Depends(dependency=AuthRequired)],
response_model=models.response.ResponseBase,
)
async def router_user_me(
session: SessionDep,
user: Annotated[models.User, Depends(AuthRequired)],
) -> models.response.ResponseBase:
"""
获取用户信息.
:return: response.ResponseModel containing user information.
:rtype: response.ResponseModel
"""
# 加载 group 及其 options 关系
group = await models.Group.get(
session,
models.Group.id == user.group_id,
load=models.Group.options
)
# 构建 GroupResponse
group_response = group.to_response() if group else None
# 异步加载 tags 关系
user_tags = await user.awaitable_attrs.tags
user_response = models.UserResponse(
id=user.id,
username=user.username,
status=user.status,
score=user.score,
nickname=user.nickname,
avatar=user.avatar,
created_at=user.created_at,
group=group_response,
tags=[tag.name for tag in user_tags] if user_tags else [],
)
return models.response.ResponseBase(data=user_response.model_dump())
@user_router.get(
path='/storage',
summary='存储信息',
description='Get user storage information.',
dependencies=[Depends(AuthRequired)],
)
async def router_user_storage(
session: SessionDep,
user: Annotated[models.user.User, Depends(AuthRequired)],
) -> models.response.ResponseBase:
"""
获取用户存储空间信息。
返回值:
- used: 已使用空间(字节)
- free: 剩余空间(字节)
- total: 总容量(字节)= 用户组容量
"""
# 获取用户组的基础存储容量
group = await models.Group.get(session, models.Group.id == user.group_id)
if not group:
raise HTTPException(status_code=500, detail="用户组不存在")
total: int = group.max_storage
used: int = user.storage
free: int = max(0, total - used)
return models.response.ResponseBase(
data={
"used": used,
"free": free,
"total": total,
}
)
@user_router.put(
path='/authn/start',
summary='WebAuthn登录初始化',
description='Initialize WebAuthn login for a user.',
dependencies=[Depends(AuthRequired)],
)
async def router_user_authn_start(
session: SessionDep,
user: Annotated[models.user.User, Depends(AuthRequired)],
) -> models.response.ResponseBase:
"""
Initialize WebAuthn login for a user.
Returns:
dict: A dictionary containing WebAuthn initialization information.
"""
# TODO: 检查 WebAuthn 是否开启,用户是否有注册过 WebAuthn 设备等
authn_setting = await models.Setting.get(
session,
and_(models.Setting.type == "authn", models.Setting.name == "authn_enabled")
)
if not authn_setting or authn_setting.value != "1":
raise HTTPException(status_code=400, detail="WebAuthn is not enabled")
site_url_setting = await models.Setting.get(
session,
and_(models.Setting.type == "basic", models.Setting.name == "siteURL")
)
site_title_setting = await models.Setting.get(
session,
and_(models.Setting.type == "basic", models.Setting.name == "siteTitle")
)
options = generate_registration_options(
rp_id=site_url_setting.value if site_url_setting else "",
rp_name=site_title_setting.value if site_title_setting else "",
user_name=user.username,
user_display_name=user.nick or user.username,
)
return models.response.ResponseBase(data=options_to_json_dict(options))
@user_router.put(
path='/authn/finish',
summary='WebAuthn登录',
description='Finish WebAuthn login for a user.',
dependencies=[Depends(AuthRequired)],
)
def router_user_authn_finish() -> models.response.ResponseBase:
"""
Finish WebAuthn login for a user.
Returns:
dict: A dictionary containing WebAuthn login information.
"""
pass
@user_settings_router.get(
path='/policies',
summary='获取用户可选存储策略',
description='Get user selectable storage policies.',
)
def router_user_settings_policies() -> models.response.ResponseBase:
"""
Get user selectable storage policies.
Returns:
dict: A dictionary containing available storage policies for the user.
"""
pass
@user_settings_router.get(
path='/nodes',
summary='获取用户可选节点',
description='Get user selectable nodes.',
dependencies=[Depends(AuthRequired)],
)
def router_user_settings_nodes() -> models.response.ResponseBase:
"""
Get user selectable nodes.
Returns:
dict: A dictionary containing available nodes for the user.
"""
pass
@user_settings_router.get(
path='/tasks',
summary='任务队列',
description='Get user task queue.',
dependencies=[Depends(AuthRequired)],
)
def router_user_settings_tasks() -> models.response.ResponseBase:
"""
Get user task queue.
Returns:
dict: A dictionary containing the user's task queue information.
"""
pass
@user_settings_router.get(
path='/',
summary='获取当前用户设定',
description='Get current user settings.',
dependencies=[Depends(AuthRequired)],
)
def router_user_settings() -> models.response.ResponseBase:
"""
Get current user settings.
Returns:
dict: A dictionary containing the current user settings.
"""
return models.response.ResponseBase(data=models.UserSettingResponse().model_dump())
@user_settings_router.post(
path='/avatar',
summary='从文件上传头像',
description='Upload user avatar from file.',
dependencies=[Depends(AuthRequired)],
)
def router_user_settings_avatar() -> models.response.ResponseBase:
"""
Upload user avatar from file.
Returns:
dict: A dictionary containing the result of the avatar upload.
"""
pass
@user_settings_router.put(
path='/avatar',
summary='设定为Gravatar头像',
description='Set user avatar to Gravatar.',
dependencies=[Depends(AuthRequired)],
)
def router_user_settings_avatar_gravatar() -> models.response.ResponseBase:
"""
Set user avatar to Gravatar.
Returns:
dict: A dictionary containing the result of setting the Gravatar avatar.
"""
pass
@user_settings_router.patch(
path='/{option}',
summary='更新用户设定',
description='Update user settings.',
dependencies=[Depends(AuthRequired)],
)
def router_user_settings_patch(option: str) -> models.response.ResponseBase:
"""
Update user settings.
Args:
option (str): The setting option to update.
Returns:
dict: A dictionary containing the result of the settings update.
"""
pass
@user_settings_router.get(
path='/2fa',
summary='获取两步验证初始化信息',
description='Get two-factor authentication initialization information.',
dependencies=[Depends(AuthRequired)],
)
async def router_user_settings_2fa(
user: Annotated[models.user.User, Depends(AuthRequired)],
) -> models.response.ResponseBase:
"""
Get two-factor authentication initialization information.
Returns:
dict: A dictionary containing two-factor authentication setup information.
"""
return models.response.ResponseBase(
data=await Password.generate_totp(user.username)
)
@user_settings_router.post(
path='/2fa',
summary='启用两步验证',
description='Enable two-factor authentication.',
dependencies=[Depends(AuthRequired)],
)
async def router_user_settings_2fa_enable(
session: SessionDep,
user: Annotated[models.user.User, Depends(AuthRequired)],
setup_token: str,
code: str,
) -> models.response.ResponseBase:
"""
Enable two-factor authentication for the user.
Returns:
dict: A dictionary containing the result of enabling two-factor authentication.
"""
serializer = URLSafeTimedSerializer(SECRET_KEY)
try:
# 1. 解包 Token设置有效期例如 600秒
secret = serializer.loads(setup_token, salt="2fa-setup-salt", max_age=600)
except SignatureExpired:
raise HTTPException(status_code=400, detail="Setup session expired")
except BadSignature:
raise HTTPException(status_code=400, detail="Invalid token")
# 2. 验证用户输入的 6 位验证码
if not Password.verify_totp(secret, code):
raise HTTPException(status_code=400, detail="Invalid OTP code")
# 3. 将 secret 存储到用户的数据库记录中,启用 2FA
user.two_factor = secret
user = await user.save(session)
return models.response.ResponseBase(
data={"message": "Two-factor authentication enabled successfully"}
)

View File

@@ -0,0 +1,104 @@
from fastapi import APIRouter, Depends
from middleware.auth import SignRequired
from models.response import ResponseBase
vas_router = APIRouter(
prefix="/vas",
tags=["vas"]
)
@vas_router.get(
path='/pack',
summary='获取容量包及配额信息',
description='Get information about storage packs and quotas.',
dependencies=[Depends(SignRequired)]
)
def router_vas_pack() -> ResponseBase:
"""
Get information about storage packs and quotas.
Returns:
ResponseModel: A model containing the response data for storage packs and quotas.
"""
pass
@vas_router.get(
path='/product',
summary='获取商品信息,同时返回支付信息',
description='Get product information along with payment details.',
dependencies=[Depends(SignRequired)]
)
def router_vas_product() -> ResponseBase:
"""
Get product information along with payment details.
Returns:
ResponseModel: A model containing the response data for products and payment information.
"""
pass
@vas_router.post(
path='/order',
summary='新建支付订单',
description='Create an order for a product.',
dependencies=[Depends(SignRequired)]
)
def router_vas_order() -> ResponseBase:
"""
Create an order for a product.
Returns:
ResponseModel: A model containing the response data for the created order.
"""
pass
@vas_router.get(
path='/order/{id}',
summary='查询订单状态',
description='Get information about a specific payment order by ID.',
dependencies=[Depends(SignRequired)]
)
def router_vas_order_get(id: str) -> ResponseBase:
"""
Get information about a specific payment order by ID.
Args:
id (str): The ID of the order to retrieve information for.
Returns:
ResponseModel: A model containing the response data for the specified order.
"""
pass
@vas_router.get(
path='/redeem',
summary='获取兑换码信息',
description='Get information about a specific redemption code.',
dependencies=[Depends(SignRequired)]
)
def router_vas_redeem(code: str) -> ResponseBase:
"""
Get information about a specific redemption code.
Args:
code (str): The redemption code to retrieve information for.
Returns:
ResponseModel: A model containing the response data for the specified redemption code.
"""
pass
@vas_router.post(
path='/redeem',
summary='执行兑换',
description='Redeem a redemption code for a product or service.',
dependencies=[Depends(SignRequired)]
)
def router_vas_redeem_post() -> ResponseBase:
"""
Redeem a redemption code for a product or service.
Returns:
ResponseModel: A model containing the response data for the redeemed code.
"""
pass

View File

@@ -0,0 +1,108 @@
from fastapi import APIRouter, Depends, Request
from middleware.auth import SignRequired
from models.response import ResponseBase
# WebDAV 管理路由
webdav_router = APIRouter(
prefix='/webdav',
tags=["webdav"],
)
@webdav_router.get(
path='/accounts',
summary='获取账号信息',
description='Get account information for WebDAV.',
dependencies=[Depends(SignRequired)],
)
def router_webdav_accounts() -> ResponseBase:
"""
Get account information for WebDAV.
Returns:
ResponseModel: A model containing the response data for the account information.
"""
pass
@webdav_router.post(
path='/accounts',
summary='新建账号',
description='Create a new WebDAV account.',
dependencies=[Depends(SignRequired)],
)
def router_webdav_create_account() -> ResponseBase:
"""
Create a new WebDAV account.
Returns:
ResponseModel: A model containing the response data for the created account.
"""
pass
@webdav_router.delete(
path='/accounts/{id}',
summary='删除账号',
description='Delete a WebDAV account by its ID.',
dependencies=[Depends(SignRequired)],
)
def router_webdav_delete_account(id: str) -> ResponseBase:
"""
Delete a WebDAV account by its ID.
Args:
id (str): The ID of the account to be deleted.
Returns:
ResponseModel: A model containing the response data for the deletion operation.
"""
pass
@webdav_router.post(
path='/mount',
summary='新建目录挂载',
description='Create a new WebDAV mount point.',
dependencies=[Depends(SignRequired)],
)
def router_webdav_create_mount() -> ResponseBase:
"""
Create a new WebDAV mount point.
Returns:
ResponseModel: A model containing the response data for the created mount point.
"""
pass
@webdav_router.delete(
path='/mount/{id}',
summary='删除目录挂载',
description='Delete a WebDAV mount point by its ID.',
dependencies=[Depends(SignRequired)],
)
def router_webdav_delete_mount(id: str) -> ResponseBase:
"""
Delete a WebDAV mount point by its ID.
Args:
id (str): The ID of the mount point to be deleted.
Returns:
ResponseModel: A model containing the response data for the deletion operation.
"""
pass
@webdav_router.patch(
path='accounts/{id}',
summary='更新账号信息',
description='Update WebDAV account information by ID.',
dependencies=[Depends(SignRequired)],
)
def router_webdav_update_account(id: str) -> ResponseBase:
"""
Update WebDAV account information by ID.
Args:
id (str): The ID of the account to be updated.
Returns:
ResponseModel: A model containing the response data for the updated account.
"""
pass