Procházet zdrojové kódy

verification system

latapp před 9 měsíci
rodič
revize
192e84936c
9 změnil soubory, kde provedl 351 přidání a 45 odebrání
  1. 0 8
      .env
  2. 2 1
      .gitignore
  3. 3 0
      app.py
  4. 46 22
      config/mails.py
  5. 4 2
      config/messages.py
  6. 1 1
      data/products.json
  7. 3 0
      models/user.py
  8. 227 0
      public/verify.html
  9. 65 11
      routes/users.py

+ 0 - 8
.env

@@ -1,8 +0,0 @@
-PORT = 6001
-SECRET_KEY = 866B3F5EE90BFED7EDAD0FCB0A9C0FC866F03166C05648478ECEF6148C9E13BEBF8E429BB9B06DCF
-OPENAI_API_KEY = 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
-NODE_ENV = development
-FUDO_API_KEY=NzZAMTEzMzc4
-FUDO_API_SECRET=FNKYiEbYGTVc3i0jLOTTXL8pVPkUIPLP
-PIN_KEY='GZUTI02vzKsRM2LLePkDy2mh8YpI5TjDEffRfkRNWLE='
-LOG_LEVEL=INFO

+ 2 - 1
.gitignore

@@ -10,4 +10,5 @@ llm_logs.*
 dksdabjhvjhSADhsbjksf.txt
 data/data.db
 /logs
-mrda.txt
+mrda.txt
+.env

+ 3 - 0
app.py

@@ -41,6 +41,9 @@ def setup_routes(app: FastAPI):
     
     # Sales routes
     app.include_router(sales.sales_router, prefix="/api/sales", tags=["Sales"], dependencies=[Depends(get_current_user)])
