Эх сурвалжийг харах

ajustes para el administrador

latapp 9 сар өмнө
parent
commit
16181feda1

+ 10 - 0
app.py

@@ -1,5 +1,6 @@
 from time import struct_time
 from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
 from starlette.middleware.sessions import SessionMiddleware
 from config.settings import SECRET_KEY, validate_config
 from routes import sales
@@ -11,6 +12,15 @@ def create_app() -> FastAPI:
     app = FastAPI(title="Web Pedidos Klein - FastAPI Backend",
                   description="Backend for the Web Pedidos Klein application using FastAPI",)
     
+    # Add CORS middleware
+    app.add_middleware(
+        CORSMiddleware,
+        allow_origins=["*"],  # Allows all origins
+        allow_credentials=True,
+        allow_methods=["*"],  # Allows all methods
+        allow_headers=["*"],  # Allows all headers
+    )
+    
     # Add SessionMiddleware
     app.add_middleware(
         SessionMiddleware,

+ 5 - 1
config/messages.py

@@ -11,6 +11,7 @@ class ErrorResponse:
     SALE_NOT_FOUND = "No se encontraron ventas para este usuario."
     INVALID_PIN = "El PIN es inválido."
     INVALID_VERIFICATION_CODE = "El código de verificación es inválido."
+    PRODDUCT_NOT_FOUND = "Producto con ID '{product_id}' no encontrado."
 
 class SuccessResponse:
     """Class to handle success messages in the response."""
@@ -22,7 +23,9 @@ class SuccessResponse:
     LOGIN_SUCCESS = "Inicio de sesión exitoso."
     USER_DELETED_SUCCESS = "Usuario eliminado exitosamente."
     VERIFICATION_NEEDED = "Se enviará un correo electrónico para verificar su cuenta."
-
+    PRODUCT_EDIT_SUCCESS = "Producto editado exitosamente."
+    PRODUCT_CREATE_SUCCESS = "Producto creado exitosamente."
+    PRODUCT_DELETE_SUCCESS = "Producto eliminado exitosamente."
 
 class UserResponse:
     """Class to handle user-related messages in the response."""
@@ -32,3 +35,4 @@ class UserResponse:
     USER_DOES_NOT_EXIST = "El usuario no existe."
     USER_ALREADY_EXISTS = "El usuario ya está registrado."
     USER_FORMAT_BLOCKED = "Demasiados intentos de inicio de sesión. Usuario bloqueado por {time}."
+    NOT_PERMITTED = "No tienes permisos para realizar esta acción."

+ 131 - 2
doc_api.md

@@ -121,13 +121,94 @@ Authorization: Bearer <token>
       {
         "id": 1,
         "name": "Producto 1",
+        "type": "bebida",
+        "description": "Descripción del producto",
         "price": 1500.0,
-        "description": "Descripción del producto"
+        "image": "url_imagen.jpg",
+        "status": 1,
+        "quantity": 1
       }
     ],
-    "message": "Productos obtenidos exitosamente"
+    "message": "Productos obtenidos correctamente"
+  }
+  ```
+
+#### Obtener producto específico
+- **GET** `/api/products/{product_id}`
+- **Descripción:** Obtiene un producto específico por su ID
+- **Headers requeridos:** `Authorization: Bearer <token>`
+- **Parámetros de ruta:**
+  - `product_id`: ID del producto
+- **Respuestas:**
+  - `200`: Producto encontrado
+  ```json
+  {
+    "product": {
+      "id": 1,
+      "name": "Producto 1",
+      "type": "bebida",
+      "description": "Descripción del producto",
+      "price": 1500.0,
+      "image": "url_imagen.jpg",
+      "status": 1,
+      "quantity": 1
+    },
+    "message": "Productos obtenidos correctamente"
+  }
+  ```
+  - `404`: Producto no encontrado
+
+#### Editar producto
+- **PATCH** `/api/products/edit`
+- **Descripción:** Edita un producto existente (requiere permisos de nivel 1 o superior)
+- **Headers requeridos:** `Authorization: Bearer <token>`
+- **Cuerpo de solicitud:**
+  ```json
+  {
+    "id": 1,
+    "name": "Producto Actualizado",
+    "type": "comida",
+    "description": "Nueva descripción",
+    "price": 2000.0,
+    "image": "nueva_imagen.jpg",
+    "status": 1,
+    "quantity": 5
+  }
+  ```
+  **Nota:** Todos los campos excepto `id` son opcionales
+- **Respuestas:**
+  - `200`: Producto editado exitosamente
+  - `403`: Sin permisos para realizar esta acción
+
+#### Crear producto
+- **POST** `/api/products/create`
+- **Descripción:** Crea un nuevo producto (requiere permisos de nivel 1 o superior)
+- **Headers requeridos:** `Authorization: Bearer <token>`
+- **Cuerpo de solicitud:**
+  ```json
+  {
+    "name": "Nuevo Producto",
+    "type": "bebida",
+    "description": "Descripción del nuevo producto",
+    "price": 1800.0,
+    "image": "imagen_producto.jpg",
+    "status": 1,
+    "quantity": 10
   }
   ```
+- **Respuestas:**
+  - `200`: Producto creado exitosamente
+  - `403`: Sin permisos para realizar esta acción
+
+#### Eliminar producto
+- **DELETE** `/api/products/{product_id}`
+- **Descripción:** Elimina un producto (requiere permisos de nivel 2 - administrador)
+- **Headers requeridos:** `Authorization: Bearer <token>`
+- **Parámetros de ruta:**
+  - `product_id`: ID del producto a eliminar
+- **Respuestas:**
+  - `200`: Producto eliminado exitosamente
+  - `403`: Sin permisos para realizar esta acción
 
 ### 🛒 Pedidos (`/api/orders`)
 *Requiere autenticación*
@@ -275,6 +356,50 @@ Authorization: Bearer <token>
 - `status`: Estado del producto (0: Inactivo, 1: Activo)
 - `quantity`: Cantidad disponible (opcional, por defecto 1)
 
+### Solicitud de Edición de Producto (ProductEditRequest)
+```json
+{
+  "id": 1,
+  "name": "Producto Actualizado",
+  "type": "comida",
+  "description": "Nueva descripción",
+  "price": 2000.0,
+  "image": "nueva_imagen.jpg",
+  "status": 1,
+  "quantity": 5
+}
+```
+**Campos:**
+- `id`: Identificador único (requerido)
+- `name`: Nombre del producto (opcional)
+- `type`: Tipo/categoría del producto (opcional)
+- `description`: Descripción del producto (opcional)
+- `price`: Precio del producto (opcional)
+- `image`: URL de la imagen (opcional)
+- `status`: Estado del producto (opcional) - 0: Inactivo, 1: Activo
+- `quantity`: Cantidad disponible (opcional)
+
+### Solicitud de Creación de Producto (ProductCreateRequest)
+```json
+{
+  "name": "Nuevo Producto",
+  "type": "bebida",
+  "description": "Descripción del nuevo producto",
+  "price": 1800.0,
+  "image": "imagen_producto.jpg",
+  "status": 1,
+  "quantity": 10
+}
+```
+**Campos:**
+- `name`: Nombre del producto (requerido)
+- `type`: Tipo/categoría del producto (requerido)
+- `description`: Descripción del producto (requerido)
+- `price`: Precio del producto (requerido)
+- `image`: URL de la imagen (requerido)
+- `status`: Estado del producto (opcional, por defecto 1) - 0: Inactivo, 1: Activo
+- `quantity`: Cantidad disponible (opcional, por defecto 1)
+
 ### Pedido (OrderWeb)
 ```json
 {
@@ -412,6 +537,10 @@ Authorization: Bearer <token>
 4. **Integración con Fudo**: Los pedidos se sincronizan automáticamente con el sistema Fudo
 5. **Impresión automática**: Los pedidos se envían automáticamente a la impresora USB configurada
 6. **Logs**: Todas las interacciones importantes se registran para auditoría
+7. **Niveles de permisos**: 
+   - **Nivel 0**: Usuario normal (solo consulta de productos)
+   - **Nivel 1**: Usuario con permisos de edición (puede crear y editar productos)
+   - **Nivel 2**: Administrador (puede eliminar productos)
 
 ## Middleware
 

+ 21 - 0
models/items.py

@@ -11,3 +11,24 @@ class Product(BaseModel):
     image: Optional[str] = None
     status: int = 1  # 0: Inactive, 1: Active
     quantity: Optional[int] = 1  # Optional quantity for the product
+
+class ProductEditRequest(BaseModel):
+    """Request model for editing a product"""
+    name: Optional[str] = None
+    type: Optional[str] = None
+    description: Optional[str] = None
+    price: Optional[float] = None
+    image: Optional[str] = None
+    status: Optional[int] = None  # 0: Inactive, 1: Active
+    quantity: Optional[int] = None  # Optional quantity for the product
+
+class ProductCreateRequest(BaseModel):
+    """Request model for creating a new product"""
+    id: int
+    name: str
+    type: str
+    description: str
+    price: float
+    image: str
+    status: Optional[int] = 1  # 0: Inactive, 1: Active
+    quantity: Optional[int] = 1  # Optional quantity for the product

+ 1 - 1
public/main/js/service/product.js

@@ -22,7 +22,7 @@ async function sendOrder(order, token) {
   }
 }
 async function getProducts(token){
-  const response = await fetch("/api/products/", {
+  const response = await fetch("/api/products?status=1", {
     headers: {
       "Content-Type": "application/json",
       "Authorization": `Bearer ${token}`

+ 185 - 8
routes/products.py

@@ -1,21 +1,198 @@
+"""
+Product Routes Module
+
+This module defines all API endpoints for product management including:
+- Fetching products (all users)
+- Creating/editing products (permissions >= 1)
+- Deleting products (permissions == 2 only)
+
+Permission levels:
+- 0: Regular user (read-only access)
+- 1: Manager (can create/edit products)
+- 2: Admin (full access including delete)
+"""
+
+# Standard library imports
 from math import prod
+from logging import getLogger
+from typing import Optional
+
+# Third-party imports
+from fastapi import APIRouter, Depends, Query
 from fastapi.responses import JSONResponse
+from h11 import Data
+
+# Local imports
 from auth.security import get_current_user
-from services.data_service import ProductDataService
-from logging import getLogger
-from fastapi import APIRouter, Depends
-from config.messages import SuccessResponse
+from models import user
+from models.items import ProductCreateRequest, ProductEditRequest
+from services.data_service import DataServiceFactory
+from config.messages import ErrorResponse, SuccessResponse, UserResponse
 
+# Initialize logger for this module
 logger = getLogger(__name__)
 
-product_data_service = ProductDataService()
+# Initialize data services for products and users
+product_data_service = DataServiceFactory.get_product_service()
+user_data_service = DataServiceFactory.get_user_service()
 
+# Create router instance for product-related endpoints
 product_router = APIRouter()
 
 @product_router.get("/")
-async def get_products(current_user = Depends(get_current_user)):
-    """Get products"""
+async def get_products(status: Optional[int] = Query(None), current_user = Depends(get_current_user)):
+    """
+    Get all products - Available to all authenticated users
+    
+    Returns:
+        JSONResponse: List of all products with success message
+    """
     logger.debug(f"Current user: {current_user.email if current_user else 'Anonymous'}")
     logger.info("Fetching all products")
+    
+    # Retrieve all products and convert to dictionary format
     all_products = list(map(lambda p: p.model_dump(), product_data_service.get_all()))
-    return JSONResponse({"products": all_products, "message": SuccessResponse.PRODUCTS_FETCH_SUCCESS})
+
+    if status is not None:
+        # Filter products by status if provided
+        all_products = [product for product in all_products if product['status'] == status]
+
+    return JSONResponse({"products": all_products, "message": SuccessResponse.PRODUCTS_FETCH_SUCCESS})
+
+@product_router.get("/{product_id}")
+async def get_product(product_id: int, current_user = Depends(get_current_user)):
+    """
+    Get a specific product by ID - Available to all authenticated users
+    
+    Args:
+        product_id (int): The ID of the product to retrieve
+        current_user: Authenticated user (dependency injection)
+        
+    Returns:
+        JSONResponse: Product data if found, error message if not found
+    """
+    logger.debug(f"Current user: {current_user.email if current_user else 'Anonymous'}")
+    logger.info(f"Fetching product with ID: {product_id}")
+    
+    # Attempt to find product by ID
+    product = product_data_service.get_by_id(product_id)
+    if product:
+        return JSONResponse({"product": product.model_dump(), "message": SuccessResponse.PRODUCTS_FETCH_SUCCESS})
+    
+    # Return 404 if product not found
+    return JSONResponse({"message": UserResponse.USER_NOT_FOUND.format(user_id=product_id)}, status_code=404)
+
+# MODERATE RISK OPERATIONS - Requires permissions >= 1 (Manager level or above)
+
+@product_router.patch("/{product_id}/edit")
+async def edit_product(product_id: int, product: ProductEditRequest, current_user = Depends(get_current_user)):
+    """
+    Edit an existing product - Requires manager permissions (level >= 1)
+    
+    Args:
+        product (ProductEditRequest): Product data to update
+        current_user: Authenticated user (dependency injection)
+        
+    Returns:
+        JSONResponse: Updated product data or permission denied message
+    """
+    logger.debug(f"Current user: {current_user.email if current_user else 'Anonymous'}")
+    logger.info(f"Editing product: {product_id}")
+    
+    # Check if user has sufficient permissions (manager level or above)
+    if user_data_service.permissions(current_user.id) > 0:
+        # Update product with provided data (excluding unset fields)
+        product_data_service.update(product_id, **product.model_dump(exclude_unset=True))
+        
+        # Retrieve updated product to return in response
+        edited_product = product_data_service.get_by_id(product_id)
+        if not edited_product:
+            return JSONResponse({"message": UserResponse.USER_NOT_FOUND.format(user_id=product_id)}, status_code=404)
+        
+        logger.info(f"Product {product_id} edited successfully")
+        return JSONResponse({"message": SuccessResponse.PRODUCT_EDIT_SUCCESS, "product": edited_product.model_dump()})
+    
+    # Return 403 if user lacks permissions
+    return JSONResponse({"message": UserResponse.NOT_PERMITTED}, status_code=403)
+
+@product_router.post("/create")
+async def create_product(product: ProductCreateRequest, current_user = Depends(get_current_user)):
+    """
+    Create a new product - Requires manager permissions (level >= 1)
+    
+    Args:
+        product (ProductCreateRequest): New product data
+        current_user: Authenticated user (dependency injection)
+        
+    Returns:
+        JSONResponse: Success message or permission denied message
+    """
+    logger.debug(f"Current user: {current_user.email if current_user else 'Anonymous'}")
+    logger.info("Creating a new product")
+    
+    # Check if user has sufficient permissions (manager level or above)
+    if user_data_service.permissions(current_user.id) > 0:
+        # Create new product with provided data
+        product_data_service.create(**product.model_dump(exclude_unset=True))
+        return JSONResponse({"message": SuccessResponse.PRODUCT_CREATE_SUCCESS, "product": product.model_dump()}, status_code=201)
+    
+    # Return 403 if user lacks permissions
+    return JSONResponse({"message": UserResponse.NOT_PERMITTED}, status_code=403)
+
+@product_router.patch("/{product_id}/swap-status")
+async def switch_product_status(product_id: int, current_user = Depends(get_current_user)): 
+    """
+    Toggle product status between active/inactive - Requires manager permissions (level >= 1)
+    
+    Args:
+        product_id (int): ID of the product to update
+        status (int): New status value (0=inactive, 1=active)
+        current_user: Authenticated user (dependency injection)
+        
+    Returns:
+        JSONResponse: Success message or permission denied message
+    """
+    logger.debug(f"Current user: {current_user.email if current_user else 'Anonymous'}")
+    logger.info(f"Switching status for product with ID: {product_id}")
+    
+    # Check if user has sufficient permissions (manager level or above)
+    if user_data_service.permissions(current_user.id) > 0:
+        # Update only the status field of the specified product
+        product = product_data_service.get_by_id(product_id)
+        if not product:
+            return JSONResponse({"message": ErrorResponse.PRODDUCT_NOT_FOUND.format(product_id=product_id)}, status_code=404)
+        status = 0 if product.status == 1 else 1
+        product_data_service.update(product_id, status=status)
+        return JSONResponse({"message": SuccessResponse.PRODUCT_EDIT_SUCCESS})
+    
+    # Return 403 if user lacks permissions
+    return JSONResponse({"message": UserResponse.NOT_PERMITTED}, status_code=403)
+
+# HIGH RISK OPERATIONS - Requires permissions == 2 (Admin level only)
+
+@product_router.delete("/{product_id}")
+async def delete_product(product_id: int, current_user = Depends(get_current_user)):
+    """
+    Delete a product permanently - Requires admin permissions (level == 2)
+    
+    This is a high-risk operation that permanently removes product data.
+    Only users with admin-level permissions can perform this action.
+    
+    Args:
+        product_id (int): ID of the product to delete
+        current_user: Authenticated user (dependency injection)
+        
+    Returns:
+        JSONResponse: Success message or permission denied message
+    """
+    logger.debug(f"Current user: {current_user.email if current_user else 'Anonymous'}")
+    logger.info(f"Deleting product with ID: {product_id}")
+    
+    # Check if user has admin permissions (exactly level 2)
+    if user_data_service.permissions(current_user.id) == 2:
+        # Permanently delete the product
+        product_data_service.delete(product_id)
+        return JSONResponse({"message": SuccessResponse.PRODUCT_DELETE_SUCCESS})
+    
+    # Return 403 if user lacks admin permissions
+    return JSONResponse({"message": UserResponse.NOT_PERMITTED}, status_code=403)

+ 12 - 1
services/data_service.py

@@ -208,6 +208,16 @@ class UserDataService(BaseDataService):
             logger.error("Login failed: Invalid email or pin.")
             return None
 
+    def permissions(self, user_id: int) -> int:
+        """Get user permissions"""
+        conn = self._get_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT permissions FROM users WHERE id = ?", (user_id,))
+        result = cursor.fetchone()
+        conn.close()
+        if result:
+            return result[0]
+        return 0
 
     def get_by_rut(self, rut: str) -> Optional[User]:
         """Get user by RUT"""
@@ -1161,7 +1171,8 @@ def initialize_db():
         rut TEXT UNIQUE NOT NULL,
         pin_hash TEXT NOT NULL,
         kleincoins TEXT NOT NULL,
-        created_at TEXT NOT NULL 
+        created_at TEXT NOT NULL,
+        permissions INTEGER DEFAULT 0 NOT NULL CHECK (permissions IN (0, 1, 2)), -- 0: Usuario normal, 1: Administrador, 2: Superusuario
     );
     """)