清理项目配置文件,移除不再使用的.idea文件和更新文档中的Python版本要求
This commit is contained in:
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@@ -1,8 +0,0 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
1
.idea/.name
generated
1
.idea/.name
generated
@@ -1 +0,0 @@
|
||||
password.py
|
||||
17
.idea/Findreve.iml
generated
17
.idea/Findreve.iml
generated
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.12 (Findreve)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
</component>
|
||||
<component name="TestRunnerService">
|
||||
<option name="PROJECT_TEST_RUNNER" value="py.test" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/copilot.data.migration.agent.xml
generated
6
.idea/copilot.data.migration.agent.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AgentMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/copilot.data.migration.ask.xml
generated
6
.idea/copilot.data.migration.ask.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AskMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/copilot.data.migration.ask2agent.xml
generated
6
.idea/copilot.data.migration.ask2agent.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Ask2AgentMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/copilot.data.migration.edit.xml
generated
6
.idea/copilot.data.migration.edit.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EditMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
6
.idea/inspectionProfiles/profiles_settings.xml
generated
@@ -1,6 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
17
.idea/material_theme_project_new.xml
generated
17
.idea/material_theme_project_new.xml
generated
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MaterialThemeProjectNewConfig">
|
||||
<option name="metadata">
|
||||
<MTProjectMetadataState>
|
||||
<option name="migrated" value="true" />
|
||||
<option name="pristineConfig" value="false" />
|
||||
<option name="userId" value="-7c28e060:19966355718:-7ffa" />
|
||||
</MTProjectMetadataState>
|
||||
</option>
|
||||
<option name="titleBarState">
|
||||
<MTProjectTitleBarConfigState>
|
||||
<option name="overrideColor" value="false" />
|
||||
</MTProjectTitleBarConfigState>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
7
.idea/misc.xml
generated
7
.idea/misc.xml
generated
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.12 (Findreve)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (Findreve)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/Findreve.iml" filepath="$PROJECT_DIR$/.idea/Findreve.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -34,7 +34,7 @@ flowchart TD
|
||||
```
|
||||
|
||||
## 编码风格与命名
|
||||
- 统一使用 Python 3.8+、四空格缩进,并在公共接口添加类型注解;仅对复杂逻辑补充文档字符串。
|
||||
- 统一使用 Python 3.13+、四空格缩进,并在公共接口添加类型注解;仅对复杂逻辑补充文档字符串。
|
||||
- 函数使用 `snake_case`,数据模型使用 `PascalCase`,配置与日志归于 `pkg/`(`pkg/logger.py` 封装`loguru`)。
|
||||
- 所有代码、注释、提交信息与评审讨论均使用简体中文。
|
||||
|
||||
|
||||
@@ -61,18 +61,16 @@ chmod +x ./findreve
|
||||
|
||||
启动后, Findreve 会在程序的根目录自动创建 SQLite 数据库,并在
|
||||
终端显示管理员账号密码。请注意,账号密码仅显示一次,请注意保管。
|
||||
账号默认为 `admin@yuxiaoqiu.cn`
|
||||
账号默认为 `admin@yxqi.cn`
|
||||
|
||||
Upon launch, Findreve will create a SQLite database in the project's root directory and
|
||||
display the administrator's account and password in the console.
|
||||
|
||||
## 构建
|
||||
|
||||
> 当前版本的 Findreve Core 无法正常工作,因为我们正在尝试[重构数据库组件以使用ORM](https://github.com/Findreve/Findreve/issues/8)
|
||||
你需要安装Python 3.13 以上的版本。然后,clone 本仓库到您的服务器并解压,然后安装下面的依赖:
|
||||
|
||||
你需要安装Python 3.8 以上的版本。然后,clone 本仓库到您的服务器并解压,然后安装下面的依赖:
|
||||
|
||||
You need to have Python 3.8 or higher installed on your server. Then, clone this repository
|
||||
You need to have Python 3.13 or higher installed on your server. Then, clone this repository
|
||||
to your server and install the required dependencies:
|
||||
|
||||
> `pip install -r requirements.txt`
|
||||
|
||||
17
app.py
17
app.py
@@ -1,6 +1,6 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi import Request, HTTPException
|
||||
from fastapi import Request
|
||||
from contextlib import asynccontextmanager
|
||||
from slowapi import Limiter, _rate_limit_exceeded_handler
|
||||
from slowapi.util import get_remote_address
|
||||
@@ -11,6 +11,7 @@ from routes import (session, admin, object)
|
||||
from model.database import Database
|
||||
import os
|
||||
import pkg.conf
|
||||
from pkg import utils
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -54,21 +55,21 @@ app.state.limiter = limiter
|
||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||
|
||||
@app.get("/")
|
||||
def read_root():
|
||||
async def frontend_index():
|
||||
if not os.path.exists("dist/index.html"):
|
||||
raise HTTPException(status_code=404)
|
||||
utils.raise_not_found("Index not found")
|
||||
return FileResponse("dist/index.html")
|
||||
|
||||
# 回退路由
|
||||
@app.get("/{path:path}")
|
||||
async def serve_spa(request: Request, path: str):
|
||||
async def frontend_path(path: str):
|
||||
if not os.path.exists("dist/index.html"):
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
utils.raise_not_found("Index not found, please build frontend first.")
|
||||
|
||||
# 排除API路由
|
||||
if path.startswith("api/"):
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
utils.raise_not_found("API route not found")
|
||||
|
||||
# 检查是否是静态资源请求
|
||||
if path.startswith("assets/") and os.path.exists(f"dist/{path}"):
|
||||
return FileResponse(f"dist/{path}")
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
from typing import Annotated, Literal
|
||||
from typing import Annotated
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
import JWT
|
||||
import jwt
|
||||
from jwt import InvalidTokenError
|
||||
from model import database
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from model import User
|
||||
|
||||
from model.user import UserTypeEnum
|
||||
from .user import get_current_user
|
||||
from pkg import utils
|
||||
from model import User
|
||||
from model import database
|
||||
|
||||
# 验证是否为管理员
|
||||
async def is_admin(
|
||||
token: Annotated[str, Depends(get_current_user)],
|
||||
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
|
||||
) -> Literal[True]:
|
||||
) -> User:
|
||||
'''
|
||||
验证是否为管理员。
|
||||
|
||||
@@ -21,14 +20,25 @@ async def is_admin(
|
||||
>>> APIRouter(dependencies=[Depends(is_admin)])
|
||||
'''
|
||||
|
||||
not_admin_exception = HTTPException(
|
||||
status_code=403,
|
||||
detail="Admin access required",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
user = await get_current_user(token, session)
|
||||
if user.role == UserTypeEnum.normal_user:
|
||||
utils.raise_forbidden("Admin access required")
|
||||
else:
|
||||
return user
|
||||
|
||||
async def is_super_admin(
|
||||
token: Annotated[str, Depends(is_admin)],
|
||||
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
|
||||
) -> User:
|
||||
'''
|
||||
验证是否为超级管理员。
|
||||
|
||||
使用方法:
|
||||
>>> APIRouter(dependencies=[Depends(is_super_admin)])
|
||||
'''
|
||||
|
||||
user = await get_current_user(token, session)
|
||||
if not user.is_admin:
|
||||
raise not_admin_exception
|
||||
if user.role != UserTypeEnum.super_admin:
|
||||
utils.raise_forbidden("Super admin access required")
|
||||
else:
|
||||
return True
|
||||
return user
|
||||
@@ -2,16 +2,14 @@ from typing import Annotated
|
||||
|
||||
import jwt
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from jwt import InvalidTokenError
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
import JWT
|
||||
from model import User
|
||||
from model.database import Database
|
||||
from pkg import utils
|
||||
|
||||
|
||||
# 验证是否为管理员
|
||||
async def get_current_user(
|
||||
token: Annotated[str, Depends(JWT.oauth2_scheme)],
|
||||
session: Annotated[AsyncSession, Depends(Database.get_session)],
|
||||
@@ -19,18 +17,13 @@ async def get_current_user(
|
||||
"""
|
||||
验证用户身份并返回当前用户信息。
|
||||
"""
|
||||
not_login_exception = HTTPException(
|
||||
status_code=401,
|
||||
detail="Login required",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, await JWT.get_secret_key(), algorithms=[JWT.ALGORITHM])
|
||||
username = payload.get("sub")
|
||||
stored_account = await User.get(session, User.email == username)
|
||||
if username is None or stored_account.email != username:
|
||||
raise not_login_exception
|
||||
utils.raise_unauthorized("Login required")
|
||||
return stored_account
|
||||
except InvalidTokenError:
|
||||
raise not_login_exception
|
||||
utils.raise_unauthorized("Login required")
|
||||
@@ -3,7 +3,6 @@ from contextlib import asynccontextmanager
|
||||
from typing import AsyncGenerator
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
@@ -2,7 +2,6 @@ from datetime import datetime
|
||||
from enum import StrEnum
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from sqlmodel import Field, Relationship
|
||||
|
||||
from .base import SQLModelBase, UUIDTableBase
|
||||
|
||||
@@ -2,7 +2,16 @@ from pydantic import BaseModel
|
||||
|
||||
from model.base import SQLModelBase
|
||||
|
||||
"""
|
||||
[TODO] 弃用,改成 ResponseBase:
|
||||
|
||||
class ResponseBase(BaseModel):
|
||||
code: int = 0
|
||||
msg: str = ""
|
||||
request_id: UUID
|
||||
|
||||
再根据需要继承
|
||||
"""
|
||||
class DefaultResponse(BaseModel):
|
||||
code: int = 0
|
||||
data: dict | list | bool | SQLModelBase | None = None
|
||||
|
||||
@@ -23,8 +23,8 @@ class User(UserBase, UUIDTableBase, table=True):
|
||||
email: EmailStr = Field(index=True, unique=True)
|
||||
"""邮箱"""
|
||||
|
||||
username: str = Field(index=True, unique=True)
|
||||
"""用户名"""
|
||||
nickname: str
|
||||
"""昵称"""
|
||||
|
||||
password: str
|
||||
"""Argon2算法哈希后的密码"""
|
||||
|
||||
@@ -2,18 +2,32 @@ import secrets
|
||||
from loguru import logger
|
||||
from argon2 import PasswordHasher
|
||||
from argon2.exceptions import VerifyMismatchError
|
||||
from enum import StrEnum
|
||||
|
||||
_ph = PasswordHasher()
|
||||
|
||||
class Password():
|
||||
class PasswordStatus(StrEnum):
|
||||
"""密码校验状态枚举"""
|
||||
|
||||
VALID = "valid"
|
||||
"""密码校验通过"""
|
||||
|
||||
INVALID = "invalid"
|
||||
"""密码校验失败"""
|
||||
|
||||
EXPIRED = "expired"
|
||||
"""密码哈希已过时,建议重新哈希"""
|
||||
|
||||
class Password:
|
||||
"""密码处理工具类,包含密码生成、哈希和验证功能"""
|
||||
|
||||
@staticmethod
|
||||
def generate(
|
||||
length: int = 8
|
||||
length: int = 8
|
||||
) -> str:
|
||||
"""
|
||||
生成指定长度的随机密码。
|
||||
|
||||
|
||||
:param length: 密码长度
|
||||
:type length: int
|
||||
:return: 随机密码
|
||||
@@ -23,7 +37,7 @@ class Password():
|
||||
|
||||
@staticmethod
|
||||
def hash(
|
||||
password: str
|
||||
password: str
|
||||
) -> str:
|
||||
"""
|
||||
使用 Argon2 生成密码的哈希值。
|
||||
@@ -37,38 +51,29 @@ class Password():
|
||||
|
||||
@staticmethod
|
||||
def verify(
|
||||
stored_password: str,
|
||||
provided_password: str,
|
||||
debug: bool = False
|
||||
) -> bool:
|
||||
hash: str,
|
||||
password: str
|
||||
) -> PasswordStatus:
|
||||
"""
|
||||
验证存储的 Argon2 哈希值与用户提供的密码是否匹配。
|
||||
|
||||
:param stored_password: 数据库中存储的 Argon2 哈希字符串
|
||||
:param provided_password: 用户本次提供的密码
|
||||
:param debug: 是否输出调试信息
|
||||
:param hash: 数据库中存储的 Argon2 哈希字符串
|
||||
:param password: 用户本次提供的密码
|
||||
:return: 如果密码匹配返回 True, 否则返回 False
|
||||
"""
|
||||
if debug:
|
||||
logger.info(f"验证密码: (哈希) {stored_password}")
|
||||
|
||||
try:
|
||||
# verify 函数会自动解析 stored_password 中的盐和参数
|
||||
_ph.verify(stored_password, provided_password)
|
||||
_ph.verify(hash, password)
|
||||
|
||||
# 检查哈希参数是否已过时。如果返回True,
|
||||
# 意味着你应该使用新的参数重新哈希密码并更新存储。
|
||||
# 这是一个很好的实践,可以随着时间推移增强安全性。
|
||||
if _ph.check_needs_rehash(stored_password):
|
||||
if _ph.check_needs_rehash(hash):
|
||||
logger.warning("密码哈希参数已过时,建议重新哈希并更新。")
|
||||
return PasswordStatus.EXPIRED
|
||||
|
||||
return True
|
||||
return PasswordStatus.VALID
|
||||
except VerifyMismatchError:
|
||||
# 这是预期的异常,当密码不匹配时触发。
|
||||
if debug:
|
||||
logger.info("密码不匹配")
|
||||
return False
|
||||
except Exception as e:
|
||||
# 捕获其他可能的错误
|
||||
logger.error(f"密码验证过程中发生未知错误: {e}")
|
||||
return False
|
||||
return PasswordStatus.INVALID
|
||||
# 其他异常(如哈希格式错误)应该传播,让调用方感知系统问题
|
||||
|
||||
31
pkg/utils.py
31
pkg/utils.py
@@ -16,7 +16,7 @@ from starlette.status import (
|
||||
HTTP_504_GATEWAY_TIMEOUT,
|
||||
)
|
||||
|
||||
# --- Request and Response Helpers ---
|
||||
# --- 400 ---
|
||||
|
||||
def ensure_request_param(to_check: Any, detail: str) -> None:
|
||||
"""
|
||||
@@ -30,21 +30,21 @@ def raise_bad_request(detail: str = '') -> NoReturn:
|
||||
"""Raises an HTTP 400 Bad Request exception."""
|
||||
raise HTTPException(status_code=HTTP_400_BAD_REQUEST, detail=detail)
|
||||
|
||||
def raise_not_found(detail: str) -> NoReturn:
|
||||
"""Raises an HTTP 404 Not Found exception."""
|
||||
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail=detail)
|
||||
def raise_unauthorized(detail: str) -> NoReturn:
|
||||
"""Raises an HTTP 401 Unauthorized exception."""
|
||||
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=detail)
|
||||
|
||||
def raise_internal_error(detail: str = "服务器出现故障,请稍后再试或联系管理员") -> NoReturn:
|
||||
"""Raises an HTTP 500 Internal Server Error exception."""
|
||||
raise HTTPException(status_code=HTTP_500_INTERNAL_SERVER_ERROR, detail=detail)
|
||||
def raise_insufficient_quota(detail: str = "积分不足,请充值") -> NoReturn:
|
||||
"""Raises an HTTP 402 Payment Required exception."""
|
||||
raise HTTPException(status_code=HTTP_402_PAYMENT_REQUIRED, detail=detail)
|
||||
|
||||
def raise_forbidden(detail: str) -> NoReturn:
|
||||
"""Raises an HTTP 403 Forbidden exception."""
|
||||
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail=detail)
|
||||
|
||||
def raise_unauthorized(detail: str) -> NoReturn:
|
||||
"""Raises an HTTP 401 Unauthorized exception."""
|
||||
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=detail)
|
||||
def raise_not_found(detail: str) -> NoReturn:
|
||||
"""Raises an HTTP 404 Not Found exception."""
|
||||
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail=detail)
|
||||
|
||||
def raise_conflict(detail: str) -> NoReturn:
|
||||
"""Raises an HTTP 409 Conflict exception."""
|
||||
@@ -54,6 +54,12 @@ def raise_too_many_requests(detail: str) -> NoReturn:
|
||||
"""Raises an HTTP 429 Too Many Requests exception."""
|
||||
raise HTTPException(status_code=HTTP_429_TOO_MANY_REQUESTS, detail=detail)
|
||||
|
||||
# --- 500 ---
|
||||
|
||||
def raise_internal_error(detail: str = "服务器出现故障,请稍后再试或联系管理员") -> NoReturn:
|
||||
"""Raises an HTTP 500 Internal Server Error exception."""
|
||||
raise HTTPException(status_code=HTTP_500_INTERNAL_SERVER_ERROR, detail=detail)
|
||||
|
||||
def raise_not_implemented(detail: str = "尚未支持这种方法") -> NoReturn:
|
||||
"""Raises an HTTP 501 Not Implemented exception."""
|
||||
raise HTTPException(status_code=HTTP_501_NOT_IMPLEMENTED, detail=detail)
|
||||
@@ -65,8 +71,3 @@ def raise_service_unavailable(detail: str) -> NoReturn:
|
||||
def raise_gateway_timeout(detail: str) -> NoReturn:
|
||||
"""Raises an HTTP 504 Gateway Timeout exception."""
|
||||
raise HTTPException(status_code=HTTP_504_GATEWAY_TIMEOUT, detail=detail)
|
||||
|
||||
def raise_insufficient_quota(detail: str = "积分不足,请充值") -> NoReturn:
|
||||
raise HTTPException(status_code=HTTP_402_PAYMENT_REQUIRED, detail=detail)
|
||||
|
||||
# --- End of Request and Response Helpers ---
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# 导入库
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from model import database
|
||||
from model.response import TokenResponse
|
||||
from services import session as session_service
|
||||
from pkg import utils
|
||||
|
||||
Router = APIRouter(tags=["令牌 session"])
|
||||
|
||||
@@ -29,10 +29,6 @@ async def login_for_access_token(
|
||||
password=form_data.password,
|
||||
)
|
||||
if not token_response:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
utils.raise_unauthorized("Incorrect username or password")
|
||||
|
||||
return token_response
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
from typing import Iterable, List
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from model import Setting
|
||||
from model import SettingResponse
|
||||
from pkg import utils
|
||||
|
||||
|
||||
async def fetch_settings(
|
||||
@@ -25,7 +25,7 @@ async def fetch_settings(
|
||||
if setting:
|
||||
data.append(SettingResponse.model_validate(setting))
|
||||
else:
|
||||
raise HTTPException(404, detail="Setting not found")
|
||||
utils.raise_not_found("Setting not found")
|
||||
else:
|
||||
settings: Iterable[Setting] | None = await Setting.get(session, fetch_mode="all")
|
||||
if settings:
|
||||
@@ -44,7 +44,7 @@ async def update_setting_value(
|
||||
"""
|
||||
setting = await Setting.get(session, Setting.name == name)
|
||||
if not setting:
|
||||
raise HTTPException(404, detail="Setting not found")
|
||||
utils.raise_not_found("Setting not found")
|
||||
|
||||
setting.value = value
|
||||
await Setting.save(session)
|
||||
|
||||
@@ -5,15 +5,14 @@
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi import status
|
||||
from loguru import logger
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from model import Item, ItemDataResponse, Setting, User
|
||||
from model.item import ItemDataUpdateRequest, ItemTypeEnum
|
||||
from pkg.sender import ServerChatBot, WeChatBot
|
||||
from pkg.utils import raise_bad_request, raise_internal_error, raise_not_found
|
||||
from starlette.status import HTTP_204_NO_CONTENT
|
||||
from pkg import utils
|
||||
|
||||
|
||||
async def list_items(
|
||||
@@ -72,7 +71,7 @@ async def create_item(
|
||||
await Item.add(session, Item.model_validate(request_dict))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.error(f"Failed to add item: {exc}")
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
utils.raise_internal_error(str(exc))
|
||||
|
||||
|
||||
async def update_item(
|
||||
@@ -86,7 +85,7 @@ async def update_item(
|
||||
"""
|
||||
obj = await Item.get(session, (Item.id == item_id) & (Item.user_id == user.id))
|
||||
if not obj:
|
||||
raise_not_found("Item not found or access denied")
|
||||
utils.raise_not_found("Item not found or access denied")
|
||||
|
||||
await obj.update(session, request, exclude_unset=True)
|
||||
|
||||
@@ -101,7 +100,7 @@ async def delete_item(
|
||||
"""
|
||||
obj = await Item.get(session, (Item.id == item_id) & (Item.user_id == user.id))
|
||||
if not obj:
|
||||
raise_not_found("Item not found or access denied")
|
||||
utils.raise_not_found("Item not found or access denied")
|
||||
await Item.delete(session, obj)
|
||||
|
||||
|
||||
@@ -116,7 +115,7 @@ async def retrieve_object(
|
||||
object_data = await Item.get(session, Item.id == item_id)
|
||||
|
||||
if not object_data:
|
||||
raise_not_found("物品不存在或出现异常")
|
||||
utils.raise_not_found("物品不存在或出现异常")
|
||||
|
||||
if object_data.status == "lost":
|
||||
object_data.find_ip = client_host
|
||||
@@ -136,12 +135,12 @@ async def notify_move_car(
|
||||
item_data = await Item.get_exist_one(session=session, id=item_id)
|
||||
|
||||
if item_data.type != ItemTypeEnum.car:
|
||||
raise_bad_request("Item is not car")
|
||||
utils.raise_bad_request("Item is not car")
|
||||
|
||||
server_chan_key = await Setting.get(session, Setting.name == "server_chan_key")
|
||||
wechat_bot_key = await Setting.get(session, Setting.name == "wechat_bot_key")
|
||||
if not (server_chan_key.value or wechat_bot_key.value):
|
||||
raise_internal_error("未配置Server酱,无法发送挪车通知")
|
||||
utils.raise_internal_error("未配置Server酱,无法发送挪车通知")
|
||||
|
||||
title = "挪车通知 - Findreve"
|
||||
description = (
|
||||
@@ -161,4 +160,4 @@ async def notify_move_car(
|
||||
version="v1",
|
||||
)
|
||||
|
||||
return HTTP_204_NO_CONTENT
|
||||
return status.HTTP_204_NO_CONTENT
|
||||
|
||||
@@ -3,32 +3,25 @@
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
import JWT
|
||||
import jwt
|
||||
from loguru import logger
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from typing import Any
|
||||
import jwt
|
||||
|
||||
from model import Setting, User
|
||||
from model.response import TokenResponse
|
||||
from pkg import Password
|
||||
|
||||
from pkg import Password, utils
|
||||
import JWT
|
||||
|
||||
async def create_access_token(
|
||||
session: AsyncSession,
|
||||
data: dict[str, Any],
|
||||
expires_delta: timedelta | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
创建访问令牌。
|
||||
"""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
else:
|
||||
jwt_exp_setting = await Setting.get(session, Setting.name == "jwt_token_exp")
|
||||
expire = datetime.now(timezone.utc) + timedelta(int(jwt_exp_setting.value))
|
||||
jwt_exp_setting = await Setting.get(session, Setting.name == "jwt_token_exp")
|
||||
expire = datetime.now(timezone.utc) + timedelta(int(jwt_exp_setting.value))
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, key=await JWT.get_secret_key(), algorithm="HS256")
|
||||
return encoded_jwt
|
||||
@@ -38,19 +31,14 @@ async def authenticate_user(
|
||||
session: AsyncSession,
|
||||
username: str,
|
||||
password: str,
|
||||
) -> User | None:
|
||||
) -> User:
|
||||
"""
|
||||
验证用户名和密码,返回认证后的用户。
|
||||
"""
|
||||
account = await User.get(session, User.email == username)
|
||||
|
||||
if not account:
|
||||
logger.error("Account or password not set in settings.")
|
||||
return None
|
||||
|
||||
if account.email != username or not Password.verify(account.password, password):
|
||||
logger.error("Invalid username or password.")
|
||||
return None
|
||||
if not account or account.email != username or not Password.verify(account.password, password):
|
||||
utils.raise_unauthorized("Account or password is incorrect")
|
||||
|
||||
return account
|
||||
|
||||
@@ -59,13 +47,11 @@ async def login_for_access_token(
|
||||
session: AsyncSession,
|
||||
username: str,
|
||||
password: str,
|
||||
) -> TokenResponse | None:
|
||||
) -> TokenResponse:
|
||||
"""
|
||||
登录并生成访问令牌。
|
||||
"""
|
||||
user = await authenticate_user(session=session, username=username, password=password)
|
||||
if not user:
|
||||
return None
|
||||
|
||||
access_token = await create_access_token(
|
||||
session=session,
|
||||
|
||||
Reference in New Issue
Block a user