feat(forms): add parsing and validation for price, RAM, and storage

- 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
This commit is contained in:
nocci 2025-12-06 14:16:39 +00:00
parent d86a5f1a99
commit 5b676d2a2c
3 changed files with 77 additions and 16 deletions

View file

@ -17,6 +17,9 @@ from .utils import (
can_encrypt, can_encrypt,
ensure_csrf_token, ensure_csrf_token,
validate_csrf, validate_csrf,
parse_decimal,
parse_ram_mb,
parse_storage_gb,
) )
from jinja2 import pass_context from jinja2 import pass_context
@ -542,14 +545,14 @@ def create_server(
ipv4: str = Form(""), ipv4: str = Form(""),
ipv6: str = Form(""), ipv6: str = Form(""),
billing_period: str = Form("monthly"), billing_period: str = Form("monthly"),
price: float = Form(0.0), price: str = Form("0"),
currency: str = Form("EUR"), currency: str = Form("EUR"),
contract_start: Optional[str] = Form(None), contract_start: Optional[str] = Form(None),
contract_end: Optional[str] = Form(None), contract_end: Optional[str] = Form(None),
cpu_model: str = Form(""), cpu_model: str = Form(""),
cpu_cores: int = Form(0), cpu_cores: int = Form(0),
ram_mb: int = Form(0), ram_mb: str = Form(""),
storage_gb: int = Form(0), storage_gb: str = Form(""),
storage_type: str = Form(""), storage_type: str = Form(""),
tags: str = Form(""), tags: str = Form(""),
mgmt_url: 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 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 enc_pwd = encrypt_secret(mgmt_password) if mgmt_password else None
# Only allow http:// or https:// URLs to avoid javascript: schemes etc. # Only allow http:// or https:// URLs to avoid javascript: schemes etc.
@ -593,14 +600,14 @@ def create_server(
ipv4=ipv4 or None, ipv4=ipv4 or None,
ipv6=ipv6 or None, ipv6=ipv6 or None,
billing_period=billing_period, billing_period=billing_period,
price=price, price=parsed_price,
currency=currency, currency=currency,
contract_start=c_start, contract_start=c_start,
contract_end=c_end, contract_end=c_end,
cpu_model=cpu_model or None, cpu_model=cpu_model or None,
cpu_cores=cpu_cores or None, cpu_cores=cpu_cores or None,
ram_mb=ram_mb or None, ram_mb=parsed_ram,
storage_gb=storage_gb or None, storage_gb=parsed_storage,
storage_type=storage_type or None, storage_type=storage_type or None,
tags=tags or None, tags=tags or None,
mgmt_url=mgmt_url_clean or None, mgmt_url=mgmt_url_clean or None,
@ -687,14 +694,14 @@ def update_server(
ipv4: str = Form(""), ipv4: str = Form(""),
ipv6: str = Form(""), ipv6: str = Form(""),
billing_period: str = Form("monthly"), billing_period: str = Form("monthly"),
price: float = Form(0.0), price: str = Form("0"),
currency: str = Form("EUR"), currency: str = Form("EUR"),
contract_start: Optional[str] = Form(None), contract_start: Optional[str] = Form(None),
contract_end: Optional[str] = Form(None), contract_end: Optional[str] = Form(None),
cpu_model: str = Form(""), cpu_model: str = Form(""),
cpu_cores: int = Form(0), cpu_cores: int = Form(0),
ram_mb: int = Form(0), ram_mb: str = Form(""),
storage_gb: int = Form(0), storage_gb: str = Form(""),
storage_type: str = Form(""), storage_type: str = Form(""),
tags: str = Form(""), tags: str = Form(""),
mgmt_url: str = Form(""), mgmt_url: str = Form(""),
@ -742,14 +749,15 @@ def update_server(
server.ipv4 = ipv4 or None server.ipv4 = ipv4 or None
server.ipv6 = ipv6 or None server.ipv6 = ipv6 or None
server.billing_period = billing_period server.billing_period = billing_period
server.price = price parsed_price = parse_decimal(price)
server.price = parsed_price or 0.0
server.currency = currency server.currency = currency
server.contract_start = c_start server.contract_start = c_start
server.contract_end = c_end server.contract_end = c_end
server.cpu_model = cpu_model or None server.cpu_model = cpu_model or None
server.cpu_cores = cpu_cores or None server.cpu_cores = cpu_cores or None
server.ram_mb = ram_mb or None server.ram_mb = parse_ram_mb(ram_mb)
server.storage_gb = storage_gb or None server.storage_gb = parse_storage_gb(storage_gb)
server.storage_type = storage_type or None server.storage_type = storage_type or None
server.tags = tags or None server.tags = tags or None
server.mgmt_url = mgmt_url_clean or None server.mgmt_url = mgmt_url_clean or None

View file

@ -123,8 +123,7 @@
<div class="space-y-1"> <div class="space-y-1">
<label class="text-xs text-slate-300">{{ t("label.amount") }}</label> <label class="text-xs text-slate-300">{{ t("label.amount") }}</label>
<input <input
type="number" type="text"
step="0.01"
name="price" name="price"
class="w-full rounded-lg border border-slate-700 bg-slate-950/60 px-3 py-2 text-sm outline-none focus:border-indigo-500" class="w-full rounded-lg border border-slate-700 bg-slate-950/60 px-3 py-2 text-sm outline-none focus:border-indigo-500"
placeholder="5.00" placeholder="5.00"
@ -199,18 +198,20 @@
<div class="space-y-1"> <div class="space-y-1">
<label class="text-xs text-slate-300">{{ t("label.ram_mb") }}</label> <label class="text-xs text-slate-300">{{ t("label.ram_mb") }}</label>
<input <input
type="number" type="text"
name="ram_mb" name="ram_mb"
class="w-full rounded-lg border border-slate-700 bg-slate-950/60 px-3 py-2 text-sm outline-none focus:border-indigo-500" class="w-full rounded-lg border border-slate-700 bg-slate-950/60 px-3 py-2 text-sm outline-none focus:border-indigo-500"
placeholder="32GB / 32768MB"
value="{{ server.ram_mb if server and server.ram_mb else '' }}" value="{{ server.ram_mb if server and server.ram_mb else '' }}"
/> />
</div> </div>
<div class="space-y-1"> <div class="space-y-1">
<label class="text-xs text-slate-300">{{ t("label.storage_gb") }}</label> <label class="text-xs text-slate-300">{{ t("label.storage_gb") }}</label>
<input <input
type="number" type="text"
name="storage_gb" name="storage_gb"
class="w-full rounded-lg border border-slate-700 bg-slate-950/60 px-3 py-2 text-sm outline-none focus:border-indigo-500" class="w-full rounded-lg border border-slate-700 bg-slate-950/60 px-3 py-2 text-sm outline-none focus:border-indigo-500"
placeholder="4TB / 500GB"
value="{{ server.storage_gb if server and server.storage_gb else '' }}" value="{{ server.storage_gb if server and server.storage_gb else '' }}"
/> />
</div> </div>

View file

@ -2,6 +2,7 @@ import base64
import hashlib import hashlib
import os import os
import secrets import secrets
import re
from typing import Optional from typing import Optional
from cryptography.fernet import Fernet, InvalidToken from cryptography.fernet import Fernet, InvalidToken
@ -49,6 +50,57 @@ def decrypt_secret(token: str) -> Optional[str]:
return None 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 helpers -----
_CSRF_SESSION_KEY = "csrf_token" _CSRF_SESSION_KEY = "csrf_token"