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

52
utils/JWT/JWT.py Normal file
View File

@@ -0,0 +1,52 @@
from datetime import datetime, timedelta, timezone
import jwt
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(
scheme_name='获取 JWT Bearer 令牌',
description='用于获取 JWT Bearer 令牌,需要以表单的形式提交',
tokenUrl="/api/user/session",
)
SECRET_KEY = ''
async def load_secret_key() -> None:
"""
从数据库读取 JWT 的密钥。
"""
# 延迟导入以避免循环依赖
from models.database import get_session
from models.setting import Setting
global SECRET_KEY
async for session in get_session():
setting = await Setting.get(
session,
(Setting.type == "auth") & (Setting.name == "secret_key")
)
if setting:
SECRET_KEY = setting.value
# 访问令牌
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> tuple[str, datetime]:
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(hours=3)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm='HS256')
return encoded_jwt, expire
# 刷新令牌
def create_refresh_token(data: dict, expires_delta: timedelta | None = None) -> tuple[str, datetime]:
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(days=30)
to_encode.update({"exp": expire, "token_type": "refresh"})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm='HS256')
return encoded_jwt, expire

1
utils/__init__.py Normal file
View File

@@ -0,0 +1 @@
from .password.pwd import Password, PasswordStatus

116
utils/conf/appmeta.py Normal file
View File

@@ -0,0 +1,116 @@
import os
from dotenv import load_dotenv
from loguru import logger as log
load_dotenv()
APP_NAME = 'DiskNext Server'
summary = '一款基于 FastAPI 的可公私兼备的网盘系统'
description = 'DiskNext Server 是一款基于 FastAPI 的网盘系统,支持个人和企业使用。它提供了高性能的文件存储和管理功能,支持多种认证方式。'
license_info = {"name": "GPLv3", "url": "https://opensource.org/license/gpl-3.0"}
BackendVersion = "0.0.1"
IsPro = False
debug: bool = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes") or False
if debug:
log.info("Debug mode is enabled. This is not recommended for production use.")
database_url: str = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///disknext.db")
tags_meta = [
{
"name": "site",
"description": "站点",
},
{
"name": "user",
"description": "用户",
},
{
"name": "user_settings",
"description": "用户设置",
},
{
"name": "share",
"description": "分享",
},
{
"name": "file",
"description": "文件",
},
{
"name": "aria2",
"description": "离线下载",
},
{
"name": "directory",
"description": "目录",
},
{
"name": "object",
"description": "对象,文件和目录的抽象",
},
{
"name": "callback",
"description": "回调接口",
},
{
"name": "oauth",
"description": "OAuth 认证",
},
{
"name": "pay",
"description": "支付回调",
},
{
"name": "upload",
"description": "上传回调",
},
{
"name": "vas",
"description": "增值服务",
},
{
"name": "tag",
"description": "用户标签",
},
{
"name": "webdav",
"description": "WebDAV管理相关",
},
{
"name": "admin",
"description": "管理员接口",
},
{
"name": "admin_group",
"description": "管理员组接口",
},
{
"name": "admin_user",
"description": "管理员用户接口",
},
{
"name": "admin_file",
"description": "管理员文件接口",
},
{
"name": "admin_aria2",
"description": "管理员离线下载接口",
},
{
"name": "admin_policy",
"description": "管理员策略接口",
},
{
"name": "admin_task",
"description": "管理员任务接口",
},
{
"name": "admin_vas",
"description": "管理员增值服务接口",
}
]

View File

