diff --git a/astrbot/dashboard/services/auth_service.py b/astrbot/dashboard/services/auth_service.py index bc6cbde50..856662acb 100644 --- a/astrbot/dashboard/services/auth_service.py +++ b/astrbot/dashboard/services/auth_service.py @@ -369,6 +369,12 @@ class AuthService: if not new_pwd and not new_username: return self.error("新用户名和新密码不能同时为空") + username_to_save = None + if new_username is not None and new_username != "": + if not isinstance(new_username, str) or len(new_username.strip()) < 3: + return self.error("用户名长度至少3位") + username_to_save = new_username.strip() + if new_pwd: if not isinstance(new_pwd, str): return self.error("新密码无效") @@ -384,8 +390,8 @@ class AuthService: await set_password_change_required(self.db, self.config, False) if is_totp_enabled(self.config): await revoke_user_trusted_devices(self.db) - if new_username: - self.config["dashboard"]["username"] = new_username + if username_to_save: + self.config["dashboard"]["username"] = username_to_save self.config.save_config() diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 16356fbab..87d4ce006 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -1360,6 +1360,105 @@ async def test_generated_password_requires_password_change_until_changed( ) +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("endpoint", "method"), + [ + ("/api/auth/account/edit", "post"), + ("/api/v1/auth/account", "patch"), + ], +) +@pytest.mark.parametrize("new_username", ["ab", " "]) +async def test_account_edit_rejects_invalid_username( + app: FastAPIAppAdapter, + core_lifecycle_td: AstrBotCoreLifecycle, + endpoint: str, + method: str, + new_username: str, +): + original_dashboard_config = copy.deepcopy( + core_lifecycle_td.astrbot_config["dashboard"] + ) + test_client = app.test_client() + current_username = core_lifecycle_td.astrbot_config["dashboard"]["username"] + current_password = _resolve_dashboard_password(core_lifecycle_td) + + try: + login_response = await test_client.post( + "/api/auth/login", + json={"username": current_username, "password": current_password}, + ) + login_data = await login_response.get_json() + assert login_data["status"] == "ok" + headers = {"Authorization": f"Bearer {login_data['data']['token']}"} + + payload = { + "password": current_password, + "new_password": "", + "confirm_password": "", + "new_username": new_username, + } + request = getattr(test_client, method) + response = await request(endpoint, headers=headers, json=payload) + data = await response.get_json() + + assert data["status"] == "error" + assert data["message"] == "用户名长度至少3位" + assert ( + core_lifecycle_td.astrbot_config["dashboard"]["username"] + == (original_dashboard_config["username"]) + ) + finally: + await _restore_dashboard_password_state( + core_lifecycle_td, + original_dashboard_config, + ) + + +@pytest.mark.asyncio +async def test_account_edit_trims_valid_username( + app: FastAPIAppAdapter, + core_lifecycle_td: AstrBotCoreLifecycle, +): + original_dashboard_config = copy.deepcopy( + core_lifecycle_td.astrbot_config["dashboard"] + ) + test_client = app.test_client() + current_username = core_lifecycle_td.astrbot_config["dashboard"]["username"] + current_password = _resolve_dashboard_password(core_lifecycle_td) + + try: + login_response = await test_client.post( + "/api/auth/login", + json={"username": current_username, "password": current_password}, + ) + login_data = await login_response.get_json() + assert login_data["status"] == "ok" + headers = {"Authorization": f"Bearer {login_data['data']['token']}"} + + response = await test_client.post( + "/api/auth/account/edit", + headers=headers, + json={ + "password": current_password, + "new_password": "", + "confirm_password": "", + "new_username": " astrbot-admin ", + }, + ) + data = await response.get_json() + + assert data["status"] == "ok" + assert core_lifecycle_td.astrbot_config["dashboard"]["username"] == ( + "astrbot-admin" + ) + finally: + await _restore_dashboard_password_state( + core_lifecycle_td, + original_dashboard_config, + ) + + @pytest.mark.asyncio async def test_local_setup_can_skip_default_password_auth( app: FastAPIAppAdapter,