diff --git a/astrbot/cli/commands/cmd_conf.py b/astrbot/cli/commands/cmd_conf.py index 5a39cb2f7e..ca7cd3778d 100644 --- a/astrbot/cli/commands/cmd_conf.py +++ b/astrbot/cli/commands/cmd_conf.py @@ -1,4 +1,3 @@ -import hashlib import json import zoneinfo from collections.abc import Callable @@ -6,6 +5,11 @@ 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: diff --git a/astrbot/core/config/astrbot_config.py b/astrbot/core/config/astrbot_config.py index 77c298cac8..c6bbb28b9f 100644 --- a/astrbot/core/config/astrbot_config.py +++ b/astrbot/core/config/astrbot_config.py @@ -4,6 +4,9 @@ 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 @@ def __init__( # 检查配置完整性,并插入 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() diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 45412bdccb..9e41032f94 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -235,7 +235,7 @@ "dashboard": { "enable": True, "username": "astrbot", - "password": "77b90590a8945a7d36c963981a307dc9", + "password": "", "jwt_secret": "", "host": "0.0.0.0", "port": 6185, diff --git a/astrbot/core/utils/auth_password.py b/astrbot/core/utils/auth_password.py new file mode 100644 index 0000000000..a32259e990 --- /dev/null +++ b/astrbot/core/utils/auth_password.py @@ -0,0 +1,167 @@ +"""Utilities for dashboard password hashing and verification.""" + +import hashlib +import hmac +import re +import secrets +from typing import Any + +_PBKDF2_ITERATIONS = 600_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 get_dashboard_login_challenge(stored_hash: str) -> dict[str, Any]: + """Return the public challenge parameters needed for proof-based login.""" + if _is_legacy_md5_hash(stored_hash): + return {"algorithm": "legacy_md5"} + + if _is_pbkdf2_hash(stored_hash): + parts: list[str] = stored_hash.split("$") + if len(parts) != 4: + raise ValueError("Invalid dashboard password hash") + _, iterations_s, salt, _ = parts + return { + "algorithm": _PBKDF2_ALGORITHM, + "iterations": int(iterations_s), + "salt": salt, + } + + raise ValueError("Unsupported dashboard password hash") + + +def verify_dashboard_login_proof( + stored_hash: str, challenge_nonce: str, proof: str +) -> bool: + """Verify an HMAC-SHA256 login proof generated from the stored password secret.""" + if ( + not isinstance(stored_hash, str) + or not isinstance(challenge_nonce, str) + or not isinstance(proof, str) + ): + return False + + proof_key: bytes + if _is_legacy_md5_hash(stored_hash): + proof_key = stored_hash.lower().encode("utf-8") + elif _is_pbkdf2_hash(stored_hash): + parts: list[str] = stored_hash.split("$") + if len(parts) != 4: + return False + _, _, _, digest = parts + try: + proof_key = bytes.fromhex(digest) + except ValueError: + return False + else: + return False + + expected = hmac.new( + proof_key, + challenge_nonce.encode("utf-8"), + hashlib.sha256, + ).hexdigest() + return hmac.compare_digest(expected.lower(), proof.lower()) + + +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: + # challenge-based clients send proof, fallback clients may send plain password or md5. + 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) diff --git a/astrbot/dashboard/routes/auth.py b/astrbot/dashboard/routes/auth.py index eac5f65b0b..9277279b88 100644 --- a/astrbot/dashboard/routes/auth.py +++ b/astrbot/dashboard/routes/auth.py @@ -1,11 +1,20 @@ -import asyncio import datetime +import secrets import jwt from quart import request from astrbot import logger from astrbot.core import DEMO_MODE +from astrbot.core.utils.auth_password import ( + get_dashboard_login_challenge, + hash_dashboard_password, + is_default_dashboard_password, + is_legacy_dashboard_password, + validate_dashboard_password, + verify_dashboard_login_proof, + verify_dashboard_password, +) from .route import Response, Route, RouteContext @@ -13,25 +22,98 @@ class AuthRoute(Route): def __init__(self, context: RouteContext) -> None: super().__init__(context) + self._login_challenges: dict[str, dict[str, object]] = {} self.routes = { + "/auth/login/challenge": ("POST", self.login_challenge), "/auth/login": ("POST", self.login), "/auth/account/edit": ("POST", self.edit_account), } self.register_routes() + async def login_challenge(self): + password = self.config["dashboard"]["password"] + self._prune_login_challenges() + + try: + challenge = get_dashboard_login_challenge(password) + except ValueError as exc: + logger.error("Failed to create dashboard login challenge: %s", exc) + return ( + Response() + .error("Unsupported dashboard password configuration") + .__dict__ + ) + + challenge_id = secrets.token_hex(16) + nonce = secrets.token_hex(32) + self._login_challenges[challenge_id] = { + "nonce": nonce, + "expires_at": datetime.datetime.now(datetime.timezone.utc) + + datetime.timedelta(minutes=1), + } + + return ( + Response() + .ok( + { + "challenge_id": challenge_id, + "nonce": nonce, + **challenge, + } + ) + .__dict__ + ) + async def login(self): 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 + ) + req_challenge_id = ( + post_data.get("challenge_id") if isinstance(post_data, dict) else None + ) + req_password_proof = ( + post_data.get("password_proof") if isinstance(post_data, dict) else None + ) + if not isinstance(req_username, str): + return Response().error("Invalid request payload").__dict__ + + login_verified = False + if isinstance(req_password, str): + login_verified = req_username == username and verify_dashboard_password( + password, req_password + ) + elif isinstance(req_challenge_id, str) and isinstance(req_password_proof, str): + challenge_nonce = self._consume_login_challenge(req_challenge_id) + login_verified = ( + req_username == username + and isinstance(challenge_nonce, str) + and verify_dashboard_login_proof( + password, challenge_nonce, req_password_proof + ) + ) + else: + return Response().error("Invalid request payload").__dict__ + + if login_verified: 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( + "The dashboard is using the default password, please change it immediately to ensure security." + ) + legacy_pwd_hint = True return ( Response() @@ -40,12 +122,30 @@ async def login(self): "token": self.generate_jwt(username), "username": username, "change_pwd_hint": change_pwd_hint, + "legacy_pwd_hint": legacy_pwd_hint, }, ) .__dict__ ) - await asyncio.sleep(3) - return Response().error("用户名或密码错误").__dict__ + return Response().error("User not found or incorrect password").__dict__ + + def _prune_login_challenges(self) -> None: + now = datetime.datetime.now(datetime.timezone.utc) + expired_ids = [ + challenge_id + for challenge_id, challenge in self._login_challenges.items() + if challenge.get("expires_at") <= now + ] + for challenge_id in expired_ids: + self._login_challenges.pop(challenge_id, None) + + def _consume_login_challenge(self, challenge_id: str) -> str | None: + self._prune_login_challenges() + challenge = self._login_challenges.pop(challenge_id, None) + if not isinstance(challenge, dict): + return None + nonce = challenge.get("nonce") + return nonce if isinstance(nonce, str) else None async def edit_account(self): if DEMO_MODE: @@ -57,8 +157,14 @@ async def edit_account(self): password = self.config["dashboard"]["password"] post_data = await request.json + if not isinstance(post_data, dict): + return Response().error("Invalid request payload").__dict__ + + req_password = post_data.get("password") + if not isinstance(req_password, str): + return Response().error("Invalid request payload").__dict__ - if post_data["password"] != password: + if not verify_dashboard_password(password, req_password): return Response().error("原密码错误").__dict__ new_pwd = post_data.get("new_password", None) @@ -68,16 +174,22 @@ async def edit_account(self): # 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 self.config.save_config() - return Response().ok(None, "修改成功").__dict__ + return Response().ok(None, "Updated account successfully").__dict__ def generate_jwt(self, username): payload = { diff --git a/astrbot/dashboard/routes/stat.py b/astrbot/dashboard/routes/stat.py index 2eb3cd400e..efeb0631e3 100644 --- a/astrbot/dashboard/routes/stat.py +++ b/astrbot/dashboard/routes/stat.py @@ -12,7 +12,7 @@ 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.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 @@ def is_default_cred(self): 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 @@ async def get_version(self): "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 @@ async def get_provider_token_stats(self): 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() diff --git a/dashboard/src/i18n/locales/en-US/core/header.json b/dashboard/src/i18n/locales/en-US/core/header.json index 4da98b8dd8..b934c9bc19 100644 --- a/dashboard/src/i18n/locales/en-US/core/header.json +++ b/dashboard/src/i18n/locales/en-US/core/header.json @@ -80,20 +80,24 @@ }, "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": "At least 12 characters, including uppercase, lowercase letters, and digits", "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 default username and password are astrbot. Please change them immediately after logging in to ensure security." }, "validation": { "passwordRequired": "Please enter password", - "passwordMinLength": "Password must be at least 8 characters", + "passwordMinLength": "Password must be at least 12 characters", + "passwordUppercase": "Password must include at least one uppercase letter", + "passwordLowercase": "Password must include at least one lowercase letter", + "passwordDigit": "Password must include at least one digit", "passwordMatch": "Passwords do not match", "usernameMinLength": "Username must be at least 3 characters" }, diff --git a/dashboard/src/i18n/locales/en-US/features/auth.json b/dashboard/src/i18n/locales/en-US/features/auth.json index 5c44558a03..e3bbb600b7 100644 --- a/dashboard/src/i18n/locales/en-US/features/auth.json +++ b/dashboard/src/i18n/locales/en-US/features/auth.json @@ -2,7 +2,7 @@ "login": "Login", "username": "Username", "password": "Password", - "defaultHint": "Default username and password: astrbot", + "defaultHint": "Default credentials: astrbot / astrbot", "logo": { "title": "AstrBot Dashboard", "subtitle": "Welcome" @@ -11,4 +11,4 @@ "switchToDark": "Switch to Dark Theme", "switchToLight": "Switch to Light Theme" } -} \ No newline at end of file +} diff --git a/dashboard/src/i18n/locales/en-US/messages/validation.json b/dashboard/src/i18n/locales/en-US/messages/validation.json index 407f638c57..1b34f5b5f1 100644 --- a/dashboard/src/i18n/locales/en-US/messages/validation.json +++ b/dashboard/src/i18n/locales/en-US/messages/validation.json @@ -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!" -} \ No newline at end of file +} diff --git a/dashboard/src/i18n/locales/ru-RU/core/header.json b/dashboard/src/i18n/locales/ru-RU/core/header.json index a23f3dfd33..e4abca0a87 100644 --- a/dashboard/src/i18n/locales/ru-RU/core/header.json +++ b/dashboard/src/i18n/locales/ru-RU/core/header.json @@ -1,4 +1,4 @@ -{ +{ "logoTitle": "Панель управления AstrBot", "version": { "hasNewVersion": "Доступна новая версия AstrBot!", @@ -80,20 +80,24 @@ }, "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 символов", + "passwordUppercase": "Пароль должен содержать хотя бы одну заглавную букву", + "passwordLowercase": "Пароль должен содержать хотя бы одну строчную букву", + "passwordDigit": "Пароль должен содержать хотя бы одну цифру", "passwordMatch": "Паролы не совпадают", "usernameMinLength": "Имя пользователя должно быть не менее 3 символов" }, @@ -105,4 +109,4 @@ "updateFailed": "Ошибка обновления, попробуйте еще раз" } } -} \ No newline at end of file +} diff --git a/dashboard/src/i18n/locales/ru-RU/features/auth.json b/dashboard/src/i18n/locales/ru-RU/features/auth.json index d6ba05dc3b..6f7ab79cf0 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/auth.json +++ b/dashboard/src/i18n/locales/ru-RU/features/auth.json @@ -2,7 +2,7 @@ "login": "Вход", "username": "Имя пользователя", "password": "Пароль", - "defaultHint": "Логин и пароль по умолчанию: astrbot", + "defaultHint": "Учетные данные по умолчанию: astrbot / astrbot", "logo": { "title": "Панель управления AstrBot", "subtitle": "Добро пожаловать" @@ -11,4 +11,4 @@ "switchToDark": "Перейти на темную тему", "switchToLight": "Перейти на светлую тему" } -} \ No newline at end of file +} diff --git a/dashboard/src/i18n/locales/ru-RU/messages/validation.json b/dashboard/src/i18n/locales/ru-RU/messages/validation.json index 07d0615796..800e541fd9 100644 --- a/dashboard/src/i18n/locales/ru-RU/messages/validation.json +++ b/dashboard/src/i18n/locales/ru-RU/messages/validation.json @@ -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": "⚠️ Это действие нельзя отменить, будьте осторожны!" -} \ No newline at end of file +} diff --git a/dashboard/src/i18n/locales/zh-CN/core/header.json b/dashboard/src/i18n/locales/zh-CN/core/header.json index 3bb9850331..86cd8928bc 100644 --- a/dashboard/src/i18n/locales/zh-CN/core/header.json +++ b/dashboard/src/i18n/locales/zh-CN/core/header.json @@ -80,20 +80,24 @@ }, "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 位", + "passwordUppercase": "密码必须包含至少一个大写字母", + "passwordLowercase": "密码必须包含至少一个小写字母", + "passwordDigit": "密码必须包含至少一个数字", "passwordMatch": "两次输入的密码不一致", "usernameMinLength": "用户名长度至少3位" }, diff --git a/dashboard/src/i18n/locales/zh-CN/features/auth.json b/dashboard/src/i18n/locales/zh-CN/features/auth.json index d6da999430..b927d9a0d9 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/auth.json +++ b/dashboard/src/i18n/locales/zh-CN/features/auth.json @@ -2,7 +2,7 @@ "login": "登录", "username": "用户名", "password": "密码", - "defaultHint": "默认账户和密码均为:astrbot", + "defaultHint": "默认用户名和密码为 astrbot,请在登录后立即修改以确保安全。", "logo": { "title": "AstrBot WebUI", "subtitle": "欢迎使用" @@ -11,4 +11,4 @@ "switchToDark": "切换到深色主题", "switchToLight": "切换到浅色主题" } -} \ No newline at end of file +} diff --git a/dashboard/src/i18n/locales/zh-CN/messages/validation.json b/dashboard/src/i18n/locales/zh-CN/messages/validation.json index da12ae79b7..98e9df8915 100644 --- a/dashboard/src/i18n/locales/zh-CN/messages/validation.json +++ b/dashboard/src/i18n/locales/zh-CN/messages/validation.json @@ -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": "⚠️ 此操作无法撤销,请谨慎选择!" -} \ No newline at end of file +} diff --git a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue index 1fb32b48a0..9cde43453f 100644 --- a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue +++ b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue @@ -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,10 @@ 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'), + (v: string) => /[A-Z]/.test(v) || t('core.header.accountDialog.validation.passwordUppercase'), + (v: string) => /[a-z]/.test(v) || t('core.header.accountDialog.validation.passwordLowercase'), + (v: string) => /\d/.test(v) || t('core.header.accountDialog.validation.passwordDigit') ]); const confirmPasswordRules = computed(() => [ (v: string) => !newPassword.value || !!v || t('core.header.accountDialog.validation.passwordRequired'), @@ -225,9 +229,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 +275,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 +297,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 +415,7 @@ function handleLogoClick() { getVersion(); checkUpdate(); +initPasswordWarningFromStorage(); const commonStore = useCommonStore(); commonStore.createEventSource(); // log @@ -889,14 +913,18 @@ onMounted(async () => { - +