feat: add models for physical files, policies, and user management

- Implement PhysicalFile model to manage physical file references and reference counting.
- Create Policy model with associated options and group links for storage policies.
- Introduce Redeem and Report models for handling redeem codes and reports.
- Add Settings model for site configuration and user settings management.
- Develop Share model for sharing objects with unique codes and associated metadata.
- Implement SourceLink model for managing download links associated with objects.
- Create StoragePack model for managing user storage packages.
- Add Tag model for user-defined tags with manual and automatic types.
- Implement Task model for managing background tasks with status tracking.
- Develop User model with comprehensive user management features including authentication.
- Introduce UserAuthn model for managing WebAuthn credentials.
- Create WebDAV model for managing WebDAV accounts associated with users.
This commit is contained in:
2026-02-10 16:25:49 +08:00
parent 62c671e07b
commit 209cb24ab4
92 changed files with 3640 additions and 1444 deletions

View File

@@ -124,7 +124,7 @@ async def test_admin_get_user_list_contains_user_data(
if len(users) > 0:
user = users[0]
assert "id" in user
assert "username" in user
assert "email" in user
@pytest.mark.asyncio
@@ -132,7 +132,7 @@ async def test_admin_create_user_requires_auth(async_client: AsyncClient):
"""测试创建用户需要认证"""
response = await async_client.post(
"/api/admin/user/create",
json={"username": "newadminuser", "password": "pass123"}
json={"email": "newadminuser@test.local", "password": "pass123"}
)
assert response.status_code == 401
@@ -146,7 +146,7 @@ async def test_admin_create_user_requires_admin(
response = await async_client.post(
"/api/admin/user/create",
headers=auth_headers,
json={"username": "newadminuser", "password": "pass123"}
json={"email": "newadminuser@test.local", "password": "pass123"}
)
assert response.status_code == 403

View File

@@ -11,7 +11,7 @@ from uuid import UUID
@pytest.mark.asyncio
async def test_directory_requires_auth(async_client: AsyncClient):
"""测试获取目录需要认证"""
response = await async_client.get("/api/directory/testuser")
response = await async_client.get("/api/directory/")
assert response.status_code == 401
@@ -24,7 +24,7 @@ async def test_directory_get_root(
):
"""测试获取用户根目录"""
response = await async_client.get(
"/api/directory/testuser",
"/api/directory/",
headers=auth_headers
)
assert response.status_code == 200
@@ -45,7 +45,7 @@ async def test_directory_get_nested(
):
"""测试获取嵌套目录"""
response = await async_client.get(
"/api/directory/testuser/docs",
"/api/directory/docs",
headers=auth_headers
)
assert response.status_code == 200
@@ -63,7 +63,7 @@ async def test_directory_get_contains_children(
):
"""测试目录包含子对象"""
response = await async_client.get(
"/api/directory/testuser/docs",
"/api/directory/docs",
headers=auth_headers
)
assert response.status_code == 200
@@ -75,19 +75,6 @@ async def test_directory_get_contains_children(
assert len(objects) >= 1
@pytest.mark.asyncio
async def test_directory_forbidden_other_user(
async_client: AsyncClient,
auth_headers: dict[str, str]
):
"""测试访问他人目录返回 403"""
response = await async_client.get(
"/api/directory/admin",
headers=auth_headers
)
assert response.status_code == 403
@pytest.mark.asyncio
async def test_directory_not_found(
async_client: AsyncClient,
@@ -95,23 +82,23 @@ async def test_directory_not_found(
):
"""测试目录不存在返回 404"""
response = await async_client.get(
"/api/directory/testuser/nonexistent",
"/api/directory/nonexistent",
headers=auth_headers
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_directory_empty_path_returns_400(
async def test_directory_root_returns_200(
async_client: AsyncClient,
auth_headers: dict[str, str]
):
"""测试空路径返回 400"""
"""测试根目录端点返回 200"""
response = await async_client.get(
"/api/directory/",
headers=auth_headers
)
assert response.status_code == 400
assert response.status_code == 200
@pytest.mark.asyncio
@@ -121,7 +108,7 @@ async def test_directory_response_includes_policy(
):
"""测试目录响应包含存储策略"""
response = await async_client.get(
"/api/directory/testuser",
"/api/directory/",
headers=auth_headers
)
assert response.status_code == 200
@@ -284,7 +271,7 @@ async def test_directory_create_other_user_parent(
"""测试在他人目录下创建目录返回 404"""
# 先用管理员账号获取管理员的根目录ID
admin_response = await async_client.get(
"/api/directory/admin",
"/api/directory/",
headers=admin_headers
)
assert admin_response.status_code == 200

View File

@@ -16,7 +16,7 @@ async def test_user_login_success(
response = await async_client.post(
"/api/user/session",
data={
"username": test_user_info["username"],
"username": test_user_info["email"],
"password": test_user_info["password"],
}
)
@@ -38,7 +38,7 @@ async def test_user_login_wrong_password(
response = await async_client.post(
"/api/user/session",
data={
"username": test_user_info["username"],
"username": test_user_info["email"],
"password": "wrongpassword",
}
)
@@ -51,7 +51,7 @@ async def test_user_login_nonexistent_user(async_client: AsyncClient):
response = await async_client.post(
"/api/user/session",
data={
"username": "nonexistent",
"username": "nonexistent@test.local",
"password": "anypassword",
}
)
@@ -67,7 +67,7 @@ async def test_user_login_user_banned(
response = await async_client.post(
"/api/user/session",
data={
"username": banned_user_info["username"],
"username": banned_user_info["email"],
"password": banned_user_info["password"],
}
)
@@ -82,7 +82,7 @@ async def test_user_register_success(async_client: AsyncClient):
response = await async_client.post(
"/api/user/",
json={
"username": "newuser",
"email": "newuser@test.local",
"password": "newpass123",
}
)
@@ -91,20 +91,20 @@ async def test_user_register_success(async_client: AsyncClient):
data = response.json()
assert "data" in data
assert "user_id" in data["data"]
assert "username" in data["data"]
assert data["data"]["username"] == "newuser"
assert "email" in data["data"]
assert data["data"]["email"] == "newuser@test.local"
@pytest.mark.asyncio
async def test_user_register_duplicate_username(
async def test_user_register_duplicate_email(
async_client: AsyncClient,
test_user_info: dict[str, str]
):
"""测试重复用户名返回 400"""
"""测试重复邮箱返回 400"""
response = await async_client.post(
"/api/user/",
json={
"username": test_user_info["username"],
"email": test_user_info["email"],
"password": "anypassword",
}
)
@@ -143,8 +143,8 @@ async def test_user_me_returns_user_info(
assert "data" in data
user_data = data["data"]
assert "id" in user_data
assert "username" in user_data
assert user_data["username"] == "testuser"
assert "email" in user_data
assert user_data["email"] == "testuser@test.local"
assert "group" in user_data
assert "tags" in user_data

View File

@@ -22,7 +22,7 @@ from sqlalchemy.orm import sessionmaker
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
from main import app
from models import Group, GroupOptions, Object, ObjectType, Policy, PolicyType, Setting, SettingsType, User
from sqlmodels import Group, GroupOptions, Object, ObjectType, Policy, PolicyType, Setting, SettingsType, User
from utils import Password
from utils.JWT import create_access_token
from utils.JWT import JWT
@@ -92,6 +92,7 @@ async def initialized_db(test_session: AsyncSession) -> AsyncSession:
Setting(type=SettingsType.VIEW, name="home_view_method", value="list"),
Setting(type=SettingsType.VIEW, name="share_view_method", value="grid"),
Setting(type=SettingsType.AUTHN, name="authn_enabled", value="0"),
Setting(type=SettingsType.CAPTCHA, name="captcha_type", value="default"),
Setting(type=SettingsType.CAPTCHA, name="captcha_ReCaptchaKey", value=""),
Setting(type=SettingsType.CAPTCHA, name="captcha_CloudflareKey", value=""),
Setting(type=SettingsType.REGISTER, name="register_enabled", value="1"),
@@ -180,7 +181,7 @@ async def initialized_db(test_session: AsyncSession) -> AsyncSession:
# 6. 创建测试用户
test_user = User(
id=uuid4(),
username="testuser",
email="testuser@test.local",
password=Password.hash("testpass123"),
nickname="测试用户",
status=True,
@@ -194,7 +195,7 @@ async def initialized_db(test_session: AsyncSession) -> AsyncSession:
admin_user = User(
id=uuid4(),
username="admin",
email="admin@disknext.local",
password=Password.hash("adminpass123"),
nickname="管理员",
status=True,
@@ -208,7 +209,7 @@ async def initialized_db(test_session: AsyncSession) -> AsyncSession:
banned_user = User(
id=uuid4(),
username="banneduser",
email="banneduser@test.local",
password=Password.hash("banned123"),
nickname="封禁用户",
status=False, # 封禁状态
@@ -230,7 +231,7 @@ async def initialized_db(test_session: AsyncSession) -> AsyncSession:
# 7. 创建用户根目录
test_user_root = Object(
id=uuid4(),
name=test_user.username,
name="/",
type=ObjectType.FOLDER,
owner_id=test_user.id,
parent_id=None,
@@ -241,7 +242,7 @@ async def initialized_db(test_session: AsyncSession) -> AsyncSession:
admin_user_root = Object(
id=uuid4(),
name=admin_user.username,
name="/",
type=ObjectType.FOLDER,
owner_id=admin_user.id,
parent_id=None,
@@ -264,7 +265,7 @@ async def initialized_db(test_session: AsyncSession) -> AsyncSession:
def test_user_info() -> dict[str, str]:
"""测试用户信息"""
return {
"username": "testuser",
"email": "testuser@test.local",
"password": "testpass123",
}
@@ -273,7 +274,7 @@ def test_user_info() -> dict[str, str]:
def admin_user_info() -> dict[str, str]:
"""管理员用户信息"""
return {
"username": "admin",
"email": "admin@disknext.local",
"password": "adminpass123",
}
@@ -282,7 +283,7 @@ def admin_user_info() -> dict[str, str]:
def banned_user_info() -> dict[str, str]:
"""封禁用户信息"""
return {
"username": "banneduser",
"email": "banneduser@test.local",
"password": "banned123",
}
@@ -293,7 +294,7 @@ def banned_user_info() -> dict[str, str]:
def test_user_token(test_user_info: dict[str, str]) -> str:
"""生成测试用户的JWT token"""
token, _ = JWT.create_access_token(
data={"sub": test_user_info["username"]},
data={"sub": test_user_info["email"]},
expires_delta=timedelta(hours=1),
)
return token
@@ -303,7 +304,7 @@ def test_user_token(test_user_info: dict[str, str]) -> str:
def admin_user_token(admin_user_info: dict[str, str]) -> str:
"""生成管理员的JWT token"""
token, _ = JWT.create_access_token(
data={"sub": admin_user_info["username"]},
data={"sub": admin_user_info["email"]},
expires_delta=timedelta(hours=1),
)
return token
@@ -313,7 +314,7 @@ def admin_user_token(admin_user_info: dict[str, str]) -> str:
def expired_token() -> str:
"""生成过期的JWT token"""
token, _ = JWT.create_access_token(
data={"sub": "testuser"},
data={"sub": "testuser@test.local"},
expires_delta=timedelta(seconds=-1), # 已过期
)
return token
@@ -362,7 +363,7 @@ async def test_directory_structure(initialized_db: AsyncSession) -> dict[str, UU
"""创建测试目录结构"""
# 获取测试用户和根目录
test_user = await User.get(initialized_db, User.username == "testuser")
test_user = await User.get(initialized_db, User.email == "testuser@test.local")
test_user_root = await Object.get_root(initialized_db, test_user.id)
default_policy = await Policy.get(initialized_db, Policy.name == "本地存储")

View File

@@ -83,7 +83,7 @@ async def test_auth_required_token_without_sub(async_client: AsyncClient):
async def test_auth_required_nonexistent_user_token(async_client: AsyncClient):
"""测试用户不存在的token返回 401"""
token, _ = JWT.create_access_token(
data={"sub": "nonexistent_user"},
data={"sub": "nonexistent_user@test.local"},
expires_delta=timedelta(hours=1)
)
@@ -178,12 +178,12 @@ async def test_auth_on_directory_endpoint(
):
"""测试目录端点应用认证"""
# 无认证
response_no_auth = await async_client.get("/api/directory/testuser")
response_no_auth = await async_client.get("/api/directory/")
assert response_no_auth.status_code == 401
# 有认证
response_with_auth = await async_client.get(
"/api/directory/testuser",
"/api/directory/",
headers=auth_headers
)
assert response_with_auth.status_code == 200
@@ -235,7 +235,7 @@ async def test_auth_on_storage_endpoint(
async def test_refresh_token_format(test_user_info: dict[str, str]):
"""测试刷新token格式正确"""
refresh_token, _ = JWT.create_refresh_token(
data={"sub": test_user_info["username"]},
data={"sub": test_user_info["email"]},
expires_delta=timedelta(days=7)
)
@@ -247,7 +247,7 @@ async def test_refresh_token_format(test_user_info: dict[str, str]):
async def test_access_token_format(test_user_info: dict[str, str]):
"""测试访问token格式正确"""
access_token, expires = JWT.create_access_token(
data={"sub": test_user_info["username"]},
data={"sub": test_user_info["email"]},
expires_delta=timedelta(hours=1)
)