Explorar el Código

login and register system

latapp hace 9 meses
padre
commit
e07f1ee480

+ 3 - 1
.env

@@ -3,4 +3,6 @@ SECRET_KEY = 866B3F5EE90BFED7EDAD0FCB0A9C0FC866F03166C05648478ECEF6148C9E13BEBF8
 OPENAI_API_KEY = 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
 NODE_ENV = development
 FUDO_API_KEY=NzZAMTEzMzc4
-FUDO_API_SECRET=FNKYiEbYGTVc3i0jLOTTXL8pVPkUIPLP
+FUDO_API_SECRET=FNKYiEbYGTVc3i0jLOTTXL8pVPkUIPLP
+PIN_KEY='GZUTI02vzKsRM2LLePkDy2mh8YpI5TjDEffRfkRNWLE='
+LOG_LEVEL=DEBUG

+ 2 - 1
.gitignore

@@ -7,4 +7,5 @@ users.json
 logs.csv
 llm_logs.*
 *.pyc
-dksdabjhvjhSADhsbjksf.txt
+dksdabjhvjhSADhsbjksf.txt
+data/data.db

+ 305 - 51
README.md

@@ -1,94 +1,348 @@
-# Web Pedidos Klein - Backend
+# Biergarten Klein - Sistema de Pedidos Express
 
-## Estructura del Proyecto
+## Descripción del Proyecto
+
+**Biergarten Klein** es una aplicación web completa para la gestión de pedidos en un bar cervecero artesanal. El sistema permite a los clientes interactuar con un asistente de IA llamado "Camilo Klein" para consultar el menú, recibir recomendaciones de cervezas artesanales, realizar pedidos y gestionar su experiencia en el establecimiento.
+
+## Características Principales
+
+### 🤖 Asistente de IA Personalizado
+- **Camilo Klein**: Chatbot especializado en el menú del bar
+- Integración con OpenAI GPT-4o-mini
+- Respuestas carismáticas con emojis
+- Base de conocimientos específica sobre cervezas artesanales
+- Sistema anti-abuso con tokens de sesión
+
+### 🍺 Gestión de Productos
+- Catálogo completo de cervezas artesanales (Burlesque, Queen Burlesque, Hoppy Mosh, Black Mamba, etc.)
+- Sistema de carrito de compras interactivo
+- Información detallada de cada cerveza (IBU, SRM, notas de sabor)
+- Precios y disponibilidad en tiempo real
 
-El backend ha sido reorganizado para mejorar la mantenibilidad y seguir buenas prácticas de desarrollo. La nueva estructura es:
+### 🧾 Sistema de Pedidos
+- Procesamiento de pedidos con validación
+- Integración con impresora térmica USB
+- Sincronización con sistema Fudo POS
+- Notificaciones por email en caso de errores
+- Logging completo de transacciones
+
+### 🔐 Seguridad y Autenticación
+- Sistema de tokens anti-abuso
+- Validación de usuarios
+- Protección de endpoints sensibles
+- Middleware de sesiones seguras
+
+## Estructura del Proyecto
 
 ```
 pedidos_express/
 ├── main.py                    # Punto de entrada principal
-├── app.py                     # Configuración de la aplicación FastAPI
-├── requirements.txt           # Dependencias
-├── .env                       # Variables de entorno
+├── app.py                     # Configuración FastAPI y rutas
+├── requirements.txt           # Dependencias del proyecto
+├── tailwind.config.js         # Configuración de Tailwind CSS
 ├── config/
 │   ├── __init__.py
-│   └── settings.py           # Configuración y variables de entorno
+│   └── settings.py           # Variables de entorno y logging
 ├── models/
 │   ├── __init__.py
-│   └── schemas.py            # Modelos Pydantic
+│   └── schemas.py            # Modelos Pydantic (Message, Order, User)
 ├── auth/
 │   ├── __init__.py
-│   └── security.py           # Autenticación y seguridad
+│   └── security.py           # Autenticación y tokens anti-abuso
 ├── services/
 │   ├── __init__.py
-│   ├── data_service.py       # Manejo de datos (productos, usuarios)
-│   ├── openai_service.py     # Servicio de OpenAI/ChatGPT
-│   ├── email_service.py      # Servicio de envío de emails
-│   ├── fudo_service.py       # Integración con Fudo
-│   └── logging_service.py    # Logging de pedidos y respuestas
+│   ├── data_service.py       # Gestión de datos y productos
+│   ├── openai_service.py     # Integración con OpenAI
+│   ├── email_service.py      # Notificaciones por email
+│   ├── fudo_service.py       # Integración con Fudo POS
+│   └── logging_service.py    # Sistema de logs
 ├── routes/
 │   ├── __init__.py
-│   ├── chat.py              # Endpoints del chat
-│   ├── users.py             # Endpoints de usuarios
-│   ├── products.py          # Endpoints de productos
-│   ├── orders.py            # Endpoints de pedidos
+│   ├── chat.py              # Endpoints del chatbot
+│   ├── users.py             # Gestión de usuarios
+│   ├── products.py          # Catálogo de productos
+│   ├── orders.py            # Procesamiento de pedidos
 │   └── static.py            # Archivos estáticos
-├── impresora/               # Módulo de impresión (existente)
-├── fudo/                    # Módulo Fudo (existente)
-└── public/                  # Archivos estáticos frontend
+├── impresora/               # Módulo de impresión térmica
+│   ├── __init__.py
+│   ├── order.py             # Modelos para órdenes de impresión
+│   └── printer.py           # Control de impresora USB
+├── fudo/                    # Integración con Fudo POS
+│   └── fudo.py              # API y gestión de tokens
+├── data/                    # Almacenamiento de datos
+│   ├── data.db              # Base de datos SQLite
+│   ├── llm_data.json        # Base de conocimientos para IA
+│   └── products.json        # Catálogo de productos
+├── public/                  # Frontend web
+│   ├── index.html           # Página principal
+│   ├── styles.css           # Estilos personalizados
+│   ├── assets/              # Recursos multimedia
+│   └── js/
+│       ├── app.js           # Lógica principal del frontend
+│       ├── interfaces.js    # Interfaces de usuario
+│       └── service/         # Servicios del frontend
+│           ├── auth.js      # Autenticación cliente
+│           ├── chat.js      # Comunicación con chatbot
+│           └── product.js   # Gestión de productos
+└── logs/                    # Archivos de log
+    └── app.log              # Logs de la aplicación
 ```
 
-## Módulos
+## Tecnologías Utilizadas
+
+### Backend
+- **FastAPI**: Framework web moderno y rápido
+- **Python 3.9+**: Lenguaje de programación principal
+- **OpenAI API**: Integración con GPT-4o-mini para el chatbot
+- **SQLite**: Base de datos local
+- **Redis**: Cache para tokens de autenticación
+- **Uvicorn**: Servidor ASGI para producción
+
+### Frontend
+- **HTML5/CSS3/JavaScript**: Tecnologías web estándar
+- **Tailwind CSS**: Framework de estilos utilitarios
+- **Vanilla JavaScript**: Sin frameworks adicionales para máximo rendimiento
+
+### Integraciones
+- **Fudo POS**: Sistema de punto de venta
+- **Impresora Térmica USB**: Para tickets de pedidos
+- **SMTP**: Envío de notificaciones por email
+
+## Módulos Principales
 
 ### config/settings.py
-- Configuración de la aplicación
-- Variables de entorno
-- Validación de configuración
+- Configuración centralizada de la aplicación
+- Gestión de variables de entorno
+- Sistema de logging configurado
+- Validación de configuración crítica
 
 ### models/schemas.py
-- Modelos Pydantic para request/response
-- Validación de datos
+- **Message**: Estructura para mensajes del chat
+- **ChatCompletionRequest**: Requests para el chatbot
+- **OrderWeb**: Modelo completo de pedidos
+- **ItemWeb**: Elementos individuales del pedido
+- **UserCodeRequest**: Validación de códigos de usuario
 
 ### auth/security.py
-- Middleware de autenticación
-- Generación de tokens anti-abuse
-- Protección de endpoints
+- **Sistema de tokens anti-abuso**: Protección contra uso excesivo
+- **Validación de sesiones**: Control de acceso a endpoints
+- **Middleware de seguridad**: Protección de rutas sensibles
+- **Generación segura de tokens**: Utilizando secrets
 
 ### services/
-- **data_service.py**: Carga y manejo de datos (productos, usuarios, datos del menú)
-- **openai_service.py**: Integración con OpenAI para el chatbot
-- **email_service.py**: Envío de notificaciones por email
-- **fudo_service.py**: Integración con el sistema Fudo
-- **logging_service.py**: Logging de pedidos y respuestas del LLM
+- **data_service.py**: Carga y gestión de productos, usuarios y datos del menú
+- **openai_service.py**: Integración completa con OpenAI para el asistente Camilo Klein
+- **email_service.py**: Sistema de notificaciones por correo electrónico
+- **fudo_service.py**: Integración bidireccional con el sistema Fudo POS
+- **logging_service.py**: Registro detallado de pedidos y respuestas del LLM
 
 ### routes/
