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:
52
utils/JWT/JWT.py
Normal file
52
utils/JWT/JWT.py
Normal 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
1
utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .password.pwd import Password, PasswordStatus
|
||||
116
utils/conf/appmeta.py
Normal file
116
utils/conf/appmeta.py
Normal 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": "管理员增值服务接口",
|
||||
}
|
||||
]
|
||||
39
utils/lifespan/lifespan.py
Normal file
39
utils/lifespan/lifespan.py
Normal 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
150
utils/password/pwd.py
Normal 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
|
||||
Reference in New Issue
Block a user