Repository navigation
Expand file tree
/
Copy pathapi.py
More file actions
311 lines (247 loc) · 9.29 KB
/
Copy pathapi.py
File metadata and controls
311 lines (247 loc) · 9.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
from fastapi import FastAPI, HTTPException, Depends, Cookie, Request, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel, EmailStr
from database import Database
from crs import get_token_manager
from datetime import datetime, timedelta
from typing import Optional, List
import sqlite3
import jwt
import config
import logging
logger = logging.getLogger(__name__)
app = FastAPI()
db = Database()
token_manager = get_token_manager()
# JWT配置
JWT_SECRET = config.JWT_SECRET_KEY # 从配置读取密钥
JWT_ALGORITHM = "HS256"
JWT_EXPIRE_DAYS = 7 # JWT有效期7天
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 挂载静态文件
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""全局异常处理器,捕获底层协议错误"""
# 忽略 h11 协议错误(客户端断开连接等情况)
error_name = type(exc).__name__
if "LocalProtocolError" in error_name or "ProtocolError" in error_name:
logger.debug(f"协议错误已忽略: {error_name}")
# 连接已关闭,无法发送响应
from fastapi.responses import Response
return Response(status_code=200) # 返回空响应,避免崩溃
# 其他异常按原样抛出
raise exc
class AccountRequest(BaseModel):
email: EmailStr
password: str
class AdminLoginRequest(BaseModel):
username: str
password: str
def verify_admin(admin_session: str = Cookie(None)):
"""验证管理员JWT令牌"""
if not admin_session:
raise HTTPException(status_code=401, detail="未授权,需要管理员登录")
try:
# 解析JWT
payload = jwt.decode(admin_session, JWT_SECRET, algorithms=[JWT_ALGORITHM])
# 验证用户名密码
username = payload.get('username')
password = payload.get('password')
if username != config.ADMIN_USERNAME or password != config.ADMIN_PASSWORD:
raise HTTPException(status_code=401, detail="令牌无效或账号已变更")
return True
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="令牌已过期,请重新登录")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="令牌无效")
@app.get("/")
async def root():
"""返回主页"""
return FileResponse("static/index.html")
@app.get("/admin")
async def admin_page():
"""返回管理页面"""
return FileResponse("static/admin.html")
@app.post("/admin/login")
async def admin_login(request: AdminLoginRequest):
"""管理员登录"""
if request.username == config.ADMIN_USERNAME and request.password == config.ADMIN_PASSWORD:
# 生成JWT token
payload = {
"username": request.username,
"password": request.password, # 密码也加入JWT,用于验证配置是否变更
"login_time": datetime.now().isoformat(),
"exp": datetime.utcnow() + timedelta(days=JWT_EXPIRE_DAYS)
}
session_token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
return {
"success": True,
"session_token": session_token,
"message": "登录成功"
}
else:
raise HTTPException(status_code=401, detail="用户名或密码错误")
@app.post("/admin/logout")
async def admin_logout():
"""管理员登出(客户端删除cookie)"""
return {"success": True, "message": "已登出"}
@app.post("/add_account")
async def add_account(account: AccountRequest):
"""添加或更新账号
- 如果邮箱不存在:新增账号
- 如果邮箱存在:更新密码并重置为待处理状态
"""
try:
account_id, is_update = db.update_or_add_account(account.email, account.password)
if is_update:
return {
"success": True,
"account_id": account_id,
"action": "updated",
"message": f"账号已存在,已更新密码并重置为待处理状态"
}
else:
return {
"success": True,
"account_id": account_id,
"action": "created",
"message": "账号已添加"
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health():
return {"status": "ok"}
@app.get("/stats")
async def get_stats():
"""获取处理统计"""
conn = sqlite3.connect(db.db_path)
cursor = conn.cursor()
# 统计各状态数量
cursor.execute("SELECT COUNT(*) FROM accounts WHERE status = 'pending'")
pending = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM accounts WHERE status = 'success'")
success = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM accounts WHERE status = 'failed'")
failed = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM accounts WHERE status = 'error'")
error = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM accounts")
total = cursor.fetchone()[0]
conn.close()
return {
"total": total,
"pending": pending,
"success": success,
"failed": failed,
"error": error,
"message": f"总计: {total} | 待处理: {pending} | 成功: {success} | 失败: {failed}"
}
@app.delete("/cleanup")
async def cleanup_processed(is_admin: bool = Depends(verify_admin)):
"""手动清理已处理的记录(需要管理员权限)"""
try:
# deleted_count = db.cleanup_processed_accounts()
# return {
# "success": True,
# "deleted": deleted_count,
# "message": f"已删除 {deleted_count} 条已处理记录"
# }
return {
"success": True,
"deleted": 0,
"message": f"已删除 {0} 条已处理记录"
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/token/info")
async def get_token_info():
"""获取外部系统token信息"""
return token_manager.get_token_info()
@app.post("/token/refresh")
async def refresh_token(is_admin: bool = Depends(verify_admin)):
"""手动刷新外部系统token(需要管理员权限)"""
success = token_manager.force_refresh()
if success:
return {
"success": True,
"message": "Token刷新成功",
"info": token_manager.get_token_info()
}
else:
raise HTTPException(status_code=500, detail="Token刷新失败")
@app.get("/failed_accounts")
async def get_failed_accounts():
"""获取处理失败的账号列表"""
try:
conn = sqlite3.connect(db.db_path)
cursor = conn.cursor()
# 查询所有失败的账号(status为'failed'或'error'),排除跳过验证码的账号
cursor.execute("""
SELECT id, email, status, processed_at, error_message
FROM accounts
WHERE status IN ('failed', 'error') AND (skip_verification = 0 OR skip_verification IS NULL)
ORDER BY processed_at DESC
""")
rows = cursor.fetchall()
conn.close()
accounts = []
for row in rows:
accounts.append({
"id": row[0],
"email": row[1],
"status": row[2],
"processed_at": row[3],
"error_message": row[4]
})
return {
"success": True,
"count": len(accounts),
"accounts": accounts
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/accounts")
async def query_accounts(
search: Optional[str] = Query(None, description="搜索关键词"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
is_admin: bool = Depends(verify_admin)
):
"""查询邮箱信息(支持分页和搜索,需要管理员权限)"""
try:
result = db.query_accounts(search=search, page=page, page_size=page_size)
return {
"success": True,
"data": result
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
class BatchResetRequest(BaseModel):
patterns: List[str]
@app.post("/accounts/batch_reset")
async def batch_reset_accounts(
request: BatchResetRequest,
is_admin: bool = Depends(verify_admin)
):
"""批量重置账号状态(支持邮箱或邮箱后缀匹配,需要管理员权限)"""
try:
if not request.patterns:
raise HTTPException(status_code=400, detail="请至少提供一个邮箱或域名后缀")
updated_count = db.batch_reset_accounts_by_patterns(request.patterns)
return {
"success": True,
"updated_count": updated_count,
"message": f"已重置 {updated_count} 个账号状态"
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))