This commit is contained in:
+7
-1
@@ -219,7 +219,7 @@ per-resource filters:
|
|||||||
| Group check | `/radgroupcheck` | GET, POST, PUT, DELETE | `groupname`, `attribute` |
|
| Group check | `/radgroupcheck` | GET, POST, PUT, DELETE | `groupname`, `attribute` |
|
||||||
| Group reply | `/radgroupreply` | GET, POST, PUT, DELETE | `groupname`, `attribute` |
|
| Group reply | `/radgroupreply` | GET, POST, PUT, DELETE | `groupname`, `attribute` |
|
||||||
| **VLANs** | `/vlan` | see below | — |
|
| **VLANs** | `/vlan` | see below | — |
|
||||||
| **Clients** | `/client` | see below | — |
|
| **Clients** | `/client` | see below | `search`, `status` |
|
||||||
| **Devices** | `/device` | GET (list), PUT (alias) | — |
|
| **Devices** | `/device` | GET (list), PUT (alias) | — |
|
||||||
| User↔group | `/radusergroup` | GET, POST, PUT, DELETE | `username`, `groupname` |
|
| User↔group | `/radusergroup` | GET, POST, PUT, DELETE | `username`, `groupname` |
|
||||||
| Accounting | `/radacct` | GET (read-only) | `username`, `nasipaddress`, `active` |
|
| Accounting | `/radacct` | GET (read-only) | `username`, `nasipaddress`, `active` |
|
||||||
@@ -263,6 +263,7 @@ at client creation.
|
|||||||
| Action | Request |
|
| Action | Request |
|
||||||
|------------------|-----------------------------------------------------|
|
|------------------|-----------------------------------------------------|
|
||||||
| List clients | `GET /client/` → `{total,limit,offset,items:[{mac_address,group,status,name,phone,alias}]}` |
|
| List clients | `GET /client/` → `{total,limit,offset,items:[{mac_address,group,status,name,phone,alias}]}` |
|
||||||
|
| Search / filter | `GET /client/?search=<term>&status=<new\|paid\|unpaid>` |
|
||||||
| Get one client | `GET /client/{mac_address}` |
|
| Get one client | `GET /client/{mac_address}` |
|
||||||
| Add a client | `POST /client/add` body `{"mac_address":"14-99-3E-74-CB-7F","group":"staff","name":"Ali Hassan","phone":"7712345","alias":"Living Room TV"}` |
|
| Add a client | `POST /client/add` body `{"mac_address":"14-99-3E-74-CB-7F","group":"staff","name":"Ali Hassan","phone":"7712345","alias":"Living Room TV"}` |
|
||||||
| Edit a client | `POST /client/edit` body `{"mac_address":"...", group?, status?, name?, phone?, alias?}` |
|
| Edit a client | `POST /client/edit` body `{"mac_address":"...", group?, status?, name?, phone?, alias?}` |
|
||||||
@@ -283,6 +284,11 @@ response.
|
|||||||
`unpaid`. In the DB the group is stored in the `radusergroup.groupname` column,
|
`unpaid`. In the DB the group is stored in the `radusergroup.groupname` column,
|
||||||
`status` in `customers.status`, and `name`/`phone`/`alias` in `radadmin_clients`.
|
`status` in `customers.status`, and `name`/`phone`/`alias` in `radadmin_clients`.
|
||||||
- Adding a client whose MAC already exists → `409`.
|
- Adding a client whose MAC already exists → `409`.
|
||||||
|
- **List** accepts two optional filters (both applied before pagination, so
|
||||||
|
`total` reflects the filtered set): `search` is a case-insensitive substring
|
||||||
|
matched across `mac_address`, `name`, `phone`, and `alias`; `status` is an exact
|
||||||
|
match on `new` / `paid` / `unpaid`. Combine them freely, e.g.
|
||||||
|
`GET /client/?search=ali&status=unpaid`.
|
||||||
- **Import** validates each row with the same rules as `add`; with `dry_run:true`
|
- **Import** validates each row with the same rules as `add`; with `dry_run:true`
|
||||||
nothing is written and it returns `{total, valid, created:0, dry_run, errors:[{row,
|
nothing is written and it returns `{total, valid, created:0, dry_run, errors:[{row,
|
||||||
mac, detail}]}` for a preview. With `dry_run:false` the valid rows are inserted
|
mac, detail}]}` for a preview. With `dry_run:false` the valid rows are inserted
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ This router hides that fan-out behind mac_address + group + status.
|
|||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
from sqlalchemy import delete, func, select, update
|
from sqlalchemy import delete, func, or_, select, update
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@@ -55,8 +55,18 @@ def _stage_client(db: Session, cli: ClientCreate) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=Page[ClientOut])
|
@router.get("/", response_model=Page[ClientOut])
|
||||||
def list_clients(page: PageParams = Depends(), db: Session = Depends(get_db)):
|
def list_clients(
|
||||||
"""List clients — MAC, group and status, joined from customers + radusergroup."""
|
page: PageParams = Depends(),
|
||||||
|
search: str | None = None,
|
||||||
|
status: str | None = None,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""List clients — MAC, group and status, joined from customers + radusergroup.
|
||||||
|
|
||||||
|
Optional filters:
|
||||||
|
- ``search`` case-insensitive substring match across MAC, name, phone, alias.
|
||||||
|
- ``status`` exact match on billing status (new/paid/unpaid).
|
||||||
|
"""
|
||||||
base = (
|
base = (
|
||||||
select(
|
select(
|
||||||
Customer.mac_address, RadUserGroup.groupname, Customer.status,
|
Customer.mac_address, RadUserGroup.groupname, Customer.status,
|
||||||
@@ -65,7 +75,28 @@ def list_clients(page: PageParams = Depends(), db: Session = Depends(get_db)):
|
|||||||
.outerjoin(RadUserGroup, RadUserGroup.username == Customer.username)
|
.outerjoin(RadUserGroup, RadUserGroup.username == Customer.username)
|
||||||
.outerjoin(RadadminClient, RadadminClient.mac_address == Customer.username)
|
.outerjoin(RadadminClient, RadadminClient.mac_address == Customer.username)
|
||||||
)
|
)
|
||||||
total = db.execute(select(func.count()).select_from(Customer)).scalar_one()
|
# Count over the same joins so filtered totals drive pagination correctly.
|
||||||
|
count_stmt = (
|
||||||
|
select(func.count())
|
||||||
|
.select_from(Customer)
|
||||||
|
.outerjoin(RadadminClient, RadadminClient.mac_address == Customer.username)
|
||||||
|
)
|
||||||
|
|
||||||
|
if search:
|
||||||
|
term = f"%{search.strip()}%"
|
||||||
|
cond = or_(
|
||||||
|
Customer.mac_address.ilike(term),
|
||||||
|
RadadminClient.name.ilike(term),
|
||||||
|
RadadminClient.phone.ilike(term),
|
||||||
|
RadadminClient.alias.ilike(term),
|
||||||
|
)
|
||||||
|
base = base.where(cond)
|
||||||
|
count_stmt = count_stmt.where(cond)
|
||||||
|
if status:
|
||||||
|
base = base.where(Customer.status == status)
|
||||||
|
count_stmt = count_stmt.where(Customer.status == status)
|
||||||
|
|
||||||
|
total = db.execute(count_stmt).scalar_one()
|
||||||
rows = db.execute(base.order_by(Customer.id.desc()).limit(page.limit).offset(page.offset)).all()
|
rows = db.execute(base.order_by(Customer.id.desc()).limit(page.limit).offset(page.offset)).all()
|
||||||
items = [
|
items = [
|
||||||
ClientOut(mac_address=mac, group=gn, status=st, name=nm, phone=ph, alias=al)
|
ClientOut(mac_address=mac, group=gn, status=st, name=nm, phone=ph, alias=al)
|
||||||
|
|||||||
Reference in New Issue
Block a user