-- **chat.py**: Endpoints relacionados con el chat (/api/chat/*)
-- **users.py**: Endpoints de usuarios (/api/existsUser)
-- **products.py**: Endpoints de productos (/api/get_products)
-- **orders.py**: Endpoints de pedidos (/api/printer/order)
-- **static.py**: Servir archivos estáticos
+- **chat.py**: 
+  - `/api/chat/init-chat` - Inicialización del chat y generación de tokens
+  - `/api/chat/completions` - Procesamiento de mensajes del chatbot
+- **users.py**: 
+  - `/api/existsUser` - Validación de códigos de usuario
+- **products.py**: 
+  - `/api/get_products` - Obtención del catálogo completo
+- **orders.py**: 
+  - `/api/printer/order` - Procesamiento e impresión de pedidos
+- **static.py**: 
+  - Servir archivos estáticos y página principal
+
+## API Endpoints
+
+### Chat y Asistente IA
+```
+GET  /api/chat/init-chat       # Inicializar sesión de chat
+POST /api/chat/completions     # Enviar mensaje al asistente
+```
+
+### Gestión de Productos
+```
+GET  /api/get_products         # Obtener catálogo de productos
+```
+
+### Gestión de Pedidos
+```
+POST /api/printer/order        # Procesar y imprimir pedido
+```
+
+### Usuarios
+```
+POST /api/existsUser          # Validar código de usuario
+```
+
+## Flujo de la Aplicación
+
+1. **Inicialización**: El usuario accede a la página principal
+2. **Autenticación**: Se genera un token anti-abuso para la sesión
+3. **Exploración**: El usuario puede consultar productos y chatear con Camilo Klein
+4. **Pedido**: Selección de productos y agregado al carrito
+5. **Procesamiento**: Validación, impresión y sincronización con Fudo POS
+6. **Confirmación**: Notificación al usuario y registro en logs
+
+## Base de Conocimientos del Asistente
+
+El asistente Camilo Klein utiliza una base de datos especializada (`llm_data.json`) que incluye:
+
+- **Información del establecimiento**: Historia y concepto del Biergarten Klein
+- **Catálogo completo de cervezas**: 15+ variedades artesanales con detalles técnicos
+- **Recomendaciones especializadas**: Sugerencias basadas en preferencias
+- **Información nutricional y técnica**: IBU, SRM, graduación alcohólica
+- **Horarios y ubicación**: Datos prácticos para los clientes
+
+## Características Técnicas Avanzadas
+
+### Sistema de Logging
+- Registro detallado de todas las interacciones
+- Logs estructurados con timestamps
+- Separación por niveles (INFO, WARNING, ERROR)
+- Rotación automática de archivos de log
+
+### Integración con Hardware
+- **Impresora térmica USB**: Detección automática y manejo de errores
+- **Códigos de barras**: Generación para tracking de pedidos
+- **Notificaciones automáticas**: Email en caso de fallos de hardware
+
+### Gestión de Estado
+- **Redis para cache**: Tokens de autenticación y sesiones
+- **SQLite para persistencia**: Datos permanentes y históricos
+- **JSON para configuración**: Datos del menú y configuraciones
 
-## Ventajas de la Nueva Estructura
+## Ventajas de la Arquitectura
 
-1. **Separación de responsabilidades**: Cada módulo tiene una función específica
-2. **Mantenibilidad**: Código más fácil de mantener y modificar
-3. **Testabilidad**: Cada módulo puede ser probado independientemente
-4. **Escalabilidad**: Fácil agregar nuevas funcionalidades
-5. **Legibilidad**: Código más organizado y fácil de entender
+1. **Modularidad**: Cada componente tiene responsabilidades específicas
+2. **Escalabilidad**: Fácil agregar nuevas funcionalidades
+3. **Mantenibilidad**: Código organizado y bien documentado
+4. **Testabilidad**: Componentes aislados para pruebas unitarias
+5. **Seguridad**: Múltiples capas de protección
+6. **Performance**: Optimizado para respuestas rápidas
 
-## Cómo Ejecutar
+## Instalación y Configuración
 
+### Prerequisitos
 ```bash
-python main.py
+Python 3.9+
+Redis Server
+Impresora térmica USB (opcional)
 ```
 
-## Variables de Entorno Requeridas
+### Instalación
+```bash
+# Clonar el repositorio
+git clone [repository-url]
+cd pedidos_express
+
+# Instalar dependencias
+pip install -r requirements.txt
+
+# Configurar variables de entorno
+cp .env.example .env
+```
 
+### Variables de Entorno Requeridas
 ```env
-OPENAI_API_KEY=tu_api_key_aqui
-SECRET_KEY=tu_secret_key_para_sessions
+# OpenAI Configuration
+OPENAI_API_KEY=tu_api_key_de_openai
+
+# Security
+SECRET_KEY=tu_clave_secreta_muy_segura
+
+# Server Configuration  
 PORT=6001
+LOG_LEVEL=INFO
 
+# Fudo POS Integration
 FUDO_API_KEY=tu_api_key_fudo
 FUDO_API_SECRET=tu_api_secret_fudo
+
+# Redis Configuration
+REDIS_HOST=localhost
+REDIS_PORT=6379
+REDIS_DB=0
+
+# Email Configuration (opcional)
+SMTP_HOST=smtp.gmail.com
+SMTP_PORT=587
+SMTP_USER=tu_email@gmail.com
+SMTP_PASSWORD=tu_password_de_app
 ```
+
+## Ejecución
+
+### Desarrollo
+```bash
+python main.py
+```
+
+### Producción
+```bash
+gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app --bind 0.0.0.0:6001
+```
+
+## Monitoreo y Logs
+
+### Archivos de Log
+- `logs/app.log`: Log principal de la aplicación
+- `logs.csv`: Registro de interacciones del LLM
+
+### Métricas Monitoreadas
+- Tiempo de respuesta del chatbot
+- Errores de impresión
+- Fallos de conexión con Fudo POS
+- Uso del sistema anti-abuso
+
+## Seguridad
+
+### Medidas Implementadas
+- **Tokens anti-abuso**: Prevención de spam y uso excesivo
+- **Validación de entrada**: Sanitización de todos los inputs
+- **Protección CSRF**: Tokens de sesión seguros
+- **Rate limiting**: Control de frecuencia de requests
+- **Logging de seguridad**: Registro de intentos de acceso no autorizado
+
+## Desarrollo y Contribución
+
+### Estructura de Commits
+- `feat:` Nuevas características
+- `fix:` Corrección de bugs  
+- `docs:` Documentación
+- `style:` Formateo de código
+- `refactor:` Refactorización
+- `test:` Pruebas
+
+### Testing
+```bash
+# Ejecutar tests unitarios
+python -m pytest tests/
+
+# Tests de integración
+python -m pytest tests/integration/
+```
+
+## Roadmap Futuro
+
+### Próximas Características
+- [ ] Sistema de reservas de mesas
+- [ ] Programa de fidelización
+- [ ] Integración con redes sociales
+- [ ] App móvil nativa
+- [ ] Dashboard de analytics
+- [ ] Sistema de reviews y ratings
+
+### Mejoras Técnicas
+- [ ] Migración a PostgreSQL
+- [ ] Implementación de microservicios
+- [ ] Containerización con Docker
+- [ ] CI/CD con GitHub Actions
+- [ ] Monitoreo con Prometheus
+
+## Soporte y Contacto
+
+Para soporte técnico o consultas sobre el proyecto, contactar al equipo de desarrollo.
+
+---
+
+**Versión**: 1.0.0  
+**Última actualización**: Julio 2025  
+**Licencia**: Privada - Biergarten Klein

+ 17 - 15
app.py

@@ -1,3 +1,4 @@
+from time import struct_time
 from fastapi import FastAPI
 from starlette.middleware.sessions import SessionMiddleware
 from config.settings import SECRET_KEY, validate_config
@@ -5,7 +6,8 @@ from config.settings import SECRET_KEY, validate_config
 
 def create_app() -> FastAPI:
     """Create and configure FastAPI application"""
-    app = FastAPI(title="Web Pedidos Klein - FastAPI Backend")
+    app = FastAPI(title="Web Pedidos Klein - FastAPI Backend",
+                  description="Backend for the Web Pedidos Klein application using FastAPI",)
     
     # Add SessionMiddleware
     app.add_middleware(
@@ -21,27 +23,27 @@ def setup_routes(app: FastAPI):
     """Setup all application routes"""
     from routes import chat, users, products, orders, static
     from fastapi import Depends
-    from auth.security import protect_chat_api
+    from auth.security import get_current_user
     
     # Chat routes
-    app.add_api_route("/api/chat/init-chat", chat.init_chat, methods=["GET"], summary="Initialize chat and get anti-abuse token")
-    app.add_api_route("/api/chat/completions", chat.chat_completions, methods=["POST"], 
-                     summary="Get chat completions from OpenAI", dependencies=[Depends(protect_chat_api)])
-    
+    app.include_router(chat.chat_router, prefix="/api/chat",tags=["Chat"], dependencies=[Depends(get_current_user)])
+
     # User routes
-    app.add_api_route("/api/existsUser", users.exists_user, methods=["POST"], summary="Check if user exists")
-    
+    app.include_router(users.user_router, prefix="/api/users", tags=["Users"])
+
     # Product routes
-    app.add_api_route("/api/get_products", products.get_products, methods=["GET"], summary="Get products")
-    
+    app.include_router(products.product_router, prefix="/api/products", tags=["Products"],dependencies=[Depends(get_current_user)])
+
     # Order routes
-    app.add_api_route("/api/printer/order", orders.printer_order, methods=["POST"], 
-                     summary="Printer order", dependencies=[Depends(protect_chat_api)])
+    app.include_router(orders.order_router, prefix="/api/orders", tags=["Orders"], dependencies=[Depends(get_current_user)])
     
     # Static routes
     from fastapi.responses import HTMLResponse
-    app.add_api_route("/", static.serve_index_html, methods=["GET"], 
+    app.add_api_route("/express", static.serve_app_html, methods=["GET"], 
                      response_class=HTMLResponse, include_in_schema=False)
-    
+    app.add_api_route("/register", static.serve_register_html, methods=["GET"],
+                     response_class=HTMLResponse, include_in_schema=False)
+
     # Mount static files
-    static.mount_static_files(app)
+    static.mount_main_static_files(app)
+    static.mount_register_static_files(app)

+ 79 - 39
auth/security.py

@@ -1,46 +1,86 @@
+from datetime import datetime, timedelta
 from typing import Union
 from venv import logger
-from fastapi import Request, HTTPException, Header, Depends
+from fastapi import Depends, HTTPException
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
 from typing import Annotated
-import secrets
 from logging import getLogger
 
+from pydantic import BaseModel
+from config.settings import SECRET_KEY
+from jose import jwt, JWTError
+from passlib.context import CryptContext
+
+from services.data_service import UserDataService
+
+
 logger = getLogger(__name__)
 
-async def get_session_token(request: Request) -> Union[str, None]:
-    """Get the anti-abuse token from the session"""
-    return request.session.get("antiAbuseToken")
-
-
-async def protect_chat_api(
-    request: Request,
-    x_app_token: Annotated[Union[str, None], Header(alias="X-App-Token")] = None,
-    session_token: Annotated[Union[str, None], Depends(get_session_token)] = None
-):
-    """Protect chat API endpoints with token validation"""
-    # Equivalent to protectChatAPI middleware
-    if not session_token:
-        if request.client:
-            logger.error(f"Session token is not initialized or invalid. IP: {request.client.host}")
-        else:
-            logger.error("Session token is not initialized or invalid.")
-        logger.error("Session token is not initialized or invalid.")
-        raise HTTPException(status_code=403, detail="Acceso denegado: Sesión inválida o token no inicializado.")
-
-    if not x_app_token:
-        if request.client:
-            logger.error(f"X-App-Token is missing. IP: {request.client.host}")
-        else:
-            logger.error("X-App-Token is missing.")
-        raise HTTPException(status_code=401, detail="Acceso denegado: Falta el token X-Chat-Token.")
-
-    if x_app_token != session_token:
-        # Log this attempt for security monitoring
-        logger.warning(f"Invalid token attempt. Expected: {session_token}, Received: {x_app_token}")
-        raise HTTPException(status_code=403, detail="Acceso denegado: Token inválido.")
-    return True  # Protection passed
-
-
-def generate_anti_abuse_token() -> str:
-    """Generate a new anti-abuse token"""
-    return secrets.token_hex(32)
+ALGORITHM = "HS256"
+ACCESS_TOKEN_EXPIRE_DAYS = 60 * 24 * 7
+security = HTTPBearer()
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+user_data_service = UserDataService()
+
+class TokenData(BaseModel):
+    email: str
+
+
+def hash_password(password: str) -> str:
+    """Hash a password using bcrypt."""
+    return pwd_context.hash(password)
+
+def generate_token(email: str):
+    """Generate a JWT token for user authentication."""
+    data = {"sub": email}
+    expires_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_DAYS)
+    token = create_access_token(data=data, expires_delta=expires_delta)
+    logger.debug(f"Generated token for email {email}: {token}")
+    return token
+
+def create_access_token(data: dict, expires_delta: timedelta) -> str:
+    """Create a JWT access token."""
+    to_encode = data.copy()
+    if expires_delta:
+        expire = datetime.utcnow() + expires_delta
+    else:
+        expire = datetime.utcnow() + timedelta(days=1)
+        
+    to_encode.update({"exp": expire})
+    logger.debug(f"Creating access token with data: {to_encode}")
+    logger.debug(f"Token expiration time: {expire}")
+    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
+
+def authenticate_user(email: str, password: str) -> bool:
+    """Authenticate a user by email and password."""
+    user = user_data_service.login(email, password)
+    if not user:
+        return False
+    return True
+
+async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
+    credentials_exception = HTTPException(
+        status_code=401,
+        detail="No se pudieron validar las credenciales",
+        headers={"WWW-Authenticate": "Bearer"},
+    )
+    
+    try:
+        token = credentials.credentials
+        logger.debug(f"Decoding token: {token}")
+        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
+        email = payload.get("sub")
+        if email is None:
+            logger.error("Token does not contain email")
+            raise credentials_exception
+        token_data = TokenData(email=email)
+    except JWTError:
+        logger.error("JWTError: Invalid token")
+        raise credentials_exception
+    
+    user = user_data_service.get_by_email(token_data.email)
+    if user is None:
+        logger.error(f"User not found: {token_data.email}")
+        raise credentials_exception
+    return user
+

+ 262 - 0
config/mails.py

@@ -0,0 +1,262 @@
+
+
+REGISTER_MAIL = {
+    "subject": "Welcome to Pedidos Express",
+    "body": """
+<html>
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>¡Bienvenido a KlowApp!</title>
+    <style>
+        body {{
+            margin: 0;
+            padding: 0;
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+            background-color: #f3f4f6;
+        }}
+        
+        .email-container {{
+            max-width: 600px;
+            margin: 0 auto;
+            background-color: #ffffff;
+            border-radius: 12px;
+            box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
+            overflow: hidden;
+        }}
+        
+        .header {{
+            background-color: #101419;
+            color: white;
+            padding: 32px;
+            text-align: center;
+        }}
+        
+        .header h1 {{
+            margin: 0;
+            font-size: 28px;
+            font-weight: bold;
+        }}
+        
+        .header p {{
+            margin: 8px 0 0 0;
+            font-size: 16px;
+            opacity: 0.9;
+        }}
+        
+        .content {{
+            padding: 32px;
+        }}
+        
+        .welcome-message {{
+            text-align: center;
+            margin-bottom: 32px;
+        }}
+        
+        .welcome-message h2 {{
+            color: #101419;
+            font-size: 24px;
+            margin: 0 0 12px 0;
+            font-weight: bold;
+        }}
+        
+        .welcome-message p {{
+            color: #6b7280;
+            font-size: 16px;
+            line-height: 1.6;
+            margin: 0;
+        }}
+        
+        .benefits-section {{
+            background-color: #f9fafb;
+            border-radius: 8px;
+            padding: 24px;
+            margin: 24px 0;
+        }}
+        
+        .benefits-title {{
+            color: #101419;
+            font-size: 18px;
+            font-weight: 600;
+            margin: 0 0 16px 0;
+            text-align: center;
+        }}
+        
+        .benefits-list {{
+            list-style: none;
+            padding: 0;
+            margin: 0;
+        }}
+        
+        .benefits-list li {{
+            color: #374151;
+            font-size: 14px;
+            line-height: 1.5;
+            margin-bottom: 8px;
+            padding-left: 20px;
+            position: relative;
+        }}
+        
+        .benefits-list li:before {{
+            content: "✓";
+            color: #10b981;
+            font-weight: bold;
+            position: absolute;
+            left: 0;
+        }}
+        
+        .promo-section {{
+            background: linear-gradient(135deg, #101419 0%, #37404a 100%);
+            border-radius: 12px;
+            padding: 32px;
+            text-align: center;
+            margin: 32px 0;
+            color: white;
+        }}
+        
+        .promo-amount {{
+            font-size: 48px;
+            font-weight: bold;
+            margin: 16px 0;
+            border: 3px solid white;
+            border-radius: 8px;
+            padding: 16px;
+            display: inline-block;
+            min-width: 120px;
+        }}
+        
+        .promo-description {{
+            font-size: 18px;
+            margin: 16px 0;
+            opacity: 0.95;
+        }}
+        
+        .cta-button {{
+            background-color: white;
+            color: #101419;
+            padding: 16px 32px;
+            border-radius: 8px;
+            text-decoration: none;
+            font-weight: 600;
+            font-size: 16px;
+            display: inline-block;
+            margin-top: 16px;
+            transition: all 0.2s ease;
+        }}
+        
+        .cta-button:hover {{
+            background-color: #f3f4f6;
+            transform: translateY(-1px);
+        }}
+        
+        .website-section {{
+            text-align: center;
+            margin: 32px 0;
+            padding: 24px;
+            background-color: #f9fafb;
+            border-radius: 8px;
+        }}
+        
+        .website-url {{
+            color: #101419;
+            font-size: 20px;
+            font-weight: 600;
+            text-decoration: none;
+            border-bottom: 2px solid #101419;
+            padding-bottom: 4px;
+        }}
+        
+        .footer {{
+            background-color: #f9fafb;
+            padding: 24px 32px;
+            text-align: center;
+            border-top: 1px solid #e5e7eb;
+        }}
+        
+        .footer p {{
+            color: #6b7280;
+            font-size: 14px;
+            margin: 0;
+            line-height: 1.5;
+        }}
+        
+        @media (max-width: 600px) {{
+            .email-container {{
+                margin: 0;
+                border-radius: 0;
+            }}
+            
+            .header, .content {{
+                padding: 24px 16px;
+            }}
+            
+            .promo-amount {{
+                font-size: 36px;
+                padding: 12px;
+            }}
+        }}
+    </style>
+</head>
+<body>
+    <div style="padding: 20px;">
+        <div class="email-container">
+            <!-- Header -->
+            <div class="header">
+                <h1>¡Hola {name}</h1>
+                <p>Bienvenido a {app_name}</p>
+            </div>
+            
+            <!-- Content -->
+            <div class="content">
+                <!-- Welcome Message -->
+                <div class="welcome-message">
+                    <h2>Te damos la bienvenida a {app_name}</h2>
+                    <p>Tu registro ha sido exitoso. <br>Estamos emocionados de tenerte con nosotros. Esperamos que puedas disfrutar de nuestros beneficios.</p>
+                </div>
+                
+                <!-- Benefits Section -->
+                <div class="benefits-section">
+                    <h3 class="benefits-title">¿Qué puedes hacer con {app_name}?</h3>
+                    <ul class="benefits-list">
+                        <li>Realizar pedidos de forma rápida y sencilla</li>
+                        <li>Acceder a promociones exclusivas</li>
+                        <li>Disfrutar de una experiencia personalizada</li>
+                        <li>Usar nuestra inteligencia artificial</li>
+                    </ul>
+                </div>
+                
+                <!-- Promo Section -->
+                <div class="promo-section">
+                    <div class="promo-description">Este es tu pin de inicio de sesion</div>
+                    <div class="promo-amount">{pin}</div>
+                    <div class="promo-description">Guardalo muy bien</div>
+                    <a href="https://www.expressklein.com" class="cta-button">Y disfruta de {app_name}</a>
+                </div>
+                
+                <!-- Website Section -->
+                <div class="website-section">
+                    <p style="color: #6b7280; margin-bottom: 12px;">Visita nuestra plataforma:</p>
+                    <a href="https://www.expressklein.com" class="website-url">www.expressklein.com</a>
+                </div>
+            </div>
+            
+            <!-- Footer -->
+            <div class="footer">
+                <p>
+                    <strong>Express Klein</strong> - Tu aplicación de pedidos favorita<br>
+                    Si tienes alguna pregunta, no dudes en contactarnos.<br>
+                    © 2025 Express Klein. Todos los derechos reservados.
+                </p>
+            </div>
+        </div>
+    </div>
+</body>
+</html>
+    """
+}
+
+PRINTER_DISCONNECTED_MAIL = {
+    "subject": "Printer Disconnected",
+    "body": """
+"""
+}

+ 6 - 1
config/settings.py

@@ -7,6 +7,8 @@ from httpx import get
 # Load environment variables from .env file
 load_dotenv()
 
+
+APPNAME = "Pedidos Express"
 LOGS_FOLDER = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs')
 if not os.path.exists(LOGS_FOLDER):
     os.makedirs(LOGS_FOLDER)
@@ -24,13 +26,16 @@ logging.basicConfig(
 # Configuration
 OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
 PORT = int(os.getenv("PORT", 6001))
-
+PIN_KEY = os.getenv("PIN_KEY", "-1")
+if PIN_KEY == "-1":
+    logging.warning("Using default PIN_KEY. Please set a strong PIN_KEY in your .env file for production.")
 # SECRET_KEY is crucial for signing session cookies.
 # Fallback to a default if not set, but warn that this is insecure for production.
 SECRET_KEY = os.getenv("SECRET_KEY", "your_very_very_secret_key_for_signing_cookies_python_v2")
 
 # Data paths
 BG_DATA_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)),'data', 'llm_data.json')
+PRODUCT_DATA_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)),'data', 'products.json')
 DB_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)),'data', 'data.db')
 
 def validate_config():

BIN
data/data.db


+ 17 - 3
data/products.json

@@ -1,51 +1,65 @@
 [
+  
   {
     "id": 6,
+    "status":1,
     "name": "Burlesque",
     "type": "Cerveza",
     "description": "Cerveza Ale ámbar, 5.0º - IBU 12",
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/6"
   },
+  
   {
     "id": 15,
+    "status":1,
     "name": "Bendicion Gitana",
     "type": "Cerveza",
     "description": "Pale Ale - 5,0º - IBU 15",
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/15"
-  },{
+  },
+  {
     "id":163,
+    "status":1,
     "name":"Hoppy Mosh",
     "type":"Cerveza",
     "description":"IPA - 6.0º - IBU 38",
     "price": 6500,
     "image":"https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/163"
   },
+  
   {
     "id": 12,
+    "status":1,
     "name": "Black Mamba",
     "type": "Cerveza",
     "description": "Porter - 6.0º - IBU 15",
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/12"
-  },{
+  },
+  {
     "id": 665,
+    "status":1,
     "name": "Marzen",
     "type": "Cerveza",
     "description": " Estilo Märzenbier, 5.0º - IBU 22",
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/665"
-  },{
+  },
+  {
     "id": 1,
+    "status":1,
     "name": "24k Gold",
     "type": "Cerveza",
     "description": "Golden Ale - 4,5º - IBU 20",
     "price": 5000,
     "image": "https://fudo-apps-storage.s3.sa-east-1.amazonaws.com/production/113378/common/products/1"
   },
+  
   {
     "id": 655,
+    "status":1,
     "name": "🌟 Summer Klein",
     "type": "Coctel",
     "description": "Gin Juno, jugo de naranja, maracuya, limon y Ginger Beer",

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 655 - 0
logs/app.log


+ 5 - 1
main.py

@@ -7,6 +7,8 @@ from logging import getLogger
 from routes.orders import order_thread
 from threading import Thread
 
+from services.data_service import initialize_db
+
 logger = getLogger("main")
 
 def main():
@@ -23,6 +25,7 @@ def main():
     # Create and configure app
     app = create_app()
     setup_routes(app)
+    initialize_db()
     
     # Display startup information
     logger.info(f"Servidor corriendo en http://localhost:{PORT}")
@@ -36,7 +39,8 @@ def main():
     thread = Thread(target=order_thread, daemon=True)
     thread.start()
     # Start the server
-    uvicorn.run(app, host="0.0.0.0", port=PORT)
+    #rut without logs
+    uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info", access_log=False)
 
 
 if __name__ == "__main__":

+ 21 - 0
models/__init__.py

@@ -1 +1,22 @@
 # Models module
+
+from .user import User, UserIDRequest, RegisterUserRequest
+from .items import Product, ProductWithQuantity
+from .sells import Sale, SellItem, ItemWeb, OrderWeb
+from .blacklist import Blacklist
+from .chat import Message, ChatCompletionRequest
+
+__all__ = [
+    "User",
+    "UserIDRequest",
+    "RegisterUserRequest",
+    "Product",
+    "ProductWithQuantity",
+    "Sale",
+    "SellItem",
+    "ItemWeb",
+    "OrderWeb", 
+    "Blacklist",
+    "Message",
+    "ChatCompletionRequest"
+]

+ 10 - 0
models/blacklist.py

@@ -0,0 +1,10 @@
+from typing import Optional
+from pydantic import BaseModel
+
+class Blacklist(BaseModel):
+    """Blacklist model matching the database schema"""
+    id: int
+    user_id: int
+    email: Optional[str] = None
+    nombre: Optional[str] = None
+    rut: Optional[str] = None

+ 13 - 0
models/chat.py

@@ -0,0 +1,13 @@
+
+from typing import List
+from pydantic import BaseModel
+
+
+class Message(BaseModel):
+    role: str
+    content: str
+
+
+class ChatCompletionRequest(BaseModel):
+    messages: List[Message]
+    user: str

+ 18 - 0
models/items.py

@@ -0,0 +1,18 @@
+from typing import List, Optional
+from pydantic import BaseModel
+
+class Product(BaseModel):
+    """Product model matching the database schema"""
+    id: int
+    name: str
+    type: Optional[str] = None
+    description: Optional[str] = None
+    price: float
+    image: Optional[str] = None
+    status: int = 1  # 0: Inactive, 1: Active
+
+class ProductWithQuantity(Product):
+    """Product model with quantity field for sales"""
+    cantidad: int = 1
+
+

+ 0 - 32
models/schemas.py

@@ -1,32 +0,0 @@
-from typing import List
-from pydantic import BaseModel
-
-
-class Message(BaseModel):
-    role: str
-    content: str
-
-
-class ChatCompletionRequest(BaseModel):
-    messages: List[Message]
-    user: str
-
-
-class ItemWeb(BaseModel):
-    id: int
-    name: str
-    quantity: int
-    price: float
-    itemTotal: float
-
-
-class OrderWeb(BaseModel):
-    customerName: str
-    items: List[ItemWeb]
-    totalAmount: float
-    orderDate: str
-    table: int
-
-
-class UserCodeRequest(BaseModel):
-    user_code: str

+ 45 - 0
models/sells.py

@@ -0,0 +1,45 @@
+from typing import List, Optional
+from pydantic import BaseModel
+
+class ItemWeb(BaseModel):
+    id: int
+    quantity: int
+
+class OrderWeb(BaseModel):
+    customerId: int
+    items: List[ItemWeb]
+    totalAmount: float
+    orderDate: str
+    table: int
+
+class Product(BaseModel):
+    """Legacy Product model - use models.items.Product instead"""
+    id: int
+    name: str
+    price: float
+    type: str
+    
+    description: str
+    image: str
+    status: int  # 0: inactive, 1: active
+    quantity: Optional[int] = 1  # Optional quantity for the product
+
+class Sale(BaseModel):
+    """Sale model matching the database schema"""
+    id: int
+    user_id: int
+    total: float
+    fudo_id: str
+    fecha: str
+    table: int
+    user_name: Optional[str] = None
+    user_email: Optional[str] = None
+
+class SellItem(BaseModel):
+    """Legacy model - use Sale instead"""
+    user_id: int
+    products: list[Product]
+    total: float
+    fudo_id: str
+    fecha: str
+    table: int  

+ 25 - 0
models/user.py

@@ -0,0 +1,25 @@
+from typing import Optional
+from pydantic import BaseModel, Field
+
+class UserIDRequest(BaseModel):
+    id: int
+
+class RegisterUserRequest(BaseModel):
+    name: str
+    email: str
+    rut: str
+
+class User(BaseModel):
+    """User model matching the database schema"""
+    id: int
+    email: str
+    nombre: str
+    rut: str
+    pin_hash: str
+    kleincoins: str
+    created_at: str
+
+
+class LoginRequest(BaseModel):
+    email: str
+    pin: str = Field(min_length=4, max_length=4, description="4-digit PIN for user authentication")

+ 0 - 0
public/js/interfaces.js → pin.key


+ 0 - 21
public/js/service/auth.js

@@ -1,21 +0,0 @@
-
-async function existsUser(userCode){
-  const response = await fetch("/api/existsUser", {
-    method: "POST",
-    headers: {
-      "Content-Type": "application/json",
-      "X-App-Token": chatToken
-    },
-    body: JSON.stringify({
-      user_code: userCode
-    })
-  });
-  if (!response.ok) {
-    const errorData = await response.json().catch(() => ({ message: "Respuesta no válida del servidor." }));
-    throw new Error(errorData.message || `Error del servidor: ${response.status}`);
-  }
-  const data = await response.json();
-  return data;
-}
-
-export { existsUser};

+ 0 - 0
public/assets/summer.jpeg → public/main/assets/summer.jpeg


+ 2 - 2
public/index.html → public/main/index.html

@@ -26,8 +26,8 @@
     </script>
   <!-- Markdown -->
   <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
-  <script src="js/app.js" type="module"></script>
-  <link rel="stylesheet" href="styles.css">
+  <script src="/express/js/app.js" type="module"></script>
+  <link rel="stylesheet" href="/express/styles.css">
   <!-- Animaciones -->
   <style>
     @keyframes slideRight {

+ 30 - 23
public/js/app.js → public/main/js/app.js

@@ -1,8 +1,10 @@
-import { initializeService , sendMessage as serviceSendMessage } from './service/chat.js';
+import { sendMessage as serviceSendMessage } from './service/chat.js';
 import { getProducts, sendOrder } from './service/product.js';
+import { login } from './service/auth.js'
 
 // --- Variables de Usuario ---
-let userName = '';
+let userId = -1;
+let userName = "Cliente";
 let userTable = null;
 let userToken = null;
 // --- Datos de Productos y Carrito ---
@@ -37,19 +39,10 @@ let globalLoaderElement = null;
 
 //#region --- Inicialización y Configuracion ---
 async function initializeApp() {
+    
+    showGlobalLoader();
     await renderProducts();
-    try {
-        const [ok, token] = await initializeService();
-        userToken = token;
-        if (ok) {
-        } else {
-            console.warn("Chat AI no pudo inicializarse.");
-            displayChatMessage("ai", "No se pudo conectar con el Chef IA en este momento.");
-        }
-    } catch (error) {
-        console.error("Error durante la inicialización del Chat AI:", error);
-        displayChatMessage("ai", "Error al iniciar la IA.");
-    }
+    hideGlobalLoader();
 
     const chatSuggestions = Array.from(chatSuggestionsElement.children);
 
@@ -76,9 +69,22 @@ function initializeLoginModal() {
             alert("Por favor, completa todos los campos.");
             return;
         }
+        try{
+            const {data} = await login(email, pin)
+            userToken = data.token;
+            userName = data.name;
+            console.log("Usuario autenticado:", data);
+            if (!userToken || data.id === undefined) {
+                alert("Error al iniciar sesión. Por favor, inténtalo de nuevo.");
+                return;
+            }
+
+            sessionModal.classList.add('hidden');
+
+            initializeApp();
+        }catch (error) {
+        }
 
-        sessionModal.classList.add('hidden');
-        initializeApp();
     });
 }
 
@@ -147,7 +153,8 @@ async function renderProducts() {
     if (!template) return;
 
     productListElement.innerHTML = "";
-    products = await getProducts();
+    console.log("Cargando productos...");
+    products = await getProducts(userToken);
 
     products.forEach(product => {
         const clone = template.content.cloneNode(true);
@@ -313,9 +320,9 @@ async function processOrder() {
 
     try {
         const orderData = {
-            customerName: userName,
+            customerId: userId,
             table: userTable,
-            items: cart.map(item => ({ id: item.id, name: item.name, quantity: item.quantity, price: item.price, itemTotal: item.price * item.quantity })),
+            items: cart.map(item => ({ id: item.id, quantity: item.quantity})),
             totalAmount: cart.reduce((sum, item) => sum + item.price * item.quantity, 0),
             orderDate: new Date().toLocaleString('sv-SE').replace(' ', 'T')
         };
@@ -365,12 +372,12 @@ async function sendMessageToAI() {
     aiLoadingIndicator.classList.remove("hidden");
 
     try {
-        const response = await serviceSendMessage(userInput, chatHistory, userName);
+        const response = await serviceSendMessage(userInput, chatHistory, userName, userToken);
         if (!response) {
             displayChatMessage("ai", "Hubo un problema al conectar con el Chef IA.");
         } else if (response === "not_init") {
-            if (await serviceInitializeChat()) {
-                const response = await serviceSendMessage(userInput, chatHistory, userName);
+            if (await initializeService()) {
+                const response = await serviceSendMessage(userInput, chatHistory, userName, userToken);
                 if (response) {
                     chatHistory = response.messageList;
                     displayChatMessage("ai", response.assistantResponse);
@@ -403,6 +410,6 @@ document.addEventListener("DOMContentLoaded", async () => {
     initializeChat()
     setupBasicListeners();
     initializeLoginModal();
-    initializeApp()
+    // initializeApp()
 
 });

+ 0 - 0
public/main/js/interfaces.js


+ 33 - 0
public/main/js/service/auth.js

@@ -0,0 +1,33 @@
+
+async function login(email,pin){
+  const response = await fetch("/api/users/login", {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json"
+    },
+    body: JSON.stringify({ email, pin })
+}
+  );
+  if (response.status == 404) {
+    const errorData = await response.json().catch(() => ({ message: "El usuario no fue encontrado." }));
+    throw new Error(errorData.message);
+  }else if (response.status == 401) {
+    const errorData = await response.json().catch(() => ({ message: "Credenciales incorrectas.", attempts: 0 }));
+    alert(errorData.message);
+    throw new Error(errorData.message);
+  }else if (response.status != 200) {
+    console.error(response.status, response.statusText);
+    const errorData = await response.json().catch(() => ({ message: "Error al iniciar sesión." }));
+    alert(errorData.message, `Intentos restantes: ${errorData.attempts_remaining || 0}`);
+    throw new Error(errorData.message);
+  }
+  const data = await response.json();
+  if (!data || !data.data.token) {
+    alert("Error al iniciar sesión.");
+    throw new Error("Error al iniciar sesión.");
+  }
+  return data;
+}
+
+
+export { login };

+ 5 - 22
public/js/service/chat.js → public/main/js/service/chat.js

@@ -1,24 +1,7 @@
-async function initializeService() {
-  try {
-    const response = await fetch("/api/chat/init-chat");
-    if (!response.ok) {
-      const errorData = await response.json().catch(() => ({ message: "Error desconocido al inicializar." }));
-      throw new Error(errorData.message || `Error del servidor: ${response.status}`);
-    }
-    const data = await response.json();
-    const token = data.chatToken;
-    if (!token) {
-      throw new Error("No se pudo obtener el token de chat.");
-    }
-    return [true, token];
-  } catch (error) {
-    console.error("Error al inicializar el chat:", error);
-  }
-}
-async function sendMessage(message, messageList, userName) {
-  if (!chatToken) { 
+
+async function sendMessage(message, messageList, userName, token) {
+  if (!token) { 
     return "not_init";
-    return;
   }
   messageList.push({ role: "user", content: message });
   const cuerpo = { messages: messageList, user: userName }
@@ -26,7 +9,7 @@ async function sendMessage(message, messageList, userName) {
     method: "POST",
     headers: {
       "Content-Type": "application/json",
-      "X-App-Token": chatToken
+      "Authorization": `Bearer ${token}`
     },
     body: JSON.stringify(cuerpo)
   });
@@ -44,4 +27,4 @@ async function sendMessage(message, messageList, userName) {
   }
 }
 
-export { initializeService , sendMessage };
+export { sendMessage };

+ 8 - 3
public/js/service/product.js → public/main/js/service/product.js

@@ -2,7 +2,7 @@
 async function sendOrder(order, token) {
   console.log("Enviando orden:", order);
   try {
-    const response = await fetch("/api/printer/order", {
+    const response = await fetch("/api/orders/send", {
       method: "POST",
       headers: {
         "Content-Type": "application/json",
@@ -21,8 +21,13 @@ async function sendOrder(order, token) {
     throw error;
   }
 }
-async function getProducts(){
-  const response = await fetch("/api/get_products");
+async function getProducts(token){
+  const response = await fetch("/api/products/", {
+    headers: {
+      "Content-Type": "application/json",
+      "Authorization": `Bearer ${token}`
+    }
+  });
   if (!response.ok) {
     const errorData = await response.json().catch(() => ({ message: "Respuesta no válida del servidor." }));
     throw new Error(errorData.message || `Error del servidor: ${response.status}`);

+ 0 - 0
public/styles.css → public/main/styles.css


+ 62 - 0
public/register/app.js

@@ -0,0 +1,62 @@
+function showRegisterModal() {
+            document.getElementById('registerModal').classList.remove('hidden');
+        }
+
+        function hideRegisterModal() {
+            document.getElementById('registerModal').classList.add('hidden');
+        }
+
+        // Formateo automático del RUT mientras se escribe
+        document.getElementById('rutInput').addEventListener('input', function(e) {
+            let rut = e.target.value.replace(/[^0-9kK]/g, '');
+            
+            if (rut.length > 1) {
+                let body = rut.slice(0, -1);
+                let dv = rut.slice(-1);
+                
+                if (body.length > 0) {
+                    // Agregar puntos cada 3 dígitos desde la derecha
+                    body = body.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
+                    rut = body + '-' + dv;
+                }
+            }
+            
+            e.target.value = rut;
+        });
+
+        // Manejo del envío del formulario
+        document.getElementById('registerForm').addEventListener('submit', function(e) {
+            e.preventDefault();
+            
+            const formData = new FormData(e.target);
+            const data = {
+                name: formData.get('name'),
+                email: formData.get('email'),
+                rut: formData.get('rut')
+            };
+            console.log('Datos del formulario:', data);
+            fetch('/api/users/register', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify(data)
+            })
+            .then(response => {
+                if (!response.ok) {
+                    return response.json().then(errorData => {
+                        throw new Error(errorData.message || 'Error al registrar el usuario.');
+                    });
+                }
+                return response.json();
+            })
+            .then(data => {
+                console.log('Registro exitoso:', data);
+                alert('Registro exitoso! Revisa tu correo electrónico para el PIN.');
+                hideRegisterModal();
+            })
+        });
+
+document.addEventListener('DOMContentLoaded', function() {
+    showRegisterModal();
+});

+ 74 - 0
public/register/index.html

@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html lang="es">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Modal de Registro</title>
+    <script src="https://cdn.tailwindcss.com"></script>
+    <script src="/register/app.js" defer></script>
+</head>
+<body class="bg-gray-100 p-8">
+
+    <!-- === MODAL DE REGISTRO === -->
+    <div id="registerModal"
+         class="fixed hidden inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
+      <form id="registerForm" class="bg-white w-full max-w-md p-8 rounded-xl shadow-xl space-y-6">
+        <div class="text-center">
+          <h2 class="text-2xl font-bold text-gray-900">¡Regístrate!</h2>
+          <p id="registerMessage" class="text-sm text-gray-600 mt-2">
+            Crea tu cuenta para realizar pedidos
+          </p>
+        </div>
+
+        <div class="space-y-4">
+          <div>
+            <label for="nameInput" class="block text-sm font-medium text-gray-700 mb-2">
+              Nombre completo
+            </label>
+            <input id="nameInput"
+                   name="name"
+                   type="text"
+                    pattern="[A-Za-zÀ-ÿ\s]{2,}"
+                   class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] focus:border-transparent outline-none transition-all"
+                   placeholder="Juan Pérez" 
+                   required />
+          </div>
+
+          <div>
+            <label for="emailRegisterInput" class="block text-sm font-medium text-gray-700 mb-2">
+              Correo electrónico
+            </label>
+            <input id="emailRegisterInput"
+                   name="email"
+                   type="email"
+                   class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] focus:border-transparent outline-none transition-all"
+                   placeholder="tu@email.com" 
+                   required />
+          </div>
+
+          <div>
+            <label for="rutInput" class="block text-sm font-medium text-gray-700 mb-2">
+              RUT
+            </label>
+            <input id="rutInput"
+                   name="rut"
+                   type="text"
+                   maxlength="12"
+                   pattern="^\d{1,2}\.\d{3}\.\d{3}-[\dkK]$"
+                   class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] focus:border-transparent outline-none transition-all"
+                   placeholder="12345678-9" 
+                   required />
+          </div>
+        </div>
+
+        <div class="flex gap-3">
+          <button id="registerAcceptBtn"
+                  type="submit"
+                  class="flex-1 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]">
+            Registrarse
+          </button>
+        </div>
+      </form>
+    </div>
+</body>
+</html>

+ 0 - 0
public/register/styles.css


+ 6 - 17
routes/chat.py

@@ -1,29 +1,18 @@
-from math import log
 from fastapi import Request, HTTPException, Depends
 from fastapi.responses import JSONResponse
-from models.schemas import ChatCompletionRequest
+from httpx import get
+from models.chat import ChatCompletionRequest
 from services.openai_service import generate_completion
 from services.logging_service import log_llm_response
-from auth.security import protect_chat_api, generate_anti_abuse_token
+from auth.security import get_current_user
 import logging
-
+from fastapi import APIRouter
 logger = logging.getLogger(__name__)
 
-async def init_chat(request: Request):
-    """Initialize chat and get anti-abuse token"""
-    if request.client:
-        logger.info(f"App initialized from client: {request.client.host}")
-
-    current_token = request.session.get("antiAbuseToken")
-    if not current_token:
-        new_token = generate_anti_abuse_token()
-        request.session["antiAbuseToken"] = new_token
-        logger.info(f"Generated new antiAbuseToken for session: {new_token}")
-        return JSONResponse({"chatToken": new_token})
-    else:
-        return JSONResponse({"chatToken": current_token})
+chat_router = APIRouter()
 
 
+@chat_router.post("/completions")
 async def chat_completions(request_data: ChatCompletionRequest, request: Request):
     """Get chat completions from OpenAI"""
     # Uses session_token (which is the antiAbuseToken) as an identifier for logging

+ 20 - 8
routes/orders.py

@@ -1,9 +1,10 @@
+from itertools import product
 from math import log
 import time
-from fastapi import HTTPException
+from fastapi import HTTPException, APIRouter
 from fastapi.responses import JSONResponse
 from fudo import fudo
-from models.schemas import OrderWeb
+from models.sells import OrderWeb
 from services.fudo_service import add_product_to_fudo
 from services.email_service import send_email
 from services.logging_service import log_order
@@ -11,12 +12,16 @@ from impresora.printer import PrinterUSB
 from impresora.order import Order, Item
 from logging import getLogger
 from threading import Thread
+from services.data_service import ProductDataService, UserDataService
+from config.mails import PRINTER_DISCONNECTED_MAIL
 
 logger = getLogger(__name__)
-
+user_data_service = UserDataService()
+product_data_service = ProductDataService()
 printer_orders = []
+order_router = APIRouter()
 
-
+@order_router.post("/send")
 async def printer_order(order: OrderWeb):
     """Process printer order"""
     logger.info("Printer order received")
@@ -25,7 +30,10 @@ async def printer_order(order: OrderWeb):
     if not PrinterUSB.check_usb_port(0xfe6, 0x811e):
         logger.error("Printer is not connected.")
         email_thread = Thread(
-            target=send_email, daemon=True)
+            target=send_email,
+            args=(PRINTER_DISCONNECTED_MAIL["subject"], PRINTER_DISCONNECTED_MAIL["body"], ["erwinjacimino2003@gmail.com", "mompyn@gmail.com"]),
+            daemon=True
+        )
         email_thread.start()
         logger.error("Email sent to admin about printer issue.")
         return JSONResponse(status_code=424, content={"message": "Printer is not connected."})
@@ -54,15 +62,19 @@ async def printer_order(order: OrderWeb):
             content={"message": "Error adding products to table.", "errors": product_errors}
         )
     
+    user = user_data_service.get_by_id(order.customerId)
+    if not user:
+        return JSONResponse(status_code=404, content={"message": "User not found."})
+    products = product_data_service.get_products([item.id for item in items])
     # Print order
     printer_orders.append(Order(
-        order.customerName, 
-        [Item(item.name, item.price, item.quantity) for item in items]
+        user=user.nombre if user else "Unknown User",
+        items=[Item(product.name, product.price, item.quantity) for product, item in zip(products, items)]
     ))
     
     
     # Log order
-    log_order(order, items)
+    log_order(user.nombre, order.table, order_date=order.orderDate, items=[product.name for product in products])
     
     return JSONResponse({"message": "Order processed successfully"})
 

+ 13 - 4
routes/products.py

@@ -1,10 +1,19 @@
+from math import prod
 from fastapi.responses import JSONResponse
-from services.data_service import all_products
+from auth.security import get_current_user
+from services.data_service import ProductDataService
 from logging import getLogger
-
+from fastapi import APIRouter, Depends
 logger = getLogger(__name__)
 
-async def get_products():
+product_data_service = ProductDataService()
+
+product_router = APIRouter()
+
+@product_router.get("/")
+async def get_products(current_user = Depends(get_current_user)):
     """Get products"""
+    logger.debug(f"Current user: {current_user.email if current_user else 'Anonymous'}")
     logger.info("Fetching all products")
-    return JSONResponse({"products": all_products})
+    all_products = list(map(lambda p: p.model_dump(), product_data_service.get_all()))
+    return JSONResponse({"products": all_products})

+ 2 - 0
routes/sells.py

@@ -0,0 +1,2 @@
+from pydantic import BaseModel
+

+ 14 - 4
routes/static.py

@@ -4,14 +4,24 @@ from fastapi.responses import HTMLResponse, FileResponse
 from fastapi.staticfiles import StaticFiles
 
 
-async def serve_index_html():
+async def serve_app_html():
     """Serve the main HTML file"""
-    index_path = os.path.join("public", "index.html")
+    index_path = os.path.join("public","main", "index.html")
     if not os.path.exists(index_path):
         raise HTTPException(status_code=404, detail="public/index.html not found.")
     return FileResponse(index_path)
 
+async def serve_register_html():
+    """Serve the register HTML file"""
+    register_path = os.path.join("public", "register", "index.html")
+    if not os.path.exists(register_path):
+        raise HTTPException(status_code=404, detail="public/register/index.html not found.")
+    return FileResponse(register_path)
 
-def mount_static_files(app):
+def mount_register_static_files(app):
+    """Mount static files for the register page"""
+    app.mount("/register/", StaticFiles(directory="public/register", html=False), name="register_static")
+
+def mount_main_static_files(app):
     """Mount static files"""
-    app.mount("/", StaticFiles(directory="public", html=False), name="public_root_assets")
+    app.mount("/express/", StaticFiles(directory="public/main", html=False), name="public_root_assets")

+ 105 - 17
routes/users.py

@@ -1,18 +1,106 @@
+from logging import getLogger
+from fastapi import APIRouter
 from fastapi.responses import JSONResponse
-from models.schemas import UserCodeRequest
-from services.data_service import load_users
-
-
-async def exists_user(request: UserCodeRequest):
-    """Check if user exists"""
-    users = load_users()
-    for user in users:
-        if user['userCode'] == request.user_code:
-            return JSONResponse({
-                "success": True,
-                "userName": user['userName']
-            })
-    return JSONResponse({
-        "success": True,
-        "userName": request.user_code
-    })
+from httpx import RequestError
+import redis
+import config
+from models import user
+from models.user import  LoginRequest, UserIDRequest, RegisterUserRequest
+from services.data_service import UserDataService
+from cryptography.fernet import Fernet
+from config.settings import PIN_KEY
+from auth.security import generate_token
+from services.email_service import send_email
+from config.mails import REGISTER_MAIL
+from config.settings import APPNAME
+fernet = Fernet(PIN_KEY.encode())
+logger = getLogger(__name__)
+user_data_service = UserDataService()
+
+user_router = APIRouter()
+
+
+def unique_pin_generate():
+    """Generate a unique 4-digit PIN"""
+    import random
+    pin = str(random.randint(1000, 9999))
+    return pin
+
+@user_router.post("/exists")
+async def exists_user(request: UserIDRequest):
+        """Check if user exists"""
+        user = user_data_service.get_by_id(request.id)
+        if user:
+            return JSONResponse(status_code=200, content={"exists": True})
+        else:
+            return JSONResponse(status_code=404, content={"exists": False, "data": {"message": "User does not exist."}})
+
+@user_router.post("/register")
+async def register_user(request: RegisterUserRequest):
+    """Register a new user"""
+    pin = unique_pin_generate()
+    userID = user_data_service.create(request.name, request.email, request.rut, pin)
+    if userID == -1:
+        return JSONResponse(status_code=400, content={"message": "User already exists."})
+    user = user_data_service.get_by_id(userID)
+    if not user:
+        return JSONResponse(status_code=500, content={"message": "Error creating user."})
+    send_email(
+        REGISTER_MAIL["subject"],
+        REGISTER_MAIL["body"],
+        [user.email], name=user.nombre, app_name=APPNAME, pin=pin
+    )
+    return JSONResponse(status_code=201, content={"message": "User created successfully.", "data": {
+        **user.model_dump(exclude={"pin_hash"}),
+        "token": generate_token(user.email)
+    }})
+
+@user_router.post("/login")
+async def login_user(request: LoginRequest):
+    """Login user with email and PIN"""
+    logger.debug(f"Login attempt for email: {request.email}")
+    logger.debug(f"Login request: {request.pin}")
+    user = user_data_service.login(request.email, request.pin)
+    if user:
+        # Successful login, return user data and token
+        return JSONResponse(status_code=200, content={"message": "Login successful.", "data": {
+            "id": user.id,
+            "name": user.nombre,
+            "email": user.email,
+            "kleincoins": user.kleincoins,
+            "created_at": user.created_at,
+            "token": generate_token(user.email)
+        }})
+    else:
+        # Failed login: increment attempts in Redis
+        redis_client = redis.Redis(host='localhost', port=6379, db=0)
+        redis_client.incr(f"login_attempts:{request.email}")
+        redis_client.expire(f"login_attempts:{request.email}", 3600)
+        redis_attempts = redis_client.get(f"login_attempts:{request.email}")
+        attempts = int(redis_attempts) if redis_attempts else 0 # type: ignore
+        if attempts and int(attempts) > 5:
+            # Too many attempts, block login
+            return JSONResponse(status_code=429, content={"message": "Too many login attempts. Please try again later."})
+        else:
+            # Set flag for last failed login
+            redis_client.set(f"last_failed_login:{request.email}", "true")
+            redis_client.expire(f"last_failed_login:{request.email}", 3600)
+            logger.warning(f"Failed login attempt for {request.email}. Attempts: {attempts}")
+            logger.error("Invalid email or PIN.")
+        # Return unauthorized with attempts remaining
+        return JSONResponse(status_code=401, content={"message": "Invalid email or PIN.", "attempts_remaining": 5 - attempts if attempts else 5})
+
+@user_router.delete("/delete")
+async def delete_user(request: UserIDRequest):
+    """Delete a user by ID"""
+    user = user_data_service.delete(request.id)
+    if user:
+        return JSONResponse(status_code=200, content={"message": "User deleted successfully.", "data": user})
+    else:
+        return JSONResponse(status_code=404, content={"message": "User not found."})
+
+@user_router.get("/all")
+async def get_all_users():
+    """Get all users"""
+    users = list(map(lambda u: u.model_dump(), user_data_service.get_all()))
+    return JSONResponse(status_code=200, content={"data": users})

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 472 - 188
services/data_service.py


+ 13 - 52
services/email_service.py

@@ -1,67 +1,28 @@
 import smtplib
 from email.message import EmailMessage
+from logging import getLogger
 
+logger = getLogger(__name__)
 
-def send_email():
-    """Send email notification when printer is disconnected"""
+def send_email(
+    subject: str,
+    body: str,
+    to: list[str],
+    **kwargs
+):
+    logger.debug(str(kwargs))
+    """Send email """
     # Datos del remitente
     EMAIL_ORIGEN = 'expresspedidos211@gmail.com'
-    EMAIL_DESTINO = ['erwinjacimino2003@gmail.com', "mompyn@gmail.com"]
     CONTRASENA = 'drkassszdtgapufg'
 
     # Crear el correo
     msg = EmailMessage()
-    msg['Subject'] = 'Impresora Desconectada weon :('
+    msg['Subject'] = subject
     msg['From'] = EMAIL_ORIGEN
-    msg['To'] = ", ".join(EMAIL_DESTINO)
+    msg['To'] = ", ".join(to)
     msg.set_content('Este correo tiene contenido HTML.')
-    msg.add_alternative("""
-    <html>
-    <body style="margin:0; padding:0; background-color:#5a67d8; font-family: Arial, sans-serif;">
-        <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="padding: 40px 0;">
-        <tr>
-            <td align="center">
-            <table border="0" cellpadding="0" cellspacing="0" width="500" style="background-color: #e3e3e3; border-radius: 25px; padding: 40px; text-align: center;">
-                <tr>
-                <td>
-                    <div style="font-size: 60px; background-color: #ff6b6b; width: 80px; height: 80px; line-height: 80px; border-radius: 15px; margin: 0 auto 20px; color: white;">
-                    🖨️
-                    </div>
-                </td>
-                </tr>
-                <tr>
-                <td>
-                    <img src="https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZGlraDhpb2tkeHEweDZ2eWdnZDZlNXFvODhmNzZieWN6OXp0b3ZqNCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3hetVnNSl0IBa/giphy.gif" alt="Gatito peleando con la impresora" width="250" style="border-radius: 12px; margin-bottom: 20px;" />
-                </td>
-                </tr>
-                <tr>
-                <td>
-                    <h1 style="font-size: 24px; color: #ff6b6b; margin-bottom: 10px;">¡Impresora Desconectada!</h1>
-                </td>
-                </tr>
-                <tr>
-                <td>
-                    <p style="font-size: 16px; color: #333333; line-height: 1.5; margin-bottom: 20px;">
-                    No se puede establecer conexión con la impresora.<br>
-                    Por favor, verifica la conexión y vuelve a intentarlo.
-                    </p>
-                </td>
-                </tr>
-                <tr>
-                <td>
-                    <span style="display: inline-block; background: #ff6b6b; color: white; padding: 12px 24px; border-radius: 25px; font-weight: bold; font-size: 16px;">
-                    🔴 Estado: Desconectada
-                    </span>
-                </td>
-                </tr>
-            </table>
-            </td>
-        </tr>
-        </table>
-    </body>
-    </html>
-
-    """, subtype='html')
+    msg.add_alternative(body.format(**kwargs), subtype='html')
 
     # Enviar el correo usando SMTP de Gmail
     with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:

+ 6 - 6
services/logging_service.py

@@ -1,10 +1,10 @@
 import csv
 import os
 from typing import List
-from models.schemas import OrderWeb, ItemWeb
+from models.sells import OrderWeb, ItemWeb
 
 
-def log_order(order: OrderWeb, items: List[ItemWeb]):
+def log_order(username, table, order_date, items: List[str]):
     """Log order information to CSV file"""
     if not os.path.exists('logs.csv'):
         with open('logs.csv', 'w', newline='') as f:
@@ -14,10 +14,10 @@ def log_order(order: OrderWeb, items: List[ItemWeb]):
     with open('logs.csv', 'a', newline='') as f:
         writer = csv.writer(f)
         writer.writerow([
-            order.customerName, 
-            order.table, 
-            order.orderDate, 
-            list(map(lambda item: item.name, items))
+            username,
+            table,
+            order_date,
+            items
         ])
 
 

+ 3 - 3
services/openai_service.py

@@ -2,8 +2,8 @@ from typing import List
 from fastapi import HTTPException
 from openai import OpenAI
 from config.settings import OPENAI_API_KEY
-from models.schemas import Message
-from services.data_service import bg_data_loaded
+from models.chat import Message
+from services.data_service import data_bg_loaded
 from logging import getLogger
 # Initialize OpenAI client
 openai_client = OpenAI(api_key=OPENAI_API_KEY)
@@ -20,7 +20,7 @@ async def generate_completion(messages_array: List[Message], session_id: str) ->
 
     data_for_prompt = [
         f'{{"pregunta": "{item.get("q", "")}", "respuesta": "{item.get("ans", "")}"}}'
-        for item in bg_data_loaded
+        for item in data_bg_loaded
     ]
     data_string = "\n".join(data_for_prompt)
 

+ 113 - 0
test_models.py

@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+"""
+Test script to verify the models are correctly defined
+"""
+
+# Test imports
+try:
+    from models.user import User, UserModel, UserIDRequest, RegisterUserRequest
+    print("✅ User models imported successfully")
+except ImportError as e:
+    print(f"❌ Error importing user models: {e}")
+
+try:
+    from models.items import Product, ProductWithQuantity
+    print("✅ Product models imported successfully")
+except ImportError as e:
+    print(f"❌ Error importing product models: {e}")
+
+try:
+    from models.sells import Sale, SellItem, ItemWeb, OrderWeb
+    print("✅ Sales models imported successfully")
+except ImportError as e:
+    print(f"❌ Error importing sales models: {e}")
+
+try:
+    from models.blacklist import Blacklist
+    print("✅ Blacklist model imported successfully")
+except ImportError as e:
+    print(f"❌ Error importing blacklist model: {e}")
+
+try:
+    from models.chat import Message, ChatCompletionRequest
+    print("✅ Chat models imported successfully")
+except ImportError as e:
+    print(f"❌ Error importing chat models: {e}")
+
+# Test model creation
+try:
+    user = User(
+        id=1,
+        email="test@example.com",
+        nombre="Test User",
+        rut="12345678-9",
+        pin_hash="hashed_pin",
+        kleincoins="100",
+        created_at="2023-01-01T00:00:00"
+    )
+    print("✅ User model creation successful")
+    print(f"   User: {user.nombre} ({user.email})")
+except Exception as e:
+    print(f"❌ Error creating user model: {e}")
+
+try:
+    product = Product(
+        id=1,
+        name="Test Product",
+        type="food",
+        description="A test product",
+        price=10.99,
+        image="test.jpg",
+        status=1
+    )
+    print("✅ Product model creation successful")
+    print(f"   Product: {product.name} - ${product.price}")
+except Exception as e:
+    print(f"❌ Error creating product model: {e}")
+
+try:
+    product_with_quantity = ProductWithQuantity(
+        id=1,
+        name="Test Product",
+        type="food",
+        description="A test product",
+        price=10.99,
+        image="test.jpg",
+        status=1,
+        cantidad=3
+    )
+    print("✅ ProductWithQuantity model creation successful")
+    print(f"   Product: {product_with_quantity.name} - Qty: {product_with_quantity.cantidad}")
+except Exception as e:
+    print(f"❌ Error creating ProductWithQuantity model: {e}")
+
+try:
+    sale = Sale(
+        id=1,
+        user_id=1,
+        total=50.99,
+        fudo_id="FUDO123",
+        fecha="2023-01-01T12:00:00",
+        table=5,
+        user_name="Test User",
+        user_email="test@example.com"
+    )
+    print("✅ Sale model creation successful")
+    print(f"   Sale: {sale.fudo_id} - Total: ${sale.total}")
+except Exception as e:
+    print(f"❌ Error creating sale model: {e}")
+
+try:
+    blacklist = Blacklist(
+        id=1,
+        user_id=1,
+        email="blocked@example.com",
+        nombre="Blocked User",
+        rut="98765432-1"
+    )
+    print("✅ Blacklist model creation successful")
+    print(f"   Blacklisted: {blacklist.nombre} ({blacklist.email})")
+except Exception as e:
+    print(f"❌ Error creating blacklist model: {e}")
+
+print("\n🎉 All model tests completed!")

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio