Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion astrbot/cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import click

from . import __version__
from .commands import conf, init, plug, run
from .commands import conf, init, password, plug, run

logo_tmpl = r"""
___ _______.___________..______ .______ ______ .___________.
Expand Down Expand Up @@ -54,6 +54,7 @@ def help(command_name: str | None) -> None:
cli.add_command(help)
cli.add_command(plug)
cli.add_command(conf)
cli.add_command(password)

if __name__ == "__main__":
cli()
3 changes: 2 additions & 1 deletion astrbot/cli/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -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"]
27 changes: 17 additions & 10 deletions astrbot/cli/commands/cmd_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
38 changes: 38 additions & 0 deletions astrbot/cli/commands/cmd_password.py
Original file line number Diff line number Diff line change
@@ -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)
Comment on lines +22 to +27
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using the value_proc argument in click.prompt allows for immediate validation of the password input. This improves the user experience by allowing click to automatically re-prompt the user if the validation fails, rather than exiting the command entirely. Note that for the retry behavior to work automatically, the validator should ideally raise click.UsageError or ValueError.

Suggested change
new_password = click.prompt(
"New dashboard password",
hide_input=True,
confirmation_prompt=True,
)
validated_password = _validate_dashboard_password(new_password)
validated_password = click.prompt(
"New dashboard password",
hide_input=True,
confirmation_prompt=True,
value_proc=_validate_dashboard_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}")
90 changes: 90 additions & 0 deletions tests/test_cli_password.py
Original file line number Diff line number Diff line change
@@ -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
Loading