@@ -0,0 +1,39 @@
from fastapi import FastAPI
from contextlib import asynccontextmanager
__on_startup: list[callable] = []
__on_shutdown: list[callable] = []
def add_startup(func: callable):
"""
注册一个函数,在应用启动时调用。
:param func: 需要注册的函数。它应该是一个异步函数。
"""
__on_startup.append(func)
def add_shutdown(func: callable):
"""
注册一个函数,在应用关闭时调用。
:param func: 需要注册的函数。
"""
__on_shutdown.append(func)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
应用程序的生命周期管理器。
此函数在应用启动时执行所有注册的启动函数,
并在应用关闭时执行所有注册的关闭函数。
"""
# Execute all startup functions
for func in __on_startup:
await func()
yield
# Execute all shutdown functions
for func in __on_shutdown:
await func()

150
utils/password/pwd.py Normal file
View File

@@ -0,0 +1,150 @@
import secrets
from loguru import logger
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
from enum import StrEnum
import pyotp
from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired
from pydantic import BaseModel, Field
from utils.JWT.JWT import SECRET_KEY
from utils.conf import appmeta
_ph = PasswordHasher()
class PasswordStatus(StrEnum):
"""密码校验状态枚举"""
VALID = "valid"
"""密码校验通过"""
INVALID = "invalid"
"""密码校验失败"""
EXPIRED = "expired"
"""密码哈希已过时,建议重新哈希"""
class TwoFactorBase(BaseModel):
"""两步验证请求 DTO"""
setup_token: str
"""用于验证的令牌"""
class TwoFactorResponse(TwoFactorBase):
"""两步验证-请求启用时的响应 DTO"""
uri: str
"""用于生成二维码的 URI"""
class TwoFactorVerifyRequest(TwoFactorBase):
"""两步验证-验证请求 DTO"""
code: int = Field(..., ge=100000, le=999999)
"""6 位验证码"""
class Password:
"""密码处理工具类,包含密码生成、哈希和验证功能"""
@staticmethod
def generate(
length: int = 8
) -> str:
"""
生成指定长度的随机密码。
:param length: 密码长度
:type length: int
:return: 随机密码
:rtype: str
"""
return secrets.token_hex(length)
@staticmethod
def hash(
password: str
) -> str:
"""
使用 Argon2 生成密码的哈希值。
返回的哈希字符串已经包含了所有需要验证的信息(盐、算法参数等)。
:param password: 需要哈希的原始密码
:return: Argon2 哈希字符串
"""
return _ph.hash(password)
@staticmethod
def verify(
hash: str,
password: str
) -> PasswordStatus:
"""
验证存储的 Argon2 哈希值与用户提供的密码是否匹配。
:param hash: 数据库中存储的 Argon2 哈希字符串
:param password: 用户本次提供的密码
:return: 如果密码匹配返回 True, 否则返回 False
"""
try:
# verify 函数会自动解析 stored_password 中的盐和参数
_ph.verify(hash, password)
# 检查哈希参数是否已过时。如果返回True
# 意味着你应该使用新的参数重新哈希密码并更新存储。
# 这是一个很好的实践,可以随着时间推移增强安全性。
if _ph.check_needs_rehash(hash):
logger.warning("密码哈希参数已过时,建议重新哈希并更新。")
return PasswordStatus.EXPIRED
return PasswordStatus.VALID
except VerifyMismatchError:
# 这是预期的异常,当密码不匹配时触发。
return PasswordStatus.INVALID
# 其他异常(如哈希格式错误)应该传播,让调用方感知系统问题
@staticmethod
async def generate_totp(
username: str
) -> TwoFactorResponse:
"""
生成 TOTP 密钥和对应的 URI用于两步验证。
:return: 包含 TOTP 密钥和 URI 的元组
"""
serializer = URLSafeTimedSerializer(SECRET_KEY)
secret = pyotp.random_base32()
setup_token = serializer.dumps(
secret,
salt="2fa-setup-salt"
)
otp_uri = pyotp.totp.TOTP(secret).provisioning_uri(
name=username,
issuer_name=appmeta.APP_NAME
)
return TwoFactorResponse(
uri=otp_uri,
setup_token=setup_token
)
@staticmethod
def verify_totp(
secret: str,
code: str
) -> PasswordStatus:
"""
验证 TOTP 验证码。
:param secret: TOTP 密钥Base32 编码)
:param code: 用户输入的 6 位验证码
:return: 验证是否成功
"""
totp = pyotp.TOTP(secret)
if totp.verify(code):
return PasswordStatus.VALID
else:
return PasswordStatus.INVALID