Skip to content

Commit 1eec68a

Browse files
feat: Add endpoint to remove all users blong to an admin
1 parent 2ecfca1 commit 1eec68a

File tree

3 files changed

+128
-3
lines changed

3 files changed

+128
-3
lines changed

app/operation/admin.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
update_admin,
1515
)
1616
from app.db.crud.bulk import activate_all_disabled_users, disable_all_active_users
17-
from app.db.crud.user import get_users
17+
from app.db.crud.user import get_users, remove_users
1818
from app.db.models import Admin as DBAdmin
1919
from app.models.admin import AdminCreate, AdminDetails, AdminModify
2020
from app.node import node_manager
2121
from app.operation import BaseOperation, OperatorType
22+
from app.operation.user import UserOperation
2223
from app.utils.logger import get_logger
2324

2425
logger = get_logger("admin-operation")
@@ -126,6 +127,32 @@ async def activate_all_disabled_users(self, db: AsyncSession, username: str, adm
126127

127128
logger.info(f'Admin "{username}" users has been activated by admin "{admin.username}"')
128129

130+
async def remove_all_users(self, db: AsyncSession, username: str, admin: AdminDetails) -> int:
131+
"""Delete all users that belong to the specified admin."""
132+
db_admin = await self.get_validated_admin(db, username=username)
133+
target_username = db_admin.username
134+
135+
if self.operator_type != OperatorType.CLI and db_admin.is_sudo:
136+
await self.raise_error(message="You're not allowed to delete sudo admin users.", code=403)
137+
138+
users = await get_users(db, admin=db_admin)
139+
if not users:
140+
return 0
141+
142+
user_operation = UserOperation(self.operator_type)
143+
serialized_users = [await user_operation.validate_user(user) for user in users]
144+
145+
await remove_users(db, users)
146+
147+
for user in serialized_users:
148+
await node_manager.remove_user(user)
149+
asyncio.create_task(notification.remove_user(user, admin))
150+
151+
logger.info(
152+
f'Admin "{admin.username}" deleted {len(serialized_users)} users belonging to admin "{target_username}"'
153+
)
154+
return len(serialized_users)
155+
129156
async def reset_admin_usage(self, db: AsyncSession, username: str, admin: AdminDetails) -> AdminDetails:
130157
db_admin = await self.get_validated_admin(db, username=username)
131158

app/routers/admin.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import asyncio
2-
from fastapi import APIRouter, Depends, HTTPException, Request, status, Header
2+
3+
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
34
from fastapi.security import OAuth2PasswordRequestForm
5+
46
from app import notification
57
from app.db import AsyncSession, get_db
68
from app.models.admin import AdminCreate, AdminDetails, AdminModify, Token
79
from app.operation import OperatorType
810
from app.operation.admin import AdminOperation
911
from app.utils import responses
1012
from app.utils.jwt import create_admin_token
13+
1114
from .authentication import check_sudo_admin, get_current, validate_admin, validate_mini_app_admin
1215

1316
router = APIRouter(tags=["Admin"], prefix="/api/admin", responses={401: responses._401, 403: responses._403})
@@ -147,6 +150,15 @@ async def activate_all_disabled_users(
147150
return {}
148151

149152

153+
@router.delete("/{username}/users", responses={403: responses._403, 404: responses._404})
154+
async def remove_all_users(
155+
username: str, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(check_sudo_admin)
156+
):
157+
"""Remove all users under a specific admin."""
158+
deleted = await admin_operator.remove_all_users(db, username=username, admin=admin)
159+
return {"detail": f"operation has been successfuly done {deleted} users deleted"}
160+
161+
150162
@router.post("/{username}/reset", response_model=AdminDetails, responses={404: responses._404})
151163
async def reset_admin_usage(
152164
username: str, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(check_sudo_admin)

tests/api/test_a_admin.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
1+
import asyncio
2+
13
from fastapi import status
24

3-
from tests.api import client
5+
from app.db.models import Group
6+
from tests.api import TestSession, client
7+
8+
9+
async def _create_group_record(name: str) -> int:
10+
async with TestSession() as session:
11+
group = Group(name=name, inbounds=[], is_disabled=False)
12+
session.add(group)
13+
await session.commit()
14+
await session.refresh(group)
15+
return group.id
16+
17+
18+
async def _delete_group_record(group_id: int):
19+
async with TestSession() as session:
20+
group = await session.get(Group, group_id)
21+
if group:
22+
await session.delete(group)
23+
await session.commit()
424

525

626
def test_admin_login():
@@ -101,6 +121,72 @@ def test_disable_admin():
101121
assert response.json()["detail"] == "your account has been disabled"
102122

103123

124+
def test_admin_delete_all_users_endpoint(access_token):
125+
"""Test deleting all users belonging to an admin."""
126+
127+
admin_username = "testadminbulkdelete"
128+
admin_password = "TestAdminBulkdelete#11"
129+
130+
response = client.post(
131+
url="/api/admin",
132+
json={"username": admin_username, "password": admin_password, "is_sudo": False},
133+
headers={"Authorization": f"Bearer {access_token}"},
134+
)
135+
assert response.status_code == status.HTTP_201_CREATED
136+
137+
group_id = asyncio.run(_create_group_record(f"{admin_username}_group"))
138+
139+
created_users = []
140+
for idx in range(2):
141+
user_name = f"{admin_username}_user_{idx}"
142+
user_response = client.post(
143+
"/api/user",
144+
headers={"Authorization": f"Bearer {access_token}"},
145+
json={
146+
"username": user_name,
147+
"proxy_settings": {},
148+
"group_ids": [group_id],
149+
"data_limit": 1024,
150+
"data_limit_reset_strategy": "no_reset",
151+
"status": "active",
152+
},
153+
)
154+
assert user_response.status_code == status.HTTP_201_CREATED
155+
created_users.append(user_name)
156+
157+
ownership_response = client.put(
158+
f"/api/user/{user_name}/set_owner",
159+
headers={"Authorization": f"Bearer {access_token}"},
160+
params={"admin_username": admin_username},
161+
)
162+
assert ownership_response.status_code == status.HTTP_200_OK
163+
assert ownership_response.json()["admin"]["username"] == admin_username
164+
165+
response = client.delete(
166+
url=f"/api/admin/{admin_username}/users",
167+
headers={"Authorization": f"Bearer {access_token}"},
168+
)
169+
assert response.status_code == status.HTTP_200_OK
170+
assert response.json()["deleted"] == len(created_users)
171+
172+
for username in created_users:
173+
user_check = client.get(
174+
"/api/users",
175+
params={"username": username},
176+
headers={"Authorization": f"Bearer {access_token}"},
177+
)
178+
assert user_check.status_code == status.HTTP_200_OK
179+
assert user_check.json()["users"] == []
180+
181+
cleanup = client.delete(
182+
url=f"/api/admin/{admin_username}",
183+
headers={"Authorization": f"Bearer {access_token}"},
184+
)
185+
assert cleanup.status_code == status.HTTP_204_NO_CONTENT
186+
187+
asyncio.run(_delete_group_record(group_id))
188+
189+
104190
def test_admin_delete(access_token):
105191
"""Test that the admin delete route is accessible."""
106192

0 commit comments

Comments
 (0)