Jelajahi Sumber

add guest user support

Erwin Jacimino 5 bulan lalu
induk
melakukan
86c81a6fcb

+ 11 - 0
.github/commit-instructions.md

@@ -0,0 +1,11 @@
+---
+applyTo: '**'
+---
+# Generador de Mensajes de Commit
+Genera el mensaje de commit siguiendo el estándar Conventional Commits **en español**.
+El formato debe ser: tipo(ámbito): descripción breve
+
+### Requisitos
+- **Idioma:** Español.
+- **Descripción breve:** En modo imperativo, minúsculas, máximo 50 caracteres.
+- **Cuerpo del mensaje (opcional):** Detalle los cambios, el por qué y el contexto.

+ 3 - 3
auth/security.py

@@ -15,7 +15,7 @@ from services.data_service import UserDataService
 logger = getLogger(__name__)
 
 ALGORITHM = "HS256"
-ACCESS_TOKEN_EXPIRE_DAYS = 7
+ACCESS_TOKEN_EXPIRE_DAYS = 31
 security = HTTPBearer()
 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
 user_data_service = UserDataService()
@@ -28,13 +28,13 @@ def hash_password(password: str) -> str:
     """Hash a password using bcrypt."""
     return pwd_context.hash(password)
 
-def generate_token(email: str): 
+def generate_token(email: str, access_token_expire_days: int = ACCESS_TOKEN_EXPIRE_DAYS):
     """Generate a JWT token for user authentication."""
     logger.info(f"Generating token for user: {email}")
     
     try:
         data = {"sub": email}
-        expires_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_DAYS)
+        expires_delta = timedelta(days=access_token_expire_days)
         token = create_access_token(data=data, expires_delta=expires_delta)
         
         

+ 2 - 2
data/product_category.py

@@ -8,7 +8,7 @@ CAT_ITEMS = {
     "Pizzas Familiares": Locations.PIZZAS,
     "Pisco": Locations.COCTELERY,
     "Hidromiel": Locations.BAR,
-    "Sin Alcohol": Locations.BAR,
+    "Sin Alcohol": Locations.COCTELERY,
     "Pizza Medianas": Locations.PIZZAS,
     "Kiosco Klein": Locations.COCTELERY,
     "Whisky & Whiskey": Locations.BAR,
@@ -31,4 +31,4 @@ CAT_ITEMS = {
     "Spritz": Locations.COCTELERY,
     "Sandwich de Autor": Locations.BURGUER,
     "Cervezas": Locations.BAR,
-}
+}

+ 24 - 11
public/main/index.html

@@ -23,10 +23,22 @@
       style='font-family:"Spline Sans","Noto Sans",sans-serif;'>
 
   <!-- ---------- HEADER ---------- -->
-  <header class="flex-col top-0 inset-x-0 z-10 bg-gray-50 p-2 flex justify-center items-center border-b border-gray-200">
+<header class="flex-col top-0 inset-x-0 z-10 bg-gray-50 p-2 flex justify-center items-center border-b border-gray-200 relative">
     <h1 id="mainTitle" class="text-[26px] font-bold text-[#101419] tracking-tight">
       Biergarten Klein
     </h1>
+    
+    <div class="absolute right-4 top-1/2 -translate-y-1/2 flex items-center gap-3">
+        <span id="headerUserName" class="hidden text-xs font-bold text-gray-600 border-r border-gray-300 pr-3"></span>
+        
+        <button id="openLoginBtn" class="hidden text-sm text-blue-600 font-medium hover:text-blue-800 transition-colors">
+           Entrar
+        </button>
+        
+        <button id="headerLogoutBtn" class="hidden text-sm text-red-500 font-medium hover:text-red-700 transition-colors">
+           Salir
+        </button>
+    </div>
   </header>
 
   <!-- ---------- MAIN  ---------- -->
@@ -35,7 +47,7 @@
     <section id="menuTab" data-index="0" class="min-h-0 overflow-y-auto h-full" data-tab>
         <div class="pt-4 pb-3">
             <!-- Barra de progreso para cerveza gratis -->
-            <div class="mx-4 mb-6 p-4 bg-gradient-to-r from-amber-50 to-orange-50 rounded-xl border border-amber-200">
+            <div id="rewardContainer" class="mx-4 mb-6 p-4 bg-gradient-to-r from-amber-50 to-orange-50 rounded-xl border border-amber-200">
                 <div class="flex items-center justify-between mb-2">
                     <div class="flex items-center gap-2">
                         <span class="text-lg">🍺</span>
@@ -151,7 +163,7 @@
       <div class="max-w-4xl mx-auto bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
     <!-- Header con nombre de usuario -->
     <div class="bg-gray-50 px-6 py-2 border-b border-gray-200">
-      <h2 id="usernameTable" class="text-[19px] font-bold text-[#101419]">Pedido de Juan Pérez</h2>
+      <h2 id="usernameTable" class="text-[19px] font-bold text-[#101419]"></h2>
     </div>
 
     <div class="overflow-x-auto max-h-[20vh]">
@@ -283,12 +295,18 @@
       </div>
     </div>
     <span id="ErrorLogin" class="hidden text-red-500 text-sm"></span>
-    <a id="recoveryPIN" href="/recovery" class="block w-full bg-gray-200 text-center text-black py-3 rounded-lg font-medium transition-colors duration-200 focus:ring-2 focus:ring-offset-2 focus:ring-[#101419]" >Olvide mi PIN</a>
+    <a id="recoveryPIN" href="/recovery" class="block w-full bg-gray-200 text-center text-black py-3 rounded-lg font-medium transition-colors duration-200 focus:ring-2 focus:ring-offset-2 focus:ring-[#101419]" >Olvidé mi PIN</a>
     <button id="sessionAcceptBtn"
             type="submit"
             class="w-full bg-[#101419] hover:bg-[#37404a] text-white py-3 rounded-lg font-medium transition-colors duration-200 focus:ring-2 focus:ring-offset-2 focus:ring-[#101419]">
       Comenzar pedido
     </button>
+    <div id="registerBtn"  class="mt-4 text-center text-sm text-gray-600">
+  ¿No tienes cuenta? 
+  <a href="/register" class="font-semibold text-[#101419] hover:underline transition-colors">
+    Regístrate aquí
+  </a>
+</div>
     <button id="logoutBtn"
             type="button"
             class="w-full hidden bg-gray-100 hover:bg-gray-200 text-gray-700 py-3 rounded-lg font-medium transition-colors duration-200 focus:ring-2 focus:ring-offset-2 focus:ring-gray-300">
@@ -335,11 +353,6 @@
               <p>Válido únicamente en Biergarten Klein para consumo en el local.</p>
             </div>
             
-            <div class="flex items-start gap-2">
-              <span class="text-amber-500 font-bold">•</span>
-              <p>La cerveza gratis debe ser reclamada el mismo día que se alcanza el 100%.</p>
-            </div>
-            
             <div class="flex items-start gap-2">
               <span class="text-amber-500 font-bold">•</span>
               <p>Aplica para cervezas de barril regulares (no incluye cervezas premium o importadas).</p>
@@ -371,7 +384,7 @@
         <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
           <h4 class="font-semibold text-blue-800 mb-2">📋 Instrucciones:</h4>
           <p class="text-blue-700 text-sm">
-            Una vez aceptes los términos, se generará un código que deberás mostrar al mesero junto con tu identificación para reclamar tu cerveza gratis.
+            Una vez aceptes los términos, el garzon vendra a entregarte tu cerveza gratis.
           </p>
         </div>
       </div>
@@ -539,7 +552,7 @@
       toast.style.animation = 'none';      // reset
       void toast.offsetWidth;              // reflow
       toast.style.opacity = '1';
-      toast.style.animation = 'popup 1s ease-out';
+      toast.style.animation = 'popup 3s ease-out';
       toast.addEventListener('animationend', () => {
         toast.style.animation = 'none';
         toast.style.opacity = '0';

File diff ditekan karena terlalu besar
+ 296 - 397
public/main/js/app.js


+ 50 - 28
public/main/js/service/auth.js

@@ -1,54 +1,77 @@
-
 import { beforeUnloadHandler } from "../app.js";
 import { showError } from "../utils/error.js";
 
-async function login(email, pin, table) {
+// Configuración: Cambia estos números para que sean únicos para tu proyecto
+const OFFSET = 99887766;  // Un número grande para asegurar longitud mínima
+const CLAVE  = 836295738; // Tu "secreto" para el XOR
+
+const desencriptar = (hash) => {
+  // 1. Revertimos el string y volvemos a base 10
+  const baseNum = parseInt(hash.split('').reverse().join(''), 12);
+  // 2. Aplicamos XOR (es reversible) y restamos el Offset
+  return (baseNum ^ CLAVE) - OFFSET;
+};
+
+function getTableFromUrl() {
+  const params = new URLSearchParams(window.location.search);
+  const table = params.get("table");
+  console.log(table);
+  return Number(desencriptar(table));
+}
+
+async function login(email, pin) {
   const response = await fetch("/api/users/login", {
     method: "POST",
     headers: {
       "Content-Type": "application/json"
     },
-    body: JSON.stringify({ email, pin })
-  }
-  );
+    body: JSON.stringify({ email, pin }) // Enviamos table si el back lo requiere en login
+  });
+  
   if (response.status == 420) {
     window.removeEventListener("beforeunload", beforeUnloadHandler);
     window.location.replace("/");
   }
-  if (response.status == 404) {
-    const errorData = await response.json()
-    const data = errorData.message
-  } else if (response.status == 401) {
-    const errorData = await response.json()
-    const data = errorData.message
-    showError(`${data} Intentos restantes: ${errorData.data.attempts_remaining || 0}`);
-    throw new Error(errorData.message);
-  } else if (response.status == 429) {
-    const errorData = await response.json()
-    const data = errorData.message
-    showError(`${data} Intenta más tarde.`);
-    throw new Error(errorData.message);
+
+  if (response.status === 401 || response.status === 429 || response.status === 404) {
+      const errorData = await response.json();
+      const msg = errorData.message || "Error de autenticación";
+      const attempts = errorData.data?.attempts_remaining ? ` Intentos restantes: ${errorData.data.attempts_remaining}` : "";
+      showError(`${msg}.${attempts}`);
+      throw new Error(msg);
   } else if (response.status != 200) {
-    console.error(response.status, response.statusText);
-    const errorData = await response.json()
-    const data = errorData.message
-    showError(`${data}`);
-    throw new Error(data);
+    const errorData = await response.json();
+    showError(errorData.message || "Error desconocido");
+    throw new Error(errorData.message);
   }
+
   const JSONdata = await response.json();
   const userData = JSONdata.data;
-  console.log(userData);
   if (!userData || !userData.token) {
     showError("Error al iniciar sesión, Intenta mas tarde.");
     throw new Error("Error al iniciar sesión.");
   }
 
-
   return userData;
 }
 
+async function guestLogin() {
+  try {
+    const response = await fetch("/api/users/guest");
+    const data = await response.json();
+
+    if (!data.success || !data.data || !data.data.token) {
+       throw new Error("Error al generar sesión de invitado");
+    }
+    return data.data; // Retorna { token: "..." }
+  } catch (e) {
+    console.error(e);
+    showError("No se pudo iniciar como invitado.");
+    throw e;
+  }
+}
+
 async function existsTable(table) {
-    // Check if table exists
   const responseTableExists = await fetch(`/api/store/tables/exists?q=${table}`, {
     method: "GET",
     headers: {
@@ -58,7 +81,6 @@ async function existsTable(table) {
   
   const data = (await responseTableExists.json()).data.exists;
   return data;
-
 }
 
-export { login, existsTable };
+export { login, existsTable, guestLogin, getTableFromUrl };

File diff ditekan karena terlalu besar
+ 0 - 0
public/main/tlw.css


+ 1 - 1
public/register/index.html

@@ -72,7 +72,7 @@
                    class="w-4 h-4 text-[#101419] border-gray-300 rounded focus:ring-[#101419] focus:ring-2 transition-all cursor-pointer"
                    required />
             <label for="18olderInput" class="text-sm font-medium text-gray-700 cursor-pointer select-none">
-              Declaro que poseo una edad mayor de 18 años
+              Confirmo que tengo 18 años o más y acepto los términos de distribución de bebidas alcohólicas.
             </label>
           </div>
         </div>

+ 6 - 1
routes/debug.py

@@ -57,7 +57,8 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(...)):
 
         # Bucle de lectura asíncrona
         while True:
-            line = await process.stdout.readline()
+    
+            line = await process.stdout.readline() #type: ignore
             if line:
                 # Decodificar bytes a string y enviar
                 await websocket.send_text(line.decode("utf-8").strip())
@@ -67,11 +68,15 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(...)):
 
     except WebSocketDisconnect:
         # El cliente se desconectó, matar el proceso de journalctl para no dejar zombies
+        if not process: #type: ignore
+            return
         if process.returncode is None:
             process.terminate()
             await process.wait()
             
     except Exception as e:
+        if not process: #type: ignore
+            return
         logger.error(f"Error en websocket logs: {e}")
         if process.returncode is None:
             process.terminate()

+ 12 - 26
routes/orders.py

@@ -132,7 +132,7 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
                     
                 #en caso contrario
                 else:
-                    if product.type == name_promo:
+                    if product.type == name_promo and current_user.name != "Guest":
                         beers_for_promo += item.quantity
                         logger.debug(f"Added {item.quantity} beers for promotion calculation")
                     
@@ -140,7 +140,7 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
                     
                     logger.info(f"Added product {item.id} to table {table} with quantity {item.quantity} in fudo")
                 
-                if not fudo_product:
+                if not fudo_product: 
                     error_msg = f"Error adding product {item.id} to table {table}."
                     product_errors.append(error_msg)
                     logger.error(error_msg)
@@ -163,21 +163,7 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
         
         return error_response(error={"errors": product_errors}, message=ErrorResponse.PRODUCT_ADD_ERROR, status_code=424)
 
-    # User validation
-    try:
-        user = user_data_service.get_by_id(order.customerId)
-        if not user:
-            logger.warning(f"User not found: {order.customerId}")
-            
-            return error_response(message=UserResponse.USER_NOT_FOUND.format(user_id=order.customerId), status_code=404)
-        
-        logger.info(f"Order customer validated: {user.email}")
-        
-        
-    except Exception as e:
-        error_msg = f"Error validating user {order.customerId}: {e}"
-        
-        return error_response(message=error_msg, status_code=500)
+
     
     # Get active sale
     try:
@@ -198,17 +184,17 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
 
     # Update user reward progress
     try:
-        new_progress = user.reward_progress + beers_for_promo * 10
-        user_data_service.set_reward_progress(user.id, new_progress)
+        new_progress = current_user.reward_progress + beers_for_promo * 10
+        user_data_service.set_reward_progress(current_user.id, new_progress)
         
-        logger.info(f"Updated reward progress for user {user.email}: {user.reward_progress} -> {new_progress}")
+        logger.info(f"Updated reward progress for user {current_user.email}: {current_user.reward_progress} -> {new_progress}")
         
         
     except Exception as e:
-        error_msg = f"Error updating reward progress for user {user.id}: {e}"
+        error_msg = f"Error updating reward progress for user {current_user.id}: {e}"
         logger.error(error_msg)
         # Don't fail the order for this, just log it
-        new_progress = user.reward_progress
+        new_progress = current_user.reward_progress
 
     # Create sale record
     try:
@@ -248,16 +234,16 @@ async def printer_order(order: OrderWeb, current_user: User = Depends(get_curren
         print(list(map(lambda x: x.model_dump(), bar_items)))
 
         if pizza_items:
-            ps.print_order(Order(table=table, items=pizza_items, customerName=user.name, totalAmount=order.totalAmount, orderDate=order.orderDate), location=Locations.PIZZAS)
+            ps.print_order(Order(table=table, items=pizza_items, customerName=current_user.name, totalAmount=order.totalAmount, orderDate=order.orderDate), location=Locations.PIZZAS)
             logger.info(f"Order pizza printed successfully for table {table}")
         if burger_items:
-            ps.print_order(Order(table=table, items=burger_items, customerName=user.name, totalAmount=order.totalAmount, orderDate=order.orderDate), location=Locations.BURGUER)
+            ps.print_order(Order(table=table, items=burger_items, customerName=current_user.name, totalAmount=order.totalAmount, orderDate=order.orderDate), location=Locations.BURGUER)
             logger.info(f"Order burger printed successfully for table {table}")
         if bar_items:
-            ps.print_order(Order(table=table, items=bar_items, customerName=user.name, totalAmount=order.totalAmount, orderDate=order.orderDate), location=Locations.BAR)
+            ps.print_order(Order(table=table, items=bar_items, customerName=current_user.name, totalAmount=order.totalAmount, orderDate=order.orderDate), location=Locations.BAR)
             logger.info(f"Order bar printed successfully for table {table}")
         if coctelery_items:
-            ps.print_order(Order(table=table, items=coctelery_items, customerName=user.name, totalAmount=order.totalAmount, orderDate=order.orderDate), location=Locations.COCTELERY)
+            ps.print_order(Order(table=table, items=coctelery_items, customerName=current_user.name, totalAmount=order.totalAmount, orderDate=order.orderDate), location=Locations.COCTELERY)
             logger.info(f"Order coctelery printed successfully for table {table}")
          
         

+ 5 - 0
routes/users.py

@@ -264,6 +264,11 @@ async def login_user(request: LoginRequest, http_request: Request):
         
         return error_response(message="Error interno del servidor", status_code=500)
 
+@user_router.get("/guest")
+async def guest_login():
+    token = generate_token("guest@guest.com", access_token_expire_days=1)
+    return success_response(data={"token": token}, message=SuccessResponse.LOGIN_SUCCESS)
+
 @user_router.delete("/delete")
 async def delete_user(request: UserIDRequest, current_user: User = Depends(get_current_user)):
     if current_user.permissions != 2:

+ 4 - 2
services/print_service.py

@@ -13,7 +13,7 @@ is_habilited_hexagon = False
 beers_habilited = []
 def get_printer_url(location: Locations, table_number: int , products: list[str] = []) -> str:
     if DEVELOPMENT:
-        return "http://10.10.12.15:5004"
+        return "http://localhost:5004"
     if location == Locations.BAR:
         if is_habilited_hexagon and table_number >= 500 and set(products).issubset(set(beers_habilited)):
             return "http://10.10.12.15:5004"
@@ -84,7 +84,7 @@ def print_order(order: Order, location:Locations ):
 def print_ticket(number_table: int):
     """Send a ticket to the printer"""
     logger.info(f"Attempting to print ticket for table {number_table}")
-    printer_url = get_printer_url(Locations.BAR)
+    printer_url = get_printer_url(Locations.BAR, number_table)
     try:
 
         
@@ -115,6 +115,8 @@ def print_ticket(number_table: int):
         
         raise
 
+
+
 def get_status(location: Locations):
     """Get the status of the printer service"""
     logger.info("Checking printer service status")

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini