Kaynağa Gözat

feat: added comprehensive audit logging for all key site events

unknown 6 saat önce
ebeveyn
işleme
21d8b9faf8

+ 15 - 0
backend/routers/orders.py

@@ -128,6 +128,21 @@ async def create_order(
         background_tasks.add_task(order_processing.process_order_slicing, order_insert_id)
         background_tasks.add_task(event_hooks.on_order_created, order_insert_id)
         
+        # LOG ACTION
+        await audit_service.log(
+            user_id=user_id or 0,
+            action="order_created",
+            target_type="order",
+            target_id=order_insert_id,
+            details={
+                "email": email_addr,
+                "mat_id": material_id,
+                "price": float(estimated_price) if estimated_price is not None else 0.0,
+                "quantity": quantity
+            },
+            request=request
+        )
+        
         # Record placement for rate limiting
         rate_limit_service.record_order_placement(email_addr, ip)
         

+ 35 - 17
backend/routers/portfolio.py

@@ -8,6 +8,9 @@ import uuid
 import shutil
 from services.global_manager import global_manager
 
+from dependencies import require_admin
+from services.audit_service import audit_service
+
 router = APIRouter(tags=["portfolio"])
 
 @router.get("/portfolio")
@@ -22,10 +25,7 @@ async def get_public_portfolio():
     return db.execute_query(query)
 
 @router.get("/admin/all-photos")
-async def admin_get_all_photos(token: str = Depends(auth_utils.oauth2_scheme)):
-    payload = auth_utils.decode_token(token)
-    if not payload or payload.get("role") != 'admin':
-        raise HTTPException(status_code=403, detail="Admin role required")
+async def admin_get_all_photos(admin: dict = Depends(require_admin)):
     query = """
     SELECT p.id, p.file_path, p.is_public, p.order_id, o.allow_portfolio, 
            o.first_name, o.last_name, COALESCE(o.material_name, 'Manual') as material_name
@@ -37,14 +37,12 @@ async def admin_get_all_photos(token: str = Depends(auth_utils.oauth2_scheme)):
 
 @router.post("/admin/orders/{order_id}/photos")
 async def admin_upload_order_photo(
+    request: Request,
     order_id: int, 
     is_public: bool = Form(False),
     file: UploadFile = File(...), 
-    token: str = Depends(auth_utils.oauth2_scheme)
+    admin: dict = Depends(require_admin)
 ):
-    payload = auth_utils.decode_token(token)
-    if not payload or payload.get("role") != 'admin':
-        raise HTTPException(status_code=403, detail="Admin role required")
     order = db.execute_query("SELECT allow_portfolio FROM orders WHERE id = %s", (order_id,))
     if not order: raise HTTPException(status_code=404, detail="Order not found")
     if is_public and not order[0]['allow_portfolio']:
@@ -60,6 +58,15 @@ async def admin_upload_order_photo(
     query = "INSERT INTO order_photos (order_id, file_path, is_public) VALUES (%s, %s, %s)"
     photo_id = db.execute_commit(query, (order_id, db_file_path, is_public))
     
+    await audit_service.log(
+        user_id=admin['id'],
+        action="upload_order_photo",
+        target_type="order",
+        target_id=order_id,
+        details={"photo_id": photo_id, "is_public": is_public},
+        request=request
+    )
+
     # NOTIFY USER VIA WEBSOCKET
     order_info = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
     if order_info:
@@ -68,10 +75,7 @@ async def admin_upload_order_photo(
     return {"id": photo_id, "file_path": db_file_path, "is_public": is_public}
 
 @router.patch("/admin/photos/{photo_id}")
-async def admin_update_photo_status(photo_id: int, data: schemas.PhotoUpdate, token: str = Depends(auth_utils.oauth2_scheme)):
-    payload = auth_utils.decode_token(token)
-    if not payload or payload.get("role") != 'admin':
-        raise HTTPException(status_code=403, detail="Admin role required")
+async def admin_update_photo_status(request: Request, photo_id: int, data: schemas.PhotoUpdate, admin: dict = Depends(require_admin)):
     query = "SELECT p.*, o.allow_portfolio FROM order_photos p JOIN orders o ON p.order_id = o.id WHERE p.id = %s"
     photo_data = db.execute_query(query, (photo_id,))
     if not photo_data: raise HTTPException(status_code=404, detail="Photo not found")
@@ -79,6 +83,15 @@ async def admin_update_photo_status(photo_id: int, data: schemas.PhotoUpdate, to
         raise HTTPException(status_code=400, detail="Cannot make public: User did not consent to portfolio usage")
     db.execute_commit("UPDATE order_photos SET is_public = %s WHERE id = %s", (data.is_public, photo_id))
     
+    await audit_service.log(
+        user_id=admin['id'],
+        action="update_photo_visibility",
+        target_type="photo",
+        target_id=photo_id,
+        details={"is_public": data.is_public},
+        request=request
+    )
+
     # NOTIFY USER VIA WEBSOCKET
     order_id = photo_data[0]['order_id']
     order_info = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
@@ -88,11 +101,7 @@ async def admin_update_photo_status(photo_id: int, data: schemas.PhotoUpdate, to
     return {"id": photo_id, "is_public": data.is_public}
 
 @router.delete("/admin/photos/{photo_id}")
-async def admin_delete_photo(photo_id: int, token: str = Depends(auth_utils.oauth2_scheme)):
-    payload = auth_utils.decode_token(token)
-    if not payload or payload.get("role") != 'admin':
-        raise HTTPException(status_code=403, detail="Admin role required")
-    
+async def admin_delete_photo(request: Request, photo_id: int, admin: dict = Depends(require_admin)):
     photo = db.execute_query("SELECT file_path, order_id FROM order_photos WHERE id = %s", (photo_id,))
     if not photo:
         raise HTTPException(status_code=404, detail="Photo not found")
@@ -108,6 +117,15 @@ async def admin_delete_photo(photo_id: int, token: str = Depends(auth_utils.oaut
         
     db.execute_commit("DELETE FROM order_photos WHERE id = %s", (photo_id,))
 
+    await audit_service.log(
+        user_id=admin['id'],
+        action="delete_photo",
+        target_type="photo",
+        target_id=photo_id,
+        details={"order_id": order_id},
+        request=request
+    )
+
     # NOTIFY USER VIA WEBSOCKET
     order_info = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
     if order_info:

+ 19 - 1
backend/services/audit_service.py

@@ -30,7 +30,25 @@ class AuditService:
             "warehouse_update_item": "📝 Обновление склада",
             "warehouse_delete_item": "🗑 Удаление со склада",
             "order_status_update": "🔔 Статус заказа изменен",
-            "user_role_update": "👤 Смена роли пользователя"
+            "user_role_update": "👤 Смена роли пользователя",
+            "user_register": "🆕 Новая регистрация",
+            "user_login": "🔑 Вход в систему",
+            "order_created": "🛒 Новый заказ оформлен",
+            "order_review": "⭐ Оставлен отзыв",
+            "update_order": "✏️ Заказ отредактирован",
+            "delete_order_entirely": "🔥 Заказ полностью удален",
+            "admin_create_user": "👤 Пользователь создан админом",
+            "admin_update_user": "✏️ Пользователь отредактирован админом",
+            "password_reset_success": "🔐 Пароль успешно сброшен",
+            "upload_order_photo": "📸 Загружено фото заказа",
+            "update_photo_visibility": "👁️ Изменена видимость фото",
+            "delete_photo": "🗑️ Удалено фото из портфолио",
+            "create_material": "🏗️ Создан новый материал",
+            "update_material": "📝 Изменен материал",
+            "delete_material": "❌ Удален материал",
+            "create_service": "🛠️ Создана новая услуга",
+            "update_service": "📝 Изменена услуга",
+            "delete_service": "❌ Удалена услуга"
         }
         return mapping.get(action, action)