fix: 修复斗鱼Cookie导出422路由冲突 & 优化登录密码错误提示
- cookies.py: 将 GET /export 路由移至 GET /{task_id} 之前,避免
动态路径 task_id:int 误匹配 'export' 导致 422 校验错误
- LoginPage.tsx: 密码错误(401)时清空密码框并自动聚焦,方便快速重试
- database.py/migrations/env.py: 应用内嵌调用 alembic 时跳过 fileConfig,
避免覆盖 uvicorn 日志配置导致启动日志丢失
- client.ts: 401 拦截排除 /auth/login,避免登录失败时误跳转吞掉错误提示
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = "",
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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<InputRef>(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}
|
||||
</Text>
|
||||
<Form onFinish={onFinish} size="large">
|
||||
<Form form={form} onFinish={onFinish} size="large">
|
||||
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
|
||||
<Input prefix={<UserOutlined />} placeholder="用户名" />
|
||||
</Form.Item>
|
||||
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="密码" />
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="密码" ref={passwordRef} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={loading} block>
|
||||
|
||||
Reference in New Issue
Block a user