Harden auth risk controls and admin reauth
This commit is contained in:
@@ -69,6 +69,9 @@ def migrate_database(conn, target_version: int) -> None:
|
||||
if current_version < 11:
|
||||
_migrate_to_v11(conn)
|
||||
current_version = 11
|
||||
if current_version < 12:
|
||||
_migrate_to_v12(conn)
|
||||
current_version = 12
|
||||
|
||||
if current_version != int(target_version):
|
||||
set_current_version(conn, int(target_version))
|
||||
@@ -472,7 +475,47 @@ def _migrate_to_v11(conn):
|
||||
)
|
||||
updated = cursor.rowcount
|
||||
conn.commit()
|
||||
|
||||
if updated:
|
||||
print(f" ✓ 已将 {updated} 个 pending 用户迁移为 approved")
|
||||
except sqlite3.OperationalError as e:
|
||||
print(f" ⚠️ v11 迁移跳过: {e}")
|
||||
|
||||
|
||||
def _migrate_to_v12(conn):
|
||||
"""迁移到版本12 - 登录设备/IP记录表"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS login_fingerprints (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
user_agent TEXT NOT NULL,
|
||||
first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_ip TEXT DEFAULT '',
|
||||
UNIQUE (user_id, user_agent),
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS login_ips (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
ip TEXT NOT NULL,
|
||||
first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (user_id, ip),
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
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)")
|
||||
|
||||
conn.commit()
|
||||
|
||||
33
db/schema.py
33
db/schema.py
@@ -41,6 +41,37 @@ def ensure_schema(conn) -> None:
|
||||
"""
|
||||
)
|
||||
|
||||
# 登录设备指纹表
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS login_fingerprints (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
user_agent TEXT NOT NULL,
|
||||
first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_ip TEXT DEFAULT '',
|
||||
UNIQUE (user_id, user_agent),
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
# 登录IP记录表
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS login_ips (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
ip TEXT NOT NULL,
|
||||
first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (user_id, ip),
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
# 账号表(关联用户)
|
||||
cursor.execute(
|
||||
"""
|
||||
@@ -237,6 +268,8 @@ def ensure_schema(conn) -> None:
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_status ON users(status)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_vip_expire ON users(vip_expire_time)")
|
||||
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_accounts_user_id ON accounts(user_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_accounts_username ON accounts(username)")
|
||||
|
||||
76
db/security.py
Normal file
76
db/security.py
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
import db_pool
|
||||
from db.utils import get_cst_now_str
|
||||
|
||||
|
||||
def record_login_context(user_id: int, ip_address: str, user_agent: str) -> Dict[str, bool]:
|
||||
"""记录登录环境信息,返回是否新设备/新IP。"""
|
||||
user_id = int(user_id)
|
||||
ip_text = str(ip_address or "").strip()[:64]
|
||||
ua_text = str(user_agent or "").strip()[:512]
|
||||
now_str = get_cst_now_str()
|
||||
|
||||
new_device = False
|
||||
new_ip = False
|
||||
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
if ua_text:
|
||||
cursor.execute(
|
||||
"SELECT id FROM login_fingerprints WHERE user_id = ? AND user_agent = ?",
|
||||
(user_id, ua_text),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE login_fingerprints
|
||||
SET last_seen = ?, last_ip = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(now_str, ip_text, row["id"] if isinstance(row, dict) else row[0]),
|
||||
)
|
||||
else:
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO login_fingerprints (user_id, user_agent, first_seen, last_seen, last_ip)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(user_id, ua_text, now_str, now_str, ip_text),
|
||||
)
|
||||
new_device = True
|
||||
|
||||
if ip_text:
|
||||
cursor.execute(
|
||||
"SELECT id FROM login_ips WHERE user_id = ? AND ip = ?",
|
||||
(user_id, ip_text),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE login_ips
|
||||
SET last_seen = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(now_str, row["id"] if isinstance(row, dict) else row[0]),
|
||||
)
|
||||
else:
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO login_ips (user_id, ip, first_seen, last_seen)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(user_id, ip_text, now_str, now_str),
|
||||
)
|
||||
new_ip = True
|
||||
|
||||
conn.commit()
|
||||
|
||||
return {"new_device": new_device, "new_ip": new_ip}
|
||||
Reference in New Issue
Block a user