Refactor password handling and model typing
Replaced custom password generation and verification logic with a new pkg/password.py module using Argon2 for secure hashing. Updated model field types to use PEP 604 union syntax (e.g., str | None) and improved type annotations. Refactored admin and session routes to use new password utilities and direct model methods for CRUD operations. Removed legacy tool-based password functions and cleaned up .idea project files.
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
|
||||
14
.idea/Findreve.iml
generated
14
.idea/Findreve.iml
generated
@@ -1,14 +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>
|
||||
</module>
|
||||
13
.idea/dataSources.xml
generated
13
.idea/dataSources.xml
generated
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="data.db" uuid="c73289bb-3157-4ec3-b3f4-744da38bc64a">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<remarks>Findreve Data</remarks>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/data.db</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
29
.idea/inspectionProfiles/Project_Default.xml
generated
29
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,29 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="PyInterpreterInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PyMissingTypeHintsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredErrors">
|
||||
<list>
|
||||
<option value="E402" />
|
||||
<option value="E713" />
|
||||
<option value="E271" />
|
||||
<option value="E302" />
|
||||
<option value="E265" />
|
||||
<option value="W292" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PyPep8NamingInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false">
|
||||
<option name="ignoredErrors">
|
||||
<list>
|
||||
<option value="N802" />
|
||||
<option value="N806" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
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="4dc9a07a:18f958e7499:-7ffe" />
|
||||
</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>
|
||||
26
.idea/runConfigurations/Findreve.xml
generated
26
.idea/runConfigurations/Findreve.xml
generated
@@ -1,26 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="启动Findreve" type="PythonConfigurationType" factoryName="Python">
|
||||
<module name="Findreve" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<envs>
|
||||
<env name="PYTHONUNBUFFERED" value="1" />
|
||||
</envs>
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="SDK_NAME" value="Python 3.12 (Findreve)" />
|
||||
<option name="WORKING_DIRECTORY" value="C:\Users\admin\Documents\Code\Findreve" />
|
||||
<option name="IS_MODULE_SDK" value="false" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/main.py" />
|
||||
<option name="PARAMETERS" value="" />
|
||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||
<option name="EMULATE_TERMINAL" value="false" />
|
||||
<option name="MODULE_MODE" value="false" />
|
||||
<option name="REDIRECT_INPUT" value="false" />
|
||||
<option name="INPUT_FILE" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
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>
|
||||
2
JWT.py
2
JWT.py
@@ -22,7 +22,7 @@ async def get_secret_key() -> str:
|
||||
global _SECRET_KEY_CACHE
|
||||
|
||||
if _SECRET_KEY_CACHE is None:
|
||||
async with Database.get_session() as session:
|
||||
async with Database.session_context() as session:
|
||||
setting = await Setting.get(
|
||||
session=session,
|
||||
condition=(Setting.name == 'SECRET_KEY')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# model/base.py
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Type, TypeVar, Union, Literal, List
|
||||
from typing import Type, TypeVar, Union, Literal, List
|
||||
|
||||
from sqlalchemy import DateTime, BinaryExpression, ClauseElement
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -27,7 +27,7 @@ class TableBase(AsyncAttrs, SQLModel):
|
||||
sa_column_kwargs={"default": utcnow, "onupdate": utcnow},
|
||||
default_factory=utcnow
|
||||
)
|
||||
deleted_at: Optional[datetime] = Field(
|
||||
deleted_at: datetime | None = Field(
|
||||
default=None,
|
||||
description="删除时间",
|
||||
sa_column={"nullable": True}
|
||||
@@ -148,4 +148,4 @@ class TableBase(AsyncAttrs, SQLModel):
|
||||
|
||||
# 需要“自增 id 主键”的模型才混入它;Setting 不混入
|
||||
class IdMixin(SQLModel):
|
||||
id: Optional[int] = Field(default=None, primary_key=True, description="主键ID")
|
||||
id: int | None = Field(default=None, primary_key=True, description="主键ID")
|
||||
@@ -42,12 +42,25 @@ class Database:
|
||||
async with _async_session_factory() as session:
|
||||
yield session
|
||||
|
||||
@staticmethod
|
||||
@asynccontextmanager
|
||||
async def session_context() -> AsyncGenerator[AsyncSession, None]:
|
||||
"""
|
||||
提供异步上下文管理器用于直接获取数据库会话
|
||||
|
||||
使用示例:
|
||||
async with Database.session_context() as session:
|
||||
# 执行数据库操作
|
||||
pass
|
||||
"""
|
||||
async with _async_session_factory() as session:
|
||||
yield session
|
||||
|
||||
async def init_db(self, url: str = ASYNC_DATABASE_URL):
|
||||
"""创建数据库结构"""
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(SQLModel.metadata.create_all)
|
||||
|
||||
# For internal use, create a temporary context manager
|
||||
get_session_cm = asynccontextmanager(self.get_session)
|
||||
async with get_session_cm() as session:
|
||||
async with self.session_context() as session:
|
||||
await migration(session) # 执行迁移脚本
|
||||
@@ -1,5 +1,4 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
class Item(BaseModel):
|
||||
id: int
|
||||
@@ -9,8 +8,7 @@ class Item(BaseModel):
|
||||
icon: str
|
||||
status: str
|
||||
phone: int
|
||||
lost_description: Optional[str]
|
||||
find_ip: Optional[str]
|
||||
lost_description: str | None
|
||||
find_ip: str | None
|
||||
create_time: str
|
||||
lost_time: Optional[str]
|
||||
|
||||
lost_time: str | None
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from loguru import logger
|
||||
from sqlmodel import select
|
||||
from .setting import Setting
|
||||
import tool
|
||||
from pkg.password import Password
|
||||
|
||||
default_settings: list[Setting] = [
|
||||
Setting(type='string', name='version', value='1.0.0'),
|
||||
@@ -18,11 +18,11 @@ async def migration(session):
|
||||
return
|
||||
|
||||
# 生成初始密码与密钥
|
||||
admin_password = tool.generate_password()
|
||||
admin_password = Password.generate()
|
||||
logger.warning(f"密码(请牢记,后续不再显示): {admin_password}")
|
||||
|
||||
settings.append(Setting(type='string', name='password', value=tool.hash_password(admin_password)))
|
||||
settings.append(Setting(type='string', name='SECRET_KEY', value=tool.generate_password(64)))
|
||||
settings.append(Setting(type='string', name='password', value=Password.hash(admin_password)))
|
||||
settings.append(Setting(type='string', name='SECRET_KEY', value=Password.generate(64)))
|
||||
|
||||
# 读取库里已存在的 name,避免主键冲突
|
||||
names = [s.name for s in settings]
|
||||
|
||||
@@ -1,45 +1,29 @@
|
||||
# my_project/models/download.py
|
||||
|
||||
from typing import Literal, Optional, TYPE_CHECKING
|
||||
from sqlmodel import Field, Column, SQLModel, String, DateTime
|
||||
from typing import Literal
|
||||
from sqlmodel import Field, Column, String, DateTime
|
||||
from .base import TableBase, IdMixin
|
||||
from datetime import datetime
|
||||
|
||||
"""
|
||||
原建表语句:
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fr_objects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
icon TEXT,
|
||||
status TEXT,
|
||||
phone TEXT,
|
||||
context TEXT,
|
||||
find_ip TEXT,
|
||||
create_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
lost_at TIMESTAMP
|
||||
"""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
class Object(IdMixin, TableBase, table=True):
|
||||
__tablename__ = 'fr_objects'
|
||||
|
||||
key: str = Field(index=True, nullable=False, description="物品外部ID")
|
||||
type: Literal['object', 'box'] = Field(
|
||||
default='object',
|
||||
type: Literal['normal', 'car'] = Field(
|
||||
default='normal',
|
||||
description="物品类型",
|
||||
sa_column=Column(String, default='object', nullable=False)
|
||||
sa_column=Column(String, default='normal', nullable=False)
|
||||
)
|
||||
name: str = Field(nullable=False, description="物品名称")
|
||||
icon: Optional[str] = Field(default=None, description="物品图标")
|
||||
status: Optional[str] = Field(default=None, description="物品状态")
|
||||
phone: Optional[str] = Field(default=None, description="联系电话")
|
||||
context: Optional[str] = Field(default=None, description="物品描述")
|
||||
find_ip: Optional[str] = Field(default=None, description="最后一次发现的IP地址")
|
||||
lost_at: Optional[datetime] = Field(
|
||||
icon: str | None = Field(default=None, description="物品图标")
|
||||
status: Literal['ok', 'lost'] = Field(
|
||||
default='ok',
|
||||
description="物品状态",
|
||||
sa_column=Column(String, default='ok', nullable=False)
|
||||
)
|
||||
phone: str | None = Field(default=None, description="联系电话")
|
||||
context: str | None = Field(default=None, description="物品描述")
|
||||
find_ip: str | None = Field(default=None, description="最后一次发现的IP地址")
|
||||
lost_at: datetime | None = Field(
|
||||
default=None,
|
||||
description="物品标记为丢失的时间",
|
||||
sa_column=Column(DateTime, nullable=True)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Literal, Optional
|
||||
from typing import Literal
|
||||
|
||||
class DefaultResponse(BaseModel):
|
||||
code: int = 0
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# model/setting.py
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from sqlmodel import Field
|
||||
from .base import TableBase
|
||||
|
||||
@@ -12,12 +11,8 @@ CREATE TABLE IF NOT EXISTS fr_settings (
|
||||
)
|
||||
"""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
class Setting(TableBase, table=True):
|
||||
__tablename__ = 'fr_settings'
|
||||
|
||||
type: str = Field(index=True, nullable=False, description="设置类型")
|
||||
name: str = Field(primary_key=True, nullable=False, description="设置名称") # name 为唯一主键
|
||||
value: Optional[str] = Field(description="设置值")
|
||||
value: str | None = Field(description="设置值")
|
||||
|
||||
72
pkg/password.py
Normal file
72
pkg/password.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import secrets
|
||||
from loguru import logger
|
||||
from argon2 import PasswordHasher
|
||||
from argon2.exceptions import VerifyMismatchError
|
||||
|
||||
_ph = PasswordHasher()
|
||||
|
||||
class Password():
|
||||
|
||||
@staticmethod
|
||||
def generate(
|
||||
length: int = 8
|
||||
) -> str:
|
||||
"""
|
||||
生成指定长度的随机密码。
|
||||
|
||||
:param length: 密码长度
|
||||
:type length: int
|
||||
:return: 随机密码
|
||||
:rtype: str
|
||||
"""
|
||||
return secrets.token_hex(length)
|
||||
|
||||
def hash(
|
||||
password: str
|
||||
) -> str:
|
||||
"""
|
||||
使用 Argon2 生成密码的哈希值。
|
||||
|
||||
返回的哈希字符串已经包含了所有需要验证的信息(盐、算法参数等)。
|
||||
|
||||
:param password: 需要哈希的原始密码
|
||||
:return: Argon2 哈希字符串
|
||||
"""
|
||||
return _ph.hash(password)
|
||||
|
||||
def verify(
|
||||
stored_password: str,
|
||||
provided_password: str,
|
||||
debug: bool = False
|
||||
) -> bool:
|
||||
"""
|
||||
验证存储的 Argon2 哈希值与用户提供的密码是否匹配。
|
||||
|
||||
:param stored_password: 数据库中存储的 Argon2 哈希字符串
|
||||
:param provided_password: 用户本次提供的密码
|
||||
:param debug: 是否输出调试信息
|
||||
:return: 如果密码匹配返回 True, 否则返回 False
|
||||
"""
|
||||
if debug:
|
||||
logger.info(f"验证密码: (哈希) {stored_password}")
|
||||
|
||||
try:
|
||||
# verify 函数会自动解析 stored_password 中的盐和参数
|
||||
_ph.verify(stored_password, provided_password)
|
||||
|
||||
# 检查哈希参数是否已过时。如果返回True,
|
||||
# 意味着你应该使用新的参数重新哈希密码并更新存储。
|
||||
# 这是一个很好的实践,可以随着时间推移增强安全性。
|
||||
if _ph.check_needs_rehash(stored_password):
|
||||
logger.warning("密码哈希参数已过时,建议重新哈希并更新。")
|
||||
|
||||
return True
|
||||
except VerifyMismatchError:
|
||||
# 这是预期的异常,当密码不匹配时触发。
|
||||
if debug:
|
||||
logger.info("密码不匹配")
|
||||
return False
|
||||
except Exception as e:
|
||||
# 捕获其他可能的错误
|
||||
logger.error(f"密码验证过程中发生未知错误: {e}")
|
||||
return False
|
||||
108
routes/admin.py
108
routes/admin.py
@@ -10,11 +10,12 @@ from model.response import DefaultResponse
|
||||
from model.items import Item
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from model import Setting
|
||||
from model.object import Object
|
||||
|
||||
# 验证是否为管理员
|
||||
async def is_admin(
|
||||
token: Annotated[str, Depends(JWT.oauth2_scheme)],
|
||||
session: Annotated[AsyncSession, Depends(database.Database.get_session)]
|
||||
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
|
||||
) -> Literal[True]:
|
||||
'''
|
||||
验证是否为管理员。
|
||||
@@ -29,9 +30,10 @@ async def is_admin(
|
||||
)
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, JWT.get_secret_key(), algorithms=[JWT.ALGORITHM])
|
||||
payload = jwt.decode(token, await JWT.get_secret_key(), algorithms=[JWT.ALGORITHM])
|
||||
username = payload.get("sub")
|
||||
if username is None or not await Setting.get(session, Setting.name == 'account') == username:
|
||||
stored_account = await Setting.get(session, Setting.name == 'account')
|
||||
if username is None or not stored_account.value == username:
|
||||
raise credentials_exception
|
||||
else:
|
||||
return True
|
||||
@@ -69,6 +71,7 @@ async def verity_admin() -> DefaultResponse:
|
||||
response_description='物品信息列表'
|
||||
)
|
||||
async def get_items(
|
||||
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
|
||||
id: int | None = Query(default=None, ge=1, description='物品ID'),
|
||||
key: str | None = Query(default=None, description='物品序列号')):
|
||||
'''
|
||||
@@ -76,29 +79,33 @@ async def get_items(
|
||||
|
||||
不传参数返回所有信息,否则可传入 `id` 或 `key` 进行筛选。
|
||||
'''
|
||||
results = await database.Database().get_object(id=id, key=key)
|
||||
# 根据条件查询物品
|
||||
if id is not None:
|
||||
results = await Object.get(session, Object.id == id)
|
||||
results = [results] if results else []
|
||||
elif key is not None:
|
||||
results = await Object.get(session, Object.key == key)
|
||||
results = [results] if results else []
|
||||
else:
|
||||
results = await Object.get(session, None, fetch_mode="all")
|
||||
|
||||
if results is not None:
|
||||
if not isinstance(results, list):
|
||||
items = [results]
|
||||
else:
|
||||
items = results
|
||||
item = []
|
||||
for i in items:
|
||||
item.append(Item(
|
||||
id=i[0],
|
||||
type=i[1],
|
||||
key=i[2],
|
||||
name=i[3],
|
||||
icon=i[4],
|
||||
status=i[5],
|
||||
phone=i[6],
|
||||
lost_description=i[7],
|
||||
find_ip=i[8],
|
||||
create_time=i[9],
|
||||
lost_time=i[10]
|
||||
if results:
|
||||
items = []
|
||||
for obj in results:
|
||||
items.append(Item(
|
||||
id=obj.id,
|
||||
type=obj.type,
|
||||
key=obj.key,
|
||||
name=obj.name,
|
||||
icon=obj.icon or "",
|
||||
status=obj.status or "",
|
||||
phone=int(obj.phone) if obj.phone and obj.phone.isdigit() else 0,
|
||||
lost_description=obj.context,
|
||||
find_ip=obj.find_ip,
|
||||
create_time=obj.created_at.isoformat(),
|
||||
lost_time=obj.lost_at.isoformat() if obj.lost_at else None
|
||||
))
|
||||
return DefaultResponse(data=item)
|
||||
return DefaultResponse(data=items)
|
||||
else:
|
||||
return DefaultResponse(data=[])
|
||||
|
||||
@@ -110,6 +117,7 @@ async def get_items(
|
||||
response_description='添加物品成功'
|
||||
)
|
||||
async def add_items(
|
||||
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
|
||||
key: str,
|
||||
type: Literal['normal', 'car'],
|
||||
name: str,
|
||||
@@ -127,13 +135,16 @@ async def add_items(
|
||||
'''
|
||||
|
||||
try:
|
||||
await database.Database().add_object(
|
||||
key=key,
|
||||
# 创建新物品对象
|
||||
new_object = Object(
|
||||
key=key,
|
||||
type=type,
|
||||
name=name,
|
||||
icon=icon,
|
||||
name=name,
|
||||
icon=icon,
|
||||
phone=phone
|
||||
)
|
||||
# 使用 base.py 中的 add 方法
|
||||
await Object.add(session, new_object)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
else:
|
||||
@@ -147,6 +158,7 @@ async def add_items(
|
||||
response_description='更新物品成功'
|
||||
)
|
||||
async def update_items(
|
||||
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
|
||||
id: int = Query(ge=1),
|
||||
key: str | None = None,
|
||||
name: str | None = None,
|
||||
@@ -174,12 +186,32 @@ async def update_items(
|
||||
|
||||
'''
|
||||
try:
|
||||
await database.Database().update_object(
|
||||
id=id,
|
||||
key=key, name=name, icon=icon, status=status, phone=phone,
|
||||
lost_description=lost_description, find_ip=find_ip,
|
||||
lost_time=lost_time
|
||||
)
|
||||
# 获取现有物品
|
||||
obj = await Object.get_exist_one(session, id)
|
||||
|
||||
# 更新字段
|
||||
if key is not None:
|
||||
obj.key = key
|
||||
if name is not None:
|
||||
obj.name = name
|
||||
if icon is not None:
|
||||
obj.icon = icon
|
||||
if status is not None:
|
||||
obj.status = status
|
||||
if phone is not None:
|
||||
obj.phone = str(phone)
|
||||
if lost_description is not None:
|
||||
obj.context = lost_description
|
||||
if find_ip is not None:
|
||||
obj.find_ip = find_ip
|
||||
if lost_time is not None:
|
||||
from datetime import datetime
|
||||
obj.lost_at = datetime.fromisoformat(lost_time)
|
||||
|
||||
# 保存更新
|
||||
await obj.save(session)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
else:
|
||||
@@ -193,6 +225,7 @@ async def update_items(
|
||||
response_description='删除物品成功'
|
||||
)
|
||||
async def delete_items(
|
||||
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
|
||||
id: int) -> DefaultResponse:
|
||||
'''
|
||||
删除物品信息。
|
||||
@@ -200,7 +233,12 @@ async def delete_items(
|
||||
- **id**: 物品的ID
|
||||
'''
|
||||
try:
|
||||
await database.Database().delete_object(id=id)
|
||||
# 获取现有物品
|
||||
obj = await Object.get_exist_one(session, id)
|
||||
# 使用 base.py 中的 delete 方法
|
||||
await Object.delete(session, obj)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
else:
|
||||
|
||||
@@ -68,7 +68,7 @@ async def notify_move_car(
|
||||
|
||||
Args:
|
||||
item_id (int): 物品ID / Item ID
|
||||
phone (str, optional): 挪车发起者电话 / Phone number of the person initiating the move. Defaults to None.
|
||||
phone (str): 挪车发起者电话 / Phone number of the person initiating the move. Defaults to None.
|
||||
"""
|
||||
db = Database()
|
||||
await db.init_db()
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi.security import OAuth2PasswordRequestForm
|
||||
from fastapi import APIRouter
|
||||
import jwt, JWT
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from tool import verify_password
|
||||
from pkg.password import Password
|
||||
from loguru import logger
|
||||
|
||||
from model.token import Token
|
||||
@@ -35,7 +35,7 @@ async def authenticate_user(session: AsyncSession, username: str, password: str)
|
||||
logger.error("Account or password not set in settings.")
|
||||
return False
|
||||
|
||||
if account.value != username or not verify_password(stored_password.value, password):
|
||||
if account.value != username or not Password.verify(stored_password.value, password):
|
||||
logger.error("Invalid username or password.")
|
||||
return False
|
||||
|
||||
|
||||
68
tool.py
68
tool.py
@@ -1,9 +1,4 @@
|
||||
import hashlib
|
||||
import binascii
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
import os
|
||||
import secrets
|
||||
|
||||
def format_phone(
|
||||
phone: str,
|
||||
@@ -36,69 +31,6 @@ def format_phone(
|
||||
|
||||
return separator.join(result)
|
||||
|
||||
def generate_password(
|
||||
length: int = 8
|
||||
) -> str:
|
||||
"""
|
||||
生成指定长度的随机密码。
|
||||
|
||||
:param length: 密码长度
|
||||
:type length: int
|
||||
:return: 随机密码
|
||||
:rtype: str
|
||||
"""
|
||||
import secrets
|
||||
|
||||
return secrets.token_hex(length)
|
||||
|
||||
def hash_password(
|
||||
password: str
|
||||
) -> str:
|
||||
"""
|
||||
生成密码的加盐哈希值。
|
||||
|
||||
:param password: 需要哈希的原始密码
|
||||
:type password: str
|
||||
:return: 包含盐值和哈希值的字符串
|
||||
:rtype: str
|
||||
|
||||
使用SHA-256和PBKDF2算法对密码进行加盐哈希,返回盐值和哈希值的组合。
|
||||
"""
|
||||
salt = hashlib.sha256(os.urandom(60)).hexdigest().encode('ascii')
|
||||
pwdhash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
|
||||
pwdhash = binascii.hexlify(pwdhash)
|
||||
return (salt + pwdhash).decode('ascii')
|
||||
|
||||
def verify_password(
|
||||
stored_password: str,
|
||||
provided_password: str,
|
||||
debug: bool = False
|
||||
) -> bool:
|
||||
"""
|
||||
验证存储的密码哈希值与用户提供的密码是否匹配。
|
||||
|
||||
:param stored_password: 存储的密码哈希值(包含盐值)
|
||||
:type stored_password: str
|
||||
:param provided_password: 用户提供的密码
|
||||
:type provided_password: str
|
||||
:param debug: 是否输出调试信息,将会输出原密码和哈希值
|
||||
:type debug: bool
|
||||
:return: 如果密码匹配返回True,否则返回False
|
||||
:rtype: bool
|
||||
|
||||
从存储的密码哈希中提取盐值,使用相同的哈希算法验证用户提供的密码。
|
||||
"""
|
||||
salt = stored_password[:64]
|
||||
stored_password = stored_password[64:]
|
||||
pwdhash = hashlib.pbkdf2_hmac('sha256',
|
||||
provided_password.encode('utf-8'),
|
||||
salt.encode('ascii'),
|
||||
100000)
|
||||
pwdhash = binascii.hexlify(pwdhash).decode('ascii')
|
||||
if debug:
|
||||
logging.info(f"原密码: {provided_password}, 哈希值: {pwdhash}, 存储哈希值: {stored_password}")
|
||||
return secrets.compare_digest(pwdhash, stored_password)
|
||||
|
||||
def format_time_diff(
|
||||
target_time: datetime | str
|
||||
) -> str:
|
||||
|
||||
Reference in New Issue
Block a user