This commit is contained in:
+110
-38
@@ -4,6 +4,17 @@ A FastAPI service exposing RESTful CRUD over the FreeRADIUS MySQL/MariaDB schema
|
||||
(`customers`, `radcheck`, `radreply`, `radgroupreply`/vlans, `radusergroup`, `nas`,
|
||||
plus read-only `radacct`, `radpostauth`, `nasreload`).
|
||||
|
||||
Two endpoints are **logical views** layered over that schema rather than raw tables:
|
||||
|
||||
- **Clients** (`/client`) — customer devices keyed by MAC, spanning `radcheck` +
|
||||
`radusergroup` + `customers` (status), with human metadata (name/phone/alias) in
|
||||
the standalone **`radadmin_clients`** table.
|
||||
- **Devices** (`/device`) — the NAS/AP boxes seen in `radacct`, keyed by **AP MAC**
|
||||
(from `calledstationid`), with a human alias in the standalone
|
||||
**`radadmin_devices`** table.
|
||||
|
||||
The two `radadmin_*` tables hold only human labels — RADIUS never reads them.
|
||||
|
||||
## Stack
|
||||
- **FastAPI** + **Uvicorn** (ASGI)
|
||||
- **SQLAlchemy 2.0** ORM + **PyMySQL** driver
|
||||
@@ -57,8 +68,9 @@ Missing/wrong key → `401`. `/`, `/health`, and `/docs` are open.
|
||||
|
||||
## UI integration guide
|
||||
|
||||
For a management portal the two primary resources are **Devices** (`/device`) and
|
||||
**VLANs** (`/vlan`). Everything else is lower-level raw-table access.
|
||||
For a management portal the primary resources are **Clients** (`/client`),
|
||||
**Devices** (`/device`), and **VLANs** (`/vlan`). Everything else is lower-level
|
||||
raw-table access.
|
||||
|
||||
**Base URL (staging):** `http://10.0.1.235:8000`
|
||||
**Every request:** header `X-API-Key: <API_KEY>` (except `/health`).
|
||||
@@ -66,7 +78,7 @@ For a management portal the two primary resources are **Devices** (`/device`) an
|
||||
|
||||
### Response shapes
|
||||
|
||||
`GET /device/` — **paginated** envelope:
|
||||
`GET /client/` — **paginated** envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -80,13 +92,20 @@ For a management portal the two primary resources are **Devices** (`/device`) an
|
||||
}
|
||||
```
|
||||
|
||||
`GET /device/{mac}`, `POST /device/add`, `POST /device/edit` — a single device object:
|
||||
`GET /client/{mac}`, `POST /client/add`, `POST /client/edit` — a single client object:
|
||||
|
||||
```json
|
||||
{ "mac_address": "AA-BB-CC-DD-EE-11", "group": "residents", "status": "paid",
|
||||
"name": "Sara Ib", "phone": "9998887", "alias": "Living Room TV" }
|
||||
```
|
||||
|
||||
`GET /device/` — **plain array** of NAS devices (not paginated):
|
||||
|
||||
```json
|
||||
[ { "ap_mac": "3C-52-A1-93-06-FC", "nasipaddress": "192.168.1.102",
|
||||
"ssids": ["VSARLINK", "Guest"], "alias": "Lobby AP" } ]
|
||||
```
|
||||
|
||||
`GET /vlan/` — **plain array** (not paginated):
|
||||
|
||||
```json
|
||||
@@ -102,7 +121,7 @@ For a management portal the two primary resources are **Devices** (`/device`) an
|
||||
Application errors (`400`, `401`, `404`, `409`) return a **string** detail:
|
||||
|
||||
```json
|
||||
{ "detail": "Device 'AA-BB-CC-DD-EE-11' already exists" }
|
||||
{ "detail": "Client 'AA-BB-CC-DD-EE-11' already exists" }
|
||||
```
|
||||
|
||||
Validation errors (`422`, bad/missing fields) return a FastAPI **array** detail:
|
||||
@@ -116,12 +135,15 @@ each entry's `msg` (and `loc`) for field-level messages.
|
||||
|
||||
### Typical portal flows
|
||||
|
||||
- **Provision a device:** `POST /device/add` with `mac_address`, `group` (an
|
||||
- **Provision a client:** `POST /client/add` with `mac_address`, `group` (an
|
||||
existing VLAN alias), `name`, `phone`, optional `alias`. Handle `400` (group
|
||||
missing), `409` (MAC exists), `422` (bad MAC / missing name·phone).
|
||||
- **Change plan state:** `POST /device/edit` `{mac_address, status}`.
|
||||
- **Move to another VLAN:** `POST /device/edit` `{mac_address, group}`.
|
||||
- **Change plan state:** `POST /client/edit` `{mac_address, status}`.
|
||||
- **Move to another VLAN:** `POST /client/edit` `{mac_address, group}`.
|
||||
- **Bulk import clients:** `POST /client/import` `{devices:[...], dry_run}` — see
|
||||
the Clients section.
|
||||
- **Populate a VLAN dropdown:** `GET /vlan/` → map `alias` (value sent as `group`).
|
||||
- **Label a NAS device:** `GET /device/` to list them, `PUT /device/{ip}` `{alias}`.
|
||||
|
||||
## Endpoints
|
||||
|
||||
@@ -141,7 +163,8 @@ per-resource filters:
|
||||
| Group check | `/radgroupcheck` | GET, POST, PUT, DELETE | `groupname`, `attribute` |
|
||||
| Group reply | `/radgroupreply` | GET, POST, PUT, DELETE | `groupname`, `attribute` |
|
||||
| **VLANs** | `/vlan` | see below | — |
|
||||
| **Devices** | `/device` | see below | — |
|
||||
| **Clients** | `/client` | see below | — |
|
||||
| **Devices** | `/device` | GET (list), PUT (alias) | — |
|
||||
| User↔group | `/radusergroup` | GET, POST, PUT, DELETE | `username`, `groupname` |
|
||||
| Accounting | `/radacct` | GET (read-only) | `username`, `nasipaddress`, `active` |
|
||||
| Post-auth log | `/radpostauth` | GET (read-only) | `username`, `reply` |
|
||||
@@ -169,37 +192,65 @@ and `Tunnel-Private-Group-Id=<vlanid>`.
|
||||
- Renaming updates `radgroupreply.groupname` only; if you also map users to groups
|
||||
in `radusergroup`, update those separately.
|
||||
|
||||
### Devices — `/device`
|
||||
### Clients — `/client`
|
||||
|
||||
A device is a client identified by its MAC address (used as the RADIUS `username`).
|
||||
One device spans three tables: `radcheck` (MAC = password), `radusergroup` (group
|
||||
membership), and `customers` (status + human metadata). MAC input is normalized to
|
||||
uppercase, hyphen-separated (`AA-BB-CC-DD-EE-FF`); colons and lowercase are accepted.
|
||||
A client is identified by its MAC address (used as the RADIUS `username`). One
|
||||
client spans four tables: `radcheck` (MAC = password), `radusergroup` (group
|
||||
membership), `customers` (status), and `radadmin_clients` (human metadata). MAC
|
||||
input is normalized to uppercase, hyphen-separated (`AA-BB-CC-DD-EE-FF`); colons and
|
||||
lowercase are accepted.
|
||||
|
||||
The `customers` table also carries **human-only metadata that RADIUS never reads**:
|
||||
`name`, `phone`, and `device_alias`. These are collected at device creation.
|
||||
The **`radadmin_clients`** table carries **human-only metadata that RADIUS never
|
||||
reads** — `name`, `phone`, and `alias`, keyed by `mac_address`. These are collected
|
||||
at client creation.
|
||||
|
||||
| Action | Request |
|
||||
|------------------|-----------------------------------------------------|
|
||||
| List devices | `GET /device/` → `{total,limit,offset,items:[{mac_address,group,status,name,phone,alias}]}` |
|
||||
| Get one device | `GET /device/{mac_address}` |
|
||||
| Add a device | `POST /device/add` body `{"mac_address":"14-99-3E-74-CB-7F","group":"staff","name":"Ali Hassan","phone":"7712345","alias":"Living Room TV"}` |
|
||||
| Edit a device | `POST /device/edit` body `{"mac_address":"...", group?, status?, name?, phone?, alias?}` |
|
||||
| Delete a device | `DELETE /device/{mac_address}` (removes all 3 rows) |
|
||||
| List clients | `GET /client/` → `{total,limit,offset,items:[{mac_address,group,status,name,phone,alias}]}` |
|
||||
| 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"}` |
|
||||
| Edit a client | `POST /client/edit` body `{"mac_address":"...", group?, status?, name?, phone?, alias?}` |
|
||||
| Import clients | `POST /client/import` body `{"devices":[{...}], "dry_run":true}` (bulk CSV import) |
|
||||
| Delete a client | `DELETE /client/{mac_address}` (removes all rows) |
|
||||
|
||||
On **add**, `name` and `phone` are **required**; `alias` is **optional**. They are
|
||||
stored in `customers.name`, `customers.phone`, `customers.device_alias` and returned
|
||||
on every device response (`alias` mirrors the `device_alias` column).
|
||||
stored in `radadmin_clients` (`name`, `phone`, `alias`) and returned on every client
|
||||
response.
|
||||
|
||||
- **Add** inserts a `radcheck` password (`Cleartext-Password := MAC`), a
|
||||
`radusergroup` row (`priority 1`), and a `customers` row (`status = paid`). The
|
||||
`group` must already exist in `radgroupreply` or you get `400`.
|
||||
`radusergroup` row (`priority 1`), a `customers` row (`status = paid`), and a
|
||||
`radadmin_clients` row (name/phone/alias). The `group` must already exist in
|
||||
`radgroupreply` or you get `400`.
|
||||
- **Edit** accepts any subset of `group`, `status`, `name`, `phone`, `alias` (at
|
||||
least one required); omitted fields are left unchanged. A new `group` must exist
|
||||
in `radgroupreply` (`400` otherwise). `status` must be one of `new` / `paid` /
|
||||
`unpaid`. In the DB the group is stored in the `radusergroup.groupname` column and
|
||||
`alias` in `customers.device_alias`.
|
||||
- Adding a device whose MAC already exists → `409`.
|
||||
`unpaid`. In the DB the group is stored in the `radusergroup.groupname` column,
|
||||
`status` in `customers.status`, and `name`/`phone`/`alias` in `radadmin_clients`.
|
||||
- Adding a client whose MAC already exists → `409`.
|
||||
- **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,
|
||||
mac, detail}]}` for a preview. With `dry_run:false` the valid rows are inserted
|
||||
best-effort (invalid rows reported, not fatal). The JSON field is `devices` (rows).
|
||||
|
||||
### Devices — `/device`
|
||||
|
||||
A device here is a **NAS/AP box**, keyed by its **AP MAC** — not a customer MAC
|
||||
(those are Clients). The list is derived from `radacct.calledstationid`
|
||||
(`<AP-MAC>:<SSID>`): rows are grouped by the AP MAC, so one physical AP is a single
|
||||
entry even when it broadcasts several SSIDs. Each entry aggregates every `ssid` seen
|
||||
and the NAS IP(s) it reported from. An editable human `alias` is stored in the
|
||||
standalone **`radadmin_devices`** table (keyed by AP MAC), which FreeRADIUS never reads.
|
||||
|
||||
| Action | Request |
|
||||
|------------------|-----------------------------------------------------|
|
||||
| List devices | `GET /device/` → `[{ap_mac, nasipaddress, ssids:[...], alias}]` |
|
||||
| Set/clear alias | `PUT /device/{ap_mac}` body `{"alias":"Lobby AP"}` (`alias:null` clears) |
|
||||
|
||||
- List is a **plain array** (not paginated), ordered by AP MAC.
|
||||
- `nasipaddress` is the NAS IP(s) the AP reports from (comma-joined if more than one);
|
||||
`ssids` is the full list of SSIDs seen for that AP MAC.
|
||||
- `PUT` upserts the `radadmin_devices` row for that AP MAC — no row need pre-exist;
|
||||
the MAC is normalized to uppercase-hyphen form.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -231,27 +282,44 @@ curl http://10.0.1.235:8000/vlan/55 \
|
||||
-X DELETE \
|
||||
-H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" -s | jq
|
||||
|
||||
# List devices
|
||||
curl http://10.0.1.235:8000/device/ \
|
||||
# List clients
|
||||
curl http://10.0.1.235:8000/client/ \
|
||||
-H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" -s | jq
|
||||
|
||||
# Add a device (MAC + existing group + name/phone required, alias optional)
|
||||
curl http://10.0.1.235:8000/device/add \
|
||||
# Add a client (MAC + existing group + name/phone required, alias optional)
|
||||
curl http://10.0.1.235:8000/client/add \
|
||||
-H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"mac_address":"14-99-3E-74-CB-7F","group":"staff","name":"Ali Hassan","phone":"7712345","alias":"Living Room TV"}' -s | jq
|
||||
|
||||
# Edit a device (any subset of group/status/name/phone/alias)
|
||||
curl http://10.0.1.235:8000/device/edit \
|
||||
# Edit a client (any subset of group/status/name/phone/alias)
|
||||
curl http://10.0.1.235:8000/client/edit \
|
||||
-H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"mac_address":"14-99-3E-74-CB-7F","status":"unpaid","alias":"Living Room TV"}' -s | jq
|
||||
|
||||
# Delete a device
|
||||
curl http://10.0.1.235:8000/device/14-99-3E-74-CB-7F \
|
||||
# Bulk-import clients (dry-run — preview only, nothing written)
|
||||
curl http://10.0.1.235:8000/client/import \
|
||||
-H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"dry_run":true,"devices":[{"mac_address":"14-99-3E-74-CB-7F","group":"staff","name":"Ali","phone":"7712345"}]}' -s | jq
|
||||
|
||||
# Delete a client
|
||||
curl http://10.0.1.235:8000/client/14-99-3E-74-CB-7F \
|
||||
-X DELETE \
|
||||
-H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" -s | jq
|
||||
|
||||
# List NAS devices (IPs seen in radacct, with alias/AP-MAC/SSID)
|
||||
curl http://10.0.1.235:8000/device/ \
|
||||
-H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" -s | jq
|
||||
|
||||
# Set a device alias by AP MAC (PUT; send {"alias":null} to clear)
|
||||
curl http://10.0.1.235:8000/device/3C-52-A1-93-06-FC \
|
||||
-X PUT \
|
||||
-H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"alias":"Lobby AP"}' -s | jq
|
||||
|
||||
# Create a customer
|
||||
curl http://10.0.1.235:8000/customers \
|
||||
-H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" \
|
||||
@@ -282,7 +350,7 @@ curl http://10.0.1.235:8000/health -s | jq
|
||||
| 204 | Deleted (no content) |
|
||||
| 400 | Bad request (e.g. referenced group/VLAN doesn't exist) |
|
||||
| 401 | Missing/invalid `X-API-Key` |
|
||||
| 404 | Row / device / VLAN not found |
|
||||
| 404 | Row / client / device / VLAN not found |
|
||||
| 409 | Duplicate / integrity conflict |
|
||||
| 422 | Request body failed validation |
|
||||
|
||||
@@ -297,7 +365,11 @@ app/
|
||||
errors.py APIError + JSON handler
|
||||
crud.py generic list/get/create/update/delete helpers
|
||||
pagination.py Page envelope + limit/offset dependency
|
||||
models.py SQLAlchemy models (one per table)
|
||||
models.py SQLAlchemy models (incl. radadmin_clients, radadmin_devices)
|
||||
schemas.py Pydantic request/response models
|
||||
routers/ one module per resource
|
||||
client.py Clients — MAC view over radcheck+radusergroup+customers
|
||||
device.py Devices — AP MACs from radacct + radadmin_devices alias
|
||||
vlan.py VLANs — view over radgroupreply
|
||||
... customers, nas, radcheck/reply, radgroup*, radacct, ...
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user