加密敏感数据并优化补CK流程
This commit is contained in:
@@ -6,6 +6,11 @@
|
|||||||
# 生成方式: python -c "import secrets; print(secrets.token_urlsafe(32))"
|
# 生成方式: python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||||
JWT_SECRET_KEY=
|
JWT_SECRET_KEY=
|
||||||
|
|
||||||
|
# 敏感字段落盘加密密钥(生产环境必须设置且长期保持不变!)
|
||||||
|
# 用于加密账号密码、邮箱密码、Cookie、代理密钥等数据库字段
|
||||||
|
# 生成方式: python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||||
|
APP_ENCRYPTION_KEY=
|
||||||
|
|
||||||
# 默认管理员账号(仅首次启动建库时生效)
|
# 默认管理员账号(仅首次启动建库时生效)
|
||||||
ADMIN_USERNAME=admin
|
ADMIN_USERNAME=admin
|
||||||
ADMIN_PASSWORD=admin123
|
ADMIN_PASSWORD=admin123
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ cd web/frontend && npm install && npm run dev
|
|||||||
|
|
||||||
访问 `http://localhost:5173`,默认账号 `admin / admin123`。
|
访问 `http://localhost:5173`,默认账号 `admin / admin123`。
|
||||||
|
|
||||||
|
### 敏感数据加密
|
||||||
|
|
||||||
|
账号密码、邮箱密码、Cookie、代理密钥等敏感字段会以密文形式落盘。生产环境请在 `.env` 中设置稳定的 `APP_ENCRYPTION_KEY`,并长期保留;更换该密钥会导致历史密文无法解密。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||||
|
```
|
||||||
|
|
||||||
## 角色权限
|
## 角色权限
|
||||||
|
|
||||||
| 角色 | 权限 |
|
| 角色 | 权限 |
|
||||||
|
|||||||
+29
-4
@@ -105,6 +105,7 @@ class DouyuLogin:
|
|||||||
self.proxy_manager = None
|
self.proxy_manager = None
|
||||||
|
|
||||||
self._current_proxy_url: Optional[str] = None
|
self._current_proxy_url: Optional[str] = None
|
||||||
|
self._cookie_enrich_error = ""
|
||||||
self._setup_session()
|
self._setup_session()
|
||||||
|
|
||||||
def _setup_session(self) -> None:
|
def _setup_session(self) -> None:
|
||||||
@@ -374,12 +375,15 @@ class DouyuLogin:
|
|||||||
# 7️⃣ 完成登录获取Cookie
|
# 7️⃣ 完成登录获取Cookie
|
||||||
logger.info("步骤7: 完成登录,获取Cookie...")
|
logger.info("步骤7: 完成登录,获取Cookie...")
|
||||||
cookie = self._complete_login(login_url)
|
cookie = self._complete_login(login_url)
|
||||||
|
message = "登录成功"
|
||||||
|
if self._cookie_enrich_error:
|
||||||
|
message = f"登录成功,补CK失败: {self._cookie_enrich_error}"
|
||||||
|
|
||||||
logger.success(f"登录成功! Cookie长度: {len(cookie)}")
|
logger.success(f"登录成功! Cookie长度: {len(cookie)}")
|
||||||
# 成功后归还代理到池,让其他账号复用
|
# 成功后归还代理到池,让其他账号复用
|
||||||
if self.proxy_manager and self._current_proxy_url:
|
if self.proxy_manager and self._current_proxy_url:
|
||||||
self.proxy_manager.release_proxy(self._current_proxy_url)
|
self.proxy_manager.release_proxy(self._current_proxy_url)
|
||||||
return LoginResult(success=True, cookie=cookie, message="登录成功")
|
return LoginResult(success=True, cookie=cookie, message=message)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
elapsed = time.monotonic() - start_time
|
elapsed = time.monotonic() - start_time
|
||||||
@@ -736,12 +740,33 @@ class DouyuLogin:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"WebLogin请求失败(不影响登录): {e}")
|
logger.warning(f"WebLogin请求失败(不影响登录): {e}")
|
||||||
|
|
||||||
# 登录成功后补齐 Web 侧 Cookie,再导出完整 CK。
|
# 登录成功后补齐 Web 侧 Cookie。补 CK 失败只影响完整度,不回滚已成功的登录态。
|
||||||
self._generate_csrf_cookie()
|
self._cookie_enrich_error = ""
|
||||||
self._generate_acf_ccn_cookie()
|
try:
|
||||||
|
self._enrich_web_cookies_with_retry()
|
||||||
|
except Exception as e:
|
||||||
|
self._cookie_enrich_error = self._truncate_error(str(e), 160)
|
||||||
|
logger.warning(f"补CK最终失败,本次仍按登录成功保存基础CK: {self._cookie_enrich_error}")
|
||||||
|
|
||||||
return self._format_cookie_string()
|
return self._format_cookie_string()
|
||||||
|
|
||||||
|
def _enrich_web_cookies_with_retry(self, max_attempts: int = 3) -> None:
|
||||||
|
"""独立重试补齐 cvl_csrf_token 和 acf_ccn,不触发整条登录链路重跑。"""
|
||||||
|
last_error: Exception | None = None
|
||||||
|
for attempt in range(1, max_attempts + 1):
|
||||||
|
try:
|
||||||
|
logger.info(f"补CK尝试 {attempt}/{max_attempts}...")
|
||||||
|
self._generate_csrf_cookie()
|
||||||
|
self._generate_acf_ccn_cookie()
|
||||||
|
logger.info("补CK完成")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
last_error = e
|
||||||
|
logger.warning(f"补CK失败 {attempt}/{max_attempts}: {e}")
|
||||||
|
if attempt < max_attempts:
|
||||||
|
time.sleep(1)
|
||||||
|
raise ValueError(f"已重试 {max_attempts} 次仍未补齐CK: {last_error}") from last_error
|
||||||
|
|
||||||
def _generate_csrf_cookie(self) -> str:
|
def _generate_csrf_cookie(self) -> str:
|
||||||
"""
|
"""
|
||||||
访问斗鱼 generateCsrf 接口,从 Set-Cookie 中同步 cvl_csrf_token。
|
访问斗鱼 generateCsrf 接口,从 Set-Cookie 中同步 cvl_csrf_token。
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
"""敏感字段落盘加密工具。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import warnings
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
|
from sqlalchemy.types import Text, TypeDecorator
|
||||||
|
|
||||||
|
|
||||||
|
_PREFIX = "enc:v1:"
|
||||||
|
_FALLBACK_SECRET = "douyu-login-py-dev-storage-key-change-me"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class _KeyCandidate:
|
||||||
|
"""可用于解密的候选密钥。"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
secret: str
|
||||||
|
primary: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def _key_candidates() -> tuple[_KeyCandidate, ...]:
|
||||||
|
"""返回主加密密钥与历史兼容解密密钥。"""
|
||||||
|
app_key = os.getenv("APP_ENCRYPTION_KEY", "").strip()
|
||||||
|
jwt_key = os.getenv("JWT_SECRET_KEY", "").strip()
|
||||||
|
|
||||||
|
if app_key:
|
||||||
|
primary = _KeyCandidate("APP_ENCRYPTION_KEY", app_key, True)
|
||||||
|
elif jwt_key:
|
||||||
|
primary = _KeyCandidate("JWT_SECRET_KEY", jwt_key, True)
|
||||||
|
else:
|
||||||
|
warnings.warn(
|
||||||
|
"未设置 APP_ENCRYPTION_KEY,敏感字段将使用开发兜底密钥加密。"
|
||||||
|
"生产环境请设置稳定且随机的 APP_ENCRYPTION_KEY。",
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
primary = _KeyCandidate("fallback", _FALLBACK_SECRET, True)
|
||||||
|
|
||||||
|
candidates = [primary]
|
||||||
|
for name, secret in (
|
||||||
|
("APP_ENCRYPTION_KEY", app_key),
|
||||||
|
("JWT_SECRET_KEY", jwt_key),
|
||||||
|
("fallback", _FALLBACK_SECRET),
|
||||||
|
):
|
||||||
|
if secret and all(item.secret != secret for item in candidates):
|
||||||
|
candidates.append(_KeyCandidate(name, secret, False))
|
||||||
|
return tuple(candidates)
|
||||||
|
|
||||||
|
|
||||||
|
def _aes_key(secret: str) -> bytes:
|
||||||
|
"""从配置密钥派生 AES-256 key。"""
|
||||||
|
return hashlib.sha256(secret.encode("utf-8")).digest()
|
||||||
|
|
||||||
|
|
||||||
|
def _b64e(data: bytes) -> str:
|
||||||
|
return base64.urlsafe_b64encode(data).decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def _b64d(data: str) -> bytes:
|
||||||
|
return base64.urlsafe_b64decode(data.encode("ascii"))
|
||||||
|
|
||||||
|
|
||||||
|
def is_encrypted(value: str | None) -> bool:
|
||||||
|
"""判断值是否已经是当前加密格式。"""
|
||||||
|
return bool(value) and value.startswith(_PREFIX)
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_value(value: str | None) -> str | None:
|
||||||
|
"""加密字符串;空值保持空值,避免影响空值筛选。"""
|
||||||
|
if value is None or value == "":
|
||||||
|
return value
|
||||||
|
if is_encrypted(value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
candidate = _key_candidates()[0]
|
||||||
|
nonce = os.urandom(12)
|
||||||
|
cipher = AES.new(_aes_key(candidate.secret), AES.MODE_GCM, nonce=nonce)
|
||||||
|
ciphertext, tag = cipher.encrypt_and_digest(value.encode("utf-8"))
|
||||||
|
return _PREFIX + _b64e(nonce + tag + ciphertext)
|
||||||
|
|
||||||
|
|
||||||
|
def _decrypt_with_candidate(value: str, candidate: _KeyCandidate) -> str:
|
||||||
|
raw = _b64d(value[len(_PREFIX):])
|
||||||
|
if len(raw) < 28:
|
||||||
|
raise ValueError("密文字段长度无效")
|
||||||
|
nonce = raw[:12]
|
||||||
|
tag = raw[12:28]
|
||||||
|
ciphertext = raw[28:]
|
||||||
|
cipher = AES.new(_aes_key(candidate.secret), AES.MODE_GCM, nonce=nonce)
|
||||||
|
return cipher.decrypt_and_verify(ciphertext, tag).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_value(value: str | None) -> str | None:
|
||||||
|
"""解密字符串;旧明文会原样返回。"""
|
||||||
|
if value is None or value == "" or not is_encrypted(value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
last_error: Exception | None = None
|
||||||
|
for candidate in _key_candidates():
|
||||||
|
try:
|
||||||
|
return _decrypt_with_candidate(value, candidate)
|
||||||
|
except Exception as exc:
|
||||||
|
last_error = exc
|
||||||
|
raise ValueError("敏感字段解密失败,请确认 APP_ENCRYPTION_KEY 是否正确") from last_error
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_value_with_key_name(value: str) -> tuple[str, str]:
|
||||||
|
"""解密并返回实际使用的密钥名称,用于历史密钥迁移。"""
|
||||||
|
if not is_encrypted(value):
|
||||||
|
return value, "plain"
|
||||||
|
last_error: Exception | None = None
|
||||||
|
for candidate in _key_candidates():
|
||||||
|
try:
|
||||||
|
return _decrypt_with_candidate(value, candidate), candidate.name
|
||||||
|
except Exception as exc:
|
||||||
|
last_error = exc
|
||||||
|
raise ValueError("敏感字段解密失败,请确认 APP_ENCRYPTION_KEY 是否正确") from last_error
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptedText(TypeDecorator):
|
||||||
|
"""SQLAlchemy 字段类型:数据库密文,Python 属性明文。"""
|
||||||
|
|
||||||
|
impl = Text
|
||||||
|
cache_ok = True
|
||||||
|
|
||||||
|
def process_bind_param(self, value, dialect):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return encrypt_value(str(value))
|
||||||
|
|
||||||
|
def process_result_value(self, value, dialect):
|
||||||
|
return decrypt_value(value)
|
||||||
|
|
||||||
|
|
||||||
|
_SENSITIVE_COLUMNS: tuple[tuple[str, str], ...] = (
|
||||||
|
("accounts", "password"),
|
||||||
|
("accounts", "email"),
|
||||||
|
("accounts", "email_password"),
|
||||||
|
("login_tasks", "cookie"),
|
||||||
|
("proxy_config", "api_url"),
|
||||||
|
("proxy_config", "http"),
|
||||||
|
("proxy_config", "https"),
|
||||||
|
("proxy_config", "whitelist_uid"),
|
||||||
|
("proxy_config", "whitelist_ukey"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _table_exists(engine: Engine, table_name: str) -> bool:
|
||||||
|
query = text("SELECT name FROM sqlite_master WHERE type='table' AND name=:name")
|
||||||
|
if not engine.url.get_backend_name().startswith("sqlite"):
|
||||||
|
return True
|
||||||
|
with engine.connect() as conn:
|
||||||
|
return conn.execute(query, {"name": table_name}).first() is not None
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_existing_sensitive_data(engine: Engine) -> int:
|
||||||
|
"""把历史明文敏感字段迁移成密文;已加密但非主密钥的值会重加密。"""
|
||||||
|
primary_name = _key_candidates()[0].name
|
||||||
|
changed = 0
|
||||||
|
|
||||||
|
with engine.begin() as conn:
|
||||||
|
for table_name, column_name in _SENSITIVE_COLUMNS:
|
||||||
|
if not _table_exists(engine, table_name):
|
||||||
|
continue
|
||||||
|
rows = conn.execute(
|
||||||
|
text(f"SELECT id, {column_name} FROM {table_name} WHERE {column_name} IS NOT NULL")
|
||||||
|
).mappings().all()
|
||||||
|
for row in rows:
|
||||||
|
raw_value = row[column_name]
|
||||||
|
if raw_value is None or raw_value == "":
|
||||||
|
continue
|
||||||
|
plain_value, key_name = decrypt_value_with_key_name(str(raw_value))
|
||||||
|
if is_encrypted(str(raw_value)) and key_name == primary_name:
|
||||||
|
continue
|
||||||
|
encrypted = encrypt_value(plain_value)
|
||||||
|
conn.execute(
|
||||||
|
text(f"UPDATE {table_name} SET {column_name} = :value WHERE id = :id"),
|
||||||
|
{"value": encrypted, "id": row["id"]},
|
||||||
|
)
|
||||||
|
changed += 1
|
||||||
|
return changed
|
||||||
@@ -35,6 +35,7 @@ def init_db():
|
|||||||
"""执行数据库迁移 + 写入初始数据。"""
|
"""执行数据库迁移 + 写入初始数据。"""
|
||||||
run_migrations()
|
run_migrations()
|
||||||
_seed()
|
_seed()
|
||||||
|
_encrypt_existing_sensitive_data()
|
||||||
|
|
||||||
|
|
||||||
def run_migrations():
|
def run_migrations():
|
||||||
@@ -70,3 +71,13 @@ def _seed():
|
|||||||
db.commit()
|
db.commit()
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _encrypt_existing_sensitive_data():
|
||||||
|
"""启动时把历史明文敏感数据迁移为密文。"""
|
||||||
|
from loguru import logger
|
||||||
|
from .crypto_storage import encrypt_existing_sensitive_data
|
||||||
|
|
||||||
|
changed = encrypt_existing_sensitive_data(engine)
|
||||||
|
if changed:
|
||||||
|
logger.info(f"已加密历史敏感字段: {changed} 个值")
|
||||||
|
|||||||
@@ -70,9 +70,9 @@ def upgrade() -> None:
|
|||||||
"accounts",
|
"accounts",
|
||||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.Column("username", sa.String(length=128), nullable=False),
|
sa.Column("username", sa.String(length=128), nullable=False),
|
||||||
sa.Column("password", sa.String(length=256), nullable=False),
|
sa.Column("password", sa.Text(), nullable=False),
|
||||||
sa.Column("email", sa.String(length=128), nullable=False),
|
sa.Column("email", sa.Text(), nullable=False),
|
||||||
sa.Column("email_password", sa.String(length=256), nullable=False),
|
sa.Column("email_password", sa.Text(), nullable=False),
|
||||||
sa.Column("email_imap_server", sa.String(length=128), nullable=True),
|
sa.Column("email_imap_server", sa.String(length=128), nullable=True),
|
||||||
sa.Column("email_imap_port", sa.Integer(), nullable=True),
|
sa.Column("email_imap_port", sa.Integer(), nullable=True),
|
||||||
sa.Column("email_imap_ssl", sa.Boolean(), nullable=True),
|
sa.Column("email_imap_ssl", sa.Boolean(), nullable=True),
|
||||||
@@ -102,12 +102,12 @@ def upgrade() -> None:
|
|||||||
"proxy_config",
|
"proxy_config",
|
||||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.Column("enabled", sa.Boolean(), nullable=True),
|
sa.Column("enabled", sa.Boolean(), nullable=True),
|
||||||
sa.Column("api_url", sa.String(length=512), nullable=True),
|
sa.Column("api_url", sa.Text(), nullable=True),
|
||||||
sa.Column("http", sa.String(length=256), nullable=True),
|
sa.Column("http", sa.Text(), nullable=True),
|
||||||
sa.Column("https", sa.String(length=256), nullable=True),
|
sa.Column("https", sa.Text(), nullable=True),
|
||||||
sa.Column("whitelist_enabled", sa.Boolean(), nullable=True),
|
sa.Column("whitelist_enabled", sa.Boolean(), nullable=True),
|
||||||
sa.Column("whitelist_uid", sa.String(length=64), nullable=True),
|
sa.Column("whitelist_uid", sa.Text(), nullable=True),
|
||||||
sa.Column("whitelist_ukey", sa.String(length=128), nullable=True),
|
sa.Column("whitelist_ukey", sa.Text(), nullable=True),
|
||||||
sa.PrimaryKeyConstraint("id"),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""放宽敏感字段列类型,便于存储加密密文
|
||||||
|
|
||||||
|
Revision ID: 20260624_0002
|
||||||
|
Revises: 20260623_0001
|
||||||
|
Create Date: 2026-06-24
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision: str = "20260624_0002"
|
||||||
|
down_revision: Union[str, None] = "20260623_0001"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
with op.batch_alter_table("accounts") as batch:
|
||||||
|
batch.alter_column("password", existing_type=sa.String(length=256), type_=sa.Text())
|
||||||
|
batch.alter_column("email", existing_type=sa.String(length=128), type_=sa.Text())
|
||||||
|
batch.alter_column("email_password", existing_type=sa.String(length=256), type_=sa.Text())
|
||||||
|
|
||||||
|
with op.batch_alter_table("proxy_config") as batch:
|
||||||
|
batch.alter_column("api_url", existing_type=sa.String(length=512), type_=sa.Text())
|
||||||
|
batch.alter_column("http", existing_type=sa.String(length=256), type_=sa.Text())
|
||||||
|
batch.alter_column("https", existing_type=sa.String(length=256), type_=sa.Text())
|
||||||
|
batch.alter_column("whitelist_uid", existing_type=sa.String(length=64), type_=sa.Text())
|
||||||
|
batch.alter_column("whitelist_ukey", existing_type=sa.String(length=128), type_=sa.Text())
|
||||||
|
|
||||||
|
with op.batch_alter_table("login_tasks") as batch:
|
||||||
|
batch.alter_column("cookie", existing_type=sa.Text(), type_=sa.Text())
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
with op.batch_alter_table("login_tasks") as batch:
|
||||||
|
batch.alter_column("cookie", existing_type=sa.Text(), type_=sa.Text())
|
||||||
|
|
||||||
|
with op.batch_alter_table("proxy_config") as batch:
|
||||||
|
batch.alter_column("whitelist_ukey", existing_type=sa.Text(), type_=sa.String(length=128))
|
||||||
|
batch.alter_column("whitelist_uid", existing_type=sa.Text(), type_=sa.String(length=64))
|
||||||
|
batch.alter_column("https", existing_type=sa.Text(), type_=sa.String(length=256))
|
||||||
|
batch.alter_column("http", existing_type=sa.Text(), type_=sa.String(length=256))
|
||||||
|
batch.alter_column("api_url", existing_type=sa.Text(), type_=sa.String(length=512))
|
||||||
|
|
||||||
|
with op.batch_alter_table("accounts") as batch:
|
||||||
|
batch.alter_column("email_password", existing_type=sa.Text(), type_=sa.String(length=256))
|
||||||
|
batch.alter_column("email", existing_type=sa.Text(), type_=sa.String(length=128))
|
||||||
|
batch.alter_column("password", existing_type=sa.Text(), type_=sa.String(length=256))
|
||||||
+10
-9
@@ -6,6 +6,7 @@ from sqlalchemy import (
|
|||||||
)
|
)
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from .database import Base
|
from .database import Base
|
||||||
|
from .crypto_storage import EncryptedText
|
||||||
|
|
||||||
|
|
||||||
def _utcnow():
|
def _utcnow():
|
||||||
@@ -37,9 +38,9 @@ class Account(Base):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
username = Column(String(128), nullable=False)
|
username = Column(String(128), nullable=False)
|
||||||
password = Column(String(256), nullable=False)
|
password = Column(EncryptedText(), nullable=False)
|
||||||
email = Column(String(128), nullable=False)
|
email = Column(EncryptedText(), nullable=False)
|
||||||
email_password = Column(String(256), nullable=False)
|
email_password = Column(EncryptedText(), nullable=False)
|
||||||
email_imap_server = Column(String(128), default="")
|
email_imap_server = Column(String(128), default="")
|
||||||
email_imap_port = Column(Integer, default=993)
|
email_imap_port = Column(Integer, default=993)
|
||||||
email_imap_ssl = Column(Boolean, default=True)
|
email_imap_ssl = Column(Boolean, default=True)
|
||||||
@@ -60,7 +61,7 @@ class LoginTask(Base):
|
|||||||
batch_id = Column(String(64), nullable=False, index=True) # 批次ID
|
batch_id = Column(String(64), nullable=False, index=True) # 批次ID
|
||||||
account_id = Column(Integer, ForeignKey("accounts.id"), nullable=False)
|
account_id = Column(Integer, ForeignKey("accounts.id"), nullable=False)
|
||||||
status = Column(String(32), default="pending") # pending / running / success / failed / error
|
status = Column(String(32), default="pending") # pending / running / success / failed / error
|
||||||
cookie = Column(Text, default="")
|
cookie = Column(EncryptedText(), default="")
|
||||||
message = Column(String(512), default="")
|
message = Column(String(512), default="")
|
||||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
created_at = Column(DateTime, default=_utcnow)
|
created_at = Column(DateTime, default=_utcnow)
|
||||||
@@ -75,13 +76,13 @@ class ProxyConfig(Base):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
enabled = Column(Boolean, default=False)
|
enabled = Column(Boolean, default=False)
|
||||||
api_url = Column(String(512), default="")
|
api_url = Column(EncryptedText(), default="")
|
||||||
http = Column(String(256), default="")
|
http = Column(EncryptedText(), default="")
|
||||||
https = Column(String(256), default="")
|
https = Column(EncryptedText(), default="")
|
||||||
# 白名单
|
# 白名单
|
||||||
whitelist_enabled = Column(Boolean, default=False)
|
whitelist_enabled = Column(Boolean, default=False)
|
||||||
whitelist_uid = Column(String(64), default="")
|
whitelist_uid = Column(EncryptedText(), default="")
|
||||||
whitelist_ukey = Column(String(128), default="")
|
whitelist_ukey = Column(EncryptedText(), default="")
|
||||||
|
|
||||||
|
|
||||||
class AuditLog(Base):
|
class AuditLog(Base):
|
||||||
|
|||||||
@@ -197,8 +197,8 @@ class LoginBatchRunner:
|
|||||||
if result.success:
|
if result.success:
|
||||||
task.status = "success"
|
task.status = "success"
|
||||||
task.cookie = result.cookie
|
task.cookie = result.cookie
|
||||||
task.message = "登录成功"
|
task.message = result.message or "登录成功"
|
||||||
self._push_log("success", f"[{current}] {acc_info['username']} 登录成功")
|
self._push_log("success", f"[{current}] {acc_info['username']} {task.message}")
|
||||||
else:
|
else:
|
||||||
task.status = "failed"
|
task.status = "failed"
|
||||||
task.message = result.message
|
task.message = result.message
|
||||||
|
|||||||
Reference in New Issue
Block a user