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"