"""Device management — a logical view spanning three tables. A "device" is a client identified by its MAC address, which is used verbatim as the RADIUS ``username``. One device touches three tables: radcheck username = MAC, Cleartext-Password := MAC (auth) radusergroup username = MAC, groupname = (VLAN/group membership) customers username = MAC, mac_address = MAC, status (billing/metadata) This router hides that fan-out behind mac_address + group + status. """ from fastapi import APIRouter, Depends from sqlalchemy import delete, func, select, update from sqlalchemy.orm import Session from ..database import get_db from ..errors import APIError from ..models import Customer, RadCheck, RadGroupReply, RadUserGroup from ..pagination import Page, PageParams from ..schemas import DeviceCreate, DeviceEdit, DeviceOut router = APIRouter(prefix="/device", tags=["device"]) def _group_exists(db: Session, group: str) -> bool: stmt = select(RadGroupReply.id).where(RadGroupReply.groupname == group).limit(1) return db.execute(stmt).first() is not None def _device_exists(db: Session, mac: str) -> bool: stmt = select(Customer.id).where(Customer.username == mac).limit(1) return db.execute(stmt).first() is not None @router.get("/", response_model=Page[DeviceOut]) def list_devices(page: PageParams = Depends(), db: Session = Depends(get_db)): """List devices — MAC, group and status, joined from customers + radusergroup.""" base = select( Customer.mac_address, RadUserGroup.groupname, Customer.status, Customer.name, Customer.phone, Customer.device_alias, ).outerjoin(RadUserGroup, RadUserGroup.username == Customer.username) total = db.execute(select(func.count()).select_from(Customer)).scalar_one() rows = db.execute(base.order_by(Customer.id.desc()).limit(page.limit).offset(page.offset)).all() items = [ DeviceOut(mac_address=mac, group=gn, status=st, name=nm, phone=ph, alias=al) for mac, gn, st, nm, ph, al in rows ] return Page(total=int(total), limit=page.limit, offset=page.offset, items=items) @router.get("/{mac_address}", response_model=DeviceOut) def get_device(mac_address: str, db: Session = Depends(get_db)): mac = mac_address.strip().upper().replace(":", "-") stmt = ( select( Customer.mac_address, RadUserGroup.groupname, Customer.status, Customer.name, Customer.phone, Customer.device_alias, ) .outerjoin(RadUserGroup, RadUserGroup.username == Customer.username) .where(Customer.username == mac) ) row = db.execute(stmt).first() if row is None: raise APIError(status_code=404, detail=f"Device '{mac}' not found") return DeviceOut( mac_address=row[0], group=row[1], status=row[2], name=row[3], phone=row[4], alias=row[5], ) @router.post("/add", response_model=DeviceOut, status_code=201) def add_device(payload: DeviceCreate, db: Session = Depends(get_db)): """Register a device: create its radcheck, radusergroup and customer rows.""" mac = payload.mac_address if not _group_exists(db, payload.group): raise APIError(status_code=400, detail=f"Group '{payload.group}' not found — create it first") if _device_exists(db, mac): raise APIError(status_code=409, detail=f"Device '{mac}' already exists") db.add(RadCheck(username=mac, attribute="Cleartext-Password", op=":=", value=mac)) db.add(RadUserGroup(username=mac, groupname=payload.group, priority=1)) db.add(Customer( username=mac, mac_address=mac, status="paid", name=payload.name, phone=payload.phone, device_alias=payload.alias, )) db.commit() return DeviceOut( mac_address=mac, group=payload.group, status="paid", name=payload.name, phone=payload.phone, alias=payload.alias, ) @router.post("/edit", response_model=DeviceOut) def edit_device(payload: DeviceEdit, db: Session = Depends(get_db)): """Edit a device — any subset of group, status, name, phone, alias.""" mac = payload.mac_address customer = db.execute(select(Customer).where(Customer.username == mac)).scalar_one_or_none() if customer is None: raise APIError(status_code=404, detail=f"Device '{mac}' not found") if payload.group is not None: if not _group_exists(db, payload.group): raise APIError(status_code=400, detail=f"Group '{payload.group}' not found") db.execute( update(RadUserGroup).where(RadUserGroup.username == mac).values(groupname=payload.group) ) if payload.status is not None: customer.status = payload.status if payload.name is not None: customer.name = payload.name if payload.phone is not None: customer.phone = payload.phone if payload.alias is not None: customer.device_alias = payload.alias db.commit() group = db.execute( select(RadUserGroup.groupname).where(RadUserGroup.username == mac).limit(1) ).scalar_one_or_none() return DeviceOut( mac_address=mac, group=group, status=customer.status, name=customer.name, phone=customer.phone, alias=customer.device_alias, ) @router.delete("/{mac_address}", status_code=204) def delete_device(mac_address: str, db: Session = Depends(get_db)): """Remove a device from radcheck, radusergroup and customers.""" mac = mac_address.strip().upper().replace(":", "-") if not _device_exists(db, mac): raise APIError(status_code=404, detail=f"Device '{mac}' not found") db.execute(delete(RadCheck).where(RadCheck.username == mac)) db.execute(delete(RadUserGroup).where(RadUserGroup.username == mac)) db.execute(delete(Customer).where(Customer.username == mac)) db.commit()