Spaces:
Running
Running
Commit Β·
2bcedf3
1
Parent(s): 8ac226e
fyp
Browse files
app/__pycache__/database.cpython-313.pyc
CHANGED
|
Binary files a/app/__pycache__/database.cpython-313.pyc and b/app/__pycache__/database.cpython-313.pyc differ
|
|
|
app/routes/admin.py
CHANGED
|
@@ -177,6 +177,45 @@ async def list_shadow_users(
|
|
| 177 |
return {"success": True, "data": users, "total": len(users)}
|
| 178 |
|
| 179 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
@router.get("/shadow-users/{shadow_user_id}", status_code=status.HTTP_200_OK)
|
| 181 |
async def get_shadow_user(
|
| 182 |
shadow_user_id: str,
|
|
|
|
| 177 |
return {"success": True, "data": users, "total": len(users)}
|
| 178 |
|
| 179 |
|
| 180 |
+
@router.delete("/users/{user_id}", status_code=status.HTTP_200_OK)
|
| 181 |
+
async def delete_user(
|
| 182 |
+
user_id: str,
|
| 183 |
+
admin: dict = Depends(require_admin),
|
| 184 |
+
):
|
| 185 |
+
"""
|
| 186 |
+
Permanently delete any user account (regular or shadow) and their listings.
|
| 187 |
+
Cannot delete other admin accounts.
|
| 188 |
+
"""
|
| 189 |
+
if not ObjectId.is_valid(user_id):
|
| 190 |
+
raise HTTPException(status_code=400, detail="Invalid user ID.")
|
| 191 |
+
|
| 192 |
+
db = await get_db()
|
| 193 |
+
target = await db.users.find_one({"_id": ObjectId(user_id)})
|
| 194 |
+
if not target:
|
| 195 |
+
raise HTTPException(status_code=404, detail="User not found.")
|
| 196 |
+
|
| 197 |
+
# Prevent deleting other admins
|
| 198 |
+
if target.get("is_admin") or target.get("role") == "admin":
|
| 199 |
+
raise HTTPException(status_code=403, detail="Cannot delete admin accounts.")
|
| 200 |
+
|
| 201 |
+
oid = ObjectId(user_id)
|
| 202 |
+
|
| 203 |
+
# Delete user's listings
|
| 204 |
+
listings_result = await db.listings.delete_many({"owner_id": user_id})
|
| 205 |
+
|
| 206 |
+
# Delete the user
|
| 207 |
+
await db.users.delete_one({"_id": oid})
|
| 208 |
+
|
| 209 |
+
logger.info(
|
| 210 |
+
f"ποΈ Admin deleted user {user_id} | listings_removed={listings_result.deleted_count}"
|
| 211 |
+
)
|
| 212 |
+
return {
|
| 213 |
+
"success": True,
|
| 214 |
+
"message": "User and their listings have been deleted.",
|
| 215 |
+
"listings_removed": listings_result.deleted_count,
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
|
| 219 |
@router.get("/shadow-users/{shadow_user_id}", status_code=status.HTTP_200_OK)
|
| 220 |
async def get_shadow_user(
|
| 221 |
shadow_user_id: str,
|
scripts/delete_shadow_users.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
delete_shadow_users.py
|
| 3 |
+
======================
|
| 4 |
+
One-time utility to wipe all shadow users (and their listings) from MongoDB.
|
| 5 |
+
|
| 6 |
+
Run from the AIDA/ directory:
|
| 7 |
+
python scripts/delete_shadow_users.py
|
| 8 |
+
|
| 9 |
+
Pass --dry-run to preview what would be deleted without actually deleting:
|
| 10 |
+
python scripts/delete_shadow_users.py --dry-run
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import asyncio
|
| 14 |
+
import logging
|
| 15 |
+
import os
|
| 16 |
+
import sys
|
| 17 |
+
|
| 18 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 19 |
+
|
| 20 |
+
from app.database import connect_db, get_db, disconnect_db
|
| 21 |
+
|
| 22 |
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
|
| 23 |
+
logger = logging.getLogger(__name__)
|
| 24 |
+
|
| 25 |
+
DRY_RUN = "--dry-run" in sys.argv
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
async def run():
|
| 29 |
+
await connect_db()
|
| 30 |
+
try:
|
| 31 |
+
db = await get_db()
|
| 32 |
+
|
| 33 |
+
# ββ 1. Find all shadow users ββββββββββββββββββββββββββββββββββββββ
|
| 34 |
+
shadow_cursor = db.users.find(
|
| 35 |
+
{"account_type": "shadow"},
|
| 36 |
+
{"_id": 1, "display_name": 1, "whatsapp_number": 1},
|
| 37 |
+
)
|
| 38 |
+
shadow_users = await shadow_cursor.to_list(length=None)
|
| 39 |
+
|
| 40 |
+
if not shadow_users:
|
| 41 |
+
logger.info("No shadow users found β nothing to delete.")
|
| 42 |
+
return
|
| 43 |
+
|
| 44 |
+
logger.info(f"Found {len(shadow_users)} shadow user(s):")
|
| 45 |
+
for u in shadow_users:
|
| 46 |
+
logger.info(
|
| 47 |
+
f" β’ {u.get('display_name', 'β')} | {u.get('whatsapp_number', 'β')} | id={u['_id']}"
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
if DRY_RUN:
|
| 51 |
+
logger.info("\n[DRY RUN] No changes made. Remove --dry-run to execute.")
|
| 52 |
+
return
|
| 53 |
+
|
| 54 |
+
# ββ 2. Collect their IDs ββββββββββββββββββββββββββββββββββββββββββ
|
| 55 |
+
ids = [str(u["_id"]) for u in shadow_users]
|
| 56 |
+
|
| 57 |
+
# ββ 3. Delete their listings βββββββββββββββββββββββββββββββββββββ
|
| 58 |
+
listings_result = await db.listings.delete_many({"owner_id": {"$in": ids}})
|
| 59 |
+
logger.info(f"Deleted {listings_result.deleted_count} listing(s) owned by shadow users.")
|
| 60 |
+
|
| 61 |
+
# ββ 4. Delete the shadow users themselves βββββββββββββββββββββββββ
|
| 62 |
+
from bson import ObjectId
|
| 63 |
+
oids = [u["_id"] for u in shadow_users]
|
| 64 |
+
users_result = await db.users.delete_many({"_id": {"$in": oids}})
|
| 65 |
+
logger.info(f"Deleted {users_result.deleted_count} shadow user(s).")
|
| 66 |
+
|
| 67 |
+
logger.info("\nβ
Done. All shadow users and their listings have been removed.")
|
| 68 |
+
|
| 69 |
+
finally:
|
| 70 |
+
await disconnect_db()
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
if __name__ == "__main__":
|
| 74 |
+
asyncio.run(run())
|