feat: 重构模型和响应结构,优化用户和对象管理逻辑,添加 Dockerfile

This commit is contained in:
2025-12-18 18:28:41 +08:00
parent 68343c710b
commit 89e837d91c
18 changed files with 493 additions and 270 deletions

13
Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
COPY . .
EXPOSE 5213
CMD ["uv", "run", "fastapi", "run", "main.py", "--host", "0.0.0.0", "--port", "5213"]

11
main.py
View File

@@ -1,10 +1,11 @@
from fastapi import FastAPI
from routers import routers
from pkg.conf import appmeta
from pkg.lifespan import lifespan
from models.database import init_db
from models.migration import migration
from pkg.lifespan import lifespan
from pkg.JWT import jwt as JWT
from pkg.JWT import JWT
from routers import routers
# 添加初始化数据库启动项
lifespan.add_startup(init_db)
@@ -32,6 +33,6 @@ if __name__ == "__main__":
import uvicorn
if appmeta.debug:
uvicorn.run(app='main:app', host=appmeta.host, port=appmeta.port, reload=True)
uvicorn.run(app='main:app', reload=True)
else:
uvicorn.run(app=app, host=appmeta.host, port=appmeta.port)
uvicorn.run(app=app)

View File

@@ -5,7 +5,7 @@ from jwt import InvalidTokenError
import jwt
from models.user import User
from pkg.JWT import jwt as JWT
from pkg.JWT import JWT
from .dependencies import SessionDep
credentials_exception = HTTPException(

View File

@@ -1,17 +1,35 @@
from . import response
from .user import User
from .user_authn import UserAuthn
from .user import (
LoginRequest,
ThemeResponse,
TokenResponse,
User,
UserBase,
UserPublic,
UserResponse,
UserSettingResponse,
WebAuthnInfo,
)
from .user_authn import AuthnResponse, UserAuthn
from .download import Download
from .object import Object, ObjectType
from .group import Group
from .group import Group, GroupBase, GroupOptionsBase, GroupResponse
from .node import Node
from .object import (
DirectoryCreateRequest,
DirectoryResponse,
Object,
ObjectBase,
ObjectResponse,
ObjectType,
PolicyResponse,
)
from .order import Order
from .policy import Policy
from .redeem import Redeem
from .report import Report
from .setting import Setting
from .setting import Setting, SettingsType, SiteConfigResponse
from .share import Share
from .source_link import SourceLink
from .storage_pack import StoragePack

View File

@@ -1,6 +1,6 @@
import uuid
from datetime import datetime, timezone
from typing import Union, List, TypeVar, Type, Literal, override, Optional, Any
from datetime import datetime
from typing import Union, List, TypeVar, Type, Literal, override, Optional
from fastapi import HTTPException
from sqlalchemy import DateTime, BinaryExpression, ClauseElement

View File

@@ -1,13 +1,75 @@
from typing import TYPE_CHECKING
from sqlmodel import Field, Relationship, text
from .base import TableBase
from .base import TableBase, SQLModelBase
if TYPE_CHECKING:
from .user import User
class GroupOptions(TableBase, table=True):
# ==================== Base 模型 ====================
class GroupBase(SQLModelBase):
"""用户组基础字段,供数据库模型和 DTO 共享"""
name: str
"""用户组名称"""
class GroupOptionsBase(SQLModelBase):
"""用户组选项基础字段,供数据库模型和 DTO 共享"""
share_download: bool = False
"""是否允许分享下载"""
share_free: bool = False
"""是否免积分分享"""
relocate: bool = False
"""是否允许文件重定位"""
source_batch: int = 0
"""批量获取源地址数量"""
select_node: bool = False
"""是否允许选择节点"""
advance_delete: bool = False
"""是否允许高级删除"""
# ==================== DTO 模型 ====================
class GroupResponse(GroupBase, GroupOptionsBase):
"""用户组响应 DTO"""
id: int
"""用户组ID"""
allow_share: bool = False
"""是否允许分享"""
allow_remote_download: bool = False
"""是否允许离线下载"""
allow_archive_download: bool = False
"""是否允许打包下载"""
compress: bool = False
"""是否允许压缩"""
webdav: bool = False
"""是否允许WebDAV"""
allow_webdav_proxy: bool = False
"""是否允许WebDAV代理"""
# ==================== 数据库模型 ====================
class GroupOptions(GroupOptionsBase, TableBase, table=True):
"""用户组选项模型"""
group_id: int = Field(foreign_key="group.id", unique=True)
@@ -19,41 +81,23 @@ class GroupOptions(TableBase, table=True):
archive_task: bool = False
"""是否允许创建打包任务"""
share_download: bool = False
"""是否允许分享下载"""
share_free: bool = False
"""是否免积分分享"""
webdav_proxy: bool = False
"""是否允许WebDAV代理"""
aria2: bool = False
"""是否允许使用aria2"""
relocate: bool = False
"""是否允许文件重定位"""
source_batch: int = 10
"""批量获取源地址数量"""
redirected_source: bool = False
"""是否使用重定向源"""
available_nodes: str = "[]"
"""可用节点ID列表JSON数组"""
select_node: bool = False
"""是否允许选择节点"""
advance_delete: bool = False
"""是否允许高级删除"""
# 反向关系
group: "Group" = Relationship(back_populates="options")
class Group(TableBase, table=True):
class Group(GroupBase, TableBase, table=True):
"""用户组模型"""
name: str = Field(max_length=255, unique=True)

View File

@@ -1,7 +1,7 @@
from .setting import Setting, SettingsType
from .user import ThemeResponse
from pkg.conf.appmeta import BackendVersion
from .response import ThemeModel
from pkg.password.pwd import Password
from loguru import logger as log
@@ -74,7 +74,7 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti
Setting(name="hot_share_num", value="10", type=SettingsType.SHARE),
Setting(name="gravatar_server", value="https://www.gravatar.com/", type=SettingsType.AVATAR),
Setting(name="defaultTheme", value="#3f51b5", type=SettingsType.BASIC),
Setting(name="themes", value=ThemeModel().model_dump_json(), type=SettingsType.BASIC),
Setting(name="themes", value=ThemeResponse().model_dump_json(), type=SettingsType.BASIC),
Setting(name="aria2_token", value="", type=SettingsType.ARIA2),
Setting(name="aria2_rpcurl", value="", type=SettingsType.ARIA2),
Setting(name="aria2_temp_path", value="", type=SettingsType.ARIA2),

View File

@@ -1,8 +1,11 @@
from typing import TYPE_CHECKING, Optional
from datetime import datetime
from typing import TYPE_CHECKING, Literal, Optional
from enum import StrEnum
from sqlmodel import Field, Relationship, UniqueConstraint, CheckConstraint, Index
from .base import TableBase
from .base import TableBase, SQLModelBase
if TYPE_CHECKING:
from .user import User
@@ -17,7 +20,90 @@ class ObjectType(StrEnum):
FOLDER = "folder"
class Object(TableBase, table=True):
# ==================== Base 模型 ====================
class ObjectBase(SQLModelBase):
"""对象基础字段,供数据库模型和 DTO 共享"""
name: str
"""对象名称(文件名或目录名)"""
type: ObjectType
"""对象类型file 或 folder"""
size: int = 0
"""文件大小(字节),目录为 0"""
# ==================== DTO 模型 ====================
class DirectoryCreateRequest(SQLModelBase):
"""创建目录请求 DTO"""
path: str
"""目录路径,如 /docs/images"""
policy_id: int | None = None
"""存储策略ID不指定则继承父目录"""
class ObjectResponse(ObjectBase):
"""对象响应 DTO"""
id: str
"""对象ID"""
path: str
"""对象路径"""
thumb: bool = False
"""是否有缩略图"""
date: datetime
"""对象修改时间"""
create_date: datetime
"""对象创建时间"""
source_enabled: bool = False
"""是否启用离线下载源"""
class PolicyResponse(SQLModelBase):
"""存储策略响应 DTO"""
id: str
"""策略ID"""
name: str
"""策略名称"""
type: Literal["local", "qiniu", "tencent", "aliyun", "onedrive", "google_drive", "dropbox", "webdav", "remote"]
"""存储类型"""
max_size: int = 0
"""单文件最大限制单位字节0表示不限制"""
file_type: list[str] = []
"""允许的文件类型列表,空列表表示不限制"""
class DirectoryResponse(SQLModelBase):
"""目录响应 DTO"""
parent: str | None = None
"""父目录ID根目录为None"""
objects: list[ObjectResponse] = []
"""目录下的对象列表"""
policy: PolicyResponse
"""存储策略"""
# ==================== 数据库模型 ====================
class Object(ObjectBase, TableBase, table=True):
"""
统一对象模型

View File

@@ -1,140 +1,26 @@
"""
响应模型定义
通用响应模型定义
"""
from pydantic import BaseModel, Field
from typing import Literal, Union, Optional
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4
class ResponseModel(BaseModel):
"""
默认响应模型
"""
code: int = Field(default=0, description="系统内部状态码, 0表示成功其他表示失败", lt=60000, gt=0)
data: Union[dict, list, str, int, float, None] = Field(None, description="响应数据")
msg: str | None = Field(default=None, description="响应消息,可以是错误消息或信息提示")
instance_id: str = Field(default_factory=lambda: str(uuid4()), description="实例ID用于标识请求的唯一性")
from sqlmodel import Field
class ThemeModel(BaseModel):
"""
主题模型
"""
primary: str = Field(default="#3f51b5", description="Primary color")
secondary: str = Field(default="#f50057", description="Secondary color")
accent: str = Field(default="#9c27b0", description="Accent color")
dark: str = Field(default="#1d1d1d", description="Dark color")
dark_page: str = Field(default="#121212", description="Dark page color")
positive: str = Field(default="#21ba45", description="Positive color")
negative: str = Field(default="#c10015", description="Negative color")
info: str = Field(default="#31ccec", description="Info color")
warning: str = Field(default="#f2c037", description="Warning color")
from .base import SQLModelBase
class TokenModel(BaseModel):
"""
访问令牌模型
"""
access_expires: datetime = Field(default=None, description="访问令牌的过期时间")
access_token: str = Field(default=None, description="访问令牌")
refresh_expires: datetime = Field(default=None, description="刷新令牌的过期时间")
refresh_token: str = Field(default=None, description="刷新令牌")
class GroupModel(BaseModel):
"""
用户组模型
"""
id: int = Field(default=None, description="用户组ID")
name: str = Field(default=None, description="用户组名称")
allowShare: bool = Field(default=False, description="是否允许分享")
allowRomoteDownload: bool = Field(default=False, description="是否允许离线下载")
allowArchiveDownload: bool = Field(default=False, description="是否允许打包下载")
shareFree: bool = Field(default=False, description="是否允许免积分下载分享")
shareDownload: bool = Field(default=False, description="是否允许下载分享")
compress: bool = Field(default=False)
webdav: bool = Field(default=False, description="是否允许WebDAV")
allowWebDAVProxy: bool = Field(default=False, description="是否允许WebDAV代理")
relocate: bool = Field(default=False, description="是否使用重定向的下载链接")
sourceBatch: int = Field(default=0)
selectNode: bool = Field(default=False, description="是否允许选择离线下载节点")
advanceDelete: bool = Field(default=False, description="是否允许高级删除")
class ResponseModel(SQLModelBase):
"""通用响应模型"""
class UserModel(BaseModel):
"""
用户模型
"""
id: int = Field(default=None, description="用户ID")
username: str = Field(default=None, description="用户名")
nickname: str = Field(default=None, description="用户昵称")
status: bool = Field(default=0, description="用户状态")
avatar: Literal['default', 'gravatar', 'file'] = Field(default='default', description="头像类型")
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), description="用户创建时间")
preferred_theme: ThemeModel = Field(default_factory=ThemeModel, description="用户首选主题")
score: int = Field(default=0, description="用户积分")
anonymous: bool = Field(default=False, description="是否为匿名用户")
group: GroupModel = Field(default_factory=None, description="用户所属用户组")
tags: list = Field(default_factory=list, description="用户标签列表")
code: int = Field(default=0, ge=0, lt=60000)
"""系统内部状态码0表示成功其他表示失败"""
class SiteConfigModel(ResponseModel):
"""
站点配置模型
"""
title: str = Field(default="DiskNext", description="网站标题")
themes: dict = Field(default_factory=dict, description="网站主题配置")
default_theme: dict = Field(description="默认主题RGB色号")
site_notice: str | None = Field(default=None, description="网站公告")
user: dict = Field(default_factory=dict, description="用户信息")
logo_light: str | None = Field(default=None, description="网站Logo URL")
logo_dark: str | None = Field(default=None, description="网站Logo URL深色模式")
captcha_type: Literal['none', 'default', 'gcaptcha', 'cloudflare turnstile'] = Field(default='none', description="验证码类型")
captcha_key: str | None = Field(default=None, description="验证码密钥")
data: dict[str, Any] | list[Any] | str | int | float | None = None
"""响应数据"""
class AuthnModel(BaseModel):
"""
WebAuthn模型
"""
id: str = Field(default=None, description="ID")
fingerprint: str = Field(default=None, description="指纹")
msg: str | None = None
"""响应消息,可以是错误消息或信息提示"""
class UserSettingModel(BaseModel):
"""
用户设置模型
"""
authn: Optional[AuthnModel] = Field(default=None, description="认证信息")
group_expires: datetime | None = Field(default=None, description="用户组过期时间")
prefer_theme: str = Field(default="#5898d4", description="用户首选主题")
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")
class ObjectModel(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 | None
"""父目录ID根目录为None"""
objects: list[ObjectModel] = Field(default_factory=list, description="目录下的对象列表")
policy: PolicyModel = Field(default_factory=PolicyModel, description="存储策略")
instance_id: str = Field(default_factory=lambda: str(uuid4()))
"""实例ID用于标识请求的唯一性"""

View File

@@ -1,7 +1,46 @@
from typing import Literal
from sqlmodel import Field, UniqueConstraint
from .base import TableBase
from .base import TableBase, SQLModelBase
from enum import StrEnum
# ==================== DTO 模型 ====================
class SiteConfigResponse(SQLModelBase):
"""站点配置响应 DTO"""
title: str = "DiskNext"
"""网站标题"""
themes: dict[str, str] = {}
"""网站主题配置"""
default_theme: dict[str, str] = {}
"""默认主题RGB色号"""
site_notice: str | None = None
"""网站公告"""
user: dict[str, str | int | bool] = {}
"""用户信息"""
logo_light: str | None = None
"""网站Logo URL"""
logo_dark: str | None = None
"""网站Logo URL深色模式"""
captcha_type: Literal["none", "default", "gcaptcha", "cloudflare turnstile"] = "none"
"""验证码类型"""
captcha_key: str | None = None
"""验证码密钥"""
# ==================== 数据库模型 ====================
class SettingsType(StrEnum):
"""设置类型枚举"""

View File

@@ -1,8 +1,7 @@
from datetime import datetime
from typing import Optional, TYPE_CHECKING
from typing import Literal, Optional, TYPE_CHECKING
from sqlmodel import Field, Relationship
from pydantic import BaseModel
from .base import TableBase, SQLModelBase
@@ -21,23 +20,48 @@ if TYPE_CHECKING:
"""
Option 需求
- 主题 跟随系统/浅色/深色
- 颜色方案 参考.response.ThemeModel
- 颜色方案 参考 ThemeResponse
- 语言
- 时区
- 切换到不同存储策略是否提醒
"""
class LoginRequest(BaseModel):
"""
登录请求模型
"""
username: str = Field(..., description="用户名或邮箱")
password: str = Field(..., description="用户密码")
captcha: str | None = Field(None, description="验证码")
twoFaCode: str | None = Field(None, description="两步验证代码")
class WebAuthnInfo(BaseModel):
"""WebAuthn 信息模型"""
# ==================== Base 模型 ====================
class UserBase(SQLModelBase):
"""用户基础字段,供数据库模型和 DTO 共享"""
username: str
"""用户名"""
status: bool = True
"""用户状态: True=正常, False=封禁"""
score: int = 0
"""用户积分"""
# ==================== DTO 模型 ====================
class LoginRequest(SQLModelBase):
"""登录请求 DTO"""
username: str
"""用户名或邮箱"""
password: str
"""用户密码"""
captcha: str | None = None
"""验证码"""
two_fa_code: str | None = None
"""两步验证代码"""
class WebAuthnInfo(SQLModelBase):
"""WebAuthn 信息 DTO"""
credential_id: str
"""凭证 ID"""
@@ -57,7 +81,147 @@ class WebAuthnInfo(BaseModel):
transports: list[str]
"""支持的传输方式"""
class User(TableBase, table=True):
class ThemeResponse(SQLModelBase):
"""主题响应 DTO"""
primary: str = "#3f51b5"
"""主色调"""
secondary: str = "#f50057"
"""次要色"""
accent: str = "#9c27b0"
"""强调色"""
dark: str = "#1d1d1d"
"""深色"""
dark_page: str = "#121212"
"""深色页面背景"""
positive: str = "#21ba45"
"""正面/成功色"""
negative: str = "#c10015"
"""负面/错误色"""
info: str = "#31ccec"
"""信息色"""
warning: str = "#f2c037"
"""警告色"""
class TokenResponse(SQLModelBase):
"""访问令牌响应 DTO"""
access_expires: datetime
"""访问令牌过期时间"""
access_token: str
"""访问令牌"""
refresh_expires: datetime
"""刷新令牌过期时间"""
refresh_token: str
"""刷新令牌"""
class UserResponse(UserBase):
"""用户响应 DTO"""
id: int
"""用户ID"""
nickname: str | None = None
"""用户昵称"""
avatar: Literal["default", "gravatar", "file"] = "default"
"""头像类型"""
created_at: datetime
"""用户创建时间"""
preferred_theme: ThemeResponse | None = None
"""用户首选主题"""
anonymous: bool = False
"""是否为匿名用户"""
group: "GroupResponse | None" = None
"""用户所属用户组"""
tags: list[str] = []
"""用户标签列表"""
class UserPublic(UserBase):
"""用户公开信息 DTO用于 API 响应"""
id: int | None = None
"""用户ID"""
nick: str | None = None
"""昵称"""
storage: int = 0
"""已用存储空间(字节)"""
avatar: str | None = None
"""头像地址"""
group_expires: datetime | None = None
"""用户组过期时间"""
group_id: int | None = None
"""所属用户组ID"""
created_at: datetime | None = None
"""创建时间"""
updated_at: datetime | None = None
"""更新时间"""
class UserSettingResponse(SQLModelBase):
"""用户设置响应 DTO"""
authn: "AuthnResponse | None" = None
"""认证信息"""
group_expires: datetime | None = None
"""用户组过期时间"""
prefer_theme: str = "#5898d4"
"""用户首选主题"""
qq: str | None = None
"""QQ号"""
themes: dict[str, str] = {}
"""用户主题配置"""
two_factor: bool = False
"""是否启用两步验证"""
uid: int = 0
"""用户UID"""
# 前向引用导入
from .group import GroupResponse # noqa: E402
from .user_authn import AuthnResponse # noqa: E402
# 更新前向引用
UserResponse.model_rebuild()
UserSettingResponse.model_rebuild()
# ==================== 数据库模型 ====================
class User(UserBase, TableBase, table=True):
"""用户模型"""
username: str = Field(max_length=50, unique=True, index=True)
@@ -81,10 +245,9 @@ class User(TableBase, table=True):
avatar: str | None = Field(default=None, max_length=255)
"""头像地址"""
options: str | None = Field(default=None)
options: str | None = None
"""[TODO] 用户个人设置 需要更改,参考上方的需求"""
github_open_id: str | None = Field(default=None, unique=True, index=True)
"""Github OpenID"""
@@ -94,7 +257,7 @@ class User(TableBase, table=True):
score: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, ge=0)
"""用户积分"""
group_expires: datetime | None = Field(default=None)
group_expires: datetime | None = None
"""当前用户组过期时间"""
phone: str | None = Field(default=None, max_length=32, unique=True, index=True)
@@ -138,40 +301,3 @@ class User(TableBase, table=True):
"""转换为公开 DTO排除敏感字段"""
return UserPublic.model_validate(self)
class UserPublic(SQLModelBase):
"""用户公开信息 DTO用于 API 响应"""
id: int | None = None
"""用户ID"""
username: str
"""用户名"""
nick: str | None = None
"""昵称"""
status: bool = True
"""用户状态"""
storage: int = 0
"""已用存储空间(字节)"""
avatar: str | None = None
"""头像地址"""
score: int = 0
"""用户积分"""
group_expires: datetime | None = None
"""用户组过期时间"""
group_id: int
"""所属用户组ID"""
created_at: datetime | None = None
"""创建时间"""
updated_at: datetime | None = None
"""更新时间"""

View File

@@ -3,12 +3,26 @@ from typing import TYPE_CHECKING
from sqlalchemy import Column, Text
from sqlmodel import Field, Relationship
from .base import TableBase
from .base import TableBase, SQLModelBase
if TYPE_CHECKING:
from .user import User
# ==================== DTO 模型 ====================
class AuthnResponse(SQLModelBase):
"""WebAuthn 响应 DTO"""
id: str
"""凭证ID"""
fingerprint: str
"""凭证指纹"""
# ==================== 数据库模型 ====================
class UserAuthn(TableBase, table=True):
"""用户 WebAuthn 凭证模型,与 User 为多对一关系"""

View File

@@ -1,31 +1,33 @@
from fastapi.security import OAuth2PasswordBearer
from models.setting import Setting
from models.database import get_session
from datetime import datetime, timedelta, timezone
from sqlalchemy import and_
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,
and_(Setting.type == "auth", Setting.name == "secret_key")
(Setting.type == "auth") & (Setting.name == "secret_key")
)
if setting:
SECRET_KEY = setting.value
break
# 访问令牌
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> tuple[str, datetime]:

4
pkg/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
# 延迟导入以避免循环依赖
# JWT 和 lifespan 应在需要时直接从子模块导入
# from .JWT import JWT
# from .lifespan import lifespan

View File

@@ -18,11 +18,6 @@ debug: bool = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes") or Fal
if debug:
log.info("Debug mode is enabled. This is not recommended for production use.")
host: str = os.getenv("HOST", "0.0.0.0")
port: int = int(os.getenv("PORT", 5213))
log.info(f"Starting DiskNext Server {BackendVersion} on {host}:{port}")
database_url: str = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///disknext.db")
tags_meta = [

View File

@@ -1,27 +1,25 @@
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from middleware.auth import AuthRequired
from middleware.dependencies import SessionDep
from models import Object, ObjectType, User, response
from models import (
DirectoryCreateRequest,
DirectoryResponse,
Object,
ObjectResponse,
ObjectType,
PolicyResponse,
User,
response,
)
directory_router = APIRouter(
prefix="/directory",
tags=["directory"]
)
class DirectoryCreateRequest(BaseModel):
"""创建目录请求"""
path: str
"""目录路径,如 /docs/images"""
policy_id: int | None = None
"""存储策略ID不指定则继承父目录"""
@directory_router.get(
path="/{path:path}",
summary="获取目录内容",
@@ -51,7 +49,7 @@ async def router_directory_get(
policy = await folder.awaitable_attrs.policy
objects = [
response.ObjectModel(
ObjectResponse(
id=str(child.id),
name=child.name,
path=f"/{child.name}", # TODO: 完整路径
@@ -66,16 +64,16 @@ async def router_directory_get(
]
return response.ResponseModel(
data=response.DirectoryModel(
data=DirectoryResponse(
parent=str(folder.parent_id) if folder.parent_id else None,
objects=objects,
policy=response.PolicyModel(
policy=PolicyResponse(
id=str(policy.id),
name=policy.name,
type=policy.type.value,
max_size=policy.max_size,
file_type=[],
)
),
)
)

View File

@@ -30,16 +30,16 @@ user_settings_router = APIRouter(
async def router_user_session(
session: SessionDep,
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
) -> models.response.TokenModel:
) -> models.TokenResponse:
username = form_data.username
password = form_data.password
result = await service.user.Login(
session,
models.user.LoginRequest(username=username, password=password),
models.LoginRequest(username=username, password=password),
)
if isinstance(result, models.response.TokenModel):
if isinstance(result, models.TokenResponse):
return result
elif result is None:
raise HTTPException(status_code=401, detail="Invalid username or password")
@@ -203,13 +203,13 @@ async def router_user_me(
"""
group = await models.Group.get(session, models.Group.id == user.group_id)
user_group = models.response.GroupModel(
user_group = models.GroupResponse(
id=group.id,
name=group.name,
allowShare=group.share_enabled,
allow_share=group.share_enabled,
)
users = models.response.UserModel(
users = models.UserResponse(
id=user.id,
username=user.username,
nickname=user.nick,
@@ -369,7 +369,7 @@ def router_user_settings() -> models.response.ResponseModel:
Returns:
dict: A dictionary containing the current user settings.
"""
return models.response.ResponseModel(data=models.response.UserSettingModel().model_dump())
return models.response.ResponseModel(data=models.UserSettingResponse().model_dump())
@user_settings_router.post(
path='/avatar',

View File

@@ -1,25 +1,22 @@
from loguru import logger as log
from sqlalchemy import and_
from sqlmodel.ext.asyncio.session import AsyncSession
from models.response import TokenModel
from models import user
from models.user import User
from pkg.JWT.jwt import create_access_token, create_refresh_token
from models import LoginRequest, TokenResponse, User
from pkg.JWT.JWT import create_access_token, create_refresh_token
async def Login(session: AsyncSession, login_request: user.LoginRequest) -> TokenModel | bool | None:
async def Login(session: AsyncSession, login_request: LoginRequest) -> TokenResponse | bool | None:
"""
根据账号密码进行登录。
如果登录成功,返回一个 TokenModel 对象,包含访问令牌和刷新令牌以及它们的过期时间。
如果登录成功,返回一个 TokenResponse 对象,包含访问令牌和刷新令牌以及它们的过期时间。
如果登录异常,返回 `False`(未完成注册或账号被封禁)。
如果登录失败,返回 `None`。
:param session: 数据库会话
:param login_request: 登录请求
:return: TokenModel 对象或状态码或 None
:return: TokenResponse 对象或状态码或 None
"""
from pkg.password.pwd import Password
@@ -52,7 +49,7 @@ async def Login(session: AsyncSession, login_request: user.LoginRequest) -> Toke
access_token, access_expire = create_access_token(data={'sub': current_user.username})
refresh_token, refresh_expire = create_refresh_token(data={'sub': current_user.username})
return TokenModel(
return TokenResponse(
access_token=access_token,
access_expires=access_expire,
refresh_token=refresh_token,