diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/Server.iml b/.idea/Server.iml new file mode 100644 index 0000000..efcade3 --- /dev/null +++ b/.idea/Server.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..4443bdd --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,30 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml new file mode 100644 index 0000000..844ac1b --- /dev/null +++ b/.idea/material_theme_project_new.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..df4982d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..8f3a104 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 8b8033a..7e1ea5f 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,25 @@ 目前正处于 `OMEGA` 实验阶段,比 `Alpha` 版还更早期,仅供测试。 +## 特性 + +- 支持将文件存储到本地、远程节点、OneDrive 以及 S3兼容API、阿里云OSS等 +- 内置离线下载服务,亦可对接Aria2/qBittorrent 下载文件,可用多个节点分担下载任务 +- 在线压缩/解压缩文件,支持批量下载 +- 部署方便,开箱即用,亦可通过配置获得强大的生态能力 +- 可信、现代化的安全能力(JWT令牌、OAuth2、WebAuthn、全盘加密) +- 兼容 WebDAV、Subsonic 接口 +- 支持多用户、多群组,分级管理权限俱全 +- 强大的分享链接管理,支持分享页README渲染、媒体元数据展示 +- 在线预览/编辑多种文件,包括但不限于视频、图片、音频、PDF、ePub、Office、Markdown、图表等 +- 自定义主题色、深浅色主题、PWA、i18n + ## :alembic: 技术栈 -* [Python ](https://www.python.org/) + [FastAPI](https://fastapi.tiangolo.com/) +* [Python](https://www.python.org/) + [FastAPI](https://fastapi.tiangolo.com/) ## :scroll: 许可证 -GPL V3 - ---- -> GitHub [@Yuerchu](https://github.com/Yuerchu)  ·  -> Twitter [@LaBoyXiaoXin](https://twitter.com/LaBoyXiaoXin) +GPL V3 \ No newline at end of file diff --git a/main.py b/main.py index 2577be9..584421c 100644 --- a/main.py +++ b/main.py @@ -5,7 +5,7 @@ from models.database import init_db from models.migration import migration from pkg.lifespan import lifespan from pkg.JWT import JWT -from pkg.log import log +from pkg.log import log, set_log_level # 添加初始化数据库启动项 lifespan.add_startup(init_db) @@ -14,7 +14,9 @@ lifespan.add_startup(JWT.load_secret_key) # 设置日志等级 if appmeta.debug: - log.set_log_level(log.LogLevelEnum.DEBUG) + set_log_level('DEBUG') +else: + set_log_level('INFO') # 创建应用实例并设置元数据 app = FastAPI( diff --git a/models/migration.py b/models/migration.py index 64632fe..86954b5 100644 --- a/models/migration.py +++ b/models/migration.py @@ -213,7 +213,7 @@ async def init_default_user() -> None: admin_user = User( email="admin@yxqi.cn", nick="admin", - status=0, # 正常状态 + status=True, # 正常状态 group_id=admin_group.id, password=hashed_admin_password, ) diff --git a/models/policy.py b/models/policy.py index b1c2d17..4909762 100644 --- a/models/policy.py +++ b/models/policy.py @@ -29,4 +29,11 @@ class Policy(BaseModel, table=True): # 关系 files: List["File"] = Relationship(back_populates="policy") - folders: List["Folder"] = Relationship(back_populates="policy") \ No newline at end of file + folders: List["Folder"] = Relationship(back_populates="policy") + + @staticmethod + async def create( + policy: Optional["Policy"] = None, + **kwargs + ): + pass \ No newline at end of file diff --git a/models/request.py b/models/request.py new file mode 100644 index 0000000..afaf2db --- /dev/null +++ b/models/request.py @@ -0,0 +1,17 @@ +""" +请求模型定义 +""" + +from pydantic import BaseModel, Field +from typing import Literal, Union, Optional +from datetime import datetime, timezone +from uuid import uuid4 + +class LoginRequest(BaseModel): + """ + 登录请求模型 + """ + username: str = Field(..., description="用户名或邮箱") + password: str = Field(..., description="用户密码") + captcha: Optional[str] = Field(None, description="验证码") + twoFaCode: Optional[str] = Field(None, description="两步验证代码") \ No newline at end of file diff --git a/models/response.py b/models/response.py index 3467edc..fdb76df 100644 --- a/models/response.py +++ b/models/response.py @@ -1,3 +1,7 @@ +""" +响应模型定义 +""" + from pydantic import BaseModel, Field from typing import Literal, Union, Optional from datetime import datetime, timezone @@ -101,4 +105,33 @@ class UserSettingModel(BaseModel): qq: str | bool = Field(default=False, description="QQ号") themes: dict = Field(default_factory=dict, description="用户主题配置") two_factor: bool = Field(default=False, description="是否启用两步验证") - uid: int = Field(default=0, description="用户UID") \ No newline at end of file + uid: int = Field(default=0, description="用户UID") + +class FoldObjectModel(BaseModel): + id: str = Field(default=..., description="对象ID") + name: str = Field(default=..., description="对象名称") + path: str = Field(default=..., description="对象路径") + thumb: bool = Field(default=False, description="是否有缩略图") + size: int = Field(default=None, description="对象大小,单位字节") + type: Literal['file', 'folder'] = Field(default=..., description="对象类型,file表示文件,folder表示文件夹") + date: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), description="对象创建或修改时间") + create_date: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), description="对象创建时间") + source_enabled: bool = Field(default=False, description="是否启用离线下载源") + +class PolicyModel(BaseModel): + ''' + 存储策略模型 + ''' + id: str = Field(default=..., description="策略ID") + name: str = Field(default=..., description="策略名称") + type: Literal['local', 'qiniu', 'tencent', 'aliyun', 'onedrive', 'google_drive', 'dropbox', 'webdav', 'remote'] = Field(default=..., description="存储类型") + max_size: int = Field(default=0, description="单文件最大限制,单位字节,0表示不限制") + file_type: list = Field(default_factory=list, description="允许的文件类型列表,空列表表示不限制") + +class DirectoryModel(BaseModel): + ''' + 目录模型 + ''' + parent: str = Field(default=..., description="父目录ID") + objects: list[FoldObjectModel] = Field(default_factory=list, description="目录下的对象列表") + policy: PolicyModel = Field(default_factory=PolicyModel, description="存储策略") \ No newline at end of file diff --git a/models/user.py b/models/user.py index 2304c12..176e597 100644 --- a/models/user.py +++ b/models/user.py @@ -26,7 +26,7 @@ class User(BaseModel, table=True): email: str = Field(max_length=100, unique=True, index=True, description="用户邮箱,唯一") nick: Optional[str] = Field(default=None, max_length=50, description="用户昵称") password: str = Field(max_length=255, description="用户密码(加密后)") - status: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="用户状态: 0=正常, 1=未激活, 2=封禁") + status: Optional[bool] = Field(default=None, sa_column_kwargs={"server_default": "0"}, description="用户状态: True=正常, None=未激活, False=封禁") storage: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="已用存储空间(字节)") two_factor: Optional[str] = Field(default=None, max_length=255, description="两步验证密钥") avatar: Optional[str] = Field(default=None, max_length=255, description="头像地址") @@ -64,17 +64,7 @@ class User(BaseModel, table=True): @staticmethod async def create( user: Optional["User"] = None, - email: str = None, - nick: Optional[str] = None, - password: str = None, - status: int = 0, - two_factor: Optional[str] = None, - avatar: Optional[str] = None, - options: Optional[str] = None, - authn: Optional[str] = None, - open_id: Optional[str] = None, - score: int = 0, - phone: Optional[str] = None + **kwargs ): """ 向数据库内添加用户。 @@ -83,19 +73,7 @@ class User(BaseModel, table=True): :type user: User """ if not user: - user = User( - email=email, - nick=nick, - password=password, - status=status, - two_factor=two_factor, - avatar=avatar, - options=options, - authn=authn, - open_id=open_id, - score=score, - phone=phone - ) + user = User(**kwargs) from .database import get_session diff --git a/pkg/conf/appmeta.py b/pkg/conf/appmeta.py index fccb7c0..324ced0 100644 --- a/pkg/conf/appmeta.py +++ b/pkg/conf/appmeta.py @@ -13,7 +13,7 @@ BackendVersion = "0.0.1" IsPro = False -debug: bool = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes") +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.") diff --git a/pkg/log/__init__.py b/pkg/log/__init__.py new file mode 100644 index 0000000..f051ea0 --- /dev/null +++ b/pkg/log/__init__.py @@ -0,0 +1,2 @@ +from .log_handle import logger as log +from .log_handle import set_log_level \ No newline at end of file diff --git a/pkg/log/log.py b/pkg/log/log.py deleted file mode 100644 index 40fb33b..0000000 --- a/pkg/log/log.py +++ /dev/null @@ -1,155 +0,0 @@ -from rich import print -from rich.console import Console -from rich.markdown import Markdown -from typing import Literal, Optional, Dict, Union -from enum import Enum -import time -import os -import inspect - -class LogLevelEnum(str, Enum): - DEBUG = 'debug' - INFO = 'info' - WARNING = 'warning' - ERROR = 'error' - SUCCESS = 'success' - -# 默认日志级别 -LogLevel = LogLevelEnum.INFO - -def set_log_level(level: Union[str, LogLevelEnum]) -> None: - """设置日志级别""" - global LogLevel - if isinstance(level, str): - try: - LogLevel = LogLevelEnum(level.lower()) - except ValueError: - print(f"[bold red]无效的日志级别: {level},使用默认级别: {LogLevel}[/bold red]") - else: - LogLevel = level - -def truncate_path(full_path: str, marker: str = "HeyAuth") -> str: - """截断路径,只保留从marker开始的部分""" - try: - marker_index = full_path.find(marker) - if marker_index != -1: - return '.' + full_path[marker_index + len(marker):] - return full_path - except Exception: - return full_path - -def get_caller_info(depth: int = 2) -> tuple: - """获取调用者信息""" - try: - frame = inspect.currentframe() - # 向上查找指定深度的调用帧 - for _ in range(depth): - if frame.f_back is None: - break - frame = frame.f_back - - filename = frame.f_code.co_filename - lineno = frame.f_lineno - return truncate_path(filename), lineno - except Exception: - return "", 0 - finally: - # 确保引用被释放 - del frame - -def log(level: str = 'debug', message: str = ''): - """ - 输出日志 - --- - 通过传入的`level`和`message`参数,输出不同级别的日志信息。
- `level`参数为日志级别,支持`红色error`、`紫色info`、`绿色success`、`黄色warning`、`淡蓝色debug`。
- `message`参数为日志信息。
- """ - level_colors: Dict[str, str] = { - 'debug': '[bold cyan][DEBUG][/bold cyan]', - 'info': '[bold blue][INFO][/bold blue]', - 'warning': '[bold yellow][WARN][/bold yellow]', - 'error': '[bold red][ERROR][/bold red]', - 'success': '[bold green][SUCCESS][/bold green]' - } - - level_value = level.lower() - lv = level_colors.get(level_value, '[bold magenta][UNKNOWN][/bold magenta]') - - # 获取调用者信息 - filename, lineno = get_caller_info(3) # 考虑lambda调用和包装函数,深度为3 - timestamp = time.strftime('%Y/%m/%d %H:%M:%S %p', time.localtime()) - log_message = f"{lv}\t{timestamp} [bold]From {filename}, line {lineno}[/bold] {message}" - - # 根据日志级别判断是否输出 - global LogLevel - should_log = False - - if level_value == 'debug' and LogLevel == LogLevelEnum.DEBUG: - should_log = True - elif level_value == 'info' and LogLevel in [LogLevelEnum.DEBUG, LogLevelEnum.INFO]: - should_log = True - elif level_value == 'warning' and LogLevel in [LogLevelEnum.DEBUG, LogLevelEnum.INFO, LogLevelEnum.WARNING]: - should_log = True - elif level_value == 'error': - should_log = True - elif level_value == 'success': - should_log = False - - if should_log: - print(log_message) - -# 便捷日志函数 -debug = lambda message: log('debug', message) -info = lambda message: log('info', message) -warning = lambda message: log('warn', message) -error = lambda message: log('error', message) -success = lambda message: log('success', message) - -def title(title: str = '海枫授权系统 HeyAuth', size: Optional[Literal['h1', 'h2', 'h3', 'h4', 'h5']] = 'h1'): - """ - 输出标题 - --- - 通过传入的`title`参数,输出一个整行的标题。
- `title`参数为标题内容。
- """ - try: - console = Console() - markdown_sizes = { - 'h1': '# ', - 'h2': '## ', - 'h3': '### ', - 'h4': '#### ', - 'h5': '##### ' - } - - markdown_tag = markdown_sizes.get(size, '# ') - console.print(Markdown(markdown_tag + title)) - except Exception as e: - error(f"输出标题失败: {e}") - finally: - if 'console' in locals(): - del console - -if True: - pass - - -if __name__ == '__main__': - # 测试代码 - title('海枫授权系统 日志组件测试', 'h1') - title('测试h2标题', 'h2') - title('测试h3标题', 'h3') - title('测试h4标题', 'h4') - title('测试h5标题', 'h5') - - print("\n默认日志级别(INFO)测试:") - debug('这是一个debug日志') # 不会显示 - info('这是一个info日志') - warning('这是一个warning日志') - error('这是一个error日志') - success('这是一个success日志') - - print("\n设置为DEBUG级别测试:") - set_log_level(LogLevelEnum.DEBUG) - debug('这是一个debug日志') # 现在会显示 \ No newline at end of file diff --git a/pkg/log/log_handle.py b/pkg/log/log_handle.py new file mode 100644 index 0000000..6b10c98 --- /dev/null +++ b/pkg/log/log_handle.py @@ -0,0 +1,34 @@ +import logging +from rich.logging import RichHandler + +FOTMAT = "%(message)s" +logging.basicConfig( + level="NOTSET", + format=FOTMAT, + datefmt="[%X]", + handlers=[RichHandler(rich_tracebacks=True)], +) + +logger = logging.getLogger("rich") + +def set_log_level(level: str): + """ + 设置日志等级。 + + :param level: 日志等级 (DEBUG, INFO, WARNING, ERROR, CRITICAL) + :type level: str + """ + level = level.upper() + if level == "DEBUG": + logger.setLevel(logging.DEBUG) + elif level == "INFO": + logger.setLevel(logging.INFO) + elif level == "WARNING": + logger.setLevel(logging.WARNING) + elif level == "ERROR": + logger.setLevel(logging.ERROR) + elif level == "CRITICAL": + logger.setLevel(logging.CRITICAL) + else: + logger.setLevel(logging.INFO) + logger.warning(f"未知的日志等级 '{level}',已设置为默认等级 'INFO'。") \ No newline at end of file diff --git a/routers/controllers/directory.py b/routers/controllers/directory.py index c640364..5683bf3 100644 --- a/routers/controllers/directory.py +++ b/routers/controllers/directory.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Depends from middleware.auth import SignRequired -from models.response import ResponseModel +from models import response directory_router = APIRouter( prefix="/directory", @@ -13,7 +13,7 @@ directory_router = APIRouter( description='Create a directory endpoint.', dependencies=[Depends(SignRequired)] ) -def router_directory_create() -> ResponseModel: +def router_directory_create() -> response.ResponseModel: """ Create a directory endpoint. @@ -28,7 +28,7 @@ def router_directory_create() -> ResponseModel: description='Get directory contents endpoint.', dependencies=[Depends(SignRequired)] ) -def router_directory_get(path: str) -> ResponseModel: +def router_directory_get(path: str) -> response.ResponseModel: """ Get directory contents endpoint. @@ -38,4 +38,8 @@ def router_directory_get(path: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the directory contents. """ - pass \ No newline at end of file + return response.ResponseModel( + data=response.DirectoryModel( + + ) + ) \ No newline at end of file diff --git a/routers/controllers/user.py b/routers/controllers/user.py index fb7b998..26983b3 100644 --- a/routers/controllers/user.py +++ b/routers/controllers/user.py @@ -1,13 +1,26 @@ from typing import Annotated from fastapi import APIRouter, Depends, HTTPException from fastapi.security import OAuth2PasswordRequestForm -from middleware.auth import AuthRequired, SignRequired +from middleware.auth import AuthRequired, AuthRequired import models -from models.response import ResponseModel, TokenModel, userModel, groupModel, UserSettingModel from deprecated import deprecated from pkg.log import log import service +from webauthn import ( + generate_registration_options, + verify_authentication_response, + options_to_json, + base64url_to_bytes, +) + +from webauthn.helpers import options_to_json_dict + +from webauthn.helpers.structs import ( + PublicKeyCredentialDescriptor, + UserVerificationRequirement, +) + user_router = APIRouter( prefix="/user", tags=["user"], @@ -16,7 +29,7 @@ user_router = APIRouter( user_settings_router = APIRouter( prefix='/user/settings', tags=["user", "user_settings"], - dependencies=[Depends(SignRequired)], + dependencies=[Depends(AuthRequired)], ) @user_router.post( @@ -26,11 +39,15 @@ user_settings_router = APIRouter( ) async def router_user_session( form_data: Annotated[OAuth2PasswordRequestForm, Depends()] -) -> TokenModel: +) -> models.response.TokenModel: username = form_data.username password = form_data.password - is_login, detail = await service.user.Login(username=username, password=password) + is_login, detail = await service.user.Login( + models.request.LoginRequest( + username=username, password=password + ) + ) if not is_login: if detail in ["User not found", "Incorrect password"]: @@ -41,7 +58,7 @@ async def router_user_session( raise HTTPException(status_code=403, detail="User account is banned") else: raise HTTPException(status_code=500, detail="Internal server error during login") - if isinstance(detail, TokenModel): + if isinstance(detail, models.response.TokenModel): return detail else: log.error(f"Unexpected return type from login service: {type(detail)}") @@ -52,7 +69,7 @@ async def router_user_session( summary='用户注册', description='User registration endpoint.', ) -def router_user_register() -> ResponseModel: +def router_user_register() -> models.response.ResponseModel: """ User registration endpoint. @@ -66,7 +83,7 @@ def router_user_register() -> ResponseModel: summary='用两步验证登录', description='Two-factor authentication login endpoint.', ) -def router_user_2fa() -> ResponseModel: +def router_user_2fa() -> models.response.ResponseModel: """ Two-factor authentication login endpoint. @@ -80,9 +97,9 @@ def router_user_2fa() -> ResponseModel: summary='发送验证码邮件', description='Send a verification code email.', ) -def router_user_email_code() -> ResponseModel: +def router_user_email_code() -> models.response.ResponseModel: """ - Send a pas + Send a verification code email. Returns: dict: A dictionary containing information about the password reset email. @@ -98,7 +115,7 @@ def router_user_email_code() -> ResponseModel: summary='通过邮件里的链接重设密码', description='Reset password via email link.', ) -def router_user_reset_patch() -> ResponseModel: +def router_user_reset_patch() -> models.response.ResponseModel: """ Reset password via email link. @@ -112,7 +129,7 @@ def router_user_reset_patch() -> ResponseModel: summary='初始化QQ登录', description='Initialize QQ login for a user.', ) -def router_user_qq() -> ResponseModel: +def router_user_qq() -> models.response.ResponseModel: """ Initialize QQ login for a user. @@ -126,16 +143,8 @@ def router_user_qq() -> ResponseModel: summary='WebAuthn登录初始化', description='Initialize WebAuthn login for a user.', ) -def router_user_authn(username: str) -> ResponseModel: - """ - Initialize WebAuthn login for a user. +async def router_user_authn(username: str) -> models.response.ResponseModel: - Args: - username (str): The username of the user. - - Returns: - dict: A dictionary containing WebAuthn initialization information. - """ pass @user_router.post( @@ -143,7 +152,7 @@ def router_user_authn(username: str) -> ResponseModel: summary='WebAuthn登录', description='Finish WebAuthn login for a user.', ) -def router_user_authn_finish(username: str) -> ResponseModel: +def router_user_authn_finish(username: str) -> models.response.ResponseModel: """ Finish WebAuthn login for a user. @@ -160,7 +169,7 @@ def router_user_authn_finish(username: str) -> ResponseModel: summary='获取用户主页展示用分享', description='Get user profile for display.', ) -def router_user_profile(id: str) -> ResponseModel: +def router_user_profile(id: str) -> models.response.ResponseModel: """ Get user profile for display. @@ -177,7 +186,7 @@ def router_user_profile(id: str) -> ResponseModel: summary='获取用户头像', description='Get user avatar by ID and size.', ) -def router_user_avatar(id: str, size: int = 128) -> ResponseModel: +def router_user_avatar(id: str, size: int = 128) -> models.response.ResponseModel: """ Get user avatar by ID and size. @@ -199,28 +208,28 @@ def router_user_avatar(id: str, size: int = 128) -> ResponseModel: summary='获取用户信息', description='Get user information.', dependencies=[Depends(dependency=AuthRequired)], - response_model=ResponseModel, + response_model=models.response.ResponseModel, ) async def router_user_me( user: Annotated[models.user.User, Depends(AuthRequired)], -) -> ResponseModel: +) -> models.response.ResponseModel: """ 获取用户信息. - :return: ResponseModel containing user information. - :rtype: ResponseModel + :return: response.ResponseModel containing user information. + :rtype: response.ResponseModel """ group = await models.Group.get(id=user.group_id) - user_group = groupModel( + user_group = models.response.groupModel( id=group.id, name=group.name, allowShare=group.share_enabled, ) - users = userModel( + users = models.response.userModel( id=user.id, username=user.email, nickname=user.nick, @@ -231,7 +240,7 @@ async def router_user_me( ).model_dump() - return ResponseModel( + return models.response.ResponseModel( data=users ) @@ -239,18 +248,18 @@ async def router_user_me( path='/storage', summary='存储信息', description='Get user storage information.', - dependencies=[Depends(SignRequired)], + dependencies=[Depends(AuthRequired)], ) def router_user_storage( user: Annotated[models.user.User, Depends(AuthRequired)], -) -> ResponseModel: +) -> models.response.ResponseModel: """ Get user storage information. Returns: dict: A dictionary containing user storage information. """ - return ResponseModel( + return models.response.ResponseModel( data={ "used": 0, "free": 0, @@ -262,24 +271,40 @@ def router_user_storage( path='/authn/start', summary='WebAuthn登录初始化', description='Initialize WebAuthn login for a user.', - dependencies=[Depends(SignRequired)], + dependencies=[Depends(AuthRequired)], ) -def router_user_authn_start() -> ResponseModel: +async def router_user_authn_start( + user: Annotated[models.user.User, Depends(AuthRequired)] +) -> models.response.ResponseModel: """ Initialize WebAuthn login for a user. Returns: dict: A dictionary containing WebAuthn initialization information. """ - pass + # [TODO] 检查 WebAuthn 是否开启,用户是否有注册过 WebAuthn 设备等 + + if not await models.Setting.get(type="authn", name="authn_enabled", format="bool"): + raise HTTPException(status_code=400, detail="WebAuthn is not enabled") + + options = generate_registration_options( + rp_id=await models.Setting.get(type="basic", name="siteURL"), + rp_name=await models.Setting.get(type="basic", name="siteTitle"), + user_name=user.email, + user_display_name=user.nick or user.email, + ) + + return models.response.ResponseModel( + data=options_to_json_dict(options) + ) @user_router.put( path='/authn/finish', summary='WebAuthn登录', description='Finish WebAuthn login for a user.', - dependencies=[Depends(SignRequired)], + dependencies=[Depends(AuthRequired)], ) -def router_user_authn_finish() -> ResponseModel: +def router_user_authn_finish() -> models.response.ResponseModel: """ Finish WebAuthn login for a user. @@ -293,7 +318,7 @@ def router_user_authn_finish() -> ResponseModel: summary='获取用户可选存储策略', description='Get user selectable storage policies.', ) -def router_user_settings_policies() -> ResponseModel: +def router_user_settings_policies() -> models.response.ResponseModel: """ Get user selectable storage policies. @@ -306,9 +331,9 @@ def router_user_settings_policies() -> ResponseModel: path='/nodes', summary='获取用户可选节点', description='Get user selectable nodes.', - dependencies=[Depends(SignRequired)], + dependencies=[Depends(AuthRequired)], ) -def router_user_settings_nodes() -> ResponseModel: +def router_user_settings_nodes() -> models.response.ResponseModel: """ Get user selectable nodes. @@ -321,9 +346,9 @@ def router_user_settings_nodes() -> ResponseModel: path='/tasks', summary='任务队列', description='Get user task queue.', - dependencies=[Depends(SignRequired)], + dependencies=[Depends(AuthRequired)], ) -def router_user_settings_tasks() -> ResponseModel: +def router_user_settings_tasks() -> models.response.ResponseModel: """ Get user task queue. @@ -336,24 +361,24 @@ def router_user_settings_tasks() -> ResponseModel: path='/', summary='获取当前用户设定', description='Get current user settings.', - dependencies=[Depends(SignRequired)], + dependencies=[Depends(AuthRequired)], ) -def router_user_settings() -> ResponseModel: +def router_user_settings() -> models.response.ResponseModel: """ Get current user settings. Returns: dict: A dictionary containing the current user settings. """ - return ResponseModel(data=UserSettingModel().model_dump()) + return models.response.ResponseModel(data=models.response.UserSettingModel().model_dump()) @user_settings_router.post( path='/avatar', summary='从文件上传头像', description='Upload user avatar from file.', - dependencies=[Depends(SignRequired)], + dependencies=[Depends(AuthRequired)], ) -def router_user_settings_avatar() -> ResponseModel: +def router_user_settings_avatar() -> models.response.ResponseModel: """ Upload user avatar from file. @@ -366,9 +391,9 @@ def router_user_settings_avatar() -> ResponseModel: path='/avatar', summary='设定为Gravatar头像', description='Set user avatar to Gravatar.', - dependencies=[Depends(SignRequired)], + dependencies=[Depends(AuthRequired)], ) -def router_user_settings_avatar_gravatar() -> ResponseModel: +def router_user_settings_avatar_gravatar() -> models.response.ResponseModel: """ Set user avatar to Gravatar. @@ -381,9 +406,9 @@ def router_user_settings_avatar_gravatar() -> ResponseModel: path='/{option}', summary='更新用户设定', description='Update user settings.', - dependencies=[Depends(SignRequired)], + dependencies=[Depends(AuthRequired)], ) -def router_user_settings_patch(option: str) -> ResponseModel: +def router_user_settings_patch(option: str) -> models.response.ResponseModel: """ Update user settings. @@ -399,9 +424,9 @@ def router_user_settings_patch(option: str) -> ResponseModel: path='/2fa', summary='获取两步验证初始化信息', description='Get two-factor authentication initialization information.', - dependencies=[Depends(SignRequired)], + dependencies=[Depends(AuthRequired)], ) -def router_user_settings_2fa() -> ResponseModel: +def router_user_settings_2fa() -> models.response.ResponseModel: """ Get two-factor authentication initialization information. diff --git a/service/oauth/qq.py b/service/oauth/qq.py new file mode 100644 index 0000000..2ab3eb8 --- /dev/null +++ b/service/oauth/qq.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel +import aiohttp + +async def get_access_token( + code: str +) \ No newline at end of file diff --git a/service/user/login.py b/service/user/login.py index 6462397..29da851 100644 --- a/service/user/login.py +++ b/service/user/login.py @@ -1,15 +1,11 @@ from typing import Optional from models.setting import Setting +from models.request import LoginRequest from models.response import TokenModel from models.user import User from pkg.log import log -async def Login( - username: str, - password: str, - captcha: Optional[str] = None, - twoFaCode: Optional[str] = None -) -> tuple[bool, TokenModel | str]: +async def Login(LoginRequest: LoginRequest) -> tuple[bool, TokenModel | str]: """ 根据账号密码进行登录。 @@ -23,7 +19,7 @@ async def Login( :type password: str :param captcha: 验证码 :type captcha: Optional[str] - :param twoFaCode: 二次验证代码 + :param twoFaCode: 两步验证代码 :type twoFaCode: Optional[str] :return: TokenModel 对象或状态码或 None @@ -37,22 +33,22 @@ async def Login( # [TODO] 验证码校验 # 验证用户是否存在 - user = await User.get(email=username) + user = await User.get(email=LoginRequest.username) if not user: - log.debug(f"Cannot find user with email: {username}") + log.debug(f"Cannot find user with email: {LoginRequest.username}") return False, "User not found" # 验证密码是否正确 - if not Password.verify(user.password, password): - log.debug(f"Password verification failed for user: {username}") + if not Password.verify(user.password, LoginRequest.password): + log.debug(f"Password verification failed for user: {LoginRequest.username}") return False, "Incorrect password" # 验证用户是否可登录 - if user.status == 1: + if user.status == None: # 未完成注册 return False, "Need to complete registration" - elif user.status == 2: + elif user.status == False: # 账号已被封禁 return False, "Account is banned"