From 5b676d2a2cf09031dbddba01871d3f9370d14a5c Mon Sep 17 00:00:00 2001 From: nocci Date: Sat, 6 Dec 2025 14:16:39 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(forms):=20add=20parsing=20and?= =?UTF-8?q?=20validation=20for=20price,=20RAM,=20and=20storage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - introduce parsing helpers for decimal, RAM, and storage values - convert form input types from number to text for flexibility - parse RAM and storage with optional units for better user input handling --- app/main.py | 32 +++++++++++++-------- app/templates/server_form.html | 9 +++--- app/utils.py | 52 ++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 16 deletions(-) diff --git a/app/main.py b/app/main.py index 4b0d7d4..046c0ea 100644 --- a/app/main.py +++ b/app/main.py @@ -17,6 +17,9 @@ from .utils import ( can_encrypt, ensure_csrf_token, validate_csrf, + parse_decimal, + parse_ram_mb, + parse_storage_gb, ) from jinja2 import pass_context @@ -542,14 +545,14 @@ def create_server( ipv4: str = Form(""), ipv6: str = Form(""), billing_period: str = Form("monthly"), - price: float = Form(0.0), + price: str = Form("0"), currency: str = Form("EUR"), contract_start: Optional[str] = Form(None), contract_end: Optional[str] = Form(None), cpu_model: str = Form(""), cpu_cores: int = Form(0), - ram_mb: int = Form(0), - storage_gb: int = Form(0), + ram_mb: str = Form(""), + storage_gb: str = Form(""), storage_type: str = Form(""), tags: str = Form(""), mgmt_url: str = Form(""), @@ -573,6 +576,10 @@ def create_server( ) c_end = datetime.fromisoformat(contract_end).date() if contract_end else None + parsed_price = parse_decimal(price) or 0.0 + parsed_ram = parse_ram_mb(ram_mb) + parsed_storage = parse_storage_gb(storage_gb) + enc_pwd = encrypt_secret(mgmt_password) if mgmt_password else None # Only allow http:// or https:// URLs to avoid javascript: schemes etc. @@ -593,14 +600,14 @@ def create_server( ipv4=ipv4 or None, ipv6=ipv6 or None, billing_period=billing_period, - price=price, + price=parsed_price, currency=currency, contract_start=c_start, contract_end=c_end, cpu_model=cpu_model or None, cpu_cores=cpu_cores or None, - ram_mb=ram_mb or None, - storage_gb=storage_gb or None, + ram_mb=parsed_ram, + storage_gb=parsed_storage, storage_type=storage_type or None, tags=tags or None, mgmt_url=mgmt_url_clean or None, @@ -687,14 +694,14 @@ def update_server( ipv4: str = Form(""), ipv6: str = Form(""), billing_period: str = Form("monthly"), - price: float = Form(0.0), + price: str = Form("0"), currency: str = Form("EUR"), contract_start: Optional[str] = Form(None), contract_end: Optional[str] = Form(None), cpu_model: str = Form(""), cpu_cores: int = Form(0), - ram_mb: int = Form(0), - storage_gb: int = Form(0), + ram_mb: str = Form(""), + storage_gb: str = Form(""), storage_type: str = Form(""), tags: str = Form(""), mgmt_url: str = Form(""), @@ -742,14 +749,15 @@ def update_server( server.ipv4 = ipv4 or None server.ipv6 = ipv6 or None server.billing_period = billing_period - server.price = price + parsed_price = parse_decimal(price) + server.price = parsed_price or 0.0 server.currency = currency server.contract_start = c_start server.contract_end = c_end server.cpu_model = cpu_model or None server.cpu_cores = cpu_cores or None - server.ram_mb = ram_mb or None - server.storage_gb = storage_gb or None + server.ram_mb = parse_ram_mb(ram_mb) + server.storage_gb = parse_storage_gb(storage_gb) server.storage_type = storage_type or None server.tags = tags or None server.mgmt_url = mgmt_url_clean or None diff --git a/app/templates/server_form.html b/app/templates/server_form.html index 711f550..386facb 100644 --- a/app/templates/server_form.html +++ b/app/templates/server_form.html @@ -123,8 +123,7 @@
diff --git a/app/utils.py b/app/utils.py index afd67de..df27763 100644 --- a/app/utils.py +++ b/app/utils.py @@ -2,6 +2,7 @@ import base64 import hashlib import os import secrets +import re from typing import Optional from cryptography.fernet import Fernet, InvalidToken @@ -49,6 +50,57 @@ def decrypt_secret(token: str) -> Optional[str]: return None +# ----- Parsing helpers ----- + +def parse_decimal(value: str) -> Optional[float]: + """Parse a decimal number allowing comma or dot.""" + if not value: + return None + try: + normalized = value.replace(",", ".").strip() + return float(normalized) + except ValueError: + return None + + +def parse_ram_mb(value: str) -> Optional[int]: + """ + Parse RAM with optional unit (MB/GB/TB). Defaults to GB if a unit is missing. + Returns MB. + """ + if not value: + return None + v = value.strip().lower() + match = re.match(r"([0-9]+(?:[\\.,][0-9]+)?)(tb|gb|mb)?", v) + if not match: + return None + number = float(match.group(1).replace(",", ".")) + unit = match.group(2) or "gb" + if unit == "tb": + return int(number * 1024 * 1024) + if unit == "gb": + return int(number * 1024) + return int(number) + + +def parse_storage_gb(value: str) -> Optional[int]: + """ + Parse storage with optional unit (GB/TB). Defaults to GB. + Returns GB. + """ + if not value: + return None + v = value.strip().lower() + match = re.match(r"([0-9]+(?:[\\.,][0-9]+)?)(tb|gb)?", v) + if not match: + return None + number = float(match.group(1).replace(",", ".")) + unit = match.group(2) or "gb" + if unit == "tb": + return int(number * 1024) + return int(number) + + # ----- CSRF helpers ----- _CSRF_SESSION_KEY = "csrf_token"