restructure: move backend into backend/
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
|
||||
class ORMModel(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# ---------- customers ----------
|
||||
class CustomerBase(BaseModel):
|
||||
username: str = Field(max_length=64)
|
||||
mac_address: str = Field(max_length=17)
|
||||
status: Literal["new", "paid", "unpaid"] = "new"
|
||||
name: str | None = Field(default=None, max_length=128)
|
||||
phone: str | None = Field(default=None, max_length=32)
|
||||
device_alias: str | None = Field(default=None, max_length=64)
|
||||
|
||||
|
||||
class CustomerCreate(CustomerBase):
|
||||
pass
|
||||
|
||||
|
||||
class CustomerUpdate(BaseModel):
|
||||
username: str | None = Field(default=None, max_length=64)
|
||||
mac_address: str | None = Field(default=None, max_length=17)
|
||||
status: Literal["new", "paid", "unpaid"] | None = None
|
||||
name: str | None = Field(default=None, max_length=128)
|
||||
phone: str | None = Field(default=None, max_length=32)
|
||||
device_alias: str | None = Field(default=None, max_length=64)
|
||||
|
||||
|
||||
class CustomerOut(ORMModel, CustomerBase):
|
||||
id: int
|
||||
created_at: datetime | None = None
|
||||
|
||||
|
||||
# ---------- nas ----------
|
||||
class NasBase(BaseModel):
|
||||
nasname: str = Field(max_length=128)
|
||||
shortname: str | None = Field(default=None, max_length=32)
|
||||
type: str | None = Field(default="other", max_length=30)
|
||||
ports: int | None = None
|
||||
secret: str = Field(default="secret", max_length=60)
|
||||
server: str | None = Field(default=None, max_length=64)
|
||||
community: str | None = Field(default=None, max_length=50)
|
||||
description: str | None = Field(default="RADIUS Client", max_length=200)
|
||||
|
||||
|
||||
class NasCreate(NasBase):
|
||||
pass
|
||||
|
||||
|
||||
class NasUpdate(BaseModel):
|
||||
nasname: str | None = Field(default=None, max_length=128)
|
||||
shortname: str | None = Field(default=None, max_length=32)
|
||||
type: str | None = Field(default=None, max_length=30)
|
||||
ports: int | None = None
|
||||
secret: str | None = Field(default=None, max_length=60)
|
||||
server: str | None = Field(default=None, max_length=64)
|
||||
community: str | None = Field(default=None, max_length=50)
|
||||
description: str | None = Field(default=None, max_length=200)
|
||||
|
||||
|
||||
class NasOut(ORMModel, NasBase):
|
||||
id: int
|
||||
|
||||
|
||||
# ---------- attribute pair tables (radcheck / radreply) ----------
|
||||
class UserAttrBase(BaseModel):
|
||||
username: str = Field(max_length=64)
|
||||
attribute: str = Field(max_length=64)
|
||||
op: str = Field(max_length=2)
|
||||
value: str = Field(max_length=253)
|
||||
|
||||
|
||||
class UserAttrCreate(UserAttrBase):
|
||||
pass
|
||||
|
||||
|
||||
class UserAttrUpdate(BaseModel):
|
||||
username: str | None = Field(default=None, max_length=64)
|
||||
attribute: str | None = Field(default=None, max_length=64)
|
||||
op: str | None = Field(default=None, max_length=2)
|
||||
value: str | None = Field(default=None, max_length=253)
|
||||
|
||||
|
||||
class UserAttrOut(ORMModel, UserAttrBase):
|
||||
id: int
|
||||
|
||||
|
||||
# ---------- group attribute tables (radgroupcheck / radgroupreply / vlans) ----------
|
||||
class GroupAttrBase(BaseModel):
|
||||
groupname: str = Field(max_length=64)
|
||||
attribute: str = Field(max_length=64)
|
||||
op: str = Field(max_length=2)
|
||||
value: str = Field(max_length=253)
|
||||
|
||||
|
||||
class GroupAttrCreate(GroupAttrBase):
|
||||
pass
|
||||
|
||||
|
||||
class GroupAttrUpdate(BaseModel):
|
||||
groupname: str | None = Field(default=None, max_length=64)
|
||||
attribute: str | None = Field(default=None, max_length=64)
|
||||
op: str | None = Field(default=None, max_length=2)
|
||||
value: str | None = Field(default=None, max_length=253)
|
||||
|
||||
|
||||
class GroupAttrOut(ORMModel, GroupAttrBase):
|
||||
id: int
|
||||
|
||||
|
||||
# ---------- vlans (logical view over radgroupreply) ----------
|
||||
class VlanOut(BaseModel):
|
||||
alias: str # radgroupreply.groupname
|
||||
vlanid: int # value of the Tunnel-Private-Group-Id attribute
|
||||
|
||||
|
||||
class VlanCreate(BaseModel):
|
||||
vlanid: int = Field(ge=1, le=4094, description="802.1Q VLAN ID")
|
||||
alias: str = Field(min_length=1, max_length=64, description="Group name for this VLAN")
|
||||
|
||||
|
||||
class VlanEdit(BaseModel):
|
||||
vlanid: int = Field(ge=1, le=4094, description="VLAN ID identifying the group to rename")
|
||||
alias: str = Field(min_length=1, max_length=64, description="New alias (groupname)")
|
||||
|
||||
|
||||
# ---------- devices (logical view over radcheck + radusergroup + customers) ----------
|
||||
_MAC_RE = r"^[0-9A-Fa-f]{2}([-:][0-9A-Fa-f]{2}){5}$"
|
||||
|
||||
|
||||
def _normalize_mac(mac: str) -> str:
|
||||
"""Canonicalize a MAC to uppercase, hyphen-separated (AA-BB-CC-DD-EE-FF)."""
|
||||
return mac.strip().upper().replace(":", "-")
|
||||
|
||||
|
||||
_PHONE_ERROR = "Enter a valid phone number"
|
||||
|
||||
|
||||
def _normalize_phone(raw: str) -> str:
|
||||
"""Validate a Maldivian mobile number and return the 7-digit local form.
|
||||
|
||||
Rules: 7 digits, starts with 9 or 7. A 960/+960 country code is stripped ONLY
|
||||
when the number is 10 digits (960 + 7) — a bare 7-digit number like 9601234 is
|
||||
a valid local number and is never stripped.
|
||||
"""
|
||||
digits = re.sub(r"\D", "", raw or "")
|
||||
if len(digits) == 10 and digits.startswith("960"):
|
||||
digits = digits[3:]
|
||||
if len(digits) == 7 and digits[0] in ("7", "9"):
|
||||
return digits
|
||||
raise ValueError(_PHONE_ERROR)
|
||||
|
||||
|
||||
class DeviceOut(BaseModel):
|
||||
mac_address: str
|
||||
group: str | None = None # radusergroup.groupname
|
||||
status: str | None = None # customers.status
|
||||
name: str | None = None # customers.name (human metadata)
|
||||
phone: str | None = None # customers.phone (human metadata)
|
||||
alias: str | None = None # customers.device_alias (human metadata)
|
||||
|
||||
|
||||
class DeviceCreate(BaseModel):
|
||||
mac_address: str = Field(pattern=_MAC_RE, max_length=17)
|
||||
group: str = Field(min_length=1, max_length=64)
|
||||
name: str = Field(min_length=1, max_length=128, description="Customer name (metadata, ignored by RADIUS)")
|
||||
phone: str = Field(min_length=1, max_length=32, description="7-digit mobile (9/7…), 960/+960 optional")
|
||||
alias: str | None = Field(default=None, max_length=64, description="Optional device alias (metadata)")
|
||||
|
||||
@field_validator("mac_address")
|
||||
@classmethod
|
||||
def _norm(cls, v: str) -> str:
|
||||
return _normalize_mac(v)
|
||||
|
||||
@field_validator("phone")
|
||||
@classmethod
|
||||
def _norm_phone(cls, v: str) -> str:
|
||||
return _normalize_phone(v)
|
||||
|
||||
|
||||
class DeviceEdit(BaseModel):
|
||||
mac_address: str = Field(pattern=_MAC_RE, max_length=17)
|
||||
group: str | None = Field(default=None, max_length=64)
|
||||
status: Literal["new", "paid", "unpaid"] | None = None
|
||||
name: str | None = Field(default=None, max_length=128)
|
||||
phone: str | None = Field(default=None, max_length=32)
|
||||
alias: str | None = Field(default=None, max_length=64)
|
||||
|
||||
@field_validator("mac_address")
|
||||
@classmethod
|
||||
def _norm(cls, v: str) -> str:
|
||||
return _normalize_mac(v)
|
||||
|
||||
@field_validator("phone")
|
||||
@classmethod
|
||||
def _norm_phone(cls, v: str | None) -> str | None:
|
||||
return None if v is None else _normalize_phone(v)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _at_least_one(self):
|
||||
if all(v is None for v in (self.group, self.status, self.name, self.phone, self.alias)):
|
||||
raise ValueError("provide at least one of: group, status, name, phone, alias")
|
||||
return self
|
||||
|
||||
|
||||
# ---------- radusergroup ----------
|
||||
class UserGroupBase(BaseModel):
|
||||
username: str = Field(max_length=64)
|
||||
groupname: str = Field(max_length=64)
|
||||
priority: int = 1
|
||||
|
||||
|
||||
class UserGroupCreate(UserGroupBase):
|
||||
pass
|
||||
|
||||
|
||||
class UserGroupUpdate(BaseModel):
|
||||
username: str | None = Field(default=None, max_length=64)
|
||||
groupname: str | None = Field(default=None, max_length=64)
|
||||
priority: int | None = None
|
||||
|
||||
|
||||
class UserGroupOut(ORMModel, UserGroupBase):
|
||||
id: int
|
||||
|
||||
|
||||
# ---------- read-only: radacct ----------
|
||||
class RadAcctOut(ORMModel):
|
||||
radacctid: int
|
||||
acctsessionid: str
|
||||
acctuniqueid: str
|
||||
username: str
|
||||
realm: str | None = None
|
||||
nasipaddress: str
|
||||
nasportid: str | None = None
|
||||
nasporttype: str | None = None
|
||||
acctstarttime: datetime | None = None
|
||||
acctupdatetime: datetime | None = None
|
||||
acctstoptime: datetime | None = None
|
||||
acctinterval: int | None = None
|
||||
acctsessiontime: int | None = None
|
||||
acctauthentic: str | None = None
|
||||
connectinfo_start: str | None = None
|
||||
connectinfo_stop: str | None = None
|
||||
acctinputoctets: int | None = None
|
||||
acctoutputoctets: int | None = None
|
||||
calledstationid: str
|
||||
callingstationid: str
|
||||
acctterminatecause: str
|
||||
servicetype: str | None = None
|
||||
framedprotocol: str | None = None
|
||||
framedipaddress: str
|
||||
class_: str | None = Field(default=None, alias="class")
|
||||
|
||||
|
||||
# ---------- read-only: radpostauth ----------
|
||||
class RadPostAuthOut(ORMModel):
|
||||
id: int
|
||||
username: str
|
||||
reply: str
|
||||
authdate: datetime | None = None
|
||||
class_: str | None = Field(default=None, alias="class")
|
||||
|
||||
|
||||
# ---------- read-only: nasreload ----------
|
||||
class NasReloadOut(ORMModel):
|
||||
nasipaddress: str
|
||||
reloadtime: datetime
|
||||
Reference in New Issue
Block a user