mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-18 10:00:40 +08:00
feat: add support for resetting dashboard initial password on startup
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -70,12 +70,14 @@ Common options:
|
||||
| --- | --- |
|
||||
| `-p, --port <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
|
||||
|
||||
@@ -70,12 +70,14 @@ astrbot run
|
||||
| --- | --- |
|
||||
| `-p, --port <PORT>` | 指定 WebUI 端口。 |
|
||||
| `-r, --reload` | 启用插件自动重载,适合插件开发调试。 |
|
||||
| `--reset-password` | 启动时重置 WebUI 初始密码,并在启动日志中打印新的初始密码。 |
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
astrbot run --port 6185
|
||||
astrbot run --reload
|
||||
astrbot run --reset-password
|
||||
```
|
||||
|
||||
## 后台服务
|
||||
|
||||
15
main.py
15
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()
|
||||
|
||||
22
tests/test_cli_run.py
Normal file
22
tests/test_cli_run.py
Normal file
@@ -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"]
|
||||
@@ -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
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user