feat(auth): 新增手机号与 DCAuth 设备认证登录

- 登录链路改为用户名 + 密码 + 短信验证码 + DCAuth v2 设备签名
- 新增 login_sms_codes / user_devices / device_challenges 三张表,迁移 0063/0064
- 设备绑定只比对 deviceId + publicKey,每账号一个 active 设备,管理员可重置
- 保留未绑定手机号账号的密码登录兜底,绑定后强制安全登录
- 登录页与用户管理页支持验证码、DCAuth 轮询、手机号绑定与设备重置
This commit is contained in:
yml2213
2026-10-21 22:00:03 +08:00
parent f21362dfe3
commit 2f5a63ae27
24 changed files with 3049 additions and 130 deletions
+23 -1
View File
@@ -6,9 +6,10 @@ import os
from pathlib import Path
from urllib.parse import quote_plus
from sqlalchemy import Engine, create_engine, text
from sqlalchemy import Engine, create_engine, inspect, text
from sqlalchemy.exc import OperationalError
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.schema import CreateColumn
def get_mysql_credentials() -> dict[str, str]:
@@ -134,10 +135,31 @@ def clear_tables(engine: Engine | None = None) -> None:
conn.execute(text("SET FOREIGN_KEY_CHECKS = 1;"))
def _add_missing_columns(engine: Engine) -> None:
"""给“已存在但结构偏旧”的测试表补列。
``Base.metadata.create_all`` 只建缺失的表,不会给已有表加列;测试库复用旧结构时,
新模型字段会在 INSERT 阶段报 Unknown column。这里只做加法(不动索引/数据),
让测试库跟模型对齐,又不会像 drop_all 那样丢掉迁移里才有的索引。
"""
inspector = inspect(engine)
with engine.begin() as conn:
for table in Base.metadata.sorted_tables:
if not inspector.has_table(table.name):
continue
existing = {column["name"] for column in inspector.get_columns(table.name)}
for column in table.columns:
if column.name in existing:
continue
ddl = CreateColumn(column).compile(dialect=engine.dialect)
conn.execute(text(f"ALTER TABLE `{table.name}` ADD COLUMN {ddl}"))
def init_test_schema(engine: Engine | None = None) -> None:
"""确保测试库中所有表结构存在并清空所有数据(幂等高效)。"""
eng = engine or get_test_engine()
try:
_add_missing_columns(eng)
Base.metadata.create_all(eng)
clear_tables(eng)
except OperationalError as exc: