feat(password): add command to change AstrBot dashboard password (#8272)

closes: #8268
This commit is contained in:
Weilong Liao
2026-05-21 21:37:10 +08:00
committed by GitHub
parent 165933545d
commit d15606d202
5 changed files with 149 additions and 12 deletions

View File

@@ -5,7 +5,7 @@ import sys
import click
from . import __version__
from .commands import conf, init, plug, run
from .commands import conf, init, password, plug, run
logo_tmpl = r"""
___ _______.___________..______ .______ ______ .___________.
@@ -54,6 +54,7 @@ cli.add_command(run)
cli.add_command(help)
cli.add_command(plug)
cli.add_command(conf)
cli.add_command(password)
if __name__ == "__main__":
cli()

View File

@@ -1,6 +1,7 @@
from .cmd_conf import conf
from .cmd_init import init
from .cmd_password import password
from .cmd_plug import plug
from .cmd_run import run
__all__ = ["conf", "init", "plug", "run"]
__all__ = ["conf", "init", "password", "plug", "run"]

View File

@@ -137,6 +137,22 @@ def _get_nested_item(obj: dict[str, Any], path: str) -> Any:
return obj
def _set_dashboard_password(config: dict[str, Any], raw_password: str) -> None:
"""Set dashboard password hashes and clear password migration flags."""
_set_nested_item(
config,
"dashboard.pbkdf2_password",
hash_dashboard_password(raw_password),
)
_set_nested_item(
config,
"dashboard.password",
hash_legacy_dashboard_password(raw_password),
)
_set_nested_item(config, "dashboard.password_storage_upgraded", True)
_set_nested_item(config, "dashboard.password_change_required", False)
@click.group(name="conf")
def conf() -> None:
"""Configuration management commands
@@ -171,16 +187,7 @@ def set_config(key: str, value: str) -> None:
old_value = _get_nested_item(config, key)
validated_value = CONFIG_VALIDATORS[key](value)
if key == "dashboard.password":
_set_nested_item(
config,
"dashboard.pbkdf2_password",
hash_dashboard_password(validated_value),
)
_set_nested_item(
config,
"dashboard.password",
hash_legacy_dashboard_password(validated_value),
)
_set_dashboard_password(config, validated_value)
else:
_set_nested_item(config, key, validated_value)
_save_config(config)

View File

@@ -0,0 +1,38 @@
import click
from .cmd_conf import (
_load_config,
_save_config,
_set_dashboard_password,
_set_nested_item,
_validate_dashboard_password,
_validate_dashboard_username,
)
@click.command(name="password")
@click.option(
"--username",
help="Optional dashboard username to set together with the new password.",
)
def password(username: str | None) -> None:
"""Change the AstrBot dashboard password."""
config = _load_config()
new_password = click.prompt(
"New dashboard password",
hide_input=True,
confirmation_prompt=True,
)
validated_password = _validate_dashboard_password(new_password)
if username is not None:
validated_username = _validate_dashboard_username(username.strip())
_set_nested_item(config, "dashboard.username", validated_username)
_set_dashboard_password(config, validated_password)
_save_config(config)
click.echo("Dashboard password updated.")
if username is not None:
click.echo(f"Dashboard username updated: {validated_username}")

View File

@@ -0,0 +1,90 @@
import copy
import json
from click.testing import CliRunner
from astrbot.cli.commands.cmd_conf import conf
from astrbot.cli.commands.cmd_password import password
from astrbot.core.config.default import DEFAULT_CONFIG
from astrbot.core.utils.auth_password import verify_dashboard_password
def _write_config(root):
(root / ".astrbot").touch()
data_dir = root / "data"
data_dir.mkdir()
config = copy.deepcopy(DEFAULT_CONFIG)
config["dashboard"]["password_change_required"] = True
config["dashboard"]["password_storage_upgraded"] = False
config_path = data_dir / "cmd_config.json"
config_path.write_text(
json.dumps(config, ensure_ascii=False, indent=2),
encoding="utf-8-sig",
)
return config_path
def _read_config(config_path):
return json.loads(config_path.read_text(encoding="utf-8-sig"))
def test_password_command_changes_dashboard_password(monkeypatch, tmp_path):
config_path = _write_config(tmp_path)
monkeypatch.chdir(tmp_path)
runner = CliRunner()
result = runner.invoke(
password,
input="AstrbotChanged123\nAstrbotChanged123\n",
)
assert result.exit_code == 0
config = _read_config(config_path)
dashboard_config = config["dashboard"]
assert verify_dashboard_password(
dashboard_config["pbkdf2_password"],
"AstrbotChanged123",
)
assert verify_dashboard_password(
dashboard_config["password"],
"AstrbotChanged123",
)
assert dashboard_config["password_storage_upgraded"] is True
assert dashboard_config["password_change_required"] is False
def test_password_command_can_update_dashboard_username(monkeypatch, tmp_path):
config_path = _write_config(tmp_path)
monkeypatch.chdir(tmp_path)
runner = CliRunner()
result = runner.invoke(
password,
["--username", "astrbot-admin"],
input="AstrbotChanged123\nAstrbotChanged123\n",
)
assert result.exit_code == 0
config = _read_config(config_path)
assert config["dashboard"]["username"] == "astrbot-admin"
def test_conf_set_dashboard_password_updates_password_state(monkeypatch, tmp_path):
config_path = _write_config(tmp_path)
monkeypatch.chdir(tmp_path)
runner = CliRunner()
result = runner.invoke(
conf,
["set", "dashboard.password", "AstrbotChanged123"],
)
assert result.exit_code == 0
config = _read_config(config_path)
dashboard_config = config["dashboard"]
assert verify_dashboard_password(
dashboard_config["pbkdf2_password"],
"AstrbotChanged123",
)
assert dashboard_config["password_storage_upgraded"] is True
assert dashboard_config["password_change_required"] is False