feat: add Space aggregate login
This commit is contained in:
20
db/admin.py
20
db/admin.py
@@ -42,6 +42,11 @@ _DEFAULT_SYSTEM_CONFIG = {
|
||||
"kdocs_admin_notify_email": "",
|
||||
"kdocs_row_start": 0,
|
||||
"kdocs_row_end": 0,
|
||||
"social_login_enabled": 0,
|
||||
"social_login_endpoint": "https://www.spacezs.cn/connect.php",
|
||||
"social_login_appid": "",
|
||||
"social_login_appkey": "",
|
||||
"social_login_providers": "qq,wx,alipay",
|
||||
}
|
||||
|
||||
_SYSTEM_CONFIG_UPDATERS = (
|
||||
@@ -71,6 +76,11 @@ _SYSTEM_CONFIG_UPDATERS = (
|
||||
("kdocs_admin_notify_email", "kdocs_admin_notify_email"),
|
||||
("kdocs_row_start", "kdocs_row_start"),
|
||||
("kdocs_row_end", "kdocs_row_end"),
|
||||
("social_login_enabled", "social_login_enabled"),
|
||||
("social_login_endpoint", "social_login_endpoint"),
|
||||
("social_login_appid", "social_login_appid"),
|
||||
("social_login_appkey", "social_login_appkey"),
|
||||
("social_login_providers", "social_login_providers"),
|
||||
)
|
||||
|
||||
|
||||
@@ -316,6 +326,11 @@ def update_system_config(
|
||||
kdocs_row_start=None,
|
||||
kdocs_row_end=None,
|
||||
db_slow_query_ms=None,
|
||||
social_login_enabled=None,
|
||||
social_login_endpoint=None,
|
||||
social_login_appid=None,
|
||||
social_login_appkey=None,
|
||||
social_login_providers=None,
|
||||
) -> bool:
|
||||
"""更新系统配置(仅更新DB,不做缓存处理)。"""
|
||||
arg_values = {
|
||||
@@ -345,6 +360,11 @@ def update_system_config(
|
||||
"kdocs_row_start": kdocs_row_start,
|
||||
"kdocs_row_end": kdocs_row_end,
|
||||
"db_slow_query_ms": db_slow_query_ms,
|
||||
"social_login_enabled": social_login_enabled,
|
||||
"social_login_endpoint": social_login_endpoint,
|
||||
"social_login_appid": social_login_appid,
|
||||
"social_login_appkey": social_login_appkey,
|
||||
"social_login_providers": social_login_providers,
|
||||
}
|
||||
|
||||
updates = []
|
||||
|
||||
@@ -76,6 +76,7 @@ def _get_migration_steps():
|
||||
(19, _migrate_to_v19),
|
||||
(20, _migrate_to_v20),
|
||||
(21, _migrate_to_v21),
|
||||
(22, _migrate_to_v22),
|
||||
]
|
||||
|
||||
|
||||
@@ -933,3 +934,71 @@ def _migrate_to_v21(conn):
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _migrate_to_v22(conn):
|
||||
"""迁移到版本22 - Space 聚合登录配置、绑定与短期待绑定凭证。"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
system_columns = _get_table_columns(cursor, "system_config")
|
||||
system_fields = [
|
||||
("social_login_enabled", "INTEGER DEFAULT 0"),
|
||||
("social_login_endpoint", "TEXT DEFAULT 'https://www.spacezs.cn/connect.php'"),
|
||||
("social_login_appid", "TEXT DEFAULT ''"),
|
||||
("social_login_appkey", "TEXT DEFAULT ''"),
|
||||
("social_login_providers", "TEXT DEFAULT 'qq,wx,alipay'"),
|
||||
]
|
||||
for field, ddl in system_fields:
|
||||
_add_column_if_missing(
|
||||
cursor,
|
||||
"system_config",
|
||||
system_columns,
|
||||
field,
|
||||
ddl,
|
||||
ok_message=f" [OK] 添加 system_config.{field} 字段",
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS social_login_bindings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
social_uid TEXT NOT NULL,
|
||||
nickname TEXT DEFAULT '',
|
||||
avatar_url TEXT DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login_at TIMESTAMP,
|
||||
UNIQUE (provider, social_uid),
|
||||
UNIQUE (user_id, provider),
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
)
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_login_bindings_user ON social_login_bindings(user_id)")
|
||||
cursor.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_social_login_bindings_provider_uid ON social_login_bindings(provider, social_uid)"
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS social_pending_binds (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
provider TEXT NOT NULL,
|
||||
social_uid TEXT NOT NULL,
|
||||
nickname TEXT DEFAULT '',
|
||||
avatar_url TEXT DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_token ON social_pending_binds(token)")
|
||||
cursor.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_social_pending_binds_provider_uid ON social_pending_binds(provider, social_uid)"
|
||||
)
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_expires ON social_pending_binds(expires_at)")
|
||||
|
||||
conn.commit()
|
||||
|
||||
46
db/schema.py
46
db/schema.py
@@ -246,11 +246,52 @@ def ensure_schema(conn) -> None:
|
||||
kdocs_admin_notify_email TEXT DEFAULT '',
|
||||
kdocs_row_start INTEGER DEFAULT 0,
|
||||
kdocs_row_end INTEGER DEFAULT 0,
|
||||
social_login_enabled INTEGER DEFAULT 0,
|
||||
social_login_endpoint TEXT DEFAULT 'https://www.spacezs.cn/connect.php',
|
||||
social_login_appid TEXT DEFAULT '',
|
||||
social_login_appkey TEXT DEFAULT '',
|
||||
social_login_providers TEXT DEFAULT 'qq,wx,alipay',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
# 聚合登录绑定表
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS social_login_bindings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
social_uid TEXT NOT NULL,
|
||||
nickname TEXT DEFAULT '',
|
||||
avatar_url TEXT DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login_at TIMESTAMP,
|
||||
UNIQUE (provider, social_uid),
|
||||
UNIQUE (user_id, provider),
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
# 聚合登录短期待绑定凭证表
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS social_pending_binds (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
provider TEXT NOT NULL,
|
||||
social_uid TEXT NOT NULL,
|
||||
nickname TEXT DEFAULT '',
|
||||
avatar_url TEXT DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
# 任务日志表
|
||||
cursor.execute(
|
||||
"""
|
||||
@@ -389,6 +430,11 @@ def ensure_schema(conn) -> None:
|
||||
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_social_login_bindings_user ON social_login_bindings(user_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_login_bindings_provider_uid ON social_login_bindings(provider, social_uid)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_token ON social_pending_binds(token)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_provider_uid ON social_pending_binds(provider, social_uid)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_expires ON social_pending_binds(expires_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)")
|
||||
|
||||
150
db/users.py
150
db/users.py
@@ -297,6 +297,156 @@ def get_user_by_username(username):
|
||||
return _get_user_by_field("username", username)
|
||||
|
||||
|
||||
# ==================== 聚合登录绑定 ====================
|
||||
|
||||
|
||||
def cleanup_expired_social_pending_binds() -> None:
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM social_pending_binds WHERE expires_at < ?", (get_cst_now_str(),))
|
||||
conn.commit()
|
||||
|
||||
|
||||
def create_social_pending_bind(*, token: str, provider: str, social_uid: str, nickname: str = "", avatar_url: str = "", expires_at: str) -> dict:
|
||||
cleanup_expired_social_pending_binds()
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"DELETE FROM social_pending_binds WHERE provider = ? AND social_uid = ?",
|
||||
(provider, social_uid),
|
||||
)
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO social_pending_binds (token, provider, social_uid, nickname, avatar_url, created_at, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
token,
|
||||
provider,
|
||||
social_uid,
|
||||
str(nickname or "")[:128],
|
||||
str(avatar_url or "")[:512],
|
||||
get_cst_now_str(),
|
||||
expires_at,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
cursor.execute("SELECT * FROM social_pending_binds WHERE token = ?", (token,))
|
||||
return _row_to_dict(cursor.fetchone())
|
||||
|
||||
|
||||
def get_social_pending_bind(token: str):
|
||||
cleanup_expired_social_pending_binds()
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM social_pending_binds WHERE token = ?", ((token or "").strip(),))
|
||||
return _row_to_dict(cursor.fetchone())
|
||||
|
||||
|
||||
def delete_social_pending_bind(token: str) -> bool:
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM social_pending_binds WHERE token = ?", ((token or "").strip(),))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
def find_social_login_binding(provider: str, social_uid: str):
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT * FROM social_login_bindings WHERE provider = ? AND social_uid = ?",
|
||||
(provider, social_uid),
|
||||
)
|
||||
return _row_to_dict(cursor.fetchone())
|
||||
|
||||
|
||||
def find_user_social_login_binding(user_id: int, provider: str):
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT * FROM social_login_bindings WHERE user_id = ? AND provider = ?",
|
||||
(int(user_id), provider),
|
||||
)
|
||||
return _row_to_dict(cursor.fetchone())
|
||||
|
||||
|
||||
def upsert_social_login_binding(*, user_id: int, provider: str, social_uid: str, nickname: str = "", avatar_url: str = ""):
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
now = get_cst_now_str()
|
||||
try:
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO social_login_bindings (
|
||||
user_id, provider, social_uid, nickname, avatar_url, created_at, updated_at, last_login_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(user_id, provider) DO UPDATE SET
|
||||
social_uid = excluded.social_uid,
|
||||
nickname = excluded.nickname,
|
||||
avatar_url = excluded.avatar_url,
|
||||
updated_at = excluded.updated_at,
|
||||
last_login_at = excluded.last_login_at
|
||||
""",
|
||||
(
|
||||
int(user_id),
|
||||
provider,
|
||||
social_uid,
|
||||
str(nickname or "")[:128],
|
||||
str(avatar_url or "")[:512],
|
||||
now,
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
except sqlite3.IntegrityError:
|
||||
conn.rollback()
|
||||
return None
|
||||
return find_user_social_login_binding(user_id, provider)
|
||||
|
||||
|
||||
def update_social_login_binding_profile(binding_id: int, *, nickname: str = "", avatar_url: str = "") -> bool:
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE social_login_bindings
|
||||
SET nickname = ?, avatar_url = ?, updated_at = ?, last_login_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(str(nickname or "")[:128], str(avatar_url or "")[:512], get_cst_now_str(), get_cst_now_str(), int(binding_id)),
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
def list_social_login_bindings(user_id: int) -> list[dict]:
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT * FROM social_login_bindings
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at ASC
|
||||
""",
|
||||
(int(user_id),),
|
||||
)
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
|
||||
def delete_social_login_binding(user_id: int, provider: str) -> bool:
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"DELETE FROM social_login_bindings WHERE user_id = ? AND provider = ?",
|
||||
(int(user_id), provider),
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
def _normalize_limit_offset(limit, offset, *, max_limit: int = 500):
|
||||
normalized_limit = None
|
||||
if limit is not None:
|
||||
|
||||
Reference in New Issue
Block a user