✨ 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:
parent
d86a5f1a99
commit
5b676d2a2c
3 changed files with 77 additions and 16 deletions
32
app/main.py
32
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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
52
app/utils.py
52
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"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue