128 lines
4.8 KiB
Python
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
|