Harden auth, CSRF, and email log UX
This commit is contained in:
23
admin-frontend/package-lock.json
generated
23
admin-frontend/package-lock.json
generated
@@ -1206,23 +1206,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/element-plus": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.12.0.tgz",
|
||||
"integrity": "sha512-M9YLSn2np9OnqrSKWsiXvGe3qnF8pd94+TScsHj1aTMCD+nSEvucXermf807qNt6hOP040le0e5Aft7E9ZfHmA==",
|
||||
"version": "2.11.3",
|
||||
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.11.3.tgz",
|
||||
"integrity": "sha512-769xsjLR4B9Vf9cl5PDXnwTEdmFJvMgAkYtthdJKPhjVjU3hdAwTJ+gXKiO+PUyo2KWFwOYKZd4Ywh6PHfkbJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^3.4.1",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@floating-ui/dom": "^1.0.1",
|
||||
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@vueuse/core": "^9.1.0",
|
||||
"async-validator": "^4.2.5",
|
||||
"dayjs": "^1.11.19",
|
||||
"dayjs": "^1.11.13",
|
||||
"escape-html": "^1.0.3",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash-unified": "^1.0.3",
|
||||
"lodash-unified": "^1.0.2",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-wheel-es": "^1.2.0"
|
||||
},
|
||||
@@ -1329,6 +1330,12 @@
|
||||
"@esbuild/win32-x64": "0.25.12"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
|
||||
@@ -12,12 +12,30 @@ function toastErrorOnce(key, message, minIntervalMs = 1500) {
|
||||
ElMessage.error(message)
|
||||
}
|
||||
|
||||
function getCookie(name) {
|
||||
const escaped = String(name || '').replace(/([.*+?^${}()|[\]\\])/g, '\\$1')
|
||||
const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`))
|
||||
return match ? decodeURIComponent(match[1]) : ''
|
||||
}
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: '/yuyx/api',
|
||||
timeout: 30_000,
|
||||
withCredentials: true,
|
||||
})
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const method = String(config?.method || 'GET').toUpperCase()
|
||||
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
||||
const token = getCookie('csrf_token')
|
||||
if (token) {
|
||||
config.headers = config.headers || {}
|
||||
config.headers['X-CSRF-Token'] = token
|
||||
}
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
|
||||
@@ -477,6 +477,12 @@ function emailTypeLabel(type) {
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
function emailLogUserLabel(row) {
|
||||
if (row?.username && row?.user_id) return `${row.username} (#${row.user_id})`
|
||||
if (row?.user_id) return `用户#${row.user_id}`
|
||||
return '系统'
|
||||
}
|
||||
|
||||
async function loadEmailStats() {
|
||||
emailStatsLoading.value = true
|
||||
try {
|
||||
@@ -709,6 +715,11 @@ onMounted(refreshAll)
|
||||
<el-table :data="emailLogs" v-loading="emailLogsLoading" style="width: 100%">
|
||||
<el-table-column prop="created_at" label="时间" width="180" />
|
||||
<el-table-column prop="email_to" label="收件人" min-width="180" />
|
||||
<el-table-column label="来源用户" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<span class="ellipsis" :title="emailLogUserLabel(row)">{{ emailLogUserLabel(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="120">
|
||||
<template #default="{ row }">{{ emailTypeLabel(row.email_type) }}</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -12,12 +12,30 @@ function toastErrorOnce(key, message, minIntervalMs = 1500) {
|
||||
ElMessage.error(message)
|
||||
}
|
||||
|
||||
function getCookie(name) {
|
||||
const escaped = String(name || '').replace(/([.*+?^${}()|[\]\\])/g, '\\$1')
|
||||
const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`))
|
||||
return match ? decodeURIComponent(match[1]) : ''
|
||||
}
|
||||
|
||||
export const publicApi = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 30_000,
|
||||
withCredentials: true,
|
||||
})
|
||||
|
||||
publicApi.interceptors.request.use((config) => {
|
||||
const method = String(config?.method || 'GET').toUpperCase()
|
||||
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
||||
const token = getCookie('csrf_token')
|
||||
if (token) {
|
||||
config.headers = config.headers || {}
|
||||
config.headers['X-CSRF-Token'] = token
|
||||
}
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
publicApi.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
|
||||
36
app.py
36
app.py
@@ -18,8 +18,8 @@ import signal
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from flask import Flask, jsonify, redirect, request, send_from_directory, url_for
|
||||
from flask_login import LoginManager
|
||||
from flask import Flask, jsonify, redirect, request, send_from_directory, session, url_for
|
||||
from flask_login import LoginManager, current_user
|
||||
from flask_socketio import SocketIO
|
||||
|
||||
import database
|
||||
@@ -27,7 +27,7 @@ import db_pool
|
||||
import email_service
|
||||
from app_config import get_config
|
||||
from app_logger import get_logger, init_logging
|
||||
from app_security import is_safe_path
|
||||
from app_security import generate_csrf_token, is_safe_path, validate_csrf_token
|
||||
from browser_pool_worker import init_browser_worker_pool, shutdown_browser_worker_pool
|
||||
from realtime.socketio_handlers import register_socketio_handlers
|
||||
from realtime.status_push import status_push_worker
|
||||
@@ -128,6 +128,36 @@ def unauthorized():
|
||||
return redirect(url_for("pages.login_page", next=request.url))
|
||||
|
||||
|
||||
@app.before_request
|
||||
def enforce_csrf_protection():
|
||||
if request.method in {"GET", "HEAD", "OPTIONS"}:
|
||||
return
|
||||
if request.path.startswith("/static/"):
|
||||
return
|
||||
if not (current_user.is_authenticated or "admin_id" in session):
|
||||
return
|
||||
token = request.headers.get("X-CSRF-Token") or request.form.get("csrf_token")
|
||||
if not token or not validate_csrf_token(token):
|
||||
return jsonify({"error": "CSRF token missing or invalid"}), 403
|
||||
|
||||
|
||||
@app.after_request
|
||||
def ensure_csrf_cookie(response):
|
||||
if request.path.startswith("/static/"):
|
||||
return response
|
||||
token = session.get("csrf_token")
|
||||
if not token:
|
||||
token = generate_csrf_token()
|
||||
response.set_cookie(
|
||||
"csrf_token",
|
||||
token,
|
||||
httponly=False,
|
||||
secure=bool(config.SESSION_COOKIE_SECURE),
|
||||
samesite=config.SESSION_COOKIE_SAMESITE,
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
# ==================== 静态文件(保持 endpoint 名称不变) ====================
|
||||
|
||||
|
||||
|
||||
@@ -143,6 +143,12 @@ class Config:
|
||||
# ==================== IP限流配置 ====================
|
||||
MAX_IP_ATTEMPTS_PER_HOUR = int(os.environ.get('MAX_IP_ATTEMPTS_PER_HOUR', '10'))
|
||||
IP_LOCK_DURATION = int(os.environ.get('IP_LOCK_DURATION', '3600')) # 秒
|
||||
IP_RATE_LIMIT_LOGIN_MAX = int(os.environ.get('IP_RATE_LIMIT_LOGIN_MAX', '20'))
|
||||
IP_RATE_LIMIT_LOGIN_WINDOW_SECONDS = int(os.environ.get('IP_RATE_LIMIT_LOGIN_WINDOW_SECONDS', '60'))
|
||||
IP_RATE_LIMIT_REGISTER_MAX = int(os.environ.get('IP_RATE_LIMIT_REGISTER_MAX', '10'))
|
||||
IP_RATE_LIMIT_REGISTER_WINDOW_SECONDS = int(os.environ.get('IP_RATE_LIMIT_REGISTER_WINDOW_SECONDS', '3600'))
|
||||
IP_RATE_LIMIT_EMAIL_MAX = int(os.environ.get('IP_RATE_LIMIT_EMAIL_MAX', '20'))
|
||||
IP_RATE_LIMIT_EMAIL_WINDOW_SECONDS = int(os.environ.get('IP_RATE_LIMIT_EMAIL_WINDOW_SECONDS', '3600'))
|
||||
|
||||
# ==================== 超时配置 ====================
|
||||
PAGE_LOAD_TIMEOUT = int(os.environ.get('PAGE_LOAD_TIMEOUT', '60000')) # 毫秒
|
||||
@@ -181,6 +187,8 @@ class Config:
|
||||
DEBUG = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
|
||||
ALLOWED_SCREENSHOT_EXTENSIONS = {'.png', '.jpg', '.jpeg'}
|
||||
MAX_SCREENSHOT_SIZE = int(os.environ.get('MAX_SCREENSHOT_SIZE', '10485760')) # 10MB
|
||||
LOGIN_CAPTCHA_AFTER_FAILURES = int(os.environ.get('LOGIN_CAPTCHA_AFTER_FAILURES', '3'))
|
||||
LOGIN_CAPTCHA_WINDOW_SECONDS = int(os.environ.get('LOGIN_CAPTCHA_WINDOW_SECONDS', '900'))
|
||||
|
||||
@classmethod
|
||||
def validate(cls):
|
||||
|
||||
@@ -10,9 +10,12 @@ import re
|
||||
import time
|
||||
import hashlib
|
||||
import secrets
|
||||
import ipaddress
|
||||
import socket
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from functools import wraps
|
||||
from urllib.parse import urlparse
|
||||
from flask import request, jsonify, session
|
||||
from collections import defaultdict
|
||||
import threading
|
||||
@@ -199,7 +202,7 @@ def require_ip_not_locked(f):
|
||||
"""装饰器:检查IP是否被锁定"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
ip_address = request.remote_addr
|
||||
ip_address = get_rate_limit_ip()
|
||||
|
||||
# P0 / O-01:统一使用 services.state 的线程安全限流状态
|
||||
try:
|
||||
@@ -450,6 +453,65 @@ def get_client_ip(trust_proxy=False):
|
||||
return request.remote_addr
|
||||
|
||||
|
||||
def get_rate_limit_ip() -> str:
|
||||
"""在可信代理场景下取真实IP,用于限流/风控。"""
|
||||
remote_addr = request.remote_addr or ""
|
||||
try:
|
||||
remote_ip = ipaddress.ip_address(remote_addr)
|
||||
except ValueError:
|
||||
remote_ip = None
|
||||
|
||||
if remote_ip and (remote_ip.is_private or remote_ip.is_loopback or remote_ip.is_link_local):
|
||||
forwarded = request.headers.get("X-Forwarded-For", "")
|
||||
if forwarded:
|
||||
candidate = forwarded.split(",")[0].strip()
|
||||
try:
|
||||
ipaddress.ip_address(candidate)
|
||||
return candidate
|
||||
except ValueError:
|
||||
pass
|
||||
real_ip = request.headers.get("X-Real-IP", "").strip()
|
||||
if real_ip:
|
||||
try:
|
||||
ipaddress.ip_address(real_ip)
|
||||
return real_ip
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return remote_addr
|
||||
|
||||
|
||||
def is_safe_outbound_url(url: str) -> bool:
|
||||
"""限制向内网/保留地址发起请求,降低SSRF风险。"""
|
||||
try:
|
||||
parsed = urlparse(str(url or "").strip())
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
return False
|
||||
|
||||
host = parsed.hostname
|
||||
if not host:
|
||||
return False
|
||||
|
||||
ips = []
|
||||
try:
|
||||
ips = [ipaddress.ip_address(host)]
|
||||
except ValueError:
|
||||
try:
|
||||
infos = socket.getaddrinfo(host, None)
|
||||
ips = [ipaddress.ip_address(info[4][0]) for info in infos]
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
for ip in ips:
|
||||
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_reserved:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试文件路径安全
|
||||
print("文件路径安全测试:")
|
||||
|
||||
@@ -11,7 +11,13 @@ import database
|
||||
import email_service
|
||||
import requests
|
||||
from app_logger import get_logger
|
||||
from app_security import require_ip_not_locked, validate_password
|
||||
from app_security import (
|
||||
get_rate_limit_ip,
|
||||
is_safe_outbound_url,
|
||||
require_ip_not_locked,
|
||||
validate_email,
|
||||
validate_password,
|
||||
)
|
||||
from flask import current_app, jsonify, redirect, request, session, url_for
|
||||
from routes.admin_api import admin_api_bp
|
||||
from routes.decorators import admin_required
|
||||
@@ -26,6 +32,10 @@ from services.state import (
|
||||
safe_iter_task_status_items,
|
||||
safe_remove_user_accounts,
|
||||
safe_verify_and_consume_captcha,
|
||||
check_ip_request_rate,
|
||||
check_login_captcha_required,
|
||||
clear_login_failures,
|
||||
record_login_failure,
|
||||
)
|
||||
from services.tasks import get_task_scheduler, submit_account_task
|
||||
from services.time_utils import BEIJING_TZ, get_beijing_now
|
||||
@@ -62,7 +72,7 @@ def debug_config():
|
||||
def admin_login():
|
||||
"""管理员登录(支持JSON和form-data两种格式)"""
|
||||
if request.is_json:
|
||||
data = request.json
|
||||
data = request.json or {}
|
||||
else:
|
||||
data = request.form
|
||||
|
||||
@@ -72,15 +82,29 @@ def admin_login():
|
||||
captcha_code = data.get("captcha", "").strip()
|
||||
need_captcha = data.get("need_captcha", False)
|
||||
|
||||
if need_captcha:
|
||||
client_ip = get_rate_limit_ip()
|
||||
allowed, error_msg = check_ip_request_rate(client_ip, "login")
|
||||
if not allowed:
|
||||
if request.is_json:
|
||||
return jsonify({"error": error_msg, "need_captcha": True}), 429
|
||||
return redirect(url_for("pages.admin_login_page"))
|
||||
|
||||
captcha_required = check_login_captcha_required(client_ip) or bool(need_captcha)
|
||||
if captcha_required:
|
||||
if not captcha_session or not captcha_code:
|
||||
if request.is_json:
|
||||
return jsonify({"error": "请填写验证码", "need_captcha": True}), 400
|
||||
return redirect(url_for("pages.admin_login_page"))
|
||||
success, message = safe_verify_and_consume_captcha(captcha_session, captcha_code)
|
||||
if not success:
|
||||
record_login_failure(client_ip)
|
||||
if request.is_json:
|
||||
return jsonify({"error": message}), 400
|
||||
return jsonify({"error": message, "need_captcha": True}), 400
|
||||
return redirect(url_for("pages.admin_login_page"))
|
||||
|
||||
admin = database.verify_admin(username, password)
|
||||
if admin:
|
||||
clear_login_failures(client_ip)
|
||||
session.pop("admin_id", None)
|
||||
session.pop("admin_username", None)
|
||||
session["admin_id"] = admin["id"]
|
||||
@@ -94,9 +118,10 @@ def admin_login():
|
||||
return jsonify({"success": True, "redirect": "/yuyx/admin"})
|
||||
return redirect(url_for("pages.admin_page"))
|
||||
|
||||
record_login_failure(client_ip)
|
||||
logger.warning(f"[admin_login] 管理员 {username} 登录失败 - 用户名或密码错误")
|
||||
if request.is_json:
|
||||
return jsonify({"error": "管理员用户名或密码错误", "need_captcha": True}), 401
|
||||
return jsonify({"error": "管理员用户名或密码错误", "need_captcha": check_login_captcha_required(client_ip)}), 401
|
||||
return redirect(url_for("pages.admin_login_page"))
|
||||
|
||||
|
||||
@@ -565,6 +590,9 @@ def test_proxy_api():
|
||||
if not api_url:
|
||||
return jsonify({"error": "请提供API地址"}), 400
|
||||
|
||||
if not is_safe_outbound_url(api_url):
|
||||
return jsonify({"error": "API地址不可用或不安全"}), 400
|
||||
|
||||
try:
|
||||
response = requests.get(api_url, timeout=10)
|
||||
if response.status_code == 200:
|
||||
@@ -1071,10 +1099,9 @@ def test_smtp_config_api(config_id):
|
||||
if not test_email:
|
||||
return jsonify({"error": "请提供测试邮箱"}), 400
|
||||
|
||||
import re
|
||||
|
||||
if not re.match(r"^[^\s@]+@[^\s@]+\.[^\s@]+$", test_email):
|
||||
return jsonify({"error": "邮箱格式不正确"}), 400
|
||||
is_valid, error_msg = validate_email(test_email)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
result = email_service.test_smtp_config(config_id, test_email)
|
||||
return jsonify(result)
|
||||
|
||||
@@ -9,15 +9,18 @@ import time
|
||||
import database
|
||||
import email_service
|
||||
from app_logger import get_logger
|
||||
from app_security import get_client_ip, require_ip_not_locked, validate_password
|
||||
from app_security import get_rate_limit_ip, require_ip_not_locked, validate_email, validate_password, validate_username
|
||||
from flask import Blueprint, jsonify, redirect, render_template, request, url_for
|
||||
from flask_login import login_required, login_user, logout_user
|
||||
from routes.pages import render_app_spa_or_legacy
|
||||
from services.accounts_service import load_user_accounts
|
||||
from services.models import User
|
||||
from services.state import (
|
||||
check_ip_rate_limit,
|
||||
check_ip_request_rate,
|
||||
check_login_captcha_required,
|
||||
clear_login_failures,
|
||||
record_failed_captcha,
|
||||
record_login_failure,
|
||||
safe_cleanup_expired_captcha,
|
||||
safe_delete_captcha,
|
||||
safe_set_captcha,
|
||||
@@ -33,23 +36,26 @@ api_auth_bp = Blueprint("api_auth", __name__)
|
||||
@require_ip_not_locked
|
||||
def register():
|
||||
"""用户注册"""
|
||||
data = request.json
|
||||
data = request.json or {}
|
||||
username = data.get("username", "").strip()
|
||||
password = data.get("password", "").strip()
|
||||
email = data.get("email", "").strip()
|
||||
email = data.get("email", "").strip().lower()
|
||||
captcha_session = data.get("captcha_session", "")
|
||||
captcha_code = data.get("captcha", "").strip()
|
||||
|
||||
if not username or not password:
|
||||
return jsonify({"error": "用户名和密码不能为空"}), 400
|
||||
|
||||
is_valid, error_msg = validate_username(username)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
is_valid, error_msg = validate_password(password)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
client_ip = get_client_ip()
|
||||
|
||||
allowed, error_msg = check_ip_rate_limit(client_ip)
|
||||
client_ip = get_rate_limit_ip()
|
||||
allowed, error_msg = check_ip_request_rate(client_ip, "register")
|
||||
if not allowed:
|
||||
return jsonify({"error": error_msg}), 429
|
||||
|
||||
@@ -66,8 +72,10 @@ def register():
|
||||
if email_verify_enabled and not email:
|
||||
return jsonify({"error": "启用邮箱验证后,邮箱为必填项"}), 400
|
||||
|
||||
if email and "@" not in email:
|
||||
return jsonify({"error": "邮箱格式不正确"}), 400
|
||||
if email:
|
||||
is_valid, error_msg = validate_email(email)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
system_config = database.get_system_config()
|
||||
auto_approve_enabled = system_config.get("auto_approve_enabled", 0) == 1
|
||||
@@ -159,17 +167,20 @@ def verify_email(token):
|
||||
@require_ip_not_locked
|
||||
def resend_verify_email():
|
||||
"""重发验证邮件"""
|
||||
data = request.json
|
||||
email = data.get("email", "").strip()
|
||||
data = request.json or {}
|
||||
email = data.get("email", "").strip().lower()
|
||||
captcha_session = data.get("captcha_session", "")
|
||||
captcha_code = data.get("captcha", "").strip()
|
||||
|
||||
if not email:
|
||||
return jsonify({"error": "请输入邮箱"}), 400
|
||||
|
||||
client_ip = get_client_ip()
|
||||
is_valid, error_msg = validate_email(email)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
allowed, error_msg = check_ip_rate_limit(client_ip)
|
||||
client_ip = get_rate_limit_ip()
|
||||
allowed, error_msg = check_ip_request_rate(client_ip, "email")
|
||||
if not allowed:
|
||||
return jsonify({"error": error_msg}), 429
|
||||
|
||||
@@ -213,17 +224,20 @@ def get_email_verify_status():
|
||||
@require_ip_not_locked
|
||||
def forgot_password():
|
||||
"""发送密码重置邮件"""
|
||||
data = request.json
|
||||
email = data.get("email", "").strip()
|
||||
data = request.json or {}
|
||||
email = data.get("email", "").strip().lower()
|
||||
captcha_session = data.get("captcha_session", "")
|
||||
captcha_code = data.get("captcha", "").strip()
|
||||
|
||||
if not email:
|
||||
return jsonify({"error": "请输入邮箱"}), 400
|
||||
|
||||
client_ip = get_client_ip()
|
||||
is_valid, error_msg = validate_email(email)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
allowed, error_msg = check_ip_rate_limit(client_ip)
|
||||
client_ip = get_rate_limit_ip()
|
||||
allowed, error_msg = check_ip_request_rate(client_ip, "email")
|
||||
if not allowed:
|
||||
return jsonify({"error": error_msg}), 429
|
||||
|
||||
@@ -267,7 +281,7 @@ def reset_password_page(token):
|
||||
@api_auth_bp.route("/api/reset-password-confirm", methods=["POST"])
|
||||
def reset_password_confirm():
|
||||
"""确认密码重置"""
|
||||
data = request.json
|
||||
data = request.json or {}
|
||||
token = data.get("token", "").strip()
|
||||
new_password = data.get("new_password", "").strip()
|
||||
|
||||
@@ -292,9 +306,9 @@ def reset_password_confirm():
|
||||
@api_auth_bp.route("/api/reset_password_request", methods=["POST"])
|
||||
def request_password_reset():
|
||||
"""用户申请重置密码(需要审核)"""
|
||||
data = request.json
|
||||
data = request.json or {}
|
||||
username = data.get("username", "").strip()
|
||||
email = data.get("email", "").strip()
|
||||
email = data.get("email", "").strip().lower()
|
||||
new_password = data.get("new_password", "").strip()
|
||||
|
||||
if not username or not new_password:
|
||||
@@ -304,6 +318,11 @@ def request_password_reset():
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
if email:
|
||||
is_valid, error_msg = validate_email(email)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
user = database.get_user_by_username(username)
|
||||
|
||||
if user:
|
||||
@@ -389,25 +408,36 @@ def generate_captcha():
|
||||
@require_ip_not_locked
|
||||
def login():
|
||||
"""用户登录"""
|
||||
data = request.json
|
||||
data = request.json or {}
|
||||
username = data.get("username", "").strip()
|
||||
password = data.get("password", "").strip()
|
||||
captcha_session = data.get("captcha_session", "")
|
||||
captcha_code = data.get("captcha", "").strip()
|
||||
need_captcha = data.get("need_captcha", False)
|
||||
|
||||
if need_captcha:
|
||||
client_ip = get_rate_limit_ip()
|
||||
allowed, error_msg = check_ip_request_rate(client_ip, "login")
|
||||
if not allowed:
|
||||
return jsonify({"error": error_msg, "need_captcha": True}), 429
|
||||
|
||||
captcha_required = check_login_captcha_required(client_ip) or bool(need_captcha)
|
||||
if captcha_required:
|
||||
if not captcha_session or not captcha_code:
|
||||
return jsonify({"error": "请填写验证码", "need_captcha": True}), 400
|
||||
success, message = safe_verify_and_consume_captcha(captcha_session, captcha_code)
|
||||
if not success:
|
||||
return jsonify({"error": message}), 400
|
||||
record_login_failure(client_ip)
|
||||
return jsonify({"error": message, "need_captcha": True}), 400
|
||||
|
||||
user = database.verify_user(username, password)
|
||||
if not user:
|
||||
return jsonify({"error": "用户名或密码错误", "need_captcha": True}), 401
|
||||
record_login_failure(client_ip)
|
||||
return jsonify({"error": "用户名或密码错误", "need_captcha": check_login_captcha_required(client_ip)}), 401
|
||||
|
||||
if user["status"] != "approved":
|
||||
return jsonify({"error": "账号未审核,请等待管理员审核", "need_captcha": False}), 401
|
||||
|
||||
clear_login_failures(client_ip)
|
||||
user_obj = User(user["id"])
|
||||
login_user(user_obj)
|
||||
load_user_accounts(user["id"])
|
||||
|
||||
@@ -5,11 +5,11 @@ from __future__ import annotations
|
||||
import database
|
||||
import email_service
|
||||
from app_logger import get_logger
|
||||
from app_security import require_ip_not_locked, validate_email, validate_password
|
||||
from app_security import get_rate_limit_ip, require_ip_not_locked, validate_email, validate_password
|
||||
from flask import Blueprint, jsonify, request
|
||||
from flask_login import current_user, login_required
|
||||
from routes.pages import render_app_spa_or_legacy
|
||||
from services.state import safe_iter_task_status_items
|
||||
from services.state import check_ip_request_rate, safe_iter_task_status_items
|
||||
|
||||
logger = get_logger("app")
|
||||
|
||||
@@ -152,12 +152,21 @@ def get_user_email():
|
||||
@require_ip_not_locked
|
||||
def bind_user_email():
|
||||
"""发送邮箱绑定验证邮件"""
|
||||
data = request.get_json()
|
||||
data = request.get_json() or {}
|
||||
email = data.get("email", "").strip().lower()
|
||||
|
||||
if not email or not validate_email(email):
|
||||
if not email:
|
||||
return jsonify({"error": "请输入有效的邮箱地址"}), 400
|
||||
|
||||
is_valid, error_msg = validate_email(email)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
client_ip = get_rate_limit_ip()
|
||||
allowed, error_msg = check_ip_request_rate(client_ip, "email")
|
||||
if not allowed:
|
||||
return jsonify({"error": error_msg}), 429
|
||||
|
||||
settings = email_service.get_email_settings()
|
||||
if not settings.get("enabled", False):
|
||||
return jsonify({"error": "邮件功能未启用,请联系管理员"}), 400
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Optional
|
||||
import requests
|
||||
|
||||
from app_logger import get_logger
|
||||
from app_security import is_safe_outbound_url
|
||||
|
||||
logger = get_logger("proxy")
|
||||
|
||||
@@ -34,6 +35,10 @@ def get_proxy_from_api(api_url: str, max_retries: int = 3) -> Optional[str]:
|
||||
ip_port_pattern = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}$")
|
||||
max_retries = max(1, int(max_retries or 1))
|
||||
|
||||
if not is_safe_outbound_url(api_url):
|
||||
logger.warning("代理API地址不可用或不安全,已拒绝请求")
|
||||
return None
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
response = requests.get(api_url, timeout=10)
|
||||
@@ -74,4 +79,3 @@ def get_proxy_from_api(api_url: str, max_retries: int = 3) -> Optional[str]:
|
||||
|
||||
logger.warning(f"获取代理失败,已重试 {max_retries} 次,将不使用代理继续")
|
||||
return None
|
||||
|
||||
|
||||
@@ -362,6 +362,111 @@ def safe_get_ip_lock_until(ip_address: str) -> float:
|
||||
return 0.0
|
||||
|
||||
|
||||
# ==================== IP request rate limit(接口频率限制) ====================
|
||||
|
||||
_ip_request_rate: Dict[str, Dict[str, Any]] = {}
|
||||
_ip_request_rate_lock = threading.RLock()
|
||||
|
||||
|
||||
def _get_action_rate_limit(action: str) -> Tuple[int, int]:
|
||||
action = str(action or "").lower()
|
||||
if action == "register":
|
||||
return int(config.IP_RATE_LIMIT_REGISTER_MAX), int(config.IP_RATE_LIMIT_REGISTER_WINDOW_SECONDS)
|
||||
if action == "email":
|
||||
return int(config.IP_RATE_LIMIT_EMAIL_MAX), int(config.IP_RATE_LIMIT_EMAIL_WINDOW_SECONDS)
|
||||
return int(config.IP_RATE_LIMIT_LOGIN_MAX), int(config.IP_RATE_LIMIT_LOGIN_WINDOW_SECONDS)
|
||||
|
||||
|
||||
def check_ip_request_rate(
|
||||
ip_address: str,
|
||||
action: str,
|
||||
*,
|
||||
max_requests: Optional[int] = None,
|
||||
window_seconds: Optional[int] = None,
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
now_ts = time.time()
|
||||
default_max, default_window = _get_action_rate_limit(action)
|
||||
max_requests = int(max_requests or default_max)
|
||||
window_seconds = int(window_seconds or default_window)
|
||||
|
||||
key = f"{action}:{ip_address}"
|
||||
with _ip_request_rate_lock:
|
||||
data = _ip_request_rate.get(key)
|
||||
if not data or (now_ts - float(data.get("window_start", 0) or 0)) >= window_seconds:
|
||||
data = {"window_start": now_ts, "count": 0}
|
||||
_ip_request_rate[key] = data
|
||||
|
||||
if int(data.get("count", 0) or 0) >= max_requests:
|
||||
remaining = max(1, int(window_seconds - (now_ts - float(data.get("window_start", 0) or 0))))
|
||||
if remaining >= 60:
|
||||
wait_hint = f"{remaining // 60 + 1}分钟"
|
||||
else:
|
||||
wait_hint = f"{remaining}秒"
|
||||
return False, f"请求过于频繁,请{wait_hint}后再试"
|
||||
|
||||
data["count"] = int(data.get("count", 0) or 0) + 1
|
||||
return True, None
|
||||
|
||||
|
||||
def cleanup_expired_ip_request_rates(now_ts: Optional[float] = None) -> int:
|
||||
now_ts = float(now_ts if now_ts is not None else time.time())
|
||||
removed = 0
|
||||
with _ip_request_rate_lock:
|
||||
for key in list(_ip_request_rate.keys()):
|
||||
data = _ip_request_rate.get(key) or {}
|
||||
action = key.split(":", 1)[0]
|
||||
_, window_seconds = _get_action_rate_limit(action)
|
||||
window_start = float(data.get("window_start", 0) or 0)
|
||||
if now_ts - window_start >= window_seconds:
|
||||
_ip_request_rate.pop(key, None)
|
||||
removed += 1
|
||||
return removed
|
||||
|
||||
|
||||
# ==================== 登录失败追踪(触发验证码) ====================
|
||||
|
||||
_login_failures: Dict[str, Dict[str, Any]] = {}
|
||||
_login_failures_lock = threading.RLock()
|
||||
|
||||
|
||||
def _get_login_captcha_config() -> Tuple[int, int]:
|
||||
return int(config.LOGIN_CAPTCHA_AFTER_FAILURES), int(config.LOGIN_CAPTCHA_WINDOW_SECONDS)
|
||||
|
||||
|
||||
def record_login_failure(ip_address: str) -> None:
|
||||
now_ts = time.time()
|
||||
max_failures, window_seconds = _get_login_captcha_config()
|
||||
ip_key = str(ip_address or "")
|
||||
with _login_failures_lock:
|
||||
data = _login_failures.get(ip_key)
|
||||
if not data or (now_ts - float(data.get("first_failed", 0) or 0)) > window_seconds:
|
||||
data = {"first_failed": now_ts, "count": 0}
|
||||
_login_failures[ip_key] = data
|
||||
data["count"] = int(data.get("count", 0) or 0) + 1
|
||||
if int(data["count"]) > max_failures * 5:
|
||||
data["count"] = max_failures * 5
|
||||
|
||||
|
||||
def clear_login_failures(ip_address: str) -> None:
|
||||
ip_key = str(ip_address or "")
|
||||
with _login_failures_lock:
|
||||
_login_failures.pop(ip_key, None)
|
||||
|
||||
|
||||
def check_login_captcha_required(ip_address: str) -> bool:
|
||||
now_ts = time.time()
|
||||
max_failures, window_seconds = _get_login_captcha_config()
|
||||
ip_key = str(ip_address or "")
|
||||
with _login_failures_lock:
|
||||
data = _login_failures.get(ip_key)
|
||||
if not data:
|
||||
return False
|
||||
if (now_ts - float(data.get("first_failed", 0) or 0)) > window_seconds:
|
||||
_login_failures.pop(ip_key, None)
|
||||
return False
|
||||
return int(data.get("count", 0) or 0) >= max_failures
|
||||
|
||||
|
||||
# ==================== Batch screenshots(批次任务截图收集) ====================
|
||||
|
||||
_batch_task_screenshots: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
{
|
||||
"_email-DkWacopQ.js": {
|
||||
"file": "assets/email-DkWacopQ.js",
|
||||
"_email-BfqhxXOq.js": {
|
||||
"file": "assets/email-BfqhxXOq.js",
|
||||
"name": "email",
|
||||
"imports": [
|
||||
"index.html"
|
||||
]
|
||||
},
|
||||
"_tasks-DQdWvww2.js": {
|
||||
"file": "assets/tasks-DQdWvww2.js",
|
||||
"_tasks-BtWKY-g7.js": {
|
||||
"file": "assets/tasks-BtWKY-g7.js",
|
||||
"name": "tasks",
|
||||
"imports": [
|
||||
"index.html"
|
||||
]
|
||||
},
|
||||
"_update-BqIWfEYV.js": {
|
||||
"file": "assets/update-BqIWfEYV.js",
|
||||
"_update-BrAMPxiF.js": {
|
||||
"file": "assets/update-BrAMPxiF.js",
|
||||
"name": "update",
|
||||
"imports": [
|
||||
"index.html"
|
||||
]
|
||||
},
|
||||
"_users-CPJP5r-B.js": {
|
||||
"file": "assets/users-CPJP5r-B.js",
|
||||
"_users-CToznuvL.js": {
|
||||
"file": "assets/users-CToznuvL.js",
|
||||
"name": "users",
|
||||
"imports": [
|
||||
"index.html"
|
||||
]
|
||||
},
|
||||
"index.html": {
|
||||
"file": "assets/index-B57Le1Kd.js",
|
||||
"file": "assets/index-Da0EvMWc.js",
|
||||
"name": "index",
|
||||
"src": "index.html",
|
||||
"isEntry": true,
|
||||
@@ -43,11 +43,11 @@
|
||||
"src/pages/SettingsPage.vue"
|
||||
],
|
||||
"css": [
|
||||
"assets/index-CBVc8utT.css"
|
||||
"assets/index-EWm4DZW8.css"
|
||||
]
|
||||
},
|
||||
"src/pages/AnnouncementsPage.vue": {
|
||||
"file": "assets/AnnouncementsPage-hvTOy02a.js",
|
||||
"file": "assets/AnnouncementsPage-CbLi3NFK.js",
|
||||
"name": "AnnouncementsPage",
|
||||
"src": "src/pages/AnnouncementsPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
@@ -59,20 +59,20 @@
|
||||
]
|
||||
},
|
||||
"src/pages/EmailPage.vue": {
|
||||
"file": "assets/EmailPage-Be5xmQro.js",
|
||||
"file": "assets/EmailPage-CaUZghxJ.js",
|
||||
"name": "EmailPage",
|
||||
"src": "src/pages/EmailPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"_email-DkWacopQ.js",
|
||||
"_email-BfqhxXOq.js",
|
||||
"index.html"
|
||||
],
|
||||
"css": [
|
||||
"assets/EmailPage-CnYKqgXc.css"
|
||||
"assets/EmailPage-DD73oBux.css"
|
||||
]
|
||||
},
|
||||
"src/pages/FeedbacksPage.vue": {
|
||||
"file": "assets/FeedbacksPage-BsAKLoZh.js",
|
||||
"file": "assets/FeedbacksPage-DCz_21CH.js",
|
||||
"name": "FeedbacksPage",
|
||||
"src": "src/pages/FeedbacksPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
@@ -84,13 +84,13 @@
|
||||
]
|
||||
},
|
||||
"src/pages/LogsPage.vue": {
|
||||
"file": "assets/LogsPage-BO90cLUJ.js",
|
||||
"file": "assets/LogsPage-k6AvTEc_.js",
|
||||
"name": "LogsPage",
|
||||
"src": "src/pages/LogsPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"_users-CPJP5r-B.js",
|
||||
"_tasks-DQdWvww2.js",
|
||||
"_users-CToznuvL.js",
|
||||
"_tasks-BtWKY-g7.js",
|
||||
"index.html"
|
||||
],
|
||||
"css": [
|
||||
@@ -98,22 +98,22 @@
|
||||
]
|
||||
},
|
||||
"src/pages/ReportPage.vue": {
|
||||
"file": "assets/ReportPage-DAANCrfo.js",
|
||||
"file": "assets/ReportPage-BkB6FuHA.js",
|
||||
"name": "ReportPage",
|
||||
"src": "src/pages/ReportPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"index.html",
|
||||
"_email-DkWacopQ.js",
|
||||
"_tasks-DQdWvww2.js",
|
||||
"_update-BqIWfEYV.js"
|
||||
"_email-BfqhxXOq.js",
|
||||
"_tasks-BtWKY-g7.js",
|
||||
"_update-BrAMPxiF.js"
|
||||
],
|
||||
"css": [
|
||||
"assets/ReportPage-TpqQWWvU.css"
|
||||
]
|
||||
},
|
||||
"src/pages/SettingsPage.vue": {
|
||||
"file": "assets/SettingsPage-t6m2MHUb.js",
|
||||
"file": "assets/SettingsPage-CeJoz6yA.js",
|
||||
"name": "SettingsPage",
|
||||
"src": "src/pages/SettingsPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
@@ -125,12 +125,12 @@
|
||||
]
|
||||
},
|
||||
"src/pages/SystemPage.vue": {
|
||||
"file": "assets/SystemPage-D69-93Jh.js",
|
||||
"file": "assets/SystemPage-Dmtz_emI.js",
|
||||
"name": "SystemPage",
|
||||
"src": "src/pages/SystemPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"_update-BqIWfEYV.js",
|
||||
"_update-BrAMPxiF.js",
|
||||
"index.html"
|
||||
],
|
||||
"css": [
|
||||
@@ -138,12 +138,12 @@
|
||||
]
|
||||
},
|
||||
"src/pages/UsersPage.vue": {
|
||||
"file": "assets/UsersPage-DMRA-wLy.js",
|
||||
"file": "assets/UsersPage-JTbL8-nm.js",
|
||||
"name": "UsersPage",
|
||||
"src": "src/pages/UsersPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"_users-CPJP5r-B.js",
|
||||
"_users-CToznuvL.js",
|
||||
"index.html"
|
||||
],
|
||||
"css": [
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
static/admin/assets/EmailPage-CaUZghxJ.js
Normal file
1
static/admin/assets/EmailPage-CaUZghxJ.js
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.page-stack[data-v-988a6b4f]{display:flex;flex-direction:column;gap:12px}.toolbar[data-v-988a6b4f]{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.card[data-v-988a6b4f]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-head[data-v-988a6b4f]{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:12px;flex-wrap:wrap}.section-title[data-v-988a6b4f]{margin:0;font-size:14px;font-weight:800}.help[data-v-988a6b4f]{margin-top:8px;font-size:12px;color:var(--app-muted)}.table-wrap[data-v-988a6b4f]{overflow-x:auto}.stat-card[data-v-988a6b4f]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.stat-value[data-v-988a6b4f]{font-size:20px;font-weight:900;line-height:1.1}.stat-label[data-v-988a6b4f]{margin-top:6px;font-size:12px;color:var(--app-muted)}.ok[data-v-988a6b4f]{color:#047857}.err[data-v-988a6b4f]{color:#b91c1c}.sub-stats[data-v-988a6b4f]{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.ellipsis[data-v-988a6b4f]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pagination[data-v-988a6b4f]{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap}.page-hint[data-v-988a6b4f]{font-size:12px}.dialog-actions[data-v-988a6b4f]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.spacer[data-v-988a6b4f]{flex:1}
|
||||
1
static/admin/assets/EmailPage-DD73oBux.css
Normal file
1
static/admin/assets/EmailPage-DD73oBux.css
Normal file
@@ -0,0 +1 @@
|
||||
.page-stack[data-v-03fa4932]{display:flex;flex-direction:column;gap:12px}.toolbar[data-v-03fa4932]{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.card[data-v-03fa4932]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-head[data-v-03fa4932]{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:12px;flex-wrap:wrap}.section-title[data-v-03fa4932]{margin:0;font-size:14px;font-weight:800}.help[data-v-03fa4932]{margin-top:8px;font-size:12px;color:var(--app-muted)}.table-wrap[data-v-03fa4932]{overflow-x:auto}.stat-card[data-v-03fa4932]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.stat-value[data-v-03fa4932]{font-size:20px;font-weight:900;line-height:1.1}.stat-label[data-v-03fa4932]{margin-top:6px;font-size:12px;color:var(--app-muted)}.ok[data-v-03fa4932]{color:#047857}.err[data-v-03fa4932]{color:#b91c1c}.sub-stats[data-v-03fa4932]{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.ellipsis[data-v-03fa4932]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pagination[data-v-03fa4932]{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap}.page-hint[data-v-03fa4932]{font-size:12px}.dialog-actions[data-v-03fa4932]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.spacer[data-v-03fa4932]{flex:1}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{S as m,_ as T,r as p,e as h,f as r,g as a,w as s,n as u,x as k,y as x,L as i,K as b}from"./index-B57Le1Kd.js";async function C(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function P(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function U(){const{data:o}=await m.post("/logout");return o}const E={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),d=p(""),n=p(!1);async function f(){try{await U()}catch{}finally{window.location.href="/yuyx"}}async function V(){const l=t.value.trim();if(!l){i.error("请输入新用户名");return}try{await b.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),i.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function B(){const l=d.value;if(!l){i.error("请输入新密码");return}if(l.length<6){i.error("密码至少6个字符");return}try{await b.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await P(l),i.success("密码修改成功,请重新登录"),d.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(l,e)=>{const w=u("el-input"),y=u("el-form-item"),v=u("el-form"),_=u("el-button"),g=u("el-card");return k(),h("div",E,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(v,{"label-width":"120px"},{default:s(()=>[a(y,{label:"新用户名"},{default:s(()=>[a(w,{modelValue:t.value,"onUpdate:modelValue":e[0]||(e[0]=c=>t.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:V},{default:s(()=>[...e[2]||(e[2]=[x("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(v,{"label-width":"120px"},{default:s(()=>[a(y,{label:"新密码"},{default:s(()=>[a(w,{modelValue:d.value,"onUpdate:modelValue":e[1]||(e[1]=c=>d.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:B},{default:s(()=>[...e[4]||(e[4]=[x("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码(至少8位且包含字母与数字)。",-1))]),_:1})])}}},A=T(N,[["__scopeId","data-v-2f4b840f"]]);export{A as default};
|
||||
import{S as m,_ as T,r as p,e as h,f as r,g as a,w as s,n as u,x as k,y as x,L as i,K as b}from"./index-Da0EvMWc.js";async function C(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function P(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function U(){const{data:o}=await m.post("/logout");return o}const E={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),d=p(""),n=p(!1);async function f(){try{await U()}catch{}finally{window.location.href="/yuyx"}}async function V(){const l=t.value.trim();if(!l){i.error("请输入新用户名");return}try{await b.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),i.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function B(){const l=d.value;if(!l){i.error("请输入新密码");return}if(l.length<6){i.error("密码至少6个字符");return}try{await b.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await P(l),i.success("密码修改成功,请重新登录"),d.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(l,e)=>{const w=u("el-input"),y=u("el-form-item"),v=u("el-form"),_=u("el-button"),g=u("el-card");return k(),h("div",E,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(v,{"label-width":"120px"},{default:s(()=>[a(y,{label:"新用户名"},{default:s(()=>[a(w,{modelValue:t.value,"onUpdate:modelValue":e[0]||(e[0]=c=>t.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:V},{default:s(()=>[...e[2]||(e[2]=[x("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(v,{"label-width":"120px"},{default:s(()=>[a(y,{label:"新密码"},{default:s(()=>[a(w,{modelValue:d.value,"onUpdate:modelValue":e[1]||(e[1]=c=>d.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:B},{default:s(()=>[...e[4]||(e[4]=[x("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码(至少8位且包含字母与数字)。",-1))]),_:1})])}}},A=T(N,[["__scopeId","data-v-2f4b840f"]]);export{A as default};
|
||||
@@ -1,4 +1,4 @@
|
||||
import{f as Ue,a as Pe,b as Ce,c as he,u as Q,e as Be,r as Te,d as Ie}from"./update-BqIWfEYV.js";import{S as X,_ as Ne,r,c as Se,o as Ae,U as Le,J as se,I as $e,e as C,f as i,g as l,w as t,n as m,h as v,x as u,y as n,j as x,F as je,p as Ee,m as y,K as L,L as b}from"./index-B57Le1Kd.js";async function Re(){const{data:h}=await X.get("/proxy/config");return h}async function De(h){const{data:w}=await X.post("/proxy/config",h);return w}async function Fe(h){const{data:w}=await X.post("/proxy/test",h);return w}const He={class:"page-stack"},Me={class:"app-page-title"},ze={class:"row-actions"},qe={class:"row-actions"},Oe={class:"row-actions",style:{"align-items":"center"}},We={class:"row-actions"},Ge={key:0},Je={key:1},Ke={key:2},Qe={key:3,class:"help"},Xe={key:4,class:"help"},Ye={__name:"SystemPage",setup(h){const w=r(!1),$=r(2),j=r(1),E=r(3),g=r(!1),R=r("02:00"),B=r("应读"),k=r(["1","2","3","4","5","6","7"]),T=r(!1),V=r(""),D=r(3),F=r(!1),H=r(10),M=r(7),G=r(!1),U=r(!1),_=r(null),z=r(""),d=r(null),q=r(""),J=r(!1),O=r(!1);let I=null;const re=[{label:"周一",value:"1"},{label:"周二",value:"2"},{label:"周三",value:"3"},{label:"周四",value:"4"},{label:"周五",value:"5"},{label:"周六",value:"6"},{label:"周日",value:"7"}],de={1:"周一",2:"周二",3:"周三",4:"周四",5:"周五",6:"周六",7:"周日"},ie=Se(()=>(k.value||[]).map(a=>de[Number(a)]||a).join("、"));function me(a){return String(a)==="注册前未读"?"注册前未读":"应读"}function N(a){const e=String(a||"").trim();return e?e.length>12?`${e.slice(0,12)}…`:e:"-"}async function S({withLog:a=!0}={}){G.value=!0,z.value="";try{const[e,s]=await Promise.all([Pe(),Ce()]);e?.ok?_.value=e.data||null:(_.value=null,z.value=e?.error||"未发现更新状态(Update-Agent 可能未运行)"),d.value=s?.ok?s.data:null;const f=d.value?.job_id;if(a&&f){const p=await he({job_id:f,max_bytes:2e5});q.value=p?.log||"",J.value=!!p?.truncated}else q.value="",J.value=!1}catch{}finally{G.value=!1}}function Y(){I||(I=setInterval(async()=>{d.value?.status==="running"&&await S()},5e3))}function pe(){I&&(clearInterval(I),I=null)}async function Z(){w.value=!0;try{const[a,e]=await Promise.all([Ue(),Re()]);$.value=a.max_concurrent_global??2,j.value=a.max_concurrent_per_account??1,E.value=a.max_screenshot_concurrent??3,g.value=(a.schedule_enabled??0)===1,R.value=a.schedule_time||"02:00",B.value=me(a.schedule_browse_type);const s=String(a.schedule_weekdays||"1,2,3,4,5,6,7").split(",").map(f=>f.trim()).filter(Boolean);k.value=s.length?s:["1","2","3","4","5","6","7"],F.value=(a.auto_approve_enabled??0)===1,H.value=a.auto_approve_hourly_limit??10,M.value=a.auto_approve_vip_days??7,T.value=(e.proxy_enabled??0)===1,V.value=e.proxy_api_url||"",D.value=e.proxy_expire_minutes??3,await S({withLog:!1}),Y()}catch{}finally{w.value=!1}}async function ce(){const a={max_concurrent_global:Number($.value),max_concurrent_per_account:Number(j.value),max_screenshot_concurrent:Number(E.value)};try{await L.confirm(`确定更新并发配置吗?
|
||||
import{f as Ue,a as Pe,b as Ce,c as he,u as Q,e as Be,r as Te,d as Ie}from"./update-BrAMPxiF.js";import{S as X,_ as Ne,r,c as Se,o as Ae,U as Le,J as se,I as $e,e as C,f as i,g as l,w as t,n as m,h as v,x as u,y as n,j as x,F as je,p as Ee,m as y,K as L,L as b}from"./index-Da0EvMWc.js";async function Re(){const{data:h}=await X.get("/proxy/config");return h}async function De(h){const{data:w}=await X.post("/proxy/config",h);return w}async function Fe(h){const{data:w}=await X.post("/proxy/test",h);return w}const He={class:"page-stack"},Me={class:"app-page-title"},ze={class:"row-actions"},qe={class:"row-actions"},Oe={class:"row-actions",style:{"align-items":"center"}},We={class:"row-actions"},Ge={key:0},Je={key:1},Ke={key:2},Qe={key:3,class:"help"},Xe={key:4,class:"help"},Ye={__name:"SystemPage",setup(h){const w=r(!1),$=r(2),j=r(1),E=r(3),g=r(!1),R=r("02:00"),B=r("应读"),k=r(["1","2","3","4","5","6","7"]),T=r(!1),V=r(""),D=r(3),F=r(!1),H=r(10),M=r(7),G=r(!1),U=r(!1),_=r(null),z=r(""),d=r(null),q=r(""),J=r(!1),O=r(!1);let I=null;const re=[{label:"周一",value:"1"},{label:"周二",value:"2"},{label:"周三",value:"3"},{label:"周四",value:"4"},{label:"周五",value:"5"},{label:"周六",value:"6"},{label:"周日",value:"7"}],de={1:"周一",2:"周二",3:"周三",4:"周四",5:"周五",6:"周六",7:"周日"},ie=Se(()=>(k.value||[]).map(a=>de[Number(a)]||a).join("、"));function me(a){return String(a)==="注册前未读"?"注册前未读":"应读"}function N(a){const e=String(a||"").trim();return e?e.length>12?`${e.slice(0,12)}…`:e:"-"}async function S({withLog:a=!0}={}){G.value=!0,z.value="";try{const[e,s]=await Promise.all([Pe(),Ce()]);e?.ok?_.value=e.data||null:(_.value=null,z.value=e?.error||"未发现更新状态(Update-Agent 可能未运行)"),d.value=s?.ok?s.data:null;const f=d.value?.job_id;if(a&&f){const p=await he({job_id:f,max_bytes:2e5});q.value=p?.log||"",J.value=!!p?.truncated}else q.value="",J.value=!1}catch{}finally{G.value=!1}}function Y(){I||(I=setInterval(async()=>{d.value?.status==="running"&&await S()},5e3))}function pe(){I&&(clearInterval(I),I=null)}async function Z(){w.value=!0;try{const[a,e]=await Promise.all([Ue(),Re()]);$.value=a.max_concurrent_global??2,j.value=a.max_concurrent_per_account??1,E.value=a.max_screenshot_concurrent??3,g.value=(a.schedule_enabled??0)===1,R.value=a.schedule_time||"02:00",B.value=me(a.schedule_browse_type);const s=String(a.schedule_weekdays||"1,2,3,4,5,6,7").split(",").map(f=>f.trim()).filter(Boolean);k.value=s.length?s:["1","2","3","4","5","6","7"],F.value=(a.auto_approve_enabled??0)===1,H.value=a.auto_approve_hourly_limit??10,M.value=a.auto_approve_vip_days??7,T.value=(e.proxy_enabled??0)===1,V.value=e.proxy_api_url||"",D.value=e.proxy_expire_minutes??3,await S({withLog:!1}),Y()}catch{}finally{w.value=!1}}async function ce(){const a={max_concurrent_global:Number($.value),max_concurrent_per_account:Number(j.value),max_screenshot_concurrent:Number(E.value)};try{await L.confirm(`确定更新并发配置吗?
|
||||
|
||||
全局并发数: ${a.max_concurrent_global}
|
||||
单账号并发数: ${a.max_concurrent_per_account}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{S as n}from"./index-B57Le1Kd.js";async function i(){const{data:a}=await n.get("/email/settings");return a}async function e(a){const{data:t}=await n.post("/email/settings",a);return t}async function c(){const{data:a}=await n.get("/email/stats");return a}async function o(a){const{data:t}=await n.get("/email/logs",{params:a});return t}async function l(a){const{data:t}=await n.post("/email/logs/cleanup",{days:a});return t}export{i as a,o as b,l as c,c as f,e as u};
|
||||
import{S as n}from"./index-Da0EvMWc.js";async function i(){const{data:a}=await n.get("/email/settings");return a}async function e(a){const{data:t}=await n.post("/email/settings",a);return t}async function c(){const{data:a}=await n.get("/email/stats");return a}async function o(a){const{data:t}=await n.get("/email/logs",{params:a});return t}async function l(a){const{data:t}=await n.post("/email/logs/cleanup",{days:a});return t}export{i as a,o as b,l as c,c as f,e as u};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
30
static/admin/assets/index-Da0EvMWc.js
Normal file
30
static/admin/assets/index-Da0EvMWc.js
Normal file
File diff suppressed because one or more lines are too long
1
static/admin/assets/index-EWm4DZW8.css
Normal file
1
static/admin/assets/index-EWm4DZW8.css
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{S as a}from"./index-B57Le1Kd.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function r(){const{data:t}=await a.get("/task/running");return t}async function i(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function f(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{r as a,c as b,e as c,i as d,f as e,o as f};
|
||||
import{S as a}from"./index-Da0EvMWc.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function r(){const{data:t}=await a.get("/task/running");return t}async function i(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function f(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{r as a,c as b,e as c,i as d,f as e,o as f};
|
||||
@@ -1 +1 @@
|
||||
import{S as a}from"./index-B57Le1Kd.js";async function s(){const{data:t}=await a.get("/system/config");return t}async function c(t){const{data:e}=await a.post("/system/config",t);return e}async function u(){const{data:t}=await a.post("/schedule/execute",{});return t}async function o(){const{data:t}=await a.get("/update/status");return t}async function r(){const{data:t}=await a.get("/update/result");return t}async function d(t={}){const{data:e}=await a.get("/update/log",{params:t});return e}async function i(){const{data:t}=await a.post("/update/check",{});return t}async function f(t={}){const{data:e}=await a.post("/update/run",t);return e}export{o as a,r as b,d as c,f as d,u as e,s as f,i as r,c as u};
|
||||
import{S as a}from"./index-Da0EvMWc.js";async function s(){const{data:t}=await a.get("/system/config");return t}async function c(t){const{data:e}=await a.post("/system/config",t);return e}async function u(){const{data:t}=await a.post("/schedule/execute",{});return t}async function o(){const{data:t}=await a.get("/update/status");return t}async function r(){const{data:t}=await a.get("/update/result");return t}async function d(t={}){const{data:e}=await a.get("/update/log",{params:t});return e}async function i(){const{data:t}=await a.post("/update/check",{});return t}async function f(t={}){const{data:e}=await a.post("/update/run",t);return e}export{o as a,r as b,d as c,f as d,u as e,s as f,i as r,c as u};
|
||||
@@ -1 +1 @@
|
||||
import{S as t}from"./index-B57Le1Kd.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s};
|
||||
import{S as t}from"./index-Da0EvMWc.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s};
|
||||
@@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>后台管理 - 知识管理平台</title>
|
||||
<script type="module" crossorigin src="./assets/index-B57Le1Kd.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-CBVc8utT.css">
|
||||
<script type="module" crossorigin src="./assets/index-Da0EvMWc.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-EWm4DZW8.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"_accounts-BFaVMUve.js": {
|
||||
"file": "assets/accounts-BFaVMUve.js",
|
||||
"_accounts-BXD0We06.js": {
|
||||
"file": "assets/accounts-BXD0We06.js",
|
||||
"name": "accounts",
|
||||
"imports": [
|
||||
"index.html"
|
||||
]
|
||||
},
|
||||
"_auth-cku83FEM.js": {
|
||||
"file": "assets/auth-cku83FEM.js",
|
||||
"_auth-cf7b3Gq2.js": {
|
||||
"file": "assets/auth-cf7b3Gq2.js",
|
||||
"name": "auth",
|
||||
"imports": [
|
||||
"index.html"
|
||||
@@ -18,7 +18,7 @@
|
||||
"name": "password"
|
||||
},
|
||||
"index.html": {
|
||||
"file": "assets/index-DSLtIIj4.js",
|
||||
"file": "assets/index-DhsLPY8p.js",
|
||||
"name": "index",
|
||||
"src": "index.html",
|
||||
"isEntry": true,
|
||||
@@ -36,12 +36,12 @@
|
||||
]
|
||||
},
|
||||
"src/pages/AccountsPage.vue": {
|
||||
"file": "assets/AccountsPage-C48gJL8c.js",
|
||||
"file": "assets/AccountsPage-38dq1Ex4.js",
|
||||
"name": "AccountsPage",
|
||||
"src": "src/pages/AccountsPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"_accounts-BFaVMUve.js",
|
||||
"_accounts-BXD0We06.js",
|
||||
"index.html"
|
||||
],
|
||||
"css": [
|
||||
@@ -49,13 +49,13 @@
|
||||
]
|
||||
},
|
||||
"src/pages/LoginPage.vue": {
|
||||
"file": "assets/LoginPage-CbeuGOZL.js",
|
||||
"file": "assets/LoginPage-B_fgHOTT.js",
|
||||
"name": "LoginPage",
|
||||
"src": "src/pages/LoginPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"index.html",
|
||||
"_auth-cku83FEM.js",
|
||||
"_auth-cf7b3Gq2.js",
|
||||
"_password-7ryi82gE.js"
|
||||
],
|
||||
"css": [
|
||||
@@ -63,26 +63,26 @@
|
||||
]
|
||||
},
|
||||
"src/pages/RegisterPage.vue": {
|
||||
"file": "assets/RegisterPage-B4t-njqd.js",
|
||||
"file": "assets/RegisterPage-B_Z92PVI.js",
|
||||
"name": "RegisterPage",
|
||||
"src": "src/pages/RegisterPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"index.html",
|
||||
"_auth-cku83FEM.js"
|
||||
"_auth-cf7b3Gq2.js"
|
||||
],
|
||||
"css": [
|
||||
"assets/RegisterPage-yylt2w7b.css"
|
||||
]
|
||||
},
|
||||
"src/pages/ResetPasswordPage.vue": {
|
||||
"file": "assets/ResetPasswordPage-Tuid_XBa.js",
|
||||
"file": "assets/ResetPasswordPage-2f8v-5j9.js",
|
||||
"name": "ResetPasswordPage",
|
||||
"src": "src/pages/ResetPasswordPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"index.html",
|
||||
"_auth-cku83FEM.js",
|
||||
"_auth-cf7b3Gq2.js",
|
||||
"_password-7ryi82gE.js"
|
||||
],
|
||||
"css": [
|
||||
@@ -90,12 +90,12 @@
|
||||
]
|
||||
},
|
||||
"src/pages/SchedulesPage.vue": {
|
||||
"file": "assets/SchedulesPage-Dw-mXbG5.js",
|
||||
"file": "assets/SchedulesPage-VLwHd9Sa.js",
|
||||
"name": "SchedulesPage",
|
||||
"src": "src/pages/SchedulesPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"_accounts-BFaVMUve.js",
|
||||
"_accounts-BXD0We06.js",
|
||||
"index.html"
|
||||
],
|
||||
"css": [
|
||||
@@ -103,7 +103,7 @@
|
||||
]
|
||||
},
|
||||
"src/pages/ScreenshotsPage.vue": {
|
||||
"file": "assets/ScreenshotsPage-C6vX2U3V.js",
|
||||
"file": "assets/ScreenshotsPage-Dtd_MXUX.js",
|
||||
"name": "ScreenshotsPage",
|
||||
"src": "src/pages/ScreenshotsPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
@@ -115,7 +115,7 @@
|
||||
]
|
||||
},
|
||||
"src/pages/VerifyResultPage.vue": {
|
||||
"file": "assets/VerifyResultPage-BzGlCgtE.js",
|
||||
"file": "assets/VerifyResultPage-8_v-5_kc.js",
|
||||
"name": "VerifyResultPage",
|
||||
"src": "src/pages/VerifyResultPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{_ as M,r as j,a as p,c as B,o as A,b as S,d as t,w as o,e as m,u as H,f as g,g as n,h as U,i as x,j as N,t as q,k as E,E as d}from"./index-DSLtIIj4.js";import{g as z,f as F,c as G}from"./auth-cku83FEM.js";const J={class:"auth-wrap"},O={class:"hint app-muted"},Q={class:"captcha-row"},W=["src"],X={class:"actions"},Y={__name:"RegisterPage",setup(Z){const T=H(),a=j({username:"",password:"",confirm_password:"",email:"",captcha:""}),v=p(!1),f=p(""),h=p(""),b=p(!1),l=p(""),_=p(""),V=p(""),K=B(()=>v.value?"邮箱 *":"邮箱(可选)"),P=B(()=>v.value?"必填,用于账号验证":"选填,用于找回密码和接收通知");async function w(){try{const u=await z();h.value=u?.session_id||"",f.value=u?.captcha_image||"",a.captcha=""}catch{h.value="",f.value=""}}async function R(){try{const u=await F();v.value=!!u?.register_verify_enabled}catch{v.value=!1}}function D(){l.value="",_.value="",V.value=""}async function k(){D();const u=a.username.trim(),e=a.password,y=a.confirm_password,s=a.email.trim(),i=a.captcha.trim();if(u.length<3){l.value="用户名至少3个字符",d.error(l.value);return}if(e.length<6){l.value="密码至少6个字符",d.error(l.value);return}if(e!==y){l.value="两次输入的密码不一致",d.error(l.value);return}if(v.value&&!s){l.value="请填写邮箱地址用于账号验证",d.error(l.value);return}if(s&&!s.includes("@")){l.value="邮箱格式不正确",d.error(l.value);return}if(!i){l.value="请输入验证码",d.error(l.value);return}b.value=!0;try{const c=await G({username:u,password:e,email:s,captcha_session:h.value,captcha:i});_.value=c?.message||"注册成功",V.value=c?.need_verify?"请检查您的邮箱(包括垃圾邮件文件夹)":"",d.success("注册成功"),a.username="",a.password="",a.confirm_password="",a.email="",a.captcha="",setTimeout(()=>{window.location.href="/login"},3e3)}catch(c){const C=c?.response?.data;l.value=C?.error||"注册失败",d.error(l.value),await w()}finally{b.value=!1}}function I(){T.push("/login")}return A(async()=>{await w(),await R()}),(u,e)=>{const y=m("el-alert"),s=m("el-input"),i=m("el-form-item"),c=m("el-button"),C=m("el-form"),L=m("el-card");return g(),S("div",J,[t(L,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:o(()=>[e[11]||(e[11]=n("div",{class:"brand"},[n("div",{class:"brand-title"},"知识管理平台"),n("div",{class:"brand-sub app-muted"},"用户注册")],-1)),l.value?(g(),U(y,{key:0,type:"error",closable:!1,title:l.value,"show-icon":"",class:"alert"},null,8,["title"])):x("",!0),_.value?(g(),U(y,{key:1,type:"success",closable:!1,title:_.value,description:V.value,"show-icon":"",class:"alert"},null,8,["title","description"])):x("",!0),t(C,{"label-position":"top"},{default:o(()=>[t(i,{label:"用户名 *"},{default:o(()=>[t(s,{modelValue:a.username,"onUpdate:modelValue":e[0]||(e[0]=r=>a.username=r),placeholder:"至少3个字符",autocomplete:"username"},null,8,["modelValue"]),e[5]||(e[5]=n("div",{class:"hint app-muted"},"至少3个字符",-1))]),_:1}),t(i,{label:"密码 *"},{default:o(()=>[t(s,{modelValue:a.password,"onUpdate:modelValue":e[1]||(e[1]=r=>a.password=r),type:"password","show-password":"",placeholder:"至少6个字符",autocomplete:"new-password"},null,8,["modelValue"]),e[6]||(e[6]=n("div",{class:"hint app-muted"},"至少6个字符",-1))]),_:1}),t(i,{label:"确认密码 *"},{default:o(()=>[t(s,{modelValue:a.confirm_password,"onUpdate:modelValue":e[2]||(e[2]=r=>a.confirm_password=r),type:"password","show-password":"",placeholder:"请再次输入密码",autocomplete:"new-password",onKeyup:N(k,["enter"])},null,8,["modelValue"])]),_:1}),t(i,{label:K.value},{default:o(()=>[t(s,{modelValue:a.email,"onUpdate:modelValue":e[3]||(e[3]=r=>a.email=r),placeholder:"name@example.com",autocomplete:"email"},null,8,["modelValue"]),n("div",O,q(P.value),1)]),_:1},8,["label"]),t(i,{label:"验证码 *"},{default:o(()=>[n("div",Q,[t(s,{modelValue:a.captcha,"onUpdate:modelValue":e[4]||(e[4]=r=>a.captcha=r),placeholder:"请输入验证码",onKeyup:N(k,["enter"])},null,8,["modelValue"]),f.value?(g(),S("img",{key:0,class:"captcha-img",src:f.value,alt:"验证码",title:"点击刷新",onClick:w},null,8,W)):x("",!0),t(c,{onClick:w},{default:o(()=>[...e[7]||(e[7]=[E("刷新",-1)])]),_:1})])]),_:1})]),_:1}),t(c,{type:"primary",class:"submit-btn",loading:b.value,onClick:k},{default:o(()=>[...e[8]||(e[8]=[E("注册",-1)])]),_:1},8,["loading"]),n("div",X,[e[10]||(e[10]=n("span",{class:"app-muted"},"已有账号?",-1)),t(c,{link:"",type:"primary",onClick:I},{default:o(()=>[...e[9]||(e[9]=[E("立即登录",-1)])]),_:1})])]),_:1})])}}},ae=M(Y,[["__scopeId","data-v-75731a6d"]]);export{ae as default};
|
||||
import{_ as M,r as j,a as p,c as B,o as A,b as S,d as t,w as o,e as m,u as H,f as g,g as n,h as U,i as x,j as N,t as q,k as E,E as d}from"./index-DhsLPY8p.js";import{g as z,f as F,c as G}from"./auth-cf7b3Gq2.js";const J={class:"auth-wrap"},O={class:"hint app-muted"},Q={class:"captcha-row"},W=["src"],X={class:"actions"},Y={__name:"RegisterPage",setup(Z){const T=H(),a=j({username:"",password:"",confirm_password:"",email:"",captcha:""}),v=p(!1),f=p(""),h=p(""),b=p(!1),l=p(""),_=p(""),V=p(""),K=B(()=>v.value?"邮箱 *":"邮箱(可选)"),P=B(()=>v.value?"必填,用于账号验证":"选填,用于找回密码和接收通知");async function w(){try{const u=await z();h.value=u?.session_id||"",f.value=u?.captcha_image||"",a.captcha=""}catch{h.value="",f.value=""}}async function R(){try{const u=await F();v.value=!!u?.register_verify_enabled}catch{v.value=!1}}function D(){l.value="",_.value="",V.value=""}async function k(){D();const u=a.username.trim(),e=a.password,y=a.confirm_password,s=a.email.trim(),i=a.captcha.trim();if(u.length<3){l.value="用户名至少3个字符",d.error(l.value);return}if(e.length<6){l.value="密码至少6个字符",d.error(l.value);return}if(e!==y){l.value="两次输入的密码不一致",d.error(l.value);return}if(v.value&&!s){l.value="请填写邮箱地址用于账号验证",d.error(l.value);return}if(s&&!s.includes("@")){l.value="邮箱格式不正确",d.error(l.value);return}if(!i){l.value="请输入验证码",d.error(l.value);return}b.value=!0;try{const c=await G({username:u,password:e,email:s,captcha_session:h.value,captcha:i});_.value=c?.message||"注册成功",V.value=c?.need_verify?"请检查您的邮箱(包括垃圾邮件文件夹)":"",d.success("注册成功"),a.username="",a.password="",a.confirm_password="",a.email="",a.captcha="",setTimeout(()=>{window.location.href="/login"},3e3)}catch(c){const C=c?.response?.data;l.value=C?.error||"注册失败",d.error(l.value),await w()}finally{b.value=!1}}function I(){T.push("/login")}return A(async()=>{await w(),await R()}),(u,e)=>{const y=m("el-alert"),s=m("el-input"),i=m("el-form-item"),c=m("el-button"),C=m("el-form"),L=m("el-card");return g(),S("div",J,[t(L,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:o(()=>[e[11]||(e[11]=n("div",{class:"brand"},[n("div",{class:"brand-title"},"知识管理平台"),n("div",{class:"brand-sub app-muted"},"用户注册")],-1)),l.value?(g(),U(y,{key:0,type:"error",closable:!1,title:l.value,"show-icon":"",class:"alert"},null,8,["title"])):x("",!0),_.value?(g(),U(y,{key:1,type:"success",closable:!1,title:_.value,description:V.value,"show-icon":"",class:"alert"},null,8,["title","description"])):x("",!0),t(C,{"label-position":"top"},{default:o(()=>[t(i,{label:"用户名 *"},{default:o(()=>[t(s,{modelValue:a.username,"onUpdate:modelValue":e[0]||(e[0]=r=>a.username=r),placeholder:"至少3个字符",autocomplete:"username"},null,8,["modelValue"]),e[5]||(e[5]=n("div",{class:"hint app-muted"},"至少3个字符",-1))]),_:1}),t(i,{label:"密码 *"},{default:o(()=>[t(s,{modelValue:a.password,"onUpdate:modelValue":e[1]||(e[1]=r=>a.password=r),type:"password","show-password":"",placeholder:"至少6个字符",autocomplete:"new-password"},null,8,["modelValue"]),e[6]||(e[6]=n("div",{class:"hint app-muted"},"至少6个字符",-1))]),_:1}),t(i,{label:"确认密码 *"},{default:o(()=>[t(s,{modelValue:a.confirm_password,"onUpdate:modelValue":e[2]||(e[2]=r=>a.confirm_password=r),type:"password","show-password":"",placeholder:"请再次输入密码",autocomplete:"new-password",onKeyup:N(k,["enter"])},null,8,["modelValue"])]),_:1}),t(i,{label:K.value},{default:o(()=>[t(s,{modelValue:a.email,"onUpdate:modelValue":e[3]||(e[3]=r=>a.email=r),placeholder:"name@example.com",autocomplete:"email"},null,8,["modelValue"]),n("div",O,q(P.value),1)]),_:1},8,["label"]),t(i,{label:"验证码 *"},{default:o(()=>[n("div",Q,[t(s,{modelValue:a.captcha,"onUpdate:modelValue":e[4]||(e[4]=r=>a.captcha=r),placeholder:"请输入验证码",onKeyup:N(k,["enter"])},null,8,["modelValue"]),f.value?(g(),S("img",{key:0,class:"captcha-img",src:f.value,alt:"验证码",title:"点击刷新",onClick:w},null,8,W)):x("",!0),t(c,{onClick:w},{default:o(()=>[...e[7]||(e[7]=[E("刷新",-1)])]),_:1})])]),_:1})]),_:1}),t(c,{type:"primary",class:"submit-btn",loading:b.value,onClick:k},{default:o(()=>[...e[8]||(e[8]=[E("注册",-1)])]),_:1},8,["loading"]),n("div",X,[e[10]||(e[10]=n("span",{class:"app-muted"},"已有账号?",-1)),t(c,{link:"",type:"primary",onClick:I},{default:o(()=>[...e[9]||(e[9]=[E("立即登录",-1)])]),_:1})])]),_:1})])}}},ae=M(Y,[["__scopeId","data-v-75731a6d"]]);export{ae as default};
|
||||
@@ -1 +1 @@
|
||||
import{_ as L,a as n,l as M,r as U,c as j,o as F,m as K,b as v,d as s,w as a,e as l,u as D,f as m,g as w,F as T,k,h as q,i as x,j as z,t as G,E as y}from"./index-DSLtIIj4.js";import{d as H}from"./auth-cku83FEM.js";import{v as J}from"./password-7ryi82gE.js";const O={class:"auth-wrap"},Q={class:"actions"},W={class:"actions"},X={key:0,class:"app-muted"},Y={__name:"ResetPasswordPage",setup(Z){const B=M(),A=D(),r=n(String(B.params.token||"")),i=n(!0),b=n(""),t=U({newPassword:"",confirmPassword:""}),g=n(!1),f=n(""),d=n(0);let u=null;function C(){if(typeof window>"u")return null;const o=window.__APP_INITIAL_STATE__;return!o||typeof o!="object"?null:(window.__APP_INITIAL_STATE__=null,o)}const I=j(()=>!!(i.value&&r.value&&!f.value));function S(){A.push("/login")}function N(){d.value=3,u=window.setInterval(()=>{d.value-=1,d.value<=0&&(window.clearInterval(u),u=null,window.location.href="/login")},1e3)}async function V(){if(!I.value)return;const o=t.newPassword,e=t.confirmPassword,c=J(o);if(!c.ok){y.error(c.message);return}if(o!==e){y.error("两次输入的密码不一致");return}g.value=!0;try{await H({token:r.value,new_password:o}),f.value="密码重置成功!3秒后跳转到登录页面...",y.success("密码重置成功"),N()}catch(p){const _=p?.response?.data;y.error(_?.error||"重置失败")}finally{g.value=!1}}return F(()=>{const o=C();o?.page==="reset_password"?(r.value=String(o?.token||r.value||""),i.value=!!o?.valid,b.value=o?.error_message||(i.value?"":"重置链接无效或已过期,请重新申请密码重置")):r.value||(i.value=!1,b.value="重置链接无效或已过期,请重新申请密码重置")}),K(()=>{u&&window.clearInterval(u)}),(o,e)=>{const c=l("el-alert"),p=l("el-button"),_=l("el-input"),h=l("el-form-item"),R=l("el-form"),E=l("el-card");return m(),v("div",O,[s(E,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:a(()=>[e[5]||(e[5]=w("div",{class:"brand"},[w("div",{class:"brand-title"},"知识管理平台"),w("div",{class:"brand-sub app-muted"},"重置密码")],-1)),i.value?(m(),v(T,{key:1},[f.value?(m(),q(c,{key:0,type:"success",closable:!1,title:"重置成功",description:f.value,"show-icon":"",class:"alert"},null,8,["description"])):x("",!0),s(R,{"label-position":"top"},{default:a(()=>[s(h,{label:"新密码(至少8位且包含字母和数字)"},{default:a(()=>[s(_,{modelValue:t.newPassword,"onUpdate:modelValue":e[0]||(e[0]=P=>t.newPassword=P),type:"password","show-password":"",placeholder:"请输入新密码",autocomplete:"new-password"},null,8,["modelValue"])]),_:1}),s(h,{label:"确认密码"},{default:a(()=>[s(_,{modelValue:t.confirmPassword,"onUpdate:modelValue":e[1]||(e[1]=P=>t.confirmPassword=P),type:"password","show-password":"",placeholder:"请再次输入新密码",autocomplete:"new-password",onKeyup:z(V,["enter"])},null,8,["modelValue"])]),_:1})]),_:1}),s(p,{type:"primary",class:"submit-btn",loading:g.value,disabled:!I.value,onClick:V},{default:a(()=>[...e[3]||(e[3]=[k(" 确认重置 ",-1)])]),_:1},8,["loading","disabled"]),w("div",W,[s(p,{link:"",type:"primary",onClick:S},{default:a(()=>[...e[4]||(e[4]=[k("返回登录",-1)])]),_:1}),d.value>0?(m(),v("span",X,G(d.value)+" 秒后自动跳转…",1)):x("",!0)])],64)):(m(),v(T,{key:0},[s(c,{type:"error",closable:!1,title:"链接已失效",description:b.value,"show-icon":""},null,8,["description"]),w("div",Q,[s(p,{type:"primary",onClick:S},{default:a(()=>[...e[2]||(e[2]=[k("返回登录",-1)])]),_:1})])],64))]),_:1})])}}},se=L(Y,[["__scopeId","data-v-0bbb511c"]]);export{se as default};
|
||||
import{_ as L,a as n,l as M,r as U,c as j,o as F,m as K,b as v,d as s,w as a,e as l,u as D,f as m,g as w,F as T,k,h as q,i as x,j as z,t as G,E as y}from"./index-DhsLPY8p.js";import{d as H}from"./auth-cf7b3Gq2.js";import{v as J}from"./password-7ryi82gE.js";const O={class:"auth-wrap"},Q={class:"actions"},W={class:"actions"},X={key:0,class:"app-muted"},Y={__name:"ResetPasswordPage",setup(Z){const B=M(),A=D(),r=n(String(B.params.token||"")),i=n(!0),b=n(""),t=U({newPassword:"",confirmPassword:""}),g=n(!1),f=n(""),d=n(0);let u=null;function C(){if(typeof window>"u")return null;const o=window.__APP_INITIAL_STATE__;return!o||typeof o!="object"?null:(window.__APP_INITIAL_STATE__=null,o)}const I=j(()=>!!(i.value&&r.value&&!f.value));function S(){A.push("/login")}function N(){d.value=3,u=window.setInterval(()=>{d.value-=1,d.value<=0&&(window.clearInterval(u),u=null,window.location.href="/login")},1e3)}async function V(){if(!I.value)return;const o=t.newPassword,e=t.confirmPassword,c=J(o);if(!c.ok){y.error(c.message);return}if(o!==e){y.error("两次输入的密码不一致");return}g.value=!0;try{await H({token:r.value,new_password:o}),f.value="密码重置成功!3秒后跳转到登录页面...",y.success("密码重置成功"),N()}catch(p){const _=p?.response?.data;y.error(_?.error||"重置失败")}finally{g.value=!1}}return F(()=>{const o=C();o?.page==="reset_password"?(r.value=String(o?.token||r.value||""),i.value=!!o?.valid,b.value=o?.error_message||(i.value?"":"重置链接无效或已过期,请重新申请密码重置")):r.value||(i.value=!1,b.value="重置链接无效或已过期,请重新申请密码重置")}),K(()=>{u&&window.clearInterval(u)}),(o,e)=>{const c=l("el-alert"),p=l("el-button"),_=l("el-input"),h=l("el-form-item"),R=l("el-form"),E=l("el-card");return m(),v("div",O,[s(E,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:a(()=>[e[5]||(e[5]=w("div",{class:"brand"},[w("div",{class:"brand-title"},"知识管理平台"),w("div",{class:"brand-sub app-muted"},"重置密码")],-1)),i.value?(m(),v(T,{key:1},[f.value?(m(),q(c,{key:0,type:"success",closable:!1,title:"重置成功",description:f.value,"show-icon":"",class:"alert"},null,8,["description"])):x("",!0),s(R,{"label-position":"top"},{default:a(()=>[s(h,{label:"新密码(至少8位且包含字母和数字)"},{default:a(()=>[s(_,{modelValue:t.newPassword,"onUpdate:modelValue":e[0]||(e[0]=P=>t.newPassword=P),type:"password","show-password":"",placeholder:"请输入新密码",autocomplete:"new-password"},null,8,["modelValue"])]),_:1}),s(h,{label:"确认密码"},{default:a(()=>[s(_,{modelValue:t.confirmPassword,"onUpdate:modelValue":e[1]||(e[1]=P=>t.confirmPassword=P),type:"password","show-password":"",placeholder:"请再次输入新密码",autocomplete:"new-password",onKeyup:z(V,["enter"])},null,8,["modelValue"])]),_:1})]),_:1}),s(p,{type:"primary",class:"submit-btn",loading:g.value,disabled:!I.value,onClick:V},{default:a(()=>[...e[3]||(e[3]=[k(" 确认重置 ",-1)])]),_:1},8,["loading","disabled"]),w("div",W,[s(p,{link:"",type:"primary",onClick:S},{default:a(()=>[...e[4]||(e[4]=[k("返回登录",-1)])]),_:1}),d.value>0?(m(),v("span",X,G(d.value)+" 秒后自动跳转…",1)):x("",!0)])],64)):(m(),v(T,{key:0},[s(c,{type:"error",closable:!1,title:"链接已失效",description:b.value,"show-icon":""},null,8,["description"]),w("div",Q,[s(p,{type:"primary",onClick:S},{default:a(()=>[...e[2]||(e[2]=[k("返回登录",-1)])]),_:1})])],64))]),_:1})])}}},se=L(Y,[["__scopeId","data-v-0bbb511c"]]);export{se as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{_ as U,a as o,c as I,o as E,m as R,b as k,d as i,w as s,e as d,u as W,f as _,g as l,i as B,h as $,k as T,t as v}from"./index-DSLtIIj4.js";const j={class:"auth-wrap"},z={class:"actions"},D={key:0,class:"countdown app-muted"},M={__name:"VerifyResultPage",setup(q){const x=W(),p=o(!1),f=o(""),m=o(""),w=o(""),y=o(""),r=o(""),u=o(""),c=o(""),n=o(0);let a=null;function C(){if(typeof window>"u")return null;const e=window.__APP_INITIAL_STATE__;return!e||typeof e!="object"?null:(window.__APP_INITIAL_STATE__=null,e)}function N(e){const t=!!e?.success;p.value=t,f.value=e?.title||(t?"验证成功":"验证失败"),m.value=e?.message||e?.error_message||(t?"操作已完成,现在可以继续使用系统。":"操作失败,请稍后重试。"),w.value=e?.primary_label||(t?"立即登录":"重新注册"),y.value=e?.primary_url||(t?"/login":"/register"),r.value=e?.secondary_label||(t?"":"返回登录"),u.value=e?.secondary_url||(t?"":"/login"),c.value=e?.redirect_url||(t?"/login":""),n.value=Number(e?.redirect_seconds||(t?5:0))||0}const A=I(()=>!!(r.value&&u.value)),b=I(()=>!!(c.value&&n.value>0));async function g(e){if(e){if(e.startsWith("http://")||e.startsWith("https://")){window.location.href=e;return}await x.push(e)}}function P(){b.value&&(a=window.setInterval(()=>{n.value-=1,n.value<=0&&(window.clearInterval(a),a=null,window.location.href=c.value)},1e3))}return E(()=>{const e=C();N(e),P()}),R(()=>{a&&window.clearInterval(a)}),(e,t)=>{const h=d("el-button"),V=d("el-result"),L=d("el-card");return _(),k("div",j,[i(L,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:s(()=>[t[2]||(t[2]=l("div",{class:"brand"},[l("div",{class:"brand-title"},"知识管理平台"),l("div",{class:"brand-sub app-muted"},"验证结果")],-1)),i(V,{icon:p.value?"success":"error",title:f.value,"sub-title":m.value,class:"result"},{extra:s(()=>[l("div",z,[i(h,{type:"primary",onClick:t[0]||(t[0]=S=>g(y.value))},{default:s(()=>[T(v(w.value),1)]),_:1}),A.value?(_(),$(h,{key:0,onClick:t[1]||(t[1]=S=>g(u.value))},{default:s(()=>[T(v(r.value),1)]),_:1})):B("",!0)]),b.value?(_(),k("div",D,v(n.value)+" 秒后自动跳转... ",1)):B("",!0)]),_:1},8,["icon","title","sub-title"])]),_:1})])}}},G=U(M,[["__scopeId","data-v-1fc6b081"]]);export{G as default};
|
||||
import{_ as U,a as o,c as I,o as E,m as R,b as k,d as i,w as s,e as d,u as W,f as _,g as l,i as B,h as $,k as T,t as v}from"./index-DhsLPY8p.js";const j={class:"auth-wrap"},z={class:"actions"},D={key:0,class:"countdown app-muted"},M={__name:"VerifyResultPage",setup(q){const x=W(),p=o(!1),f=o(""),m=o(""),w=o(""),y=o(""),r=o(""),u=o(""),c=o(""),n=o(0);let a=null;function C(){if(typeof window>"u")return null;const e=window.__APP_INITIAL_STATE__;return!e||typeof e!="object"?null:(window.__APP_INITIAL_STATE__=null,e)}function N(e){const t=!!e?.success;p.value=t,f.value=e?.title||(t?"验证成功":"验证失败"),m.value=e?.message||e?.error_message||(t?"操作已完成,现在可以继续使用系统。":"操作失败,请稍后重试。"),w.value=e?.primary_label||(t?"立即登录":"重新注册"),y.value=e?.primary_url||(t?"/login":"/register"),r.value=e?.secondary_label||(t?"":"返回登录"),u.value=e?.secondary_url||(t?"":"/login"),c.value=e?.redirect_url||(t?"/login":""),n.value=Number(e?.redirect_seconds||(t?5:0))||0}const A=I(()=>!!(r.value&&u.value)),b=I(()=>!!(c.value&&n.value>0));async function g(e){if(e){if(e.startsWith("http://")||e.startsWith("https://")){window.location.href=e;return}await x.push(e)}}function P(){b.value&&(a=window.setInterval(()=>{n.value-=1,n.value<=0&&(window.clearInterval(a),a=null,window.location.href=c.value)},1e3))}return E(()=>{const e=C();N(e),P()}),R(()=>{a&&window.clearInterval(a)}),(e,t)=>{const h=d("el-button"),V=d("el-result"),L=d("el-card");return _(),k("div",j,[i(L,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:s(()=>[t[2]||(t[2]=l("div",{class:"brand"},[l("div",{class:"brand-title"},"知识管理平台"),l("div",{class:"brand-sub app-muted"},"验证结果")],-1)),i(V,{icon:p.value?"success":"error",title:f.value,"sub-title":m.value,class:"result"},{extra:s(()=>[l("div",z,[i(h,{type:"primary",onClick:t[0]||(t[0]=S=>g(y.value))},{default:s(()=>[T(v(w.value),1)]),_:1}),A.value?(_(),$(h,{key:0,onClick:t[1]||(t[1]=S=>g(u.value))},{default:s(()=>[T(v(r.value),1)]),_:1})):B("",!0)]),b.value?(_(),k("div",D,v(n.value)+" 秒后自动跳转... ",1)):B("",!0)]),_:1},8,["icon","title","sub-title"])]),_:1})])}}},G=U(M,[["__scopeId","data-v-1fc6b081"]]);export{G as default};
|
||||
@@ -1 +1 @@
|
||||
import{p as c}from"./index-DSLtIIj4.js";async function o(t={}){const{data:a}=await c.get("/accounts",{params:t});return a}async function u(t){const{data:a}=await c.post("/accounts",t);return a}async function r(t,a){const{data:n}=await c.put(`/accounts/${t}`,a);return n}async function e(t){const{data:a}=await c.delete(`/accounts/${t}`);return a}async function i(t,a){const{data:n}=await c.put(`/accounts/${t}/remark`,a);return n}async function p(t,a){const{data:n}=await c.post(`/accounts/${t}/start`,a);return n}async function d(t){const{data:a}=await c.post(`/accounts/${t}/stop`,{});return a}async function f(t){const{data:a}=await c.post("/accounts/batch/start",t);return a}async function w(t){const{data:a}=await c.post("/accounts/batch/stop",t);return a}async function y(){const{data:t}=await c.post("/accounts/clear",{});return t}async function A(t,a={}){const{data:n}=await c.post(`/accounts/${t}/screenshot`,a);return n}export{w as a,f as b,y as c,d,e,o as f,u as g,i as h,p as s,A as t,r as u};
|
||||
import{p as c}from"./index-DhsLPY8p.js";async function o(t={}){const{data:a}=await c.get("/accounts",{params:t});return a}async function u(t){const{data:a}=await c.post("/accounts",t);return a}async function r(t,a){const{data:n}=await c.put(`/accounts/${t}`,a);return n}async function e(t){const{data:a}=await c.delete(`/accounts/${t}`);return a}async function i(t,a){const{data:n}=await c.put(`/accounts/${t}/remark`,a);return n}async function p(t,a){const{data:n}=await c.post(`/accounts/${t}/start`,a);return n}async function d(t){const{data:a}=await c.post(`/accounts/${t}/stop`,{});return a}async function f(t){const{data:a}=await c.post("/accounts/batch/start",t);return a}async function w(t){const{data:a}=await c.post("/accounts/batch/stop",t);return a}async function y(){const{data:t}=await c.post("/accounts/clear",{});return t}async function A(t,a={}){const{data:n}=await c.post(`/accounts/${t}/screenshot`,a);return n}export{w as a,f as b,y as c,d,e,o as f,u as g,i as h,p as s,A as t,r as u};
|
||||
@@ -1 +1 @@
|
||||
import{p as s}from"./index-DSLtIIj4.js";async function r(){const{data:t}=await s.get("/email/verify-status");return t}async function e(){const{data:t}=await s.post("/generate_captcha",{});return t}async function o(t){const{data:a}=await s.post("/login",t);return a}async function i(t){const{data:a}=await s.post("/register",t);return a}async function c(t){const{data:a}=await s.post("/resend-verify-email",t);return a}async function u(t){const{data:a}=await s.post("/forgot-password",t);return a}async function f(t){const{data:a}=await s.post("/reset_password_request",t);return a}async function d(t){const{data:a}=await s.post("/reset-password-confirm",t);return a}export{u as a,c as b,i as c,d,r as f,e as g,o as l,f as r};
|
||||
import{p as s}from"./index-DhsLPY8p.js";async function r(){const{data:t}=await s.get("/email/verify-status");return t}async function e(){const{data:t}=await s.post("/generate_captcha",{});return t}async function o(t){const{data:a}=await s.post("/login",t);return a}async function i(t){const{data:a}=await s.post("/register",t);return a}async function c(t){const{data:a}=await s.post("/resend-verify-email",t);return a}async function u(t){const{data:a}=await s.post("/forgot-password",t);return a}async function f(t){const{data:a}=await s.post("/reset_password_request",t);return a}async function d(t){const{data:a}=await s.post("/reset-password-confirm",t);return a}export{u as a,c as b,i as c,d,r as f,e as g,o as l,f as r};
|
||||
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
|
||||
<title>知识管理平台</title>
|
||||
<script type="module" crossorigin src="./assets/index-DSLtIIj4.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-DhsLPY8p.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-CD3NfpmF.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1510,6 +1510,23 @@
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function getCsrfToken() {
|
||||
const match = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||
return match ? decodeURIComponent(match[1]) : '';
|
||||
}
|
||||
|
||||
const originalFetch = window.fetch.bind(window);
|
||||
window.fetch = (input, init = {}) => {
|
||||
const method = String(init.method || 'GET').toUpperCase();
|
||||
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
||||
const headers = new Headers(init.headers || {});
|
||||
const token = getCsrfToken();
|
||||
if (token) headers.set('X-CSRF-Token', token);
|
||||
init = { ...init, headers };
|
||||
}
|
||||
return originalFetch(input, init);
|
||||
};
|
||||
|
||||
// 页面加载时初始化
|
||||
window.addEventListener('load', () => {
|
||||
loadStats();
|
||||
@@ -1853,11 +1870,11 @@
|
||||
<tr>
|
||||
<td>${user.id}</td>
|
||||
<td>
|
||||
<div><strong>${user.username}</strong> ${getVipBadge(user)}</div>
|
||||
<div><strong>${escapeHtml(user.username)}</strong> ${getVipBadge(user)}</div>
|
||||
${getVipExpire(user)}
|
||||
</td>
|
||||
<td>${user.email || '-'}</td>
|
||||
<td>${user.created_at}</td>
|
||||
<td>${escapeHtml(user.email || '-')}</td>
|
||||
<td>${escapeHtml(user.created_at)}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-small btn-success" onclick="approveUser(${user.id})">通过</button>
|
||||
@@ -1903,9 +1920,9 @@
|
||||
<tr>
|
||||
<td>${user.id}</td>
|
||||
<td>
|
||||
<div><strong>${user.username}</strong> ${getVipBadge(user)}</div>
|
||||
<div><strong>${escapeHtml(user.username)}</strong> ${getVipBadge(user)}</div>
|
||||
${getVipExpire(user)}
|
||||
${user.email ? '<div class="user-info">'+user.email+'</div>' : ''}
|
||||
${user.email ? '<div class="user-info">'+escapeHtml(user.email)+'</div>' : ''}
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge status-${user.status}">
|
||||
@@ -1913,8 +1930,8 @@
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
${user.created_at}
|
||||
${user.approved_at ? '<div class="user-info">审核:'+user.approved_at+'</div>' : ''}
|
||||
${escapeHtml(user.created_at)}
|
||||
${user.approved_at ? '<div class="user-info">审核:'+escapeHtml(user.approved_at)+'</div>' : ''}
|
||||
</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
@@ -2481,7 +2498,8 @@
|
||||
runningList.innerHTML = '<div style="color: #999; text-align: center; padding: 10px;">暂无运行中的任务</div>';
|
||||
} else {
|
||||
runningList.innerHTML = data.running.map(task => {
|
||||
const source = sourceMap[task.source] || {text: task.source, color: '#666'};
|
||||
const sourceKey = String(task.source || '');
|
||||
const source = sourceMap[sourceKey] || {text: escapeHtml(sourceKey), color: '#666'};
|
||||
// 状态颜色映射
|
||||
const statusColorMap = {
|
||||
'初始化': '#6c757d',
|
||||
@@ -2491,6 +2509,11 @@
|
||||
'正在截图': '#17a2b8'
|
||||
};
|
||||
const statusColor = statusColorMap[task.detail_status] || '#666';
|
||||
const safeUser = escapeHtml(task.user_username || '');
|
||||
const safeAccount = escapeHtml(task.username || '');
|
||||
const safeBrowse = escapeHtml(task.browse_type || '');
|
||||
const safeDetail = escapeHtml(task.detail_status || '');
|
||||
const safeElapsed = escapeHtml(task.elapsed_display || '');
|
||||
// 进度显示
|
||||
const progressText = task.progress_items > 0 || task.progress_attachments > 0
|
||||
? `(${task.progress_items}/${task.progress_attachments})`
|
||||
@@ -2499,18 +2522,18 @@
|
||||
<div style="flex: 1;">
|
||||
<div style="display: flex; align-items: center; flex-wrap: wrap; gap: 5px;">
|
||||
<span style="color: ${source.color}; font-weight: 500; font-size: 12px;">[${source.text}]</span>
|
||||
<span style="color: #333;">${task.user_username}</span>
|
||||
<span style="color: #333;">${safeUser}</span>
|
||||
<span style="color: #666;">→</span>
|
||||
<span style="color: #007bff; font-weight: 500;">${task.username}</span>
|
||||
<span style="background: #e9ecef; padding: 2px 6px; border-radius: 3px; font-size: 11px; color: #666;">${task.browse_type}</span>
|
||||
<span style="color: #007bff; font-weight: 500;">${safeAccount}</span>
|
||||
<span style="background: #e9ecef; padding: 2px 6px; border-radius: 3px; font-size: 11px; color: #666;">${safeBrowse}</span>
|
||||
</div>
|
||||
<div style="margin-top: 4px; display: flex; align-items: center; gap: 8px;">
|
||||
<span style="color: ${statusColor}; font-weight: 500; font-size: 12px;">● ${task.detail_status}</span>
|
||||
<span style="color: ${statusColor}; font-weight: 500; font-size: 12px;">● ${safeDetail}</span>
|
||||
${progressText ? `<span style="color: #999; font-size: 11px;">内容/附件: ${progressText}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: right; min-width: 70px;">
|
||||
<div style="color: #28a745; font-weight: 500;">${task.elapsed_display}</div>
|
||||
<div style="color: #28a745; font-weight: 500;">${safeElapsed}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
@@ -2522,22 +2545,28 @@
|
||||
queuingList.innerHTML = '<div style="color: #999; text-align: center; padding: 10px;">暂无排队中的任务</div>';
|
||||
} else {
|
||||
queuingList.innerHTML = data.queuing.map(task => {
|
||||
const source = sourceMap[task.source] || {text: task.source, color: '#666'};
|
||||
const sourceKey = String(task.source || '');
|
||||
const source = sourceMap[sourceKey] || {text: escapeHtml(sourceKey), color: '#666'};
|
||||
const safeUser = escapeHtml(task.user_username || '');
|
||||
const safeAccount = escapeHtml(task.username || '');
|
||||
const safeBrowse = escapeHtml(task.browse_type || '');
|
||||
const safeDetail = escapeHtml(task.detail_status || '等待资源');
|
||||
const safeElapsed = escapeHtml(task.elapsed_display || '');
|
||||
return `<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #fff8e6; border-radius: 5px; margin-bottom: 5px; border-left: 3px solid #fd7e14;">
|
||||
<div style="flex: 1;">
|
||||
<div style="display: flex; align-items: center; flex-wrap: wrap; gap: 5px;">
|
||||
<span style="color: ${source.color}; font-weight: 500; font-size: 12px;">[${source.text}]</span>
|
||||
<span style="color: #333;">${task.user_username}</span>
|
||||
<span style="color: #333;">${safeUser}</span>
|
||||
<span style="color: #666;">→</span>
|
||||
<span style="color: #007bff; font-weight: 500;">${task.username}</span>
|
||||
<span style="background: #ffeeba; padding: 2px 6px; border-radius: 3px; font-size: 11px; color: #856404;">${task.browse_type}</span>
|
||||
<span style="color: #007bff; font-weight: 500;">${safeAccount}</span>
|
||||
<span style="background: #ffeeba; padding: 2px 6px; border-radius: 3px; font-size: 11px; color: #856404;">${safeBrowse}</span>
|
||||
</div>
|
||||
<div style="margin-top: 4px;">
|
||||
<span style="color: #fd7e14; font-size: 12px;">● ${task.detail_status || '等待资源'}</span>
|
||||
<span style="color: #fd7e14; font-size: 12px;">● ${safeDetail}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: right; min-width: 80px;">
|
||||
<div style="color: #fd7e14; font-weight: 500;">等待 ${task.elapsed_display}</div>
|
||||
<div style="color: #fd7e14; font-weight: 500;">等待 ${safeElapsed}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
@@ -2563,7 +2592,7 @@
|
||||
const select = document.getElementById('logUserFilter');
|
||||
select.innerHTML = '<option value="">全部</option>';
|
||||
users.forEach(user => {
|
||||
select.innerHTML += `<option value="${user.id}">${user.username}</option>`;
|
||||
select.innerHTML += `<option value="${user.id}">${escapeHtml(user.username)}</option>`;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -2653,18 +2682,24 @@
|
||||
'immediate': {text: '即时', color: '#fd7e14'},
|
||||
'resumed': {text: '恢复', color: '#6c757d'}
|
||||
};
|
||||
const sourceInfo = sourceMap[log.source] || {text: log.source || '手动', color: '#28a745'};
|
||||
const sourceKey = log.source || 'manual';
|
||||
const sourceInfo = sourceMap[sourceKey] || {text: escapeHtml(sourceKey), color: '#28a745'};
|
||||
const safeCreatedAt = escapeHtml(log.created_at || '');
|
||||
const safeUser = escapeHtml(log.user_username || 'N/A');
|
||||
const safeAccount = escapeHtml(log.username || '');
|
||||
const safeBrowse = escapeHtml(log.browse_type || '');
|
||||
const safeError = escapeHtml(log.error_message || '-');
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${log.created_at}</td>
|
||||
<td>${safeCreatedAt}</td>
|
||||
<td><span style="color: ${sourceInfo.color}; font-weight: 500;">${sourceInfo.text}</span></td>
|
||||
<td>${log.user_username || 'N/A'}</td>
|
||||
<td>${log.username}</td>
|
||||
<td>${log.browse_type}</td>
|
||||
<td>${safeUser}</td>
|
||||
<td>${safeAccount}</td>
|
||||
<td>${safeBrowse}</td>
|
||||
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
|
||||
<td>${log.total_items} / ${log.total_attachments}</td>
|
||||
<td style="color: #2F80ED; font-weight: 500;">${formatDuration(log.duration)}</td>
|
||||
<td style="color: #dc3545; font-size: 11px;">${log.error_message || '-'}</td>
|
||||
<td style="color: #dc3545; font-size: 11px;">${safeError}</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
@@ -2782,9 +2817,9 @@
|
||||
${passwordResets.map(reset => `
|
||||
<tr>
|
||||
<td>${reset.id}</td>
|
||||
<td><strong>${reset.username}</strong></td>
|
||||
<td>${reset.email || '-'}</td>
|
||||
<td>${reset.created_at}</td>
|
||||
<td><strong>${escapeHtml(reset.username)}</strong></td>
|
||||
<td>${escapeHtml(reset.email || '-')}</td>
|
||||
<td>${escapeHtml(reset.created_at)}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-small btn-success" onclick="approvePasswordReset(${reset.id})">批准</button>
|
||||
@@ -2935,13 +2970,13 @@
|
||||
feedbacksList.forEach(fb => {
|
||||
html += '<tr>';
|
||||
html += '<td>' + fb.id + '</td>';
|
||||
html += '<td><strong>' + (fb.username || 'N/A') + '</strong></td>';
|
||||
html += '<td style="max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + (fb.title||'') + '">' + (fb.title||'') + '</td>';
|
||||
html += '<td style="max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + (fb.description||'') + '">' + (fb.description||'') + '</td>';
|
||||
html += '<td>' + (fb.contact || '-') + '</td>';
|
||||
html += '<td><strong>' + escapeHtml(fb.username || 'N/A') + '</strong></td>';
|
||||
html += '<td style="max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + escapeHtml(fb.title || '') + '">' + escapeHtml(fb.title || '') + '</td>';
|
||||
html += '<td style="max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + escapeHtml(fb.description || '') + '">' + escapeHtml(fb.description || '') + '</td>';
|
||||
html += '<td>' + escapeHtml(fb.contact || '-') + '</td>';
|
||||
html += '<td>' + getStatusBadge(fb.status) + '</td>';
|
||||
html += '<td>' + fb.created_at + '</td>';
|
||||
html += '<td style="max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + (fb.admin_reply || '') + '">' + (fb.admin_reply || '-') + '</td>';
|
||||
html += '<td>' + escapeHtml(fb.created_at) + '</td>';
|
||||
html += '<td style="max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + escapeHtml(fb.admin_reply || '') + '">' + escapeHtml(fb.admin_reply || '-') + '</td>';
|
||||
html += '<td><div class="action-buttons">';
|
||||
if (fb.status !== 'closed') {
|
||||
html += '<button class="btn btn-small btn-primary" onclick="replyFeedback(' + fb.id + ')">回复</button>';
|
||||
@@ -3393,20 +3428,24 @@
|
||||
};
|
||||
|
||||
let html = '<div class="table-container"><table style="width: 100%; font-size: 12px;"><thead><tr>';
|
||||
html += '<th>时间</th><th>收件人</th><th>类型</th><th>主题</th><th>状态</th><th>错误</th>';
|
||||
html += '<th>时间</th><th>收件人</th><th>来源用户</th><th>类型</th><th>主题</th><th>状态</th><th>错误</th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
logs.forEach(log => {
|
||||
const statusClass = log.status === 'success' ? 'color: #27ae60;' : 'color: #e74c3c;';
|
||||
const statusText = log.status === 'success' ? '成功' : '失败';
|
||||
const userLabel = log.username
|
||||
? `${log.username} (#${log.user_id})`
|
||||
: (log.user_id ? `用户#${log.user_id}` : '系统');
|
||||
|
||||
html += '<tr>';
|
||||
html += `<td style="white-space: nowrap;">${log.created_at}</td>`;
|
||||
html += `<td>${log.email_to}</td>`;
|
||||
html += `<td>${typeMap[log.email_type] || log.email_type}</td>`;
|
||||
html += `<td style="max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${log.subject}">${log.subject}</td>`;
|
||||
html += `<td style="white-space: nowrap;">${escapeHtml(log.created_at)}</td>`;
|
||||
html += `<td>${escapeHtml(log.email_to)}</td>`;
|
||||
html += `<td>${escapeHtml(userLabel)}</td>`;
|
||||
html += `<td>${escapeHtml(typeMap[log.email_type] || log.email_type)}</td>`;
|
||||
html += `<td style="max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${escapeHtml(log.subject)}">${escapeHtml(log.subject)}</td>`;
|
||||
html += `<td style="${statusClass} font-weight: bold;">${statusText}</td>`;
|
||||
html += `<td style="max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #999;" title="${log.error_message || ''}">${log.error_message || '-'}</td>`;
|
||||
html += `<td style="max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #999;" title="${escapeHtml(log.error_message || '')}">${escapeHtml(log.error_message || '-')}</td>`;
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
|
||||
@@ -2151,6 +2151,23 @@
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function getCsrfToken() {
|
||||
const match = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||
return match ? decodeURIComponent(match[1]) : '';
|
||||
}
|
||||
|
||||
const originalFetch = window.fetch.bind(window);
|
||||
window.fetch = (input, init = {}) => {
|
||||
const method = String(init.method || 'GET').toUpperCase();
|
||||
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
||||
const headers = new Headers(init.headers || {});
|
||||
const token = getCsrfToken();
|
||||
if (token) headers.set('X-CSRF-Token', token);
|
||||
init = { ...init, headers };
|
||||
}
|
||||
return originalFetch(input, init);
|
||||
};
|
||||
|
||||
function logout() {
|
||||
try {
|
||||
for (let i = sessionStorage.length - 1; i >= 0; i--) {
|
||||
|
||||
Reference in New Issue
Block a user