diff --git a/web/backend/database.py b/web/backend/database.py index 8fe8e6d..b9c8f5d 100644 --- a/web/backend/database.py +++ b/web/backend/database.py @@ -106,7 +106,12 @@ def run_migrations(): config = Config(str(PROJECT_ROOT / "alembic.ini")) config.set_main_option("script_location", str(PROJECT_ROOT / "web" / "backend" / "migrations")) config.set_main_option("sqlalchemy.url", DATABASE_URL) - command.upgrade(config, "head") + # 标记为应用内嵌调用,env.py 据此跳过 fileConfig,避免覆盖 uvicorn 日志配置。 + os.environ["ALEMBIC_EMBEDDED"] = "1" + try: + command.upgrade(config, "head") + finally: + os.environ.pop("ALEMBIC_EMBEDDED", None) def _seed(): diff --git a/web/backend/migrations/env.py b/web/backend/migrations/env.py index ea53746..6116008 100644 --- a/web/backend/migrations/env.py +++ b/web/backend/migrations/env.py @@ -2,6 +2,7 @@ from logging.config import fileConfig from pathlib import Path +import os import sys from alembic import context @@ -17,7 +18,10 @@ from web.backend import models # noqa: F401,E402 config = context.config config.set_main_option("sqlalchemy.url", DATABASE_URL) -if config.config_file_name is not None: +# 仅在独立运行 alembic CLI 时配置 logging;应用进程内(如 uvicorn 启动调用 +# run_migrations)跳过 fileConfig,避免它覆盖 uvicorn 已配置的日志处理器, +# 否则会导致 "Application startup complete" 等启动日志无法输出。 +if config.config_file_name is not None and not os.getenv("ALEMBIC_EMBEDDED"): fileConfig(config.config_file_name) target_metadata = Base.metadata diff --git a/web/backend/routers/cookies.py b/web/backend/routers/cookies.py index c9643af..aee2a9b 100644 --- a/web/backend/routers/cookies.py +++ b/web/backend/routers/cookies.py @@ -133,37 +133,6 @@ def cookies_summary( } -@router.get("/{task_id}") -def get_cookie( - task_id: int, - db: Session = Depends(get_db), - current: User = Depends(get_current_user), -): - """获取单条 Cookie 详情,供复制操作按需读取完整敏感字段。""" - task = _visible_cookie_tasks_query(db, current).filter(LoginTask.id == task_id).first() - if not task: - raise HTTPException(status_code=404, detail="记录不存在") - acc = db.query(Account).filter(Account.id == task.account_id).first() - item = { - "id": task.id, - "batch_id": task.batch_id, - "account_id": task.account_id, - "account_username": acc.username if acc else "", - "assigned_to": acc.assigned_to if acc else None, - "assigned_username": acc.assigned_user.username if acc and acc.assigned_user else None, - "created_at": _fmt_dt(task.finished_at), - "cookie": "", - "cookie_preview": "***", - "account_password": "", - } - if user_has_permission(current, "cookie:view"): - cookie = task.cookie or "" - item["cookie"] = cookie - item["cookie_preview"] = cookie[:50] + "..." if len(cookie) > 50 else cookie - item["account_password"] = acc.password if acc else "" - return item - - @router.get("/export") def export_cookies( format: str = "csv", @@ -215,6 +184,37 @@ def export_cookies( ) +@router.get("/{task_id}") +def get_cookie( + task_id: int, + db: Session = Depends(get_db), + current: User = Depends(get_current_user), +): + """获取单条 Cookie 详情,供复制操作按需读取完整敏感字段。""" + task = _visible_cookie_tasks_query(db, current).filter(LoginTask.id == task_id).first() + if not task: + raise HTTPException(status_code=404, detail="记录不存在") + acc = db.query(Account).filter(Account.id == task.account_id).first() + item = { + "id": task.id, + "batch_id": task.batch_id, + "account_id": task.account_id, + "account_username": acc.username if acc else "", + "assigned_to": acc.assigned_to if acc else None, + "assigned_username": acc.assigned_user.username if acc and acc.assigned_user else None, + "created_at": _fmt_dt(task.finished_at), + "cookie": "", + "cookie_preview": "***", + "account_password": "", + } + if user_has_permission(current, "cookie:view"): + cookie = task.cookie or "" + item["cookie"] = cookie + item["cookie_preview"] = cookie[:50] + "..." if len(cookie) > 50 else cookie + item["account_password"] = acc.password if acc else "" + return item + + @router.delete("/batch") def delete_cookies_batch( task_ids: str = "", diff --git a/web/frontend/src/api/client.ts b/web/frontend/src/api/client.ts index 076b0c2..119b5bc 100644 --- a/web/frontend/src/api/client.ts +++ b/web/frontend/src/api/client.ts @@ -10,7 +10,9 @@ const api = axios.create({ api.interceptors.response.use( (response) => response.data, (error) => { - if (error.response?.status === 401) { + // 仅在“已登录后 token 失效”时清除登录态并跳转登录页。 + // 登录接口本身返回 401(用户名/密码错误)时不应跳转,否则会吞掉错误提示。 + if (error.response?.status === 401 && error.config?.url !== '/auth/login') { localStorage.removeItem('user'); window.location.href = '/login'; } diff --git a/web/frontend/src/pages/LoginPage.tsx b/web/frontend/src/pages/LoginPage.tsx index 81f120d..fe906ad 100644 --- a/web/frontend/src/pages/LoginPage.tsx +++ b/web/frontend/src/pages/LoginPage.tsx @@ -1,5 +1,6 @@ -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { Card, Form, Input, Button, Typography } from 'antd'; +import type { InputRef } from 'antd'; import { message } from '../utils/antdMessage'; import { LockOutlined, UserOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; @@ -13,6 +14,8 @@ const { Title, Text } = Typography; export default function LoginPage({ onLogin }: { onLogin?: () => void }) { const [loading, setLoading] = useState(false); + const [form] = Form.useForm(); + const passwordRef = useRef(null); const navigate = useNavigate(); const { isDark } = useTheme(); const appInfo = useAppInfo(); @@ -33,7 +36,13 @@ export default function LoginPage({ onLogin }: { onLogin?: () => void }) { onLogin?.(); // 触发 App 重渲染 navigate('/', { replace: true }); } catch (e: unknown) { - message.error(getErrorMessage(e) || '登录失败'); + const msg = getErrorMessage(e) || '登录失败'; + message.error(msg); + // 密码错误(401)时清空密码并自动聚焦,方便快速重试 + if (msg.includes('密码') || msg.includes('用户名')) { + form.setFieldValue('password', ''); + passwordRef.current?.focus(); + } } finally { setLoading(false); } @@ -62,12 +71,12 @@ export default function LoginPage({ onLogin }: { onLogin?: () => void }) { }}> 当前版本 {versionText} -
+ } placeholder="用户名" /> - } placeholder="密码" /> + } placeholder="密码" ref={passwordRef} />