Sfoglia il codice sorgente

feat: implement comprehensive audit logging and fix UI display

unknown 1 giorno fa
parent
commit
2b0dc21943

+ 46 - 2
backend/routers/auth.py

@@ -14,6 +14,7 @@ import config
 import secrets
 from services.email_service import send_verification_email, send_password_reset_email
 from services.token_service import token_service
+from services.audit_service import audit_service
 
 try:
     from google.oauth2 import id_token
@@ -48,6 +49,15 @@ async def register(request: Request, user: schemas.UserCreate, lang: str = "en")
     send_verification_email(user.email, token, user.preferred_language or lang)
 
     new_user = db.execute_query("SELECT id, email, first_name, last_name, phone, shipping_address, preferred_language, role, can_chat, is_active, is_company, company_name, company_pib, company_address, ip_address, created_at FROM users WHERE id = %s", (user_id,))
+    
+    await audit_service.log(
+        user_id=user_id,
+        action="user_register",
+        target_type="user",
+        target_id=user_id,
+        details={"email": user.email, "ip": ip_address},
+        request=request
+    )
     return new_user[0]
 
 @router.get("/verify-email")
@@ -104,6 +114,15 @@ async def login(request: Request, user_data: schemas.UserLogin, lang: str = "en"
     access_token = auth_utils.create_access_token(
         data={"sub": user[0]['email'], "id": user[0]['id'], "role": user[0]['role']}
     )
+    
+    await audit_service.log(
+        user_id=user[0]['id'],
+        action="user_login",
+        target_type="user",
+        target_id=user[0]['id'],
+        details={"ip": ip, "method": "credentials"},
+        request=request
+    )
     return {"access_token": access_token, "token_type": "bearer"}
 
 @router.post("/social-login", response_model=schemas.Token)
@@ -221,6 +240,13 @@ async def reset_password(request: schemas.ResetPassword):
     token_service.cleanup_reset_tokens(user_id)
     session_utils.delete_all_user_sessions(user_id)
     
+    await audit_service.log(
+        user_id=user_id,
+        action="password_reset_success",
+        target_type="user",
+        target_id=user_id
+    )
+    
     return {"message": "Password updated successfully"}
 
 @router.get("/me", response_model=schemas.UserResponse)
@@ -267,7 +293,7 @@ async def admin_get_users(page: int = 1, size: int = 50, search: Optional[str] =
     return {"users": users, "total": total, "page": page, "size": size}
 
 @router.post("/admin/users", response_model=schemas.UserResponse)
-async def admin_create_user(data: schemas.UserCreate, admin: dict = Depends(require_admin)):
+async def admin_create_user(request: Request, data: schemas.UserCreate, admin: dict = Depends(require_admin)):
     
     existing_user = db.execute_query("SELECT id FROM users WHERE email = %s", (data.email,))
     if existing_user:
@@ -279,11 +305,20 @@ async def admin_create_user(data: schemas.UserCreate, admin: dict = Depends(requ
         (data.email, hashed_password, data.first_name, data.last_name, data.phone, 'user', True)
     )
     
+    await audit_service.log(
+        user_id=admin.get("id"),
+        action="admin_create_user",
+        target_type="user",
+        target_id=user_id,
+        details={"email": data.email, "role": "user"},
+        request=request
+    )
+    
     user = db.execute_query("SELECT id, email, first_name, last_name, phone, shipping_address, preferred_language, role, can_chat, is_active, is_company, company_name, company_pib, company_address, ip_address, created_at FROM users WHERE id = %s", (user_id,))
     return user[0]
 
 @router.patch("/users/{target_id}/admin", response_model=schemas.UserResponse)
-async def admin_update_user(target_id: int, data: schemas.AdminUserUpdate, admin: dict = Depends(require_admin)):
+async def admin_update_user(request: Request, target_id: int, data: schemas.AdminUserUpdate, admin: dict = Depends(require_admin)):
     
     update_fields = []
     params = []
@@ -303,6 +338,15 @@ async def admin_update_user(target_id: int, data: schemas.AdminUserUpdate, admin
         params.append(target_id)
         db.execute_commit(query, tuple(params))
         
+        await audit_service.log(
+            user_id=admin.get("id"),
+            action="admin_update_user",
+            target_type="user",
+            target_id=target_id,
+            details={"updated_fields": {k: ('***' if k == 'password_hash' else v) for k, v in update_dict.items()}},
+            request=request
+        )
+        
         # If user was deactivated, kick from active sessions
         if update_dict.get("is_active") is False:
             await global_manager.kick_user(target_id)

+ 62 - 11
backend/routers/catalog.py

@@ -1,10 +1,11 @@
-from fastapi import APIRouter, Depends, HTTPException
+from fastapi import APIRouter, Depends, HTTPException, Request
 from typing import List, Optional
 import db
 import schemas
 import auth_utils
 import json
 from dependencies import require_admin
+from services.audit_service import audit_service
 
 router = APIRouter(tags=["catalog"])
 
@@ -31,15 +32,24 @@ async def admin_get_materials(admin: dict = Depends(require_admin)):
     return rows
 
 @router.post("/admin/materials")
-async def admin_create_material(data: schemas.MaterialCreate, admin: dict = Depends(require_admin)):
+async def admin_create_material(request: Request, data: schemas.MaterialCreate, admin: dict = Depends(require_admin)):
     colors_json = json.dumps(data.available_colors) if data.available_colors else None
     query = "INSERT INTO materials (name_en, name_ru, name_ua, name_me, desc_en, desc_ru, desc_ua, desc_me, long_desc_en, long_desc_ru, long_desc_ua, long_desc_me, price_per_cm3, available_colors, is_active) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)"
     params = (data.name_en, data.name_ru, data.name_ua, data.name_me, data.desc_en, data.desc_ru, data.desc_ua, data.desc_me, data.long_desc_en, data.long_desc_ru, data.long_desc_ua, data.long_desc_me, data.price_per_cm3, colors_json, data.is_active)
     mat_id = db.execute_commit(query, params)
+    
+    await audit_service.log(
+        user_id=admin.get("id"),
+        action="create_material",
+        target_type="material",
+        target_id=mat_id,
+        details=data.dict(),
+        request=request
+    )
     return {"id": mat_id}
 
 @router.patch("/admin/materials/{mat_id}")
-async def admin_update_material(mat_id: int, data: schemas.MaterialUpdate, admin: dict = Depends(require_admin)):
+async def admin_update_material(request: Request, mat_id: int, data: schemas.MaterialUpdate, admin: dict = Depends(require_admin)):
     update_fields = []
     params = []
     for field, value in data.dict(exclude_unset=True).items():
@@ -52,25 +62,38 @@ async def admin_update_material(mat_id: int, data: schemas.MaterialUpdate, admin
         query = f"UPDATE materials SET {', '.join(update_fields)} WHERE id = %s"
         params.append(mat_id)
         db.execute_commit(query, tuple(params))
+        
+        await audit_service.log(
+            user_id=admin.get("id"),
+            action="update_material",
+            target_type="material",
+            target_id=mat_id,
+            details={"updated_fields": data.dict(exclude_unset=True)},
+            request=request
+        )
     return {"id": mat_id}
 
-@router.delete("/admin/materials/{mat_id}")
-async def admin_delete_material(mat_id: int, admin: dict = Depends(require_admin)):
-    db.execute_commit("DELETE FROM materials WHERE id = %s", (mat_id,))
-    return {"id": mat_id, "status": "deleted"}
-
 @router.get("/admin/services", response_model=List[schemas.ServiceBase])
 async def admin_get_services(admin: dict = Depends(require_admin)):
     return db.execute_query("SELECT * FROM services ORDER BY id DESC")
 
 @router.post("/admin/services")
-async def admin_create_service(data: schemas.ServiceCreate, admin: dict = Depends(require_admin)):
+async def admin_create_service(request: Request, data: schemas.ServiceCreate, admin: dict = Depends(require_admin)):
     query = "INSERT INTO services (name_en, name_ru, name_ua, name_me, desc_en, desc_ru, desc_ua, desc_me, tech_type, is_active) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)"
     srv_id = db.execute_commit(query, (data.name_en, data.name_ru, data.name_ua, data.name_me, data.desc_en, data.desc_ru, data.desc_ua, data.desc_me, data.tech_type, data.is_active))
+    
+    await audit_service.log(
+        user_id=admin.get("id"),
+        action="create_service",
+        target_type="service",
+        target_id=srv_id,
+        details=data.dict(),
+        request=request
+    )
     return {"id": srv_id}
 
 @router.patch("/admin/services/{srv_id}")
-async def admin_update_service(srv_id: int, data: schemas.ServiceUpdate, admin: dict = Depends(require_admin)):
+async def admin_update_service(request: Request, srv_id: int, data: schemas.ServiceUpdate, admin: dict = Depends(require_admin)):
     update_fields = []
     params = []
     for field, value in data.dict(exclude_unset=True).items():
@@ -83,9 +106,37 @@ async def admin_update_service(srv_id: int, data: schemas.ServiceUpdate, admin:
         query = f"UPDATE services SET {', '.join(update_fields)} WHERE id = %s"
         params.append(srv_id)
         db.execute_commit(query, tuple(params))
+        
+        await audit_service.log(
+            user_id=admin.get("id"),
+            action="update_service",
+            target_type="service",
+            target_id=srv_id,
+            details={"updated_fields": data.dict(exclude_unset=True)},
+            request=request
+        )
     return {"id": srv_id}
 
+@router.delete("/admin/materials/{mat_id}")
+async def admin_delete_material(request: Request, mat_id: int, admin: dict = Depends(require_admin)):
+    db.execute_commit("DELETE FROM materials WHERE id = %s", (mat_id,))
+    await audit_service.log(
+        user_id=admin.get("id"),
+        action="delete_material",
+        target_type="material",
+        target_id=mat_id,
+        request=request
+    )
+    return {"id": mat_id, "status": "deleted"}
+
 @router.delete("/admin/services/{srv_id}")
-async def admin_delete_service(srv_id: int, admin: dict = Depends(require_admin)):
+async def admin_delete_service(request: Request, srv_id: int, admin: dict = Depends(require_admin)):
     db.execute_commit("DELETE FROM services WHERE id = %s", (srv_id,))
+    await audit_service.log(
+        user_id=admin.get("id"),
+        action="delete_service",
+        target_type="service",
+        target_id=srv_id,
+        request=request
+    )
     return {"id": srv_id, "status": "deleted"}

+ 15 - 3
backend/routers/orders.py

@@ -191,14 +191,14 @@ async def post_order_review(order_id: int, review: schemas.OrderReview, user: di
     )
     
     # Create audit log
-    audit_service.log(user['id'], "ORDER_REVIEW", f"Posted review for order {order_id}", order_id)
+    await audit_service.log(user['id'], "ORDER_REVIEW", f"Posted review for order {order_id}", order_id)
     
     return {"message": "Review submitted successfully and is awaiting moderation"}
 
 @router.patch("/{order_id}/review/approve")
 async def approve_order_review(order_id: int, admin: dict = Depends(require_admin)):
     db.execute_commit("UPDATE orders SET review_approved = TRUE WHERE id = %s", (order_id,))
-    audit_service.log(admin['id'], "ORDER_REVIEW_APPROVE", f"Approved review for order {order_id}", order_id)
+    await audit_service.log(admin['id'], "ORDER_REVIEW_APPROVE", f"Approved review for order {order_id}", order_id)
     return {"message": "Review approved successfully"}
 
 @router.get("/reviews/public", response_model=List[schemas.PublicReview])
@@ -501,10 +501,11 @@ async def get_order_items(order_id: int):
     return items
 
 @router.put("/{order_id}/items")
-async def update_order_items(order_id: int, items: List[OrderItemSchema], admin: dict = Depends(require_admin)):
+async def update_order_items(request: Request, order_id: int, items: List[OrderItemSchema], admin: dict = Depends(require_admin)):
     db.execute_commit("DELETE FROM order_items WHERE order_id = %s", (order_id,))
     
     total_order_price = 0
+    items_summary = []
     for item in items:
         tot_p = round(item.quantity * item.unit_price, 2)
         total_order_price += tot_p
@@ -512,10 +513,21 @@ async def update_order_items(order_id: int, items: List[OrderItemSchema], admin:
             "INSERT INTO order_items (order_id, description, quantity, unit_price, total_price) VALUES (%s, %s, %s, %s, %s)",
             (order_id, item.description, item.quantity, item.unit_price, tot_p)
         )
+        items_summary.append(item.dict())
     
     # Sync main order total_price
     db.execute_commit("UPDATE orders SET total_price = %s WHERE id = %s", (total_order_price, order_id))
     
+    # LOG ACTION
+    await audit_service.log(
+        user_id=admin.get("id"),
+        action="update_order_items",
+        target_type="order",
+        target_id=order_id,
+        details={"total_price": total_order_price, "items": items_summary},
+        request=request
+    )
+
     # NOTIFY USER VIA WEBSOCKET
     order_info = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
     if order_info:

+ 18 - 3
src/components/admin/AuditSection.vue

@@ -42,9 +42,9 @@
                 {{ log.target_type }} <span class="text-[9px] font-mono opacity-50 ml-1">#{{ log.target_id }}</span>
               </td>
               <td class="p-4">
-                <p class="text-[11px] text-muted-foreground leading-relaxed max-w-xs truncate" :title="log.description">
-                  {{ log.description }}
-                </p>
+                <div class="text-[10px] text-muted-foreground leading-relaxed max-w-sm font-mono opacity-80 break-words">
+                  {{ formatDetails(log.details) }}
+                </div>
               </td>
             </tr>
           </tbody>
@@ -80,4 +80,19 @@ defineEmits(['update-page']);
 const formatDateTime = (date: string) => {
   return new Date(date).toLocaleString();
 };
+
+const formatDetails = (details: any) => {
+  if (!details) return '-';
+  try {
+    const data = typeof details === 'string' ? JSON.parse(details) : details;
+    if (typeof data === 'object') {
+      return Object.entries(data)
+        .map(([k, v]) => `${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`)
+        .join(' | ');
+    }
+    return data;
+  } catch (e) {
+    return details;
+  }
+};
 </script>