Add admin button for delete user from bot and panel

This commit is contained in:
Bogdan Strielecki
2025-10-30 17:55:08 +03:00
parent 207e7751cd
commit 3b0b88e9a0
6 changed files with 340 additions and 19 deletions
+221 -2
View File
@@ -138,8 +138,14 @@ def get_user_card_keyboard(user_id: int, i18n_instance, lang: str) -> InlineKeyb
text=_(key="admin_user_refresh_button", default="🔄 Обновить"),
callback_data=f"user_action:refresh:{user_id}"
)
# Row 4: Destructive action
builder.button(
text=_(key="admin_user_delete_button", default="❌ Удалить пользователя"),
callback_data=f"user_action:delete_user:{user_id}"
)
# Row 4: Back button
# Row 5: Navigation
builder.button(
text=_(key="admin_user_search_new_button", default="🔍 Найти другого"),
callback_data="admin_action:users_management"
@@ -149,7 +155,7 @@ def get_user_card_keyboard(user_id: int, i18n_instance, lang: str) -> InlineKeyb
callback_data="admin_action:main"
)
builder.adjust(2, 2, 2, 2)
builder.adjust(2, 2, 2, 1, 2)
return builder
@@ -367,6 +373,10 @@ async def user_action_handler(callback: types.CallbackQuery, state: FSMContext,
await handle_view_user_logs(callback, user, session, settings, i18n, current_lang)
elif action == "refresh":
await handle_refresh_user_card(callback, user, subscription_service, session, i18n, current_lang)
elif action == "delete_user":
await handle_delete_user_prompt(
callback, state, user, settings, i18n, current_lang, session
)
else:
await callback.answer(_("admin_unknown_action"), show_alert=True)
@@ -592,8 +602,217 @@ async def handle_refresh_user_card(callback: types.CallbackQuery, user: User,
await callback.answer("Error refreshing user card", show_alert=True)
# Destructive deletion flow
async def handle_delete_user_prompt(callback: types.CallbackQuery, state: FSMContext,
user: User, settings: Settings, i18n_instance,
lang: str, session: AsyncSession):
"""Trigger confirmation workflow for destructive deletion."""
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
admin = callback.from_user
admin_id = admin.id if admin else None
if not admin_id or admin_id not in settings.ADMIN_IDS:
logging.warning(
f"Unauthorized delete attempt by user {admin_id} targeting {user.user_id}."
)
await callback.answer(
_(
"admin_user_delete_not_allowed",
default="❌ У вас нет прав для удаления пользователей.",
),
show_alert=True,
)
return
await state.update_data(
target_user_id=user.user_id,
delete_initiator_id=admin_id,
)
await state.set_state(AdminStates.waiting_for_user_delete_confirmation)
prompt_text = _(
"admin_user_delete_confirmation_prompt",
default=(
"⚠️ Вы хотите полностью удалить пользователя {user_id}.\n\n"
"Отправьте точный Telegram ID этого пользователя, чтобы подтвердить удаление.\n"
"Любой другой ответ отменит операцию."
),
user_id=hcode(str(user.user_id)),
)
try:
await callback.message.answer(prompt_text, parse_mode="HTML")
except Exception as e:
logging.error(
f"Failed to send delete confirmation prompt for user {user.user_id}: {e}"
)
await callback.message.reply(prompt_text, parse_mode="HTML")
await callback.answer()
async def _log_admin_user_deletion(
session: AsyncSession,
admin_id: int,
admin_user: Optional[types.User],
target_user_id: int,
) -> None:
"""Store audit log for successful deletion."""
try:
await message_log_dal.create_message_log_no_commit(
session,
{
"user_id": admin_id,
"telegram_username": admin_user.username if admin_user else None,
"telegram_first_name": admin_user.first_name if admin_user else None,
"event_type": "admin:user_deleted",
"content": f"Admin {admin_id} deleted user {target_user_id}",
"raw_update_preview": None,
"is_admin_event": True,
"target_user_id": target_user_id,
"timestamp": datetime.now(timezone.utc),
},
)
except Exception as e:
logging.error(
f"Failed to log deletion audit for admin {admin_id} -> user {target_user_id}: {e}",
exc_info=True,
)
# Message handlers for state-based inputs
@router.message(AdminStates.waiting_for_user_delete_confirmation, F.text)
async def process_delete_user_confirmation_handler(message: types.Message,
state: FSMContext,
settings: Settings,
i18n_data: dict,
panel_service: PanelApiService,
session: AsyncSession):
"""Confirm and execute destructive user deletion."""
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n:
await message.reply("Language service error.")
await state.clear()
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
admin = message.from_user
admin_id = admin.id if admin else None
if not admin_id or admin_id not in settings.ADMIN_IDS:
logging.warning(
f"Unauthorized delete confirmation attempt by user {admin_id}."
)
await message.answer(
_(
"admin_user_delete_not_allowed",
default="❌ У вас нет прав для удаления пользователей.",
)
)
await state.clear()
return
data = await state.get_data()
target_user_id = data.get("target_user_id")
if not target_user_id:
await message.answer(
_(
"admin_user_delete_state_missing",
default="⚠️ Нет активной операции удаления. Начните заново.",
)
)
await state.clear()
return
confirmation_input = message.text.strip() if message.text else ""
if confirmation_input.lower() in {"/cancel", "cancel", "отмена"}:
await message.answer(
_(
"admin_user_delete_cancelled",
default="Операция удаления отменена по запросу.",
)
)
await state.clear()
return
if confirmation_input != str(target_user_id):
await message.answer(
_(
"admin_user_delete_mismatch",
default="⚠️ ID не совпадает. Удаление отменено.",
)
)
await state.clear()
return
user_model = await user_dal.get_user_by_id(session, target_user_id)
if not user_model:
await message.answer(
_(
"admin_user_delete_already_removed",
default="ℹ️ Пользователь уже удален.",
)
)
await state.clear()
return
try:
if user_model.panel_user_uuid:
panel_deleted = await panel_service.delete_user_from_panel(
user_model.panel_user_uuid
)
if not panel_deleted:
await message.answer(
_(
"admin_user_delete_panel_error",
default=(
"❌ Не удалось удалить пользователя на панели. "
"Операция прервана."
),
)
)
await session.rollback()
await state.clear()
return
deleted = await user_dal.delete_user_and_relations(
session, target_user_id
)
if not deleted:
await message.answer(
_(
"admin_user_delete_already_removed",
default="ℹ️ Пользователь уже удален.",
)
)
await state.clear()
return
await _log_admin_user_deletion(session, admin_id, admin, target_user_id)
await session.commit()
await message.answer(
_(
"admin_user_delete_success",
default="✅ Пользователь {user_id} удален из бота и панели.",
user_id=hcode(str(target_user_id)),
),
parse_mode="HTML",
)
except Exception as e:
logging.error(f"Error deleting user {target_user_id}: {e}", exc_info=True)
await session.rollback()
await message.answer(
_(
"admin_user_delete_error",
default="❌ Не удалось завершить удаление пользователя. Попробуйте позже.",
)
)
finally:
await state.clear()
@router.message(AdminStates.waiting_for_subscription_days_to_add, F.text)
async def process_subscription_days_handler(message: types.Message, state: FSMContext,
settings: Settings, i18n_data: dict,
+31
View File
@@ -455,6 +455,37 @@ class PanelApiService:
)
return False
async def delete_user_from_panel(self,
user_uuid: str,
log_response: bool = True) -> bool:
"""Delete a user from the panel. Treat not-found as already deleted."""
endpoint = f"/users/{user_uuid}"
response_data = await self._request(
"DELETE", endpoint, log_full_response=log_response
)
if not response_data:
logging.error(
f"Panel API delete_user_from_panel returned no data for user {user_uuid}."
)
return False
if response_data.get("error"):
details = response_data.get("details") or {}
error_code = details.get("errorCode") or response_data.get("errorCode")
if error_code in {"A062", "A040"}:
logging.info(
f"Panel user {user_uuid} already absent (errorCode {error_code}). Treating as deleted."
)
return True
logging.error(
f"Failed to delete user {user_uuid} on panel. Response: {response_data}"
)
return False
logging.info(f"Panel user {user_uuid} deleted successfully.")
return True
async def get_subscription_link(
self,
short_uuid_or_sub_uuid: str,
+1
View File
@@ -28,6 +28,7 @@ class AdminStates(StatesGroup):
waiting_for_user_search = State()
waiting_for_subscription_days_to_add = State()
waiting_for_direct_message_to_user = State()
waiting_for_user_delete_confirmation = State()
# Ads campaigns
waiting_for_ad_source = State()