mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
fix(webui): enforce 12-char dashboard password policy with backend+frontend validation
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import hashlib
|
||||
import json
|
||||
import zoneinfo
|
||||
from collections.abc import Callable
|
||||
@@ -6,6 +5,11 @@ from typing import Any
|
||||
|
||||
import click
|
||||
|
||||
from astrbot.core.utils.auth_password import (
|
||||
hash_dashboard_password,
|
||||
validate_dashboard_password,
|
||||
)
|
||||
|
||||
from ..utils import check_astrbot_root, get_astrbot_root
|
||||
|
||||
|
||||
@@ -39,9 +43,11 @@ def _validate_dashboard_username(value: str) -> str:
|
||||
|
||||
def _validate_dashboard_password(value: str) -> str:
|
||||
"""Validate Dashboard password"""
|
||||
if not value:
|
||||
raise click.ClickException("Password cannot be empty")
|
||||
return hashlib.md5(value.encode()).hexdigest()
|
||||
try:
|
||||
validate_dashboard_password(value)
|
||||
except ValueError as e:
|
||||
raise click.ClickException(str(e))
|
||||
return hash_dashboard_password(value)
|
||||
|
||||
|
||||
def _validate_timezone(value: str) -> str:
|
||||
|
||||
@@ -4,6 +4,9 @@ import logging
|
||||
import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.auth_password import (
|
||||
normalize_dashboard_password_hash,
|
||||
)
|
||||
|
||||
from .default import DEFAULT_CONFIG, DEFAULT_VALUE_MAP
|
||||
|
||||
@@ -59,6 +62,13 @@ class AstrBotConfig(dict):
|
||||
|
||||
# 检查配置完整性,并插入
|
||||
has_new = self.check_config_integrity(default_config, conf)
|
||||
if (
|
||||
"dashboard" in conf
|
||||
and isinstance(conf["dashboard"], dict)
|
||||
and not conf["dashboard"].get("password")
|
||||
):
|
||||
conf["dashboard"]["password"] = normalize_dashboard_password_hash("")
|
||||
has_new = True
|
||||
self.update(conf)
|
||||
if has_new:
|
||||
self.save_config()
|
||||
|
||||
@@ -235,7 +235,7 @@ DEFAULT_CONFIG = {
|
||||
"dashboard": {
|
||||
"enable": True,
|
||||
"username": "astrbot",
|
||||
"password": "77b90590a8945a7d36c963981a307dc9",
|
||||
"password": "",
|
||||
"jwt_secret": "",
|
||||
"host": "0.0.0.0",
|
||||
"port": 6185,
|
||||
|
||||
113
astrbot/core/utils/auth_password.py
Normal file
113
astrbot/core/utils/auth_password.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""Utilities for dashboard password hashing and verification."""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import re
|
||||
import secrets
|
||||
|
||||
_PBKDF2_ITERATIONS = 200_000
|
||||
_PBKDF2_SALT_BYTES = 16
|
||||
_PBKDF2_ALGORITHM = "pbkdf2_sha256"
|
||||
_PBKDF2_FORMAT = f"{_PBKDF2_ALGORITHM}$"
|
||||
_LEGACY_MD5_LENGTH = 32
|
||||
_DASHBOARD_PASSWORD_MIN_LENGTH = 12
|
||||
DEFAULT_DASHBOARD_PASSWORD = "astrbot"
|
||||
|
||||
|
||||
def hash_dashboard_password(raw_password: str) -> str:
|
||||
"""Return a salted hash for dashboard password using PBKDF2-HMAC-SHA256."""
|
||||
if not isinstance(raw_password, str) or raw_password == "":
|
||||
raise ValueError("Password cannot be empty")
|
||||
|
||||
salt = secrets.token_hex(_PBKDF2_SALT_BYTES)
|
||||
digest = hashlib.pbkdf2_hmac(
|
||||
"sha256",
|
||||
raw_password.encode("utf-8"),
|
||||
bytes.fromhex(salt),
|
||||
_PBKDF2_ITERATIONS,
|
||||
).hex()
|
||||
return f"{_PBKDF2_FORMAT}{_PBKDF2_ITERATIONS}${salt}${digest}"
|
||||
|
||||
|
||||
def validate_dashboard_password(raw_password: str) -> None:
|
||||
"""Validate whether dashboard password meets the minimal complexity policy."""
|
||||
if not isinstance(raw_password, str) or raw_password == "":
|
||||
raise ValueError("Password cannot be empty")
|
||||
if len(raw_password) < _DASHBOARD_PASSWORD_MIN_LENGTH:
|
||||
raise ValueError(
|
||||
f"Password must be at least {_DASHBOARD_PASSWORD_MIN_LENGTH} characters long"
|
||||
)
|
||||
|
||||
if not re.search(r"[A-Z]", raw_password):
|
||||
raise ValueError("Password must include at least one uppercase letter")
|
||||
if not re.search(r"[a-z]", raw_password):
|
||||
raise ValueError("Password must include at least one lowercase letter")
|
||||
if not re.search(r"\d", raw_password):
|
||||
raise ValueError("Password must include at least one digit")
|
||||
|
||||
|
||||
def normalize_dashboard_password_hash(stored_password: str) -> str:
|
||||
"""Ensure dashboard password has a value, fallback to default dashboard password hash."""
|
||||
if not stored_password:
|
||||
return hash_dashboard_password(DEFAULT_DASHBOARD_PASSWORD)
|
||||
return stored_password
|
||||
|
||||
|
||||
def _is_legacy_md5_hash(stored: str) -> bool:
|
||||
return (
|
||||
isinstance(stored, str)
|
||||
and len(stored) == _LEGACY_MD5_LENGTH
|
||||
and all(c in "0123456789abcdefABCDEF" for c in stored)
|
||||
)
|
||||
|
||||
|
||||
def _is_pbkdf2_hash(stored: str) -> bool:
|
||||
return isinstance(stored, str) and stored.startswith(_PBKDF2_FORMAT)
|
||||
|
||||
|
||||
def verify_dashboard_password(stored_hash: str, candidate_password: str) -> bool:
|
||||
"""Verify password against legacy md5 or new PBKDF2-SHA256 format."""
|
||||
if not isinstance(stored_hash, str) or not isinstance(candidate_password, str):
|
||||
return False
|
||||
|
||||
if _is_legacy_md5_hash(stored_hash):
|
||||
# Keep compatibility with existing md5-based deployments:
|
||||
# new clients send plain password, old clients may send md5 of it.
|
||||
candidate_md5 = hashlib.md5(candidate_password.encode("utf-8")).hexdigest()
|
||||
return hmac.compare_digest(
|
||||
stored_hash.lower(), candidate_md5.lower()
|
||||
) or hmac.compare_digest(
|
||||
stored_hash.lower(),
|
||||
candidate_password.lower(),
|
||||
)
|
||||
|
||||
if _is_pbkdf2_hash(stored_hash):
|
||||
parts: list[str] = stored_hash.split("$")
|
||||
if len(parts) != 4:
|
||||
return False
|
||||
_, iterations_s, salt, digest = parts
|
||||
try:
|
||||
iterations = int(iterations_s)
|
||||
stored_key = bytes.fromhex(digest)
|
||||
salt_bytes = bytes.fromhex(salt)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
candidate_key = hashlib.pbkdf2_hmac(
|
||||
"sha256",
|
||||
candidate_password.encode("utf-8"),
|
||||
salt_bytes,
|
||||
iterations,
|
||||
)
|
||||
return hmac.compare_digest(stored_key, candidate_key)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_default_dashboard_password(stored_hash: str) -> bool:
|
||||
"""Check whether the password still equals the built-in default value."""
|
||||
return verify_dashboard_password(stored_hash, DEFAULT_DASHBOARD_PASSWORD)
|
||||
|
||||
|
||||
def is_legacy_dashboard_password(stored_hash: str) -> bool:
|
||||
"""Check whether the password is still stored with legacy MD5."""
|
||||
return _is_legacy_md5_hash(stored_hash)
|
||||
@@ -6,6 +6,13 @@ from quart import request
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot.core import DEMO_MODE
|
||||
from astrbot.core.utils.auth_password import (
|
||||
hash_dashboard_password,
|
||||
is_default_dashboard_password,
|
||||
is_legacy_dashboard_password,
|
||||
validate_dashboard_password,
|
||||
verify_dashboard_password,
|
||||
)
|
||||
|
||||
from .route import Response, Route, RouteContext
|
||||
|
||||
@@ -23,15 +30,29 @@ class AuthRoute(Route):
|
||||
username = self.config["dashboard"]["username"]
|
||||
password = self.config["dashboard"]["password"]
|
||||
post_data = await request.json
|
||||
if post_data["username"] == username and post_data["password"] == password:
|
||||
|
||||
req_username = (
|
||||
post_data.get("username") if isinstance(post_data, dict) else None
|
||||
)
|
||||
req_password = (
|
||||
post_data.get("password") if isinstance(post_data, dict) else None
|
||||
)
|
||||
if not isinstance(req_username, str) or not isinstance(req_password, str):
|
||||
return Response().error("Invalid request payload").__dict__
|
||||
|
||||
if req_username == username and verify_dashboard_password(
|
||||
password, req_password
|
||||
):
|
||||
change_pwd_hint = False
|
||||
legacy_pwd_hint = is_legacy_dashboard_password(password)
|
||||
if (
|
||||
username == "astrbot"
|
||||
and password == "77b90590a8945a7d36c963981a307dc9"
|
||||
and is_default_dashboard_password(password)
|
||||
and not DEMO_MODE
|
||||
):
|
||||
change_pwd_hint = True
|
||||
logger.warning("为了保证安全,请尽快修改默认密码。")
|
||||
logger.warning("检测到默认管理员凭据,请尽快修改密码。")
|
||||
legacy_pwd_hint = True
|
||||
|
||||
return (
|
||||
Response()
|
||||
@@ -40,6 +61,7 @@ class AuthRoute(Route):
|
||||
"token": self.generate_jwt(username),
|
||||
"username": username,
|
||||
"change_pwd_hint": change_pwd_hint,
|
||||
"legacy_pwd_hint": legacy_pwd_hint,
|
||||
},
|
||||
)
|
||||
.__dict__
|
||||
@@ -57,8 +79,14 @@ class AuthRoute(Route):
|
||||
|
||||
password = self.config["dashboard"]["password"]
|
||||
post_data = await request.json
|
||||
if not isinstance(post_data, dict):
|
||||
return Response().error("Invalid request payload").__dict__
|
||||
|
||||
if post_data["password"] != password:
|
||||
req_password = post_data.get("password")
|
||||
if not isinstance(req_password, str):
|
||||
return Response().error("Invalid request payload").__dict__
|
||||
|
||||
if not verify_dashboard_password(password, req_password):
|
||||
return Response().error("原密码错误").__dict__
|
||||
|
||||
new_pwd = post_data.get("new_password", None)
|
||||
@@ -68,10 +96,16 @@ class AuthRoute(Route):
|
||||
|
||||
# Verify password confirmation
|
||||
if new_pwd:
|
||||
if not isinstance(new_pwd, str):
|
||||
return Response().error("新密码无效").__dict__
|
||||
confirm_pwd = post_data.get("confirm_password", None)
|
||||
if confirm_pwd != new_pwd:
|
||||
if not isinstance(confirm_pwd, str) or confirm_pwd != new_pwd:
|
||||
return Response().error("两次输入的新密码不一致").__dict__
|
||||
self.config["dashboard"]["password"] = new_pwd
|
||||
try:
|
||||
validate_dashboard_password(new_pwd)
|
||||
except ValueError as e:
|
||||
return Response().error(str(e)).__dict__
|
||||
self.config["dashboard"]["password"] = hash_dashboard_password(new_pwd)
|
||||
if new_username:
|
||||
self.config["dashboard"]["username"] = new_username
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
||||
import aiohttp
|
||||
import psutil
|
||||
from quart import request
|
||||
from sqlmodel import select
|
||||
from sqlmodel import col, select
|
||||
|
||||
from astrbot.core import DEMO_MODE, logger
|
||||
from astrbot.core.config import VERSION
|
||||
@@ -21,6 +21,10 @@ from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.db.migration.helper import check_migration_needed_v4
|
||||
from astrbot.core.db.po import ProviderStat
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_path
|
||||
from astrbot.core.utils.auth_password import (
|
||||
is_default_dashboard_password,
|
||||
is_legacy_dashboard_password,
|
||||
)
|
||||
from astrbot.core.utils.io import get_dashboard_version
|
||||
from astrbot.core.utils.storage_cleaner import StorageCleaner
|
||||
from astrbot.core.utils.version_comparator import VersionComparator
|
||||
@@ -76,7 +80,7 @@ class StatRoute(Route):
|
||||
password = self.config["dashboard"]["password"]
|
||||
return (
|
||||
username == "astrbot"
|
||||
and password == "77b90590a8945a7d36c963981a307dc9"
|
||||
and is_default_dashboard_password(password)
|
||||
and not DEMO_MODE
|
||||
)
|
||||
|
||||
@@ -90,6 +94,9 @@ class StatRoute(Route):
|
||||
"version": VERSION,
|
||||
"dashboard_version": await get_dashboard_version(),
|
||||
"change_pwd_hint": self.is_default_cred(),
|
||||
"legacy_pwd_hint": is_legacy_dashboard_password(
|
||||
self.config["dashboard"]["password"]
|
||||
),
|
||||
"need_migration": need_migration,
|
||||
},
|
||||
)
|
||||
@@ -226,7 +233,7 @@ class StatRoute(Route):
|
||||
ProviderStat.agent_type == "internal",
|
||||
ProviderStat.created_at >= query_start_utc,
|
||||
)
|
||||
.order_by(ProviderStat.created_at.asc())
|
||||
.order_by(col(ProviderStat.created_at).asc())
|
||||
)
|
||||
records = result.scalars().all()
|
||||
|
||||
|
||||
@@ -80,20 +80,21 @@
|
||||
},
|
||||
"accountDialog": {
|
||||
"title": "Modify Account",
|
||||
"securityWarning": "Security Reminder: Please change the default password to ensure account security",
|
||||
"securityWarning": "Security Reminder: Please change your password to secure your account",
|
||||
"securityWarningLegacy": "The new AstrBot version has improved security. Please change your password.",
|
||||
"form": {
|
||||
"currentPassword": "Current Password",
|
||||
"newPassword": "New Password",
|
||||
"confirmPassword": "Confirm New Password",
|
||||
"newUsername": "New Username (Optional)",
|
||||
"passwordHint": "Password must be at least 8 characters",
|
||||
"passwordHint": "Password must be at least 12 characters",
|
||||
"confirmPasswordHint": "Please enter new password again to confirm",
|
||||
"usernameHint": "Leave blank to keep current username",
|
||||
"defaultCredentials": "Default username and password are both astrbot"
|
||||
"defaultCredentials": "The new AstrBot version has improved security. Please change your password."
|
||||
},
|
||||
"validation": {
|
||||
"passwordRequired": "Please enter password",
|
||||
"passwordMinLength": "Password must be at least 8 characters",
|
||||
"passwordMinLength": "Password must be at least 12 characters",
|
||||
"passwordMatch": "Passwords do not match",
|
||||
"usernameMinLength": "Username must be at least 3 characters"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"login": "Login",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"defaultHint": "Default username and password: astrbot",
|
||||
"defaultHint": "The new AstrBot version has improved security. Please change your password.",
|
||||
"logo": {
|
||||
"title": "AstrBot Dashboard",
|
||||
"subtitle": "Welcome"
|
||||
@@ -11,4 +11,4 @@
|
||||
"switchToDark": "Switch to Dark Theme",
|
||||
"switchToLight": "Switch to Light Theme"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"fileType": "Unsupported file type",
|
||||
"required_field": "Please fill in the required field",
|
||||
"invalid_format": "Invalid format",
|
||||
"password_too_short": "Password must be at least 8 characters",
|
||||
"password_too_short": "Password must be at least 12 characters",
|
||||
"password_too_weak": "Password is too weak",
|
||||
"invalid_phone": "Please enter a valid phone number",
|
||||
"invalid_date": "Please enter a valid date",
|
||||
@@ -22,4 +22,4 @@
|
||||
"upload_failed": "File upload failed",
|
||||
"network_error": "Network connection error, please try again",
|
||||
"operation_cannot_be_undone": "⚠️ This operation cannot be undone, please choose carefully!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"logoTitle": "Панель управления AstrBot",
|
||||
"version": {
|
||||
"hasNewVersion": "Доступна новая версия AstrBot!",
|
||||
@@ -80,20 +80,21 @@
|
||||
},
|
||||
"accountDialog": {
|
||||
"title": "Изменить аккаунт",
|
||||
"securityWarning": "Безопасность: Пожалуйста, смените пароль по умолчанию для защиты аккаунта",
|
||||
"securityWarning": "Безопасность: Пожалуйста, смените пароль для защиты аккаунта",
|
||||
"securityWarningLegacy": "Новая версия AstrBot улучшила безопасность. Пожалуйста, измените пароль.",
|
||||
"form": {
|
||||
"currentPassword": "Текущий пароль",
|
||||
"newPassword": "Новый пароль",
|
||||
"confirmPassword": "Подтвердите новый пароль",
|
||||
"newUsername": "Новое имя пользователя (опционально)",
|
||||
"passwordHint": "Пароль должен быть не менее 8 символов",
|
||||
"passwordHint": "Пароль должен быть не менее 12 символов",
|
||||
"confirmPasswordHint": "Введите новый пароль еще раз",
|
||||
"usernameHint": "Оставьте пустым, если не хотите менять имя пользователя",
|
||||
"defaultCredentials": "Логин и пароль по умолчанию: astrbot"
|
||||
"defaultCredentials": "Новая версия AstrBot улучшила безопасность. Пожалуйста, измените пароль."
|
||||
},
|
||||
"validation": {
|
||||
"passwordRequired": "Введите пароль",
|
||||
"passwordMinLength": "Пароль должен быть не менее 8 символов",
|
||||
"passwordMinLength": "Пароль должен быть не менее 12 символов",
|
||||
"passwordMatch": "Паролы не совпадают",
|
||||
"usernameMinLength": "Имя пользователя должно быть не менее 3 символов"
|
||||
},
|
||||
@@ -105,4 +106,4 @@
|
||||
"updateFailed": "Ошибка обновления, попробуйте еще раз"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"login": "Вход",
|
||||
"username": "Имя пользователя",
|
||||
"password": "Пароль",
|
||||
"defaultHint": "Логин и пароль по умолчанию: astrbot",
|
||||
"defaultHint": "Новая версия AstrBot улучшила безопасность. Пожалуйста, измените пароль.",
|
||||
"logo": {
|
||||
"title": "Панель управления AstrBot",
|
||||
"subtitle": "Добро пожаловать"
|
||||
@@ -11,4 +11,4 @@
|
||||
"switchToDark": "Перейти на темную тему",
|
||||
"switchToLight": "Перейти на светлую тему"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"fileType": "Неподдерживаемый тип файла",
|
||||
"required_field": "Заполните обязательные поля",
|
||||
"invalid_format": "Некорректный формат",
|
||||
"password_too_short": "Пароль должен быть не менее 8 символов",
|
||||
"password_too_short": "Пароль должен быть не менее 12 символов",
|
||||
"password_too_weak": "Пароль слишком слабый",
|
||||
"invalid_phone": "Некорректный номер телефона",
|
||||
"invalid_date": "Некорректная дата",
|
||||
@@ -22,4 +22,4 @@
|
||||
"upload_failed": "Загрузка не удалась",
|
||||
"network_error": "Ошибка сети, попробуйте снова",
|
||||
"operation_cannot_be_undone": "⚠️ Это действие нельзя отменить, будьте осторожны!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,20 +80,21 @@
|
||||
},
|
||||
"accountDialog": {
|
||||
"title": "修改账户",
|
||||
"securityWarning": "安全提醒: 请修改默认密码以确保账户安全",
|
||||
"securityWarning": "安全提醒: 请修改密码以确保账户安全",
|
||||
"securityWarningLegacy": "新的 AstrBot 版本强化了安全策略,请重新修改密码",
|
||||
"form": {
|
||||
"currentPassword": "当前密码",
|
||||
"newPassword": "新密码",
|
||||
"confirmPassword": "确认新密码",
|
||||
"newUsername": "新用户名 (可选)",
|
||||
"passwordHint": "密码长度至少 8 位",
|
||||
"passwordHint": "密码长度至少 12 位",
|
||||
"confirmPasswordHint": "请再次输入新密码以确认",
|
||||
"usernameHint": "留空表示不修改用户名",
|
||||
"defaultCredentials": "默认用户名和密码均为 astrbot"
|
||||
"defaultCredentials": "默认用户名和密码为 astrbot,请在登录后立即修改以确保安全。"
|
||||
},
|
||||
"validation": {
|
||||
"passwordRequired": "请输入密码",
|
||||
"passwordMinLength": "密码长度至少 8 位",
|
||||
"passwordMinLength": "密码长度至少 12 位",
|
||||
"passwordMatch": "两次输入的密码不一致",
|
||||
"usernameMinLength": "用户名长度至少3位"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"login": "登录",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"defaultHint": "默认账户和密码均为:astrbot",
|
||||
"defaultHint": "默认用户名和密码为 astrbot,请在登录后立即修改以确保安全。",
|
||||
"logo": {
|
||||
"title": "AstrBot WebUI",
|
||||
"subtitle": "欢迎使用"
|
||||
@@ -11,4 +11,4 @@
|
||||
"switchToDark": "切换到深色主题",
|
||||
"switchToLight": "切换到浅色主题"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"fileType": "不支持的文件类型",
|
||||
"required_field": "请填写必填字段",
|
||||
"invalid_format": "格式无效",
|
||||
"password_too_short": "密码至少需要8个字符",
|
||||
"password_too_short": "密码至少需要12个字符",
|
||||
"password_too_weak": "密码强度太弱",
|
||||
"invalid_phone": "请输入有效的手机号码",
|
||||
"invalid_date": "请输入有效的日期",
|
||||
@@ -22,4 +22,4 @@
|
||||
"upload_failed": "文件上传失败",
|
||||
"network_error": "网络连接错误,请重试",
|
||||
"operation_cannot_be_undone": "⚠️ 此操作无法撤销,请谨慎选择!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
import axios from 'axios';
|
||||
import Logo from '@/components/shared/Logo.vue';
|
||||
import { md5 } from 'js-md5';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
import { MarkdownRender, enableKatex, enableMermaid } from 'markstream-vue';
|
||||
@@ -19,6 +18,7 @@ import { useLanguageSwitcher } from '@/i18n/composables';
|
||||
import type { Locale } from '@/i18n/types';
|
||||
import AboutPage from '@/views/AboutPage.vue';
|
||||
import { getDesktopRuntimeInfo } from '@/utils/desktopRuntime';
|
||||
import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue';
|
||||
|
||||
enableKatex();
|
||||
enableMermaid();
|
||||
@@ -31,6 +31,7 @@ const LAST_BOT_ROUTE_KEY = 'astrbot:last_bot_route';
|
||||
const LAST_CHAT_ROUTE_KEY = 'astrbot:last_chat_route';
|
||||
let dialog = ref(false);
|
||||
let accountWarning = ref(false)
|
||||
let accountWarningLegacy = ref(false);
|
||||
let updateStatusDialog = ref(false);
|
||||
let aboutDialog = ref(false);
|
||||
const username = localStorage.getItem('user');
|
||||
@@ -100,7 +101,7 @@ const releasesHeader = computed(() => [
|
||||
const formValid = ref(true);
|
||||
const passwordRules = computed(() => [
|
||||
(v: string) => !!v || t('core.header.accountDialog.validation.passwordRequired'),
|
||||
(v: string) => v.length >= 8 || t('core.header.accountDialog.validation.passwordMinLength')
|
||||
(v: string) => v.length >= 12 || t('core.header.accountDialog.validation.passwordMinLength')
|
||||
]);
|
||||
const confirmPasswordRules = computed(() => [
|
||||
(v: string) => !newPassword.value || !!v || t('core.header.accountDialog.validation.passwordRequired'),
|
||||
@@ -225,9 +226,9 @@ function accountEdit() {
|
||||
accountEditStatus.value.error = false;
|
||||
accountEditStatus.value.success = false;
|
||||
|
||||
const passwordHash = password.value ? md5(password.value) : '';
|
||||
const newPasswordHash = newPassword.value ? md5(newPassword.value) : '';
|
||||
const confirmPasswordHash = confirmPassword.value ? md5(confirmPassword.value) : '';
|
||||
const passwordHash = password.value ? password.value : '';
|
||||
const newPasswordHash = newPassword.value ? newPassword.value : '';
|
||||
const confirmPasswordHash = confirmPassword.value ? confirmPassword.value : '';
|
||||
|
||||
axios.post('/api/auth/account/edit', {
|
||||
password: passwordHash,
|
||||
@@ -271,12 +272,21 @@ function getVersion() {
|
||||
botCurrVersion.value = "v" + res.data.data.version;
|
||||
dashboardCurrentVersion.value = res.data.data?.dashboard_version;
|
||||
let change_pwd_hint = res.data.data?.change_pwd_hint;
|
||||
if (change_pwd_hint) {
|
||||
const legacy_pwd_hint = res.data.data?.legacy_pwd_hint;
|
||||
if (change_pwd_hint || legacy_pwd_hint) {
|
||||
dialog.value = true;
|
||||
accountWarning.value = true;
|
||||
accountWarningLegacy.value = !!legacy_pwd_hint;
|
||||
localStorage.setItem('change_pwd_hint', 'true');
|
||||
if (legacy_pwd_hint) {
|
||||
localStorage.setItem('legacy_pwd_hint', 'true');
|
||||
} else {
|
||||
localStorage.removeItem('legacy_pwd_hint');
|
||||
}
|
||||
} else {
|
||||
accountWarningLegacy.value = false;
|
||||
localStorage.removeItem('change_pwd_hint');
|
||||
localStorage.removeItem('legacy_pwd_hint');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -284,6 +294,16 @@ function getVersion() {
|
||||
});
|
||||
}
|
||||
|
||||
function initPasswordWarningFromStorage() {
|
||||
const hasChangePwdHint = localStorage.getItem('change_pwd_hint') === 'true';
|
||||
const hasLegacyPwdHint = localStorage.getItem('legacy_pwd_hint') === 'true';
|
||||
if (hasChangePwdHint || hasLegacyPwdHint) {
|
||||
dialog.value = true;
|
||||
accountWarning.value = true;
|
||||
accountWarningLegacy.value = hasLegacyPwdHint;
|
||||
}
|
||||
}
|
||||
|
||||
function checkUpdate() {
|
||||
updateStatus.value = t('core.header.updateDialog.status.checking');
|
||||
axios.get('/api/update/check')
|
||||
@@ -392,6 +412,7 @@ function handleLogoClick() {
|
||||
|
||||
getVersion();
|
||||
checkUpdate();
|
||||
initPasswordWarningFromStorage();
|
||||
|
||||
const commonStore = useCommonStore();
|
||||
commonStore.createEventSource(); // log
|
||||
@@ -889,14 +910,18 @@ onMounted(async () => {
|
||||
</v-dialog>
|
||||
|
||||
<!-- 账户对话框 -->
|
||||
<v-dialog v-model="dialog" persistent :max-width="$vuetify.display.xs ? '90%' : '500'">
|
||||
<v-dialog v-model="dialog" persistent :max-width="$vuetify.display.xs ? '90%' : '500'">
|
||||
<v-card class="account-dialog">
|
||||
<v-card-text class="py-6">
|
||||
<div class="d-flex flex-column align-center mb-6">
|
||||
<logo :title="t('core.header.logoTitle')" :subtitle="t('core.header.accountDialog.title')"></logo>
|
||||
<div class="account-dialog-header mb-6">
|
||||
<div class="d-flex justify-space-between align-center w-100">
|
||||
<img width="80" src="@/assets/images/icon-no-shadow.svg" alt="AstrBot Logo">
|
||||
</div>
|
||||
<div class="ml-2" style="font-size: 26px;">{{ t('core.header.logoTitle') }}</div>
|
||||
<div class="mt-2 ml-2" style="font-size: 14px; color: grey;">{{ t('core.header.accountDialog.title') }}</div>
|
||||
</div>
|
||||
<v-alert v-if="accountWarning" type="warning" variant="tonal" border="start" class="mb-4">
|
||||
<strong>{{ t('core.header.accountDialog.securityWarning') }}</strong>
|
||||
<strong>{{ t(accountWarningLegacy ? 'core.header.accountDialog.securityWarningLegacy' : 'core.header.accountDialog.securityWarning') }}</strong>
|
||||
</v-alert>
|
||||
|
||||
<v-alert v-if="accountEditStatus.success" type="success" variant="tonal" border="start" class="mb-4">
|
||||
@@ -936,8 +961,6 @@ onMounted(async () => {
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="!accountWarning" variant="tonal" color="secondary" @click="dialog = false"
|
||||
@@ -1006,6 +1029,20 @@ onMounted(async () => {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.account-dialog-header {
|
||||
.theme-toggle-btn {
|
||||
opacity: 0.85;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme-toggle-btn {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* 响应式布局样式 */
|
||||
.logo-container {
|
||||
margin-left: 10px;
|
||||
|
||||
@@ -24,7 +24,17 @@ export const useAuthStore = defineStore({
|
||||
this.username = res.data.data.username
|
||||
localStorage.setItem('user', this.username);
|
||||
localStorage.setItem('token', res.data.data.token);
|
||||
localStorage.setItem('change_pwd_hint', res.data.data?.change_pwd_hint);
|
||||
if (res.data.data?.change_pwd_hint || res.data.data?.legacy_pwd_hint) {
|
||||
localStorage.setItem('change_pwd_hint', 'true');
|
||||
if (res.data.data?.legacy_pwd_hint) {
|
||||
localStorage.setItem('legacy_pwd_hint', 'true');
|
||||
} else {
|
||||
localStorage.removeItem('legacy_pwd_hint');
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem('change_pwd_hint');
|
||||
localStorage.removeItem('legacy_pwd_hint');
|
||||
}
|
||||
|
||||
const onboardingCompleted = await this.checkOnboardingCompleted();
|
||||
this.returnUrl = null;
|
||||
@@ -70,6 +80,8 @@ export const useAuthStore = defineStore({
|
||||
this.username = '';
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('change_pwd_hint');
|
||||
localStorage.removeItem('legacy_pwd_hint');
|
||||
router.push('/auth/login');
|
||||
},
|
||||
has_token(): boolean {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { ref, useCssModule } from 'vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { Form } from 'vee-validate';
|
||||
import md5 from 'js-md5';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
const { tm: t } = useModuleI18n('features/auth');
|
||||
@@ -17,17 +16,10 @@ const loading = ref(false);
|
||||
async function validate(values: any, { setErrors }: any) {
|
||||
loading.value = true;
|
||||
|
||||
// md5加密
|
||||
let password_ = password.value;
|
||||
if (password.value != '') {
|
||||
// @ts-ignore
|
||||
password_ = md5(password.value);
|
||||
}
|
||||
|
||||
const authStore = useAuthStore();
|
||||
// @ts-ignore
|
||||
authStore.returnUrl = new URLSearchParams(window.location.search).get('redirect');
|
||||
return authStore.login(username.value, password_).then((res) => {
|
||||
return authStore.login(username.value, password.value).then((res) => {
|
||||
console.log(res);
|
||||
loading.value = false;
|
||||
}).catch((err) => {
|
||||
|
||||
Reference in New Issue
Block a user