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,
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

View file

@ -123,8 +123,7 @@
<div class="space-y-1">
<label class="text-xs text-slate-300">{{ t("label.amount") }}</label>
<input
type="number"
step="0.01"
type="text"
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"
placeholder="5.00"
@ -199,18 +198,20 @@
<div class="space-y-1">
<label class="text-xs text-slate-300">{{ t("label.ram_mb") }}</label>
<input
type="number"
type="text"
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"
placeholder="32GB / 32768MB"
value="{{ server.ram_mb if server and server.ram_mb else '' }}"
/>
</div>
<div class="space-y-1">
<label class="text-xs text-slate-300">{{ t("label.storage_gb") }}</label>
<input
type="number"
type="text"
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"
placeholder="4TB / 500GB"
value="{{ server.storage_gb if server and server.storage_gb else '' }}"
/>
</div>

View file

@ -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"