From aa7bd5e5adeafc77897f9317cc52296661cf9411 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 22 May 2026 19:34:22 +0800 Subject: [PATCH] feat: add support for resetting dashboard initial password on startup Co-authored-by: Copilot --- astrbot/cli/commands/cmd_run.py | 12 ++++- astrbot/core/config/astrbot_config.py | 30 ++++++++---- docs/en/use/cli.md | 2 + docs/zh/use/cli.md | 2 + main.py | 15 ++++++ tests/test_cli_run.py | 22 +++++++++ tests/unit/test_config.py | 70 ++++++++++++++++++++++++--- 7 files changed, 135 insertions(+), 18 deletions(-) create mode 100644 tests/test_cli_run.py diff --git a/astrbot/cli/commands/cmd_run.py b/astrbot/cli/commands/cmd_run.py index de09e5852..4da818a09 100644 --- a/astrbot/cli/commands/cmd_run.py +++ b/astrbot/cli/commands/cmd_run.py @@ -9,6 +9,8 @@ from filelock import FileLock, Timeout from ..utils import check_astrbot_root, check_dashboard, get_astrbot_root +DASHBOARD_RESET_PASSWORD_ENV = "ASTRBOT_DASHBOARD_RESET_PASSWORD" + async def run_astrbot(astrbot_root: Path) -> None: """Run AstrBot""" @@ -28,8 +30,13 @@ async def run_astrbot(astrbot_root: Path) -> None: @click.option("--reload", "-r", is_flag=True, help="Auto-reload plugins") @click.option("--port", "-p", help="AstrBot Dashboard port", required=False, type=str) +@click.option( + "--reset-password", + is_flag=True, + help="Force reset the dashboard initial password on startup.", +) @click.command() -def run(reload: bool, port: str) -> None: +def run(reload: bool, port: str, reset_password: bool) -> None: """Run AstrBot""" try: os.environ["ASTRBOT_CLI"] = "1" @@ -50,6 +57,9 @@ def run(reload: bool, port: str) -> None: click.echo("Plugin auto-reload enabled") os.environ["ASTRBOT_RELOAD"] = "1" + if reset_password: + os.environ[DASHBOARD_RESET_PASSWORD_ENV] = "1" + lock_file = astrbot_root / "astrbot.lock" lock = FileLock(lock_file, timeout=5) with lock.acquire(): diff --git a/astrbot/core/config/astrbot_config.py b/astrbot/core/config/astrbot_config.py index 4d62becb5..117489be6 100644 --- a/astrbot/core/config/astrbot_config.py +++ b/astrbot/core/config/astrbot_config.py @@ -15,6 +15,7 @@ from .default import DEFAULT_CONFIG, DEFAULT_VALUE_MAP ASTRBOT_CONFIG_PATH = os.path.join(get_astrbot_data_path(), "cmd_config.json") DASHBOARD_INITIAL_PASSWORD_ENV = "ASTRBOT_DASHBOARD_INITIAL_PASSWORD" +DASHBOARD_RESET_PASSWORD_ENV = "ASTRBOT_DASHBOARD_RESET_PASSWORD" logger = logging.getLogger("astrbot") @@ -76,21 +77,21 @@ class AstrBotConfig(dict): ) # 检查配置完整性,并插入 has_new = self.check_config_integrity(default_config, conf) + dashboard_reset_requested = self._is_dashboard_password_reset_requested() if ( "dashboard" in conf and isinstance(conf["dashboard"], dict) - and not conf["dashboard"].get("pbkdf2_password") - and not conf["dashboard"].get("password") - ): - self._reset_generated_dashboard_password(conf) - has_new = True - elif ( - "dashboard" in conf - and isinstance(conf["dashboard"], dict) - and legacy_dashboard_password_change_required - and conf["dashboard"].get("pbkdf2_password") + and ( + dashboard_reset_requested + or ( + not conf["dashboard"].get("pbkdf2_password") + and not conf["dashboard"].get("password") + ) + ) ): self._reset_generated_dashboard_password(conf) + if dashboard_reset_requested: + os.environ[DASHBOARD_RESET_PASSWORD_ENV] = "0" has_new = True self.update(conf) if has_new: @@ -127,6 +128,15 @@ class AstrBotConfig(dict): validate_dashboard_password(env_password) return env_password + @staticmethod + def _is_dashboard_password_reset_requested() -> bool: + return os.environ.get(DASHBOARD_RESET_PASSWORD_ENV, "").strip().lower() in { + "1", + "true", + "yes", + "on", + } + def _config_schema_to_default_config(self, schema: dict) -> dict: """将 Schema 转换成 Config""" conf = {} diff --git a/docs/en/use/cli.md b/docs/en/use/cli.md index 04ec2599b..a0dbb1bdb 100644 --- a/docs/en/use/cli.md +++ b/docs/en/use/cli.md @@ -70,12 +70,14 @@ Common options: | --- | --- | | `-p, --port ` | Set the WebUI port. | | `-r, --reload` | Enable plugin auto-reload for plugin development. | +| `--reset-password` | Reset the WebUI initial password on startup and print the new initial password in startup logs. | Examples: ```bash astrbot run --port 6185 astrbot run --reload +astrbot run --reset-password ``` ## Background Service diff --git a/docs/zh/use/cli.md b/docs/zh/use/cli.md index 19b715ec6..71a19f746 100644 --- a/docs/zh/use/cli.md +++ b/docs/zh/use/cli.md @@ -70,12 +70,14 @@ astrbot run | --- | --- | | `-p, --port ` | 指定 WebUI 端口。 | | `-r, --reload` | 启用插件自动重载,适合插件开发调试。 | +| `--reset-password` | 启动时重置 WebUI 初始密码,并在启动日志中打印新的初始密码。 | 示例: ```bash astrbot run --port 6185 astrbot run --reload +astrbot run --reset-password ``` ## 后台服务 diff --git a/main.py b/main.py index 781f00637..c620f1af3 100644 --- a/main.py +++ b/main.py @@ -9,6 +9,16 @@ import runtime_bootstrap runtime_bootstrap.initialize_runtime_bootstrap() +DASHBOARD_RESET_PASSWORD_ENV = "ASTRBOT_DASHBOARD_RESET_PASSWORD" + + +def _prime_startup_flags(argv: list[str]) -> None: + if "--reset-password" in argv: + os.environ[DASHBOARD_RESET_PASSWORD_ENV] = "1" + + +_prime_startup_flags(sys.argv[1:]) + from astrbot.core import LogBroker, LogManager, db_helper, logger # noqa: E402 from astrbot.core.config.default import VERSION # noqa: E402 from astrbot.core.initial_loader import InitialLoader # noqa: E402 @@ -140,6 +150,11 @@ if __name__ == "__main__": help="Specify the directory path for WebUI static files", default=None, ) + parser.add_argument( + "--reset-password", + action="store_true", + help="Force reset the dashboard initial password on startup.", + ) args = parser.parse_args() check_env() diff --git a/tests/test_cli_run.py b/tests/test_cli_run.py new file mode 100644 index 000000000..ff6c26a81 --- /dev/null +++ b/tests/test_cli_run.py @@ -0,0 +1,22 @@ +import os + +from click.testing import CliRunner + +from astrbot.cli.commands import cmd_run + + +def test_run_reset_password_sets_startup_env(monkeypatch, tmp_path): + monkeypatch.chdir(tmp_path) + monkeypatch.delenv(cmd_run.DASHBOARD_RESET_PASSWORD_ENV, raising=False) + (tmp_path / ".astrbot").touch() + observed_reset_flags = [] + + async def fake_run_astrbot(_astrbot_root): + observed_reset_flags.append(os.environ.get(cmd_run.DASHBOARD_RESET_PASSWORD_ENV)) + + monkeypatch.setattr(cmd_run, "run_astrbot", fake_run_astrbot) + + result = CliRunner().invoke(cmd_run.run, ["--reset-password"]) + + assert result.exit_code == 0 + assert observed_reset_flags == ["1"] diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 7afe82ebe..f168c0a4f 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -10,6 +10,8 @@ from astrbot.core.config.default import DEFAULT_VALUE_MAP from astrbot.core.config.i18n_utils import ConfigMetadataI18n from astrbot.core.utils.auth_password import ( DEFAULT_DASHBOARD_PASSWORD, + hash_dashboard_password, + hash_legacy_dashboard_password, validate_dashboard_password, verify_dashboard_password, ) @@ -276,15 +278,20 @@ class TestAstrBotConfigLoad: default_config=default_config, ) - def test_legacy_password_change_required_rotates_and_keeps_config_flag( + def test_password_change_required_keeps_existing_password( self, temp_config_path ): - """Test that the setup flag stays in dashboard config.""" + """Test that the setup flag no longer rotates the initial password.""" + existing_password = "ExistingPass123" + existing_pbkdf2_password = hash_dashboard_password(existing_password) + existing_legacy_password = hash_legacy_dashboard_password(existing_password) default_config = { "dashboard": { "username": "astrbot", "password": "", "pbkdf2_password": "", + "password_storage_upgraded": False, + "password_change_required": False, }, } with open(temp_config_path, "w", encoding="utf-8") as f: @@ -292,8 +299,9 @@ class TestAstrBotConfigLoad: { "dashboard": { "username": "astrbot", - "password": "", - "pbkdf2_password": "pbkdf2_sha256$600000$00$00", + "password": existing_legacy_password, + "pbkdf2_password": existing_pbkdf2_password, + "password_storage_upgraded": True, "password_change_required": True, } }, @@ -306,7 +314,7 @@ class TestAstrBotConfigLoad: ) generated_password = getattr(config, "_generated_dashboard_password", None) - assert isinstance(generated_password, str) + assert generated_password is None assert config["dashboard"]["password_change_required"] is True assert config["dashboard"]["password_storage_upgraded"] is True assert ( @@ -314,12 +322,60 @@ class TestAstrBotConfigLoad: is True ) assert verify_dashboard_password( - config["dashboard"]["pbkdf2_password"], generated_password + config["dashboard"]["pbkdf2_password"], existing_password ) assert verify_dashboard_password( - config["dashboard"]["password"], generated_password + config["dashboard"]["password"], existing_password ) + def test_reset_password_env_rotates_existing_password( + self, temp_config_path, monkeypatch + ): + """Test that explicit reset rotates dashboard password on startup.""" + existing_password = "ExistingPass123" + reset_password = "ResetPass123" + monkeypatch.setenv("ASTRBOT_DASHBOARD_RESET_PASSWORD", "1") + monkeypatch.setenv("ASTRBOT_DASHBOARD_INITIAL_PASSWORD", reset_password) + default_config = { + "dashboard": { + "username": "astrbot", + "password": "", + "pbkdf2_password": "", + "password_storage_upgraded": False, + "password_change_required": False, + }, + } + with open(temp_config_path, "w", encoding="utf-8") as f: + json.dump( + { + "dashboard": { + "username": "astrbot", + "password": hash_legacy_dashboard_password(existing_password), + "pbkdf2_password": hash_dashboard_password(existing_password), + "password_storage_upgraded": True, + "password_change_required": False, + } + }, + f, + ) + + config = AstrBotConfig( + config_path=temp_config_path, + default_config=default_config, + ) + + assert getattr(config, "_generated_dashboard_password", None) == reset_password + assert verify_dashboard_password( + config["dashboard"]["pbkdf2_password"], reset_password + ) + assert verify_dashboard_password(config["dashboard"]["password"], reset_password) + assert not verify_dashboard_password( + config["dashboard"]["pbkdf2_password"], existing_password + ) + assert config["dashboard"]["password_change_required"] is True + assert config["dashboard"]["password_storage_upgraded"] is True + assert os.environ["ASTRBOT_DASHBOARD_RESET_PASSWORD"] == "0" + def test_legacy_astrbot_user_without_change_flag_keeps_legacy_password( self, temp_config_path ):