ifixkart-backend/app/core/security/LockValidator.py

128 lines
4.8 KiB
Python

import re
import base64
from decimal import Decimal, ROUND_HALF_UP
from typing import Tuple, Optional
from fastapi import HTTPException, status
def validate_lock_credentials(lock_type: str, lock_passcode: Optional[str]) -> Optional[str]:
"""
Validates lock_type and lock_passcode strictly according to security requirements.
Returns cleaned passcode or None.
"""
valid_types = {"NONE", "PIN", "PASSWORD", "PATTERN"}
if lock_type not in valid_types:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Invalid lock_type '{lock_type}'. Must be one of: NONE, PIN, PASSWORD, PATTERN."
)
if lock_type == "NONE":
return None
if not lock_passcode or not lock_passcode.strip():
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Passcode is required when lock_type is '{lock_type}'."
)
passcode = lock_passcode.strip()
if lock_type == "PIN":
if not re.match(r"^\d{4,8}$", passcode):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="PIN must consist strictly of 4 to 8 numeric digits (0-9)."
)
return passcode
elif lock_type == "PASSWORD":
if len(passcode) < 1 or len(passcode) > 64:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Password length must be between 1 and 64 characters."
)
return passcode
elif lock_type == "PATTERN":
# Format e.g. "1-4-7-8-9" or "1-2-3-6-9"
parts = passcode.split("-")
if len(parts) < 4 or len(parts) > 9:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Pattern sequence must contain between 4 and 9 nodes."
)
seen_nodes = set()
for node in parts:
if not node.isdigit():
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Invalid pattern node '{node}'. All nodes must be numbers between 1 and 9."
)
n_int = int(node)
if n_int < 1 or n_int > 9:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Pattern node '{n_int}' is out of bounds (must be 1-9)."
)
if n_int in seen_nodes:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Duplicate node '{n_int}' in pattern sequence. A node cannot be visited twice."
)
seen_nodes.add(n_int)
return passcode
return None
def calculate_fulfillment_pricing(base_price_val: float, fulfillment_type: str) -> Tuple[Decimal, Decimal, Decimal, Decimal, Decimal]:
"""
Authoritative server calculation using Decimal arithmetic:
returns (base_price, fulfillment_fee, total_price, advance_deposit, remaining_balance)
"""
base = Decimal(str(base_price_val)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
if fulfillment_type == "DOORSTEP_PICKUP":
fee = Decimal("250.00")
else: # WALK_IN or COURIER
fee = Decimal("0.00")
total = base + fee
advance = (total * Decimal("0.20")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
balance = total - advance
return base, fee, total, advance, balance
def encrypt_credential(plain_text: str) -> Optional[str]:
"""Encrypts device credential at rest."""
if not plain_text:
return None
try:
from cryptography.fernet import Fernet
import os
key = os.getenv("SECRET_KEY", "uO_v6N9kK7X6W_N7b5V8X3Z1Y9W5V3Z1Y9W5V3Z1Y9W=")
key_bytes = base64.urlsafe_b64encode(key.encode()[:32].ljust(32, b"0"))
f = Fernet(key_bytes)
return f.encrypt(plain_text.encode()).decode()
except Exception:
encoded = base64.b64encode(plain_text.encode()).decode()
return f"ENC_{encoded}"
def decrypt_credential(cipher_text: str) -> Optional[str]:
"""Decrypts stored device credential for authorized technician viewing."""
if not cipher_text:
return None
try:
if cipher_text.startswith("ENC_"):
raw = cipher_text[4:]
return base64.b64decode(raw.encode()).decode()
from cryptography.fernet import Fernet
import os
key = os.getenv("SECRET_KEY", "uO_v6N9kK7X6W_N7b5V8X3Z1Y9W5V3Z1Y9W5V3Z1Y9W=")
key_bytes = base64.urlsafe_b64encode(key.encode()[:32].ljust(32, b"0"))
f = Fernet(key_bytes)
return f.decrypt(cipher_text.encode()).decode()
except Exception:
return None