feat: 完成 Passkey 能力与前后台加载优化
更新说明:\n1. 新增用户端与管理员端 Passkey 登录/注册/设备管理(最多3台,支持设备备注、删除设备)。\n2. 修复 Passkey 注册与登录流程中的浏览器/证书/CSRF相关问题,增强错误提示。\n3. 前台登录页改为独立入口,首屏仅加载必要资源,其他页面按需加载。\n4. 系统配置页改为静默获取金山文档状态,避免首屏阻塞,并优化状态展示为“检测中/已登录/未登录/异常”。\n5. 补充后端接口与页面渲染适配,修复多入口下样式依赖注入问题。\n6. 同步更新前后台构建产物与相关静态资源。
This commit is contained in:
18
db/admin.py
18
db/admin.py
@@ -165,6 +165,24 @@ def verify_admin(username: str, password: str):
|
||||
return None
|
||||
|
||||
|
||||
def get_admin_by_username(username: str):
|
||||
"""根据用户名获取管理员记录"""
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM admins WHERE username = ?", (username,))
|
||||
row = cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
def get_admin_by_id(admin_id: int):
|
||||
"""根据ID获取管理员记录"""
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM admins WHERE id = ?", (int(admin_id),))
|
||||
row = cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
def update_admin_password(username: str, new_password: str) -> bool:
|
||||
"""更新管理员密码"""
|
||||
with db_pool.get_db() as conn:
|
||||
|
||||
@@ -75,6 +75,7 @@ def _get_migration_steps():
|
||||
(18, _migrate_to_v18),
|
||||
(19, _migrate_to_v19),
|
||||
(20, _migrate_to_v20),
|
||||
(21, _migrate_to_v21),
|
||||
]
|
||||
|
||||
|
||||
@@ -903,3 +904,32 @@ def _migrate_to_v20(conn):
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _migrate_to_v21(conn):
|
||||
"""迁移到版本21 - Passkey 认证设备表"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS passkeys (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
owner_type TEXT NOT NULL,
|
||||
owner_id INTEGER NOT NULL,
|
||||
device_name TEXT NOT NULL,
|
||||
credential_id TEXT UNIQUE NOT NULL,
|
||||
public_key TEXT NOT NULL,
|
||||
sign_count INTEGER DEFAULT 0,
|
||||
transports TEXT DEFAULT '',
|
||||
aaguid TEXT DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_passkeys_owner ON passkeys(owner_type, owner_id)")
|
||||
cursor.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_passkeys_owner_last_used ON passkeys(owner_type, owner_id, last_used_at)"
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
|
||||
173
db/passkeys.py
Normal file
173
db/passkeys.py
Normal file
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
|
||||
import db_pool
|
||||
from db.utils import get_cst_now_str
|
||||
|
||||
_OWNER_TYPES = {"user", "admin"}
|
||||
|
||||
|
||||
def _normalize_owner_type(owner_type: str) -> str:
|
||||
normalized = str(owner_type or "").strip().lower()
|
||||
if normalized not in _OWNER_TYPES:
|
||||
raise ValueError(f"invalid owner_type: {owner_type}")
|
||||
return normalized
|
||||
|
||||
|
||||
def list_passkeys(owner_type: str, owner_id: int) -> list[dict]:
|
||||
owner = _normalize_owner_type(owner_type)
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, owner_type, owner_id, device_name, credential_id, transports,
|
||||
sign_count, aaguid, created_at, last_used_at
|
||||
FROM passkeys
|
||||
WHERE owner_type = ? AND owner_id = ?
|
||||
ORDER BY datetime(created_at) DESC, id DESC
|
||||
""",
|
||||
(owner, int(owner_id)),
|
||||
)
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
|
||||
def count_passkeys(owner_type: str, owner_id: int) -> int:
|
||||
owner = _normalize_owner_type(owner_type)
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) AS count FROM passkeys WHERE owner_type = ? AND owner_id = ?",
|
||||
(owner, int(owner_id)),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return 0
|
||||
try:
|
||||
return int(row["count"] or 0)
|
||||
except Exception:
|
||||
try:
|
||||
return int(row[0] or 0)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def get_passkey_by_credential_id(credential_id: str) -> dict | None:
|
||||
credential = str(credential_id or "").strip()
|
||||
if not credential:
|
||||
return None
|
||||
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, owner_type, owner_id, device_name, credential_id, public_key,
|
||||
sign_count, transports, aaguid, created_at, last_used_at
|
||||
FROM passkeys
|
||||
WHERE credential_id = ?
|
||||
""",
|
||||
(credential,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
def get_passkey_by_id(owner_type: str, owner_id: int, passkey_id: int) -> dict | None:
|
||||
owner = _normalize_owner_type(owner_type)
|
||||
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, owner_type, owner_id, device_name, credential_id, public_key,
|
||||
sign_count, transports, aaguid, created_at, last_used_at
|
||||
FROM passkeys
|
||||
WHERE id = ? AND owner_type = ? AND owner_id = ?
|
||||
""",
|
||||
(int(passkey_id), owner, int(owner_id)),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
def create_passkey(
|
||||
owner_type: str,
|
||||
owner_id: int,
|
||||
*,
|
||||
credential_id: str,
|
||||
public_key: str,
|
||||
sign_count: int,
|
||||
device_name: str,
|
||||
transports: str = "",
|
||||
aaguid: str = "",
|
||||
) -> int | None:
|
||||
owner = _normalize_owner_type(owner_type)
|
||||
now = get_cst_now_str()
|
||||
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO passkeys (
|
||||
owner_type,
|
||||
owner_id,
|
||||
device_name,
|
||||
credential_id,
|
||||
public_key,
|
||||
sign_count,
|
||||
transports,
|
||||
aaguid,
|
||||
created_at,
|
||||
last_used_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
owner,
|
||||
int(owner_id),
|
||||
str(device_name or "").strip(),
|
||||
str(credential_id or "").strip(),
|
||||
str(public_key or "").strip(),
|
||||
int(sign_count or 0),
|
||||
str(transports or "").strip(),
|
||||
str(aaguid or "").strip(),
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return int(cursor.lastrowid)
|
||||
except sqlite3.IntegrityError:
|
||||
return None
|
||||
|
||||
|
||||
def update_passkey_usage(passkey_id: int, new_sign_count: int) -> bool:
|
||||
now = get_cst_now_str()
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE passkeys
|
||||
SET sign_count = ?,
|
||||
last_used_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(int(new_sign_count or 0), now, int(passkey_id)),
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
def delete_passkey(owner_type: str, owner_id: int, passkey_id: int) -> bool:
|
||||
owner = _normalize_owner_type(owner_type)
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"DELETE FROM passkeys WHERE id = ? AND owner_type = ? AND owner_id = ?",
|
||||
(int(passkey_id), owner, int(owner_id)),
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
21
db/schema.py
21
db/schema.py
@@ -74,6 +74,25 @@ def ensure_schema(conn) -> None:
|
||||
"""
|
||||
)
|
||||
|
||||
# Passkey 认证设备表(用户/管理员)
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS passkeys (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
owner_type TEXT NOT NULL,
|
||||
owner_id INTEGER NOT NULL,
|
||||
device_name TEXT NOT NULL,
|
||||
credential_id TEXT UNIQUE NOT NULL,
|
||||
public_key TEXT NOT NULL,
|
||||
sign_count INTEGER DEFAULT 0,
|
||||
transports TEXT DEFAULT '',
|
||||
aaguid TEXT DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
# ==================== 安全防护:威胁检测相关表 ====================
|
||||
|
||||
# 威胁事件日志表
|
||||
@@ -368,6 +387,8 @@ def ensure_schema(conn) -> None:
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_status_created_at ON users(status, created_at)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_login_fingerprints_user ON login_fingerprints(user_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_login_ips_user ON login_ips(user_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_passkeys_owner ON passkeys(owner_type, owner_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_passkeys_owner_last_used ON passkeys(owner_type, owner_id, last_used_at)")
|
||||
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_events_created_at ON threat_events(created_at)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_events_ip ON threat_events(ip)")
|
||||
|
||||
Reference in New Issue
Block a user