Optimize database and deployment: fix SQL injection, async auth, persist data

- Move SQLite DB to data/ directory and track in git for portability
- Fix SQL injection in cleanup_old_emails (use parameterized query)
- Replace sync requests with async httpx in auth.py
- Enable WAL mode and foreign keys for SQLite
- Add UNIQUE constraint and foreign key to account_tags table
- Remove redundant indexes on primary key columns
- Mount data/ volume in docker-compose for persistence
- Remove unused requests dependency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-06 00:57:35 +08:00
parent 5b01caf8e3
commit 889f4f15d5
6 changed files with 37 additions and 30 deletions

8
.gitignore vendored
View File

@@ -1,6 +1,8 @@
config.txt config.txt
outlook_manager.db
.codebuddy .codebuddy
_pychache_ __pycache__
*.pyc *.pyc
config.txt .env
# WAL临时文件不需要跟踪
data/*.db-wal
data/*.db-shm

11
auth.py
View File

@@ -4,7 +4,7 @@ OAuth2认证模块
处理Microsoft OAuth2令牌获取和刷新 处理Microsoft OAuth2令牌获取和刷新
""" """
import requests import httpx
import logging import logging
from typing import Optional from typing import Optional
from fastapi import HTTPException from fastapi import HTTPException
@@ -36,8 +36,8 @@ async def get_access_token(refresh_token: str, check_only: bool = False, client_
} }
try: try:
# 使用requests而不是httpx因为exp.py验证有效 async with httpx.AsyncClient(timeout=30.0) as client:
response = requests.post(TOKEN_URL, data=data) response = await client.post(TOKEN_URL, data=data)
response.raise_for_status() response.raise_for_status()
token_data = response.json() token_data = response.json()
@@ -56,16 +56,15 @@ async def get_access_token(refresh_token: str, check_only: bool = False, client_
return access_token return access_token
except requests.exceptions.HTTPError as http_err: except httpx.HTTPStatusError as http_err:
logger.error(f"请求 access_token 时发生HTTP错误: {http_err}") logger.error(f"请求 access_token 时发生HTTP错误: {http_err}")
if http_err.response is not None:
logger.error(f"服务器响应: {http_err.response.status_code} - {http_err.response.text}") logger.error(f"服务器响应: {http_err.response.status_code} - {http_err.response.text}")
if check_only: if check_only:
return None return None
raise HTTPException(status_code=401, detail="Refresh token已过期或无效需要重新获取授权") raise HTTPException(status_code=401, detail="Refresh token已过期或无效需要重新获取授权")
except requests.exceptions.RequestException as e: except httpx.RequestError as e:
logger.error(f"请求 access_token 时发生网络错误: {e}") logger.error(f"请求 access_token 时发生网络错误: {e}")
if check_only: if check_only:
return None return None

BIN
data/outlook_manager.db Normal file

Binary file not shown.

View File

@@ -18,9 +18,11 @@ logger = logging.getLogger(__name__)
class DatabaseManager: class DatabaseManager:
"""数据库管理器""" """数据库管理器"""
def __init__(self, db_path: str = "outlook_manager.db"): def __init__(self, db_path: str = "data/outlook_manager.db"):
self.db_path = db_path self.db_path = db_path
self._local = threading.local() self._local = threading.local()
# 确保数据目录存在
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
self.init_database() self.init_database()
def get_connection(self) -> sqlite3.Connection: def get_connection(self) -> sqlite3.Connection:
@@ -28,6 +30,9 @@ class DatabaseManager:
if not hasattr(self._local, 'connection'): if not hasattr(self._local, 'connection'):
self._local.connection = sqlite3.connect(self.db_path) self._local.connection = sqlite3.connect(self.db_path)
self._local.connection.row_factory = sqlite3.Row self._local.connection.row_factory = sqlite3.Row
# 启用WAL模式提升并发读性能
self._local.connection.execute('PRAGMA journal_mode=WAL')
self._local.connection.execute('PRAGMA foreign_keys=ON')
return self._local.connection return self._local.connection
def init_database(self): def init_database(self):
@@ -51,10 +56,11 @@ class DatabaseManager:
cursor.execute(''' cursor.execute('''
CREATE TABLE IF NOT EXISTS account_tags ( CREATE TABLE IF NOT EXISTS account_tags (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL, email TEXT NOT NULL UNIQUE,
tags TEXT NOT NULL, -- JSON格式存储标签数组 tags TEXT NOT NULL, -- JSON格式存储标签数组
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (email) REFERENCES accounts(email) ON DELETE CASCADE
) )
''') ''')
@@ -114,9 +120,8 @@ class DatabaseManager:
cursor.execute(f"ALTER TABLE claude_payment_status ADD COLUMN {col} TEXT {default}") cursor.execute(f"ALTER TABLE claude_payment_status ADD COLUMN {col} TEXT {default}")
logger.info(f"已为 claude_payment_status 添加 {col}") logger.info(f"已为 claude_payment_status 添加 {col}")
# 创建索引 # 创建索引accounts.email 是 PRIMARY KEY无需额外索引
cursor.execute('CREATE INDEX IF NOT EXISTS idx_accounts_email ON accounts(email)') # account_tags.email 已有 UNIQUE 约束,无需额外索引
cursor.execute('CREATE INDEX IF NOT EXISTS idx_account_tags_email ON account_tags(email)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_email_cache_email ON email_cache(email)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_email_cache_email ON email_cache(email)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_email_cache_message_id ON email_cache(message_id)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_email_cache_message_id ON email_cache(message_id)')
@@ -310,8 +315,8 @@ class DatabaseManager:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(''' cursor.execute('''
DELETE FROM email_cache DELETE FROM email_cache
WHERE created_at < datetime('now', '-{} days') WHERE created_at < datetime('now', ? || ' days')
'''.format(days)) ''', (f'-{days}',))
deleted_count = cursor.rowcount deleted_count = cursor.rowcount
conn.commit() conn.commit()
return deleted_count return deleted_count

View File

@@ -9,6 +9,8 @@ services:
volumes: volumes:
# 挂载配置文件,便于修改邮箱配置 # 挂载配置文件,便于修改邮箱配置
- ./config.txt:/app/config.txt - ./config.txt:/app/config.txt
# 持久化SQLite数据库防止容器重建丢失数据
- ./data:/app/data
# 可选:挂载日志目录 # 可选:挂载日志目录
- ./logs:/app/logs - ./logs:/app/logs
environment: environment:

View File

@@ -2,7 +2,6 @@ fastapi==0.104.1
uvicorn[standard]==0.24.0 uvicorn[standard]==0.24.0
pydantic==2.5.0 pydantic==2.5.0
httpx==0.25.2 httpx==0.25.2
requests==2.31.0
python-multipart==0.0.6 python-multipart==0.0.6
jinja2==3.1.2 jinja2==3.1.2
aiofiles==23.2.1 aiofiles==23.2.1