+
+    # Verification routes
+    app.include_router(users.verify_router, prefix="/verify", tags=["Verification"])
     # Static routes
     from fastapi.responses import HTMLResponse
     app.add_api_route("/", static.serve_app_html, methods=["GET"], 

+ 46 - 22
config/mails.py

@@ -1,7 +1,6 @@
 
-
 REGISTER_MAIL = {
-    "subject": "Bienvenido a Pedidos Express",
+    "subject": "Bienvenido a Pedidos Express - Confirma tu registro",
     "body": """
 <html>
 <head>
@@ -105,7 +104,7 @@ REGISTER_MAIL = {
             left: 0;
         }}
         
-        .promo-section {{
+        .verification-section {{
             background: linear-gradient(135deg, #101419 0%, #37404a 100%);
             border-radius: 12px;
             padding: 32px;
@@ -114,18 +113,19 @@ REGISTER_MAIL = {
             color: white;
         }}
         
-        .promo-amount {{
+        .verification-icon {{
             font-size: 48px;
-            font-weight: bold;
             margin: 16px 0;
             border: 3px solid white;
-            border-radius: 8px;
-            padding: 16px;
+            border-radius: 50%;
+            padding: 20px;
             display: inline-block;
-            min-width: 120px;
+            width: 80px;
+            height: 80px;
+            line-height: 80px;
         }}
         
-        .promo-description {{
+        .verification-description {{
             font-size: 18px;
             margin: 16px 0;
             opacity: 0.95;
@@ -149,6 +149,22 @@ REGISTER_MAIL = {
             transform: translateY(-1px);
         }}
         
+        .security-note {{
+            background-color: #fef3c7;
+            border: 1px solid #f59e0b;
+            border-radius: 8px;
+            padding: 16px;
+            margin: 24px 0;
+            text-align: center;
+        }}
+        
+        .security-note p {{
+            color: #92400e;
+            font-size: 14px;
+            margin: 0;
+            font-weight: 500;
+        }}
+        
         .website-section {{
             text-align: center;
             margin: 32px 0;
@@ -190,9 +206,12 @@ REGISTER_MAIL = {
                 padding: 24px 16px;
             }}
             
-            .promo-amount {{
+            .verification-icon {{
                 font-size: 36px;
-                padding: 12px;
+                padding: 16px;
+                width: 60px;
+                height: 60px;
+                line-height: 60px;
             }}
         }}
     </style>
@@ -202,7 +221,7 @@ REGISTER_MAIL = {
         <div class="email-container">
             <!-- Header -->
             <div class="header">
-                <h1>¡Hola {name}</h1>
+                <h1>¡Hola {name}!</h1>
                 <p>Bienvenido a {app_name}</p>
             </div>
             
@@ -211,7 +230,7 @@ REGISTER_MAIL = {
                 <!-- 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>
+                    <p>Tu registro ha sido exitoso. <br>Estamos emocionados de tenerte con nosotros. Para completar tu registro, necesitas crear tu PIN de seguridad.</p>
                 </div>
                 
                 <!-- Benefits Section -->
@@ -225,17 +244,23 @@ REGISTER_MAIL = {
                     </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>
+                <!-- Verification Section -->
+                <div class="verification-section">
+                    <div class="verification-description">¡Solo falta un paso más!</div>
+                    <div class="verification-icon">🔐</div>
+                    <div class="verification-description">Crea tu PIN de seguridad para comenzar</div>
+                    <div class="verification-description">Vence en <strong>1 hora</strong></div>
+                    <a href="http://10.10.10.2:6001/verify?q={verification_code}" class="cta-button">Crear mi PIN ahora</a>
+                </div>
+                
+                <!-- Security Note -->
+                <div class="security-note">
+                    <p>🔒 Tu PIN será tu clave personal para acceder de forma segura a {app_name}</p>
                 </div>
                 
                 <!-- Website Section -->
                 <div class="website-section">
-                    <p style="color: #6b7280; margin-bottom: 12px;">Visita nuestra plataforma:</p>
+                    <p style="color: #6b7280; margin-bottom: 12px;">También puedes acceder desde:</p>
                     <a href="https://www.expressklein.com" class="website-url">www.expressklein.com</a>
                 </div>
             </div>
@@ -254,7 +279,6 @@ REGISTER_MAIL = {
 </html>
     """
 }
-
 PRINTER_DISCONNECTED_MAIL = {
     "subject": "Printer Disconnected",
     "body": """
@@ -380,7 +404,7 @@ PRINTER_DISCONNECTED_MAIL = {
 """
 }
 
-PIN_RECOVERTY_MAIL = {
+PIN_RECOVERY_MAIL = {
     "subject": "Recuperación de PIN | Pedidos Express",
     "body": """
         <!DOCTYPE html>

+ 4 - 2
config/messages.py

@@ -9,7 +9,8 @@ class ErrorResponse:
     INVALID_CREDENTIALS = "Correo electrónico o PIN inválidos."
     TOO_MANY_ATTEMPTS = "Demasiados intentos de inicio de sesión. Por favor, inténtelo más tarde."
     SALE_NOT_FOUND = "No se encontraron ventas para este usuario."
-
+    INVALID_PIN = "El PIN es inválido."
+    INVALID_VERIFICATION_CODE = "El código de verificación es inválido."
 
 class SuccessResponse:
     """Class to handle success messages in the response."""
@@ -17,9 +18,10 @@ class SuccessResponse:
     ORDER_SUCCESS = "Orden enviada correctamente, se está procesando."
     PRODUCTS_FETCH_SUCCESS = "Productos obtenidos correctamente."
     CHAT_RESPONSE_SUCCESS = "Respuesta generada correctamente."
-    USER_CREATED_SUCCESS = "Usuario creado exitosamente."
+    USER_CREATED_SUCCESS = "Usuario creado exitosamente"
     LOGIN_SUCCESS = "Inicio de sesión exitoso."
     USER_DELETED_SUCCESS = "Usuario eliminado exitosamente."
+    VERIFICATION_NEEDED = "Se enviará un correo electrónico para verificar su cuenta."
 
 
 class UserResponse:

+ 1 - 1
data/products.json

@@ -64,6 +64,6 @@
     "type": "Coctel",
     "description": "Gin Juno, jugo de naranja, maracuya, limon y Ginger Beer",
     "price": 6500,
-    "image": "/assets/summer.jpeg"
+    "image": "main/assets/summer.jpeg"
   }
 ]

+ 3 - 0
models/user.py

@@ -9,6 +9,9 @@ class RegisterUserRequest(BaseModel):
     email: str
     rut: str
 
+class PinUserRequest(BaseModel):
+    pin: str = Field(min_length=4, max_length=4, description="4-digit PIN for user authentication")
+
 class User(BaseModel):
     """User model matching the database schema"""
     id: int

+ 227 - 0
public/verify.html

@@ -0,0 +1,227 @@
+<!DOCTYPE html>
+<html lang="es">
+<head>
+  <meta charset="UTF-8" />
+  <title>Crear PIN - Biergarten Klein</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+  <link rel="stylesheet" as="style" onload="this.rel='stylesheet'"
+        href="https://fonts.googleapis.com/css2?display=swap&family=Noto+Sans:wght@400;500;700;900&family=Spline+Sans:wght@400;500;700">
+  <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
+   <script>
+        tailwind.config = {
+            theme: {
+                extend: {
+                    colors: {
+                        'custom-dark': '#101419',
+                        'custom-dark-hover': '#37404a',
+                        'gray-50': '#f9fafb',
+                        'gray-100': '#f3f4f6',
+                    }
+                }
+            }
+        }
+    </script>
+</head>
+
+<body class="h-[100vh] bg-gray-50 flex items-center justify-center p-4" style='font-family:"Spline Sans","Noto Sans",sans-serif;'>
+  
+  <div class="w-full max-w-md">
+    <!-- Header -->
+    <div class="text-center mb-8">
+      <h1 class="text-[26px] font-bold text-[#101419] tracking-tight mb-2">
+        Biergarten Klein
+      </h1>
+      <p class="text-[#58728d] text-sm">
+        Crea tu PIN de seguridad
+      </p>
+    </div>
+
+    <!-- Formulario -->
+    <form id="pinForm" class="bg-white rounded-xl shadow-sm border border-gray-200 p-8 space-y-6">
+      <div class="text-center">
+        <h2 class="text-[19px] font-bold text-[#101419] mb-2">Crear tu PIN</h2>
+        <p class="text-sm text-[#58728d]">
+          Elige un PIN de 4 dígitos que usarás para acceder a tu cuenta
+        </p>
+      </div>
+
+      <div class="space-y-4">
+        <div>
+          <label for="pinInput" class="block text-sm font-medium text-[#101419] mb-2">
+            Nuevo PIN (4 dígitos)
+          </label>
+          <input 
+            id="pinInput"
+            name="pin"
+            type="password"
+            maxlength="4"
+            pattern="[0-9]{4}"
+            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 text-center text-lg tracking-widest"
+            placeholder="••••" 
+            required 
+            autocomplete="new-password"
+          />
+        </div>
+
+        <div>
+          <label for="confirmPinInput" class="block text-sm font-medium text-[#101419] mb-2">
+            Confirmar PIN
+          </label>
+          <input 
+            id="confirmPinInput"
+            name="confirmPin"
+            type="password"
+            maxlength="4"
+            pattern="[0-9]{4}"
+            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 text-center text-lg tracking-widest"
+            placeholder="••••" 
+            required 
+            autocomplete="new-password"
+          />
+        </div>
+
+        <!-- Mensaje de error -->
+        <div id="errorMessage" class="hidden text-red-500 text-sm text-center"></div>
+
+        <!-- Mensaje de éxito -->
+        <div id="successMessage" class="hidden text-green-600 text-sm text-center">
+          ✓ PIN creado correctamente
+        </div>
+
+        <!-- Indicador de fuerza del PIN -->
+        <div class="text-xs text-[#58728d] text-center">
+          💡 Consejo: Evita usar PINs obvios como 1234 o 0000
+        </div>
+      </div>
+
+      <button 
+        id="submitBtn"
+        type="submit"
+        class="w-full bg-[#101419] hover:bg-[#37404a] disabled:opacity-50 disabled:cursor-not-allowed text-white py-3 rounded-lg font-medium transition-colors duration-200 focus:ring-2 focus:ring-offset-2 focus:ring-[#101419]"
+      >
+        Crear PIN
+      </button>
+    </form>
+  </div>
+
+  <script>
+    const pinForm = document.getElementById('pinForm');
+    const pinInput = document.getElementById('pinInput');
+    const confirmPinInput = document.getElementById('confirmPinInput');
+    const submitBtn = document.getElementById('submitBtn');
+    const errorMessage = document.getElementById('errorMessage');
+    const successMessage = document.getElementById('successMessage');
+
+    // Auto-focus en el primer input al cargar
+    pinInput.focus();
+
+    // Solo permitir números en ambos inputs
+    [pinInput, confirmPinInput].forEach(input => {
+      input.addEventListener('input', function(e) {
+        e.target.value = e.target.value.replace(/[^0-9]/g, '');
+        
+        // Ocultar mensajes cuando el usuario empiece a escribir
+        errorMessage.classList.add('hidden');
+        successMessage.classList.add('hidden');
+        resetInputStyles();
+      });
+    });
+
+    // Envío del formulario
+    pinForm.addEventListener('submit', function(e) {
+      e.preventDefault();
+      
+      const pin = pinInput.value.trim();
+      const confirmPin = confirmPinInput.value.trim();
+      
+      // Validar que ambos campos tengan 4 dígitos
+      if (pin.length !== 4) {
+        showError('El PIN debe tener exactamente 4 dígitos');
+        pinInput.focus();
+        return;
+      }
+      
+      if (confirmPin.length !== 4) {
+        showError('Debes confirmar tu PIN con 4 dígitos');
+        confirmPinInput.focus();
+        return;
+      }
+      
+      // Validar que los PINs coincidan
+      if (pin !== confirmPin) {
+        showError('Los PINs no coinciden. Inténtalo de nuevo.');
+        confirmPinInput.value = '';
+        confirmPinInput.focus();
+        return;
+      }
+      
+      // Validar que no sea un PIN muy obvio
+      const obviousPins = ['1234', '0000', '1111', '2222', '3333', '4444', '5555', '6666', '7777', '8888', '9999'];
+      if (obviousPins.includes(pin)) {
+        showError('Por seguridad, elige un PIN menos obvio');
+        pinInput.focus();
+        return;
+      }
+      //get q url query
+      const urlParams = new URLSearchParams(window.location.search);
+      const q = urlParams.get('q');
+      // PIN válido - simular guardado
+      showSuccess();
+      fetch(`/api/users/create-user?q=${q}`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          pin: pin
+        })
+      }).catch(error => {
+        console.error('Error al crear el PIN:', error);
+        showError('Ocurrió un error al crear el PIN. Inténtalo de nuevo más tarde.');
+      }).then(response => {
+        if (response.status === 201){
+          alert("Bienvenido a Biergarten Klein, tu PIN ha sido creado exitosamente.");
+          window.location.href = '/';
+        }else{
+          response.json().then(data => {
+            showError(data.message || 'Ocurrió un error al crear el PIN. Inténtalo de nuevo más tarde.');
+          });
+        }
+      })
+    });
+
+    function showError(message) {
+      errorMessage.textContent = message;
+      errorMessage.classList.remove('hidden');
+      successMessage.classList.add('hidden');
+    }
+
+    function showSuccess() {
+      successMessage.classList.remove('hidden');
+      errorMessage.classList.add('hidden');
+      submitBtn.disabled = true;
+      submitBtn.textContent = 'PIN Creado ✓';
+      pinInput.classList.add('border-green-300');
+      confirmPinInput.classList.add('border-green-300');
+    }
+
+    function resetInputStyles() {
+      pinInput.classList.remove('border-red-300', 'border-green-300');
+      confirmPinInput.classList.remove('border-red-300', 'border-green-300');
+      pinInput.classList.add('border-gray-300');
+      confirmPinInput.classList.add('border-gray-300');
+      submitBtn.disabled = false;
+      submitBtn.textContent = 'Crear PIN';
+    }
+
+    // Avanzar al siguiente campo automáticamente
+    pinInput.addEventListener('input', function() {
+      if (this.value.length === 4) {
+        confirmPinInput.focus();
+      }
+    });
+  </script>
+
+</body>
+</html>

+ 65 - 11
routes/users.py

@@ -1,12 +1,18 @@
+from email.policy import HTTP
+import json
 from logging import getLogger
 from math import log
-from fastapi import APIRouter
-from fastapi.responses import JSONResponse
+from os import name
+from uuid import uuid4
+from click import File
+from fastapi import APIRouter, HTTPException
+from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
 from httpx import RequestError
 import redis
+from rsa import verify
 import config
 from models import user
-from models.user import  LoginRequest, UserIDRequest, RegisterUserRequest
+from models.user import  LoginRequest, PinUserRequest, UserIDRequest, RegisterUserRequest
 from services.data_service import UserDataService
 from cryptography.fernet import Fernet
 from config.settings import PIN_KEY
@@ -40,18 +46,52 @@ async def exists_user(request: UserIDRequest):
 @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:
+    user = user_data_service.get_by_email(request.email)
+    if user:
         return JSONResponse(status_code=400, content={"message": UserResponse.USER_ALREADY_EXISTS})
-    user = user_data_service.get_by_id(userID)
-    if not user:
-        return JSONResponse(status_code=500, content={"message": ErrorResponse.USER_CREATION_ERROR})
+    user = user_data_service.get_by_rut(request.rut)
+    if user:
+        return JSONResponse(status_code=400, content={"message": UserResponse.USER_ALREADY_EXISTS})
+    
+    redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
+    verification_code = str(uuid4())
+    redis_client.set(f"verify:{verification_code}", json.dumps({
+        "name": request.name,
+        "email": request.email,
+        "rut": request.rut
+    }))
+    redis_client.expire(f"verify:{verification_code}", 3600)  # Expire in 1 hour
+
     send_email(
         REGISTER_MAIL["subject"],
         REGISTER_MAIL["body"],
-        [user.email], name=user.name, app_name=APPNAME, pin=pin
+        [request.email], name=request.name, app_name=APPNAME, verification_code=verification_code
     )
+    return JSONResponse(status_code=201, content={"message": SuccessResponse.USER_CREATED_SUCCESS})
+
+@user_router.post("/create-user")
+async def create_user(request: PinUserRequest, q: str):
+    """Create a new user with PIN"""
+    redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
+    data = redis_client.get(f"verify:{q}")
+    if not redis_client.get(f"verify:{q}"):
+        return JSONResponse(status_code=400, content={"message": ErrorResponse.INVALID_VERIFICATION_CODE})
+    else:
+        data = json.loads(str(data))
+    
+    name = data.get("name")
+    email = data.get("email")
+    rut = data.get("rut")
+    pin = request.pin
+    if not request.pin or len(request.pin) != 4:
+        return JSONResponse(status_code=400, content={"message": ErrorResponse.INVALID_PIN})
+    userID = user_data_service.create(name, email, rut, pin)
+    if userID == -1:
+        return JSONResponse(status_code=400, content={"message": UserResponse.USER_ALREADY_EXISTS})
+    user = user_data_service.get_by_id(userID)
+    if not user:
+        return JSONResponse(status_code=500, content={"message": ErrorResponse.USER_CREATION_ERROR})
+
     return JSONResponse(status_code=201, content={"message": SuccessResponse.USER_CREATED_SUCCESS, "data": {
         **user.model_dump(exclude={"pin_hash"}),
         "token": generate_token(user.email)
@@ -114,4 +154,18 @@ async def delete_user(request: UserIDRequest):
 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})
+    return JSONResponse(status_code=200, content={"data": users})
+
+from fastapi import Query
+
+verify_router = APIRouter()
+
+@verify_router.get("/")
+async def verify_user(q: str = Query(..., description="q parameter")):
+    """Verify a user by ID"""
+    # get url params
+    redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
+    return FileResponse(
+        "public/verify.html",
+        media_type="text/html",
+    )