v.012
This commit is contained in:
parent
fb82b99e0e
commit
9f67d87e9d
|
|
@ -153,7 +153,7 @@
|
|||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"client_id\": 1234,\n \"info\": {\n \"Name\": \"Customer Name\",\n \"Location\": \"Address line 1\",\n \"area_code\": \"area/postal code\",\n \"area\": \"City\",\n \"AltLookup\": true,\n \"AltAlarmNo\": \"SIA1000101\",\n \"ConvertType\": \"None\",\n \"NoSigsMon\": \"None\",\n \"SinceDays\": 0,\n \"SinceHrs\": 0,\n \"SinceMins\": 0,\n \"ResetNosigsIgnored\": true,\n \"ResetNosigsDays\": 0,\n \"ResetNosigsHrs\": 0,\n \"ResetNosigsMins\": 0,\n \"PanelName\": \"Panel Type\",\n \"PanelSite\": \"Panel location\"\n }\n}",
|
||||
"raw": "{\n \"client_id\": 123456789,\n \"info\": {\n \"Name\": \"Anders Knutsen\",\n \"Alias\": \"000FD267\",\n \"Location\": \"Lislebyveien 58\",\n \"area_code\": \"1604\",\n \"area\": \"Fredrikstad\",\n \"BusPhone\": \"69310000\",\n \"Email\": \"post@ostsik.no\",\n \"OKPassword\": \"franzjager\",\n \"SpecRequest\": \"Dette skal gjøres ved alarm på denne kunden.\",\n \"NoSigsMon\": \"ActiveAny\",\n \"SinceDays\": 1,\n \"SinceHrs\": 0,\n \"SinceMins\": 30,\n \"ResetNosigsIgnored\": true,\n \"ResetNosigsDays\": 7,\n \"ResetNosigsHrs\": 0,\n \"ResetNosigsMins\": 0,\n \"InstallDateTime\": \"2023-02-20\",\n \"PanelName\": \"Ajax\",\n \"PanelSite\": \"Stue\",\n \"KeypadLocation\": \"Inngang\",\n \"SPPage\": \"Ekstra informasjon som kan være relevant.\"\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
|
|
@ -186,7 +186,7 @@
|
|||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"client_id\": 1234,\n \"info\": {\n \"Location\": \"New street 99\"\n }\n}",
|
||||
"raw": "{\n \"client_id\": 123456789,\n \"info\": {\n \"Name\": \"optional\"\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
|
|
@ -202,45 +202,9 @@
|
|||
"clients"
|
||||
]
|
||||
},
|
||||
"description": "Updates specific fields on an existing client.\n\nPossible keys:\n\n\"Name\": \"Customer Name\", \n\"Location\": \"New street 99\", \n\"area_code\": \"area/postal code\", \n\"area\": \"City\", \n\"AltLookup\": true, \n\"AltAlarmNo\": \"SIA1000101\", \n\"ConvertType\": \"None\", \n\"NoSigsMon\": \"None\", \n\"SinceDays\": 0, \n\"SinceHrs\": 0, \n\"SinceMins\": 0, \n\"ResetNosigsIgnored\": true, \n\"ResetNosigsDays\": 0, \n\"ResetNosigsHrs\": 0, \n\"ResetNosigsMins\": 0, \n\"PanelName\": \"Panel Type\", \n\"PanelSite\": \"Panel location\"\n\nExample:\n\n{ \n\"client_id\": 1234, \n\"info\": { \n\"Location\": \"New street 99\" \n} \n}"
|
||||
"description": "Updates specific fields on an existing client.\n\nPossible keys: \n\"Alias\": \"optional\", \n\"Location\": \"optional\", \n\"area_code\": \"optional\", \n\"area\": \"optional\", \n\"BusPhone\": \"optional\", \n\"Email\": \"optional\", \n\"OKPassword\": \"optional\", \n\"SpecRequest\": \"optional\", \n\"NoSigsMon\": \"optional\", \n\"SinceDays\": 0, \n\"SinceHrs\": 0, \n\"SinceMins\": 0, \n\"ResetNosigsIgnored\": true, \n\"ResetNosigsDays\": 0, \n\"ResetNosigsHrs\": 0, \n\"ResetNosigsMins\": 0, \n\"InstallDateTime\": \"optional\", \n\"PanelName\": \"optional\", \n\"PanelSite\": \"optional\", \n\"KeypadLocation\": \"optional\", \n\"SPPage\": \"optional\"\n\nExample:\n\n{ \n\"client_id\": 1234, \n\"info\": { \n\"Location\": \"New street 99\" \n} \n}"
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "Update client",
|
||||
"originalRequest": {
|
||||
"method": "PATCH",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{apiKey}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"client_id\": 1234,\n \"info\": {\n \"Location\": \"New street 99\"\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/clients",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"clients"
|
||||
]
|
||||
}
|
||||
},
|
||||
"_postman_previewlanguage": "",
|
||||
"header": [],
|
||||
"cookie": [],
|
||||
"body": ""
|
||||
}
|
||||
]
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Create/Update zone",
|
||||
|
|
@ -255,7 +219,7 @@
|
|||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"client_id\": 1234,\n \"zone_id\": 1,\n \"Zone_area\": \"Entrance\",\n \"ModuleNo\": 0\n}\n",
|
||||
"raw": "{\n \"client_id\": 123456789,\n \"zone_id\": 3,\n \"Zone_area\": \"RD Stue\",\n \"ModuleNo\": 0\n}\n",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
|
|
@ -289,7 +253,7 @@
|
|||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"client_id\": {{clientId}},\n \"user\": {\n \"User_Name\": \"User Number 1\",\n \"MobileNo\": \"+4712345678\",\n \"MobileNoOrder\": 1,\n \"Email\": \"user@email.com\",\n \"Type\": \"U\",\n \"UserNo\": 1,\n \"Instructions\": \"Optional\",\n \"CallOrder\": 0\n }\n}",
|
||||
"raw": "{\n \"client_id\": {{clientId}},\n \"user\": {\n \"User_Name\": \"Anders Knutsen\",\n \"MobileNo\": \"+4740642018\",\n \"MobileNoOrder\": 1,\n \"Email\": \"anders@ostsik.no\",\n \"Type\": \"U\",\n \"UserNo\": 1,\n \"Instructions\": \"\",\n \"CallOrder\": 1\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
|
|
@ -396,7 +360,7 @@
|
|||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"client_id\": {{clientId}},\n \"user_no\": 1\n}\n",
|
||||
"raw": "{\n \"client_id\": {{clientId}},\n \"user_no\": 199\n}\n",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
|
|
|
|||
78
keys.json
78
keys.json
|
|
@ -1,6 +1,76 @@
|
|||
[
|
||||
{ "key_name": "anders", "key": "_2sW6roe2ZQ4V6Cldo0v295fakHT8vBHqHScfliX445tZuzxDwMRqjPeCE7FDcVVr", "enabled": true, "valid_to": "2099-12-31T23:59:59Z" },
|
||||
{ "key_name": "knut", "key": "_JCiGDfmgyoZGFiz6mtEC2YBCoxkmu5apvtGrCeDw0SWTui2B9kAIoSCK0wK85UIy", "enabled": true, "valid_to": "2099-12-31T23:59:59Z" },
|
||||
{ "key_name": "novus", "key": "_6uOesirJ6QfdACcm2uQIddsstTvXRIP8YjhcKwMuyp9W7mzV2jI1mDisVip1hay5", "enabled": true, "valid_to": "2099-12-31T23:59:59Z" },
|
||||
{ "key_name": "futurehome", "key": "_8942WfJxRPA9SiblpR4pD8slkK1jCWlmEJ7XCDmzpJDiG4KlBIbyl5syXxS3RHya", "enabled": true, "valid_to": "2099-01-01T00:00:00Z" }
|
||||
{
|
||||
"key_name": "anders",
|
||||
"key": "_2sW6roe2ZQ4V6Cldo0v295fakHT8vBHqHScfliX445tZuzxDwMRqjPeCE7FDcVVr",
|
||||
"enabled": true,
|
||||
"valid_to": "2030-01-01T00:00:00Z",
|
||||
|
||||
"port": "BASE11",
|
||||
|
||||
"installer_name": "Østfold Sikkerhetsservice AS",
|
||||
"installer_email": "service@ostsik.no",
|
||||
|
||||
"use_glob_callouts": true,
|
||||
"show_on_callouts": false,
|
||||
"glob_callouts": "TLF01BASE01",
|
||||
|
||||
"use_glob_callouts2": true,
|
||||
"show_on_callouts2": false,
|
||||
"glob_callouts2": "TLF02BASE01",
|
||||
|
||||
"alt_lookup": true,
|
||||
"alt_alarm_no": "CID4BASE01",
|
||||
"convert_type": "None",
|
||||
"siginterpret": "None",
|
||||
|
||||
"client_groupings": [
|
||||
{
|
||||
"description": "Alarm24",
|
||||
"grouping_type_description": "Alarm24 Tilgang",
|
||||
"grouping_allow_multiple": true
|
||||
},
|
||||
{
|
||||
"description": "Import",
|
||||
"grouping_type_description": "Østfold Sikkerhetsservice AS",
|
||||
"grouping_allow_multiple": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key_name": "futurehome",
|
||||
"key": "_8942WfJxRPA9SiblpR4pD8slkK1jCWlmEJ7XCDmzpJDiG4KlBIbyl5syXxS3RHya",
|
||||
"enabled": true,
|
||||
"valid_to": "2030-01-01T00:00:00Z",
|
||||
|
||||
"port": "BASE88",
|
||||
|
||||
"installer_name": "Futurehome",
|
||||
"installer_email": "anders@ostsik.no",
|
||||
|
||||
"use_glob_callouts": true,
|
||||
"show_on_callouts": false,
|
||||
"glob_callouts": "TLF01BASE01",
|
||||
|
||||
"use_glob_callouts2": true,
|
||||
"show_on_callouts2": false,
|
||||
"glob_callouts2": "TLF02BASE01",
|
||||
|
||||
"alt_lookup": true,
|
||||
"alt_alarm_no": "SIA1000101",
|
||||
"convert_type": "None",
|
||||
"siginterpret": "SIADecimal",
|
||||
|
||||
"client_groupings": [
|
||||
{
|
||||
"description": "Alarm24",
|
||||
"grouping_type_description": "Alarm24 Tilgang",
|
||||
"grouping_allow_multiple": true
|
||||
},
|
||||
{
|
||||
"description": "Alarm",
|
||||
"grouping_type_description": "Futurehome",
|
||||
"grouping_allow_multiple": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
487
main.py
487
main.py
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
import json, os, threading, re, logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Optional, List
|
||||
from time import time
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Depends, Header, Request, Response, status
|
||||
|
|
@ -9,22 +9,59 @@ from pydantic import BaseModel, Field, EmailStr, field_validator
|
|||
|
||||
SAFE_NAME = re.compile(r"[^A-Za-z0-9._-]")
|
||||
|
||||
|
||||
def _safe_folder(name: str) -> str:
|
||||
cleaned = SAFE_NAME.sub("_", name.strip())
|
||||
return cleaned or "unknown"
|
||||
|
||||
|
||||
# ------------ Config ------------
|
||||
DATA_FILE = os.getenv("DATA_FILE", "data.json")
|
||||
KEY_FILE = os.getenv("KEY_FILE", "keys.json")
|
||||
XML_DIR = os.getenv("XML_DIR", "out/clients")
|
||||
KEY_FILE = os.getenv("KEY_FILE", "keys.json")
|
||||
XML_DIR = os.getenv("XML_DIR", "out/clients")
|
||||
|
||||
# ------------ Models used in keys.json ------------
|
||||
|
||||
|
||||
class ClientGroupingConfig(BaseModel):
|
||||
description: str
|
||||
grouping_type_description: str
|
||||
grouping_allow_multiple: bool = True
|
||||
|
||||
|
||||
# ------------ Auth (hot-reloaded key store) ------------
|
||||
|
||||
|
||||
class KeyRecord(BaseModel):
|
||||
key_name: str
|
||||
key: str
|
||||
enabled: bool = True
|
||||
valid_to: str # ISO-8601
|
||||
|
||||
# Used for __id in XML: <__id>{client_id}{port}</__id>
|
||||
port: str = ""
|
||||
|
||||
# Per-key forced XML fields
|
||||
installer_name: str = "" # <Installer>, user 199 name, grouping #2
|
||||
installer_email: str = "" # user 199 email
|
||||
|
||||
use_glob_callouts: bool = True # <UseGlobCallOuts>
|
||||
show_on_callouts: bool = False # <ShowOnCallOuts>
|
||||
glob_callouts: str = "" # <GlobCallOuts>
|
||||
|
||||
use_glob_callouts2: bool = True # <UseGlobCallOuts2>
|
||||
show_on_callouts2: bool = False # <ShowOnCallOuts2>
|
||||
glob_callouts2: str = "" # <GlobCallOuts2>
|
||||
|
||||
alt_lookup: bool = True # <AltLookup>
|
||||
alt_alarm_no: str = "CID4BASE01" # <AltAlarmNo>
|
||||
convert_type: str = "None" # <ConvertType>
|
||||
siginterpret: str = "SIADecimal" # <SIGINTERPRET>
|
||||
|
||||
# NEW: per-key client groupings (rendered to <ClientGroupings>)
|
||||
client_groupings: List[ClientGroupingConfig] = Field(default_factory=list)
|
||||
|
||||
|
||||
class KeyStore:
|
||||
def __init__(self, path: str):
|
||||
self.path = path
|
||||
|
|
@ -80,39 +117,55 @@ class KeyStore:
|
|||
raise HTTPException(status_code=401, detail="Token expired")
|
||||
return rec
|
||||
|
||||
|
||||
_key_store = KeyStore(KEY_FILE)
|
||||
|
||||
|
||||
def require_api_key(authorization: Optional[str] = Header(None)) -> KeyRecord:
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
raise HTTPException(status_code=401, detail="Missing bearer token")
|
||||
token = authorization.split(" ", 1)[1].strip()
|
||||
return _key_store.validate_bearer(token)
|
||||
|
||||
# ------------ Models ------------
|
||||
|
||||
# ------------ Models (client payloads) ------------
|
||||
|
||||
|
||||
class ClientInfo(BaseModel):
|
||||
# From API / client section
|
||||
Name: str = Field(..., min_length=1, max_length=200)
|
||||
Alias: Optional[str] = Field(None, max_length=200)
|
||||
Location: str = Field(..., min_length=1, max_length=200)
|
||||
|
||||
# split fields used by the API
|
||||
# split fields used by the API → XML loc2 = "{area_code} {area}"
|
||||
area_code: str = Field(..., min_length=1, max_length=20)
|
||||
area: str = Field(..., min_length=1, max_length=200)
|
||||
|
||||
AltLookup: bool = True
|
||||
AltAlarmNo: str | None = None
|
||||
ConvertType: str = "None"
|
||||
NoSigsMon: str = "None"
|
||||
BusPhone: Optional[str] = Field(None, max_length=50)
|
||||
Email: Optional[EmailStr] = None
|
||||
OKPassword: Optional[str] = Field(None, max_length=100)
|
||||
SpecRequest: Optional[str] = Field(None, max_length=1000)
|
||||
|
||||
SinceDays: int = Field(0, ge=0)
|
||||
# No-signal monitoring
|
||||
NoSigsMon: str = "ActiveAny"
|
||||
SinceDays: int = Field(1, ge=0)
|
||||
SinceHrs: int = Field(0, ge=0)
|
||||
SinceMins: int = Field(0, ge=0)
|
||||
SinceMins: int = Field(30, ge=0)
|
||||
|
||||
ResetNosigsIgnored: bool = True
|
||||
ResetNosigsDays: int = Field(0, ge=0)
|
||||
ResetNosigsDays: int = Field(7, ge=0)
|
||||
ResetNosigsHrs: int = Field(0, ge=0)
|
||||
ResetNosigsMins: int = Field(0, ge=0)
|
||||
|
||||
InstallDateTime: Optional[str] = Field(
|
||||
None, description="Installation date/time, e.g. '2023-02-20'"
|
||||
)
|
||||
|
||||
PanelName: str = "Panel Type"
|
||||
PanelSite: str = "Panel location"
|
||||
KeypadLocation: Optional[str] = Field(None, max_length=200)
|
||||
SPPage: Optional[str] = Field(None, max_length=2000)
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
User_Name: str = Field(..., min_length=1, max_length=120)
|
||||
|
|
@ -130,22 +183,27 @@ class User(BaseModel):
|
|||
raise ValueError("MobileNo must contain digits")
|
||||
return v.strip()
|
||||
|
||||
|
||||
class ClientCreate(BaseModel):
|
||||
client_id: int = Field(..., ge=1)
|
||||
info: ClientInfo
|
||||
|
||||
|
||||
class ClientInfoPatch(BaseModel):
|
||||
# all optional, for PATCH
|
||||
Name: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||
Alias: Optional[str] = Field(None, max_length=200)
|
||||
Location: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||
|
||||
area_code: Optional[str] = Field(None, min_length=1, max_length=20)
|
||||
area: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||
|
||||
AltLookup: Optional[bool] = None
|
||||
AltAlarmNo: Optional[str] = None
|
||||
ConvertType: Optional[str] = None
|
||||
NoSigsMon: Optional[str] = None
|
||||
BusPhone: Optional[str] = Field(None, max_length=50)
|
||||
Email: Optional[EmailStr] = None
|
||||
OKPassword: Optional[str] = Field(None, max_length=100)
|
||||
SpecRequest: Optional[str] = Field(None, max_length=1000)
|
||||
|
||||
NoSigsMon: Optional[str] = None
|
||||
SinceDays: Optional[int] = Field(None, ge=0)
|
||||
SinceHrs: Optional[int] = Field(None, ge=0)
|
||||
SinceMins: Optional[int] = Field(None, ge=0)
|
||||
|
|
@ -155,99 +213,103 @@ class ClientInfoPatch(BaseModel):
|
|||
ResetNosigsHrs: Optional[int] = Field(None, ge=0)
|
||||
ResetNosigsMins: Optional[int] = Field(None, ge=0)
|
||||
|
||||
InstallDateTime: Optional[str] = None
|
||||
|
||||
PanelName: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||
PanelSite: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||
KeypadLocation: Optional[str] = Field(None, max_length=200)
|
||||
SPPage: Optional[str] = Field(None, max_length=2000)
|
||||
|
||||
|
||||
class ClientPatch(BaseModel):
|
||||
client_id: int = Field(..., ge=1)
|
||||
info: Optional[ClientInfoPatch] = None
|
||||
|
||||
|
||||
class ClientID(BaseModel):
|
||||
client_id: int = Field(..., ge=1)
|
||||
|
||||
|
||||
class ZoneUpdate(BaseModel):
|
||||
client_id: int = Field(..., ge=1)
|
||||
zone_id: int = Field(..., ge=1, le=999)
|
||||
Zone_area: str = Field(..., min_length=1, max_length=200)
|
||||
ModuleNo: int = Field(0, ge=0)
|
||||
|
||||
|
||||
class ZoneDelete(BaseModel):
|
||||
client_id: int = Field(..., ge=1)
|
||||
zone_id: int = Field(..., ge=1, le=999)
|
||||
|
||||
|
||||
class UserWithClient(BaseModel):
|
||||
client_id: int = Field(..., ge=1)
|
||||
user: User
|
||||
|
||||
|
||||
class UserKey(BaseModel):
|
||||
client_id: int = Field(..., ge=1)
|
||||
user_no: int = Field(..., ge=1, le=999)
|
||||
|
||||
|
||||
# ------------ In-memory store + persistence ------------
|
||||
# Store is namespaced per API key: "key_name:client_id"
|
||||
|
||||
|
||||
_store_lock = threading.Lock()
|
||||
# Multi-tenant: {"keys": { key_name: { "clients": { client_id_str: {...} } } } }
|
||||
_store: Dict = {"keys": {}}
|
||||
_store: Dict = {"clients": {}}
|
||||
|
||||
def _client_key(client_id: int) -> str:
|
||||
return str(client_id)
|
||||
|
||||
def _tenant_store(key_name: str) -> Dict:
|
||||
"""Return the per-key tenant store, creating if needed."""
|
||||
tenants = _store.setdefault("keys", {})
|
||||
if key_name not in tenants:
|
||||
tenants[key_name] = {"clients": {}}
|
||||
return tenants[key_name]
|
||||
|
||||
def _clients_for(key_name: str) -> Dict[str, Dict]:
|
||||
"""Return dict of clients for a given key_name."""
|
||||
tenant = _tenant_store(key_name)
|
||||
return tenant.setdefault("clients", {})
|
||||
|
||||
def _require_client_for(key_name: str, client_id: int) -> Dict:
|
||||
"""Fetch a client for this key only; 404 if not found."""
|
||||
clients = _clients_for(key_name)
|
||||
cli = clients.get(_client_key(client_id))
|
||||
if not cli:
|
||||
raise HTTPException(status_code=404, detail="Client not found")
|
||||
return cli
|
||||
|
||||
def _load_store():
|
||||
global _store
|
||||
if os.path.exists(DATA_FILE):
|
||||
with open(DATA_FILE, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
# Backwards compatibility: old single-tenant shape {"clients": {...}}
|
||||
if isinstance(data, dict) and "keys" in data:
|
||||
_store = data
|
||||
elif isinstance(data, dict) and "clients" in data:
|
||||
# Legacy: put all clients under a pseudo-tenant
|
||||
_store = {"keys": {"__legacy__": {"clients": data["clients"]}}}
|
||||
else:
|
||||
_store = {"keys": {}}
|
||||
_store = json.load(f)
|
||||
else:
|
||||
_store = {"keys": {}}
|
||||
_flush_store()
|
||||
|
||||
|
||||
def _flush_store():
|
||||
tmp = DATA_FILE + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(_store, f, ensure_ascii=False, indent=2)
|
||||
os.replace(tmp, DATA_FILE)
|
||||
|
||||
|
||||
def _store_key(client_id: int, keyrec: KeyRecord) -> str:
|
||||
return f"{keyrec.key_name}:{client_id}"
|
||||
|
||||
|
||||
def _require_client(client_id: int, keyrec: KeyRecord) -> Dict:
|
||||
skey = _store_key(client_id, keyrec)
|
||||
cli = _store["clients"].get(skey)
|
||||
if not cli:
|
||||
raise HTTPException(status_code=404, detail="Client not found")
|
||||
return cli
|
||||
|
||||
|
||||
# ------------ XML writer ------------
|
||||
|
||||
|
||||
def _ensure_dir(path: str):
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
def _xml_path_for(client_id: int, key_name: str) -> str:
|
||||
client_base = os.path.join(XML_DIR, _safe_folder(key_name))
|
||||
|
||||
def _xml_path_for(client_id: int, keyrec: KeyRecord) -> str:
|
||||
client_base = os.path.join(XML_DIR, _safe_folder(keyrec.key_name))
|
||||
_ensure_dir(client_base)
|
||||
return os.path.join(client_base, f"{client_id}.xml")
|
||||
|
||||
def write_client_xml(client_id: int, key_name: str):
|
||||
|
||||
def _bool_text(val: bool) -> str:
|
||||
return "True" if val else "False"
|
||||
|
||||
|
||||
def write_client_xml(client_id: int, keyrec: KeyRecord):
|
||||
from xml.etree.ElementTree import Element, SubElement, ElementTree
|
||||
clients = _clients_for(key_name)
|
||||
key = _client_key(client_id)
|
||||
cli = clients.get(key)
|
||||
|
||||
skey = _store_key(client_id, keyrec)
|
||||
cli = _store["clients"].get(skey)
|
||||
if not cli:
|
||||
return
|
||||
|
||||
|
|
@ -255,28 +317,71 @@ def write_client_xml(client_id: int, key_name: str):
|
|||
|
||||
root = Element("Clients")
|
||||
row = SubElement(root, "Row")
|
||||
SubElement(row, "__id").text = str(client_id)
|
||||
|
||||
# __id = client_id + port, e.g. "9998BASE11"
|
||||
combined_id = f"{client_id}{keyrec.port}" if keyrec.port else str(client_id)
|
||||
SubElement(row, "__id").text = combined_id
|
||||
|
||||
# Client fields from API
|
||||
SubElement(row, "Name").text = info.Name
|
||||
SubElement(row, "Alias").text = info.Alias or ""
|
||||
SubElement(row, "Location").text = info.Location
|
||||
SubElement(row, "loc2").text = f"{info.area_code} {info.area}".strip()
|
||||
|
||||
SubElement(row, "AltLookup").text = "True" if info.AltLookup else "False"
|
||||
SubElement(row, "AltAlarmNo").text = info.AltAlarmNo or ""
|
||||
SubElement(row, "ConvertType").text = info.ConvertType
|
||||
SubElement(row, "NoSigsMon").text = info.NoSigsMon
|
||||
SubElement(row, "BusPhone").text = info.BusPhone or ""
|
||||
SubElement(row, "Email").text = info.Email or ""
|
||||
SubElement(row, "OKPassword").text = info.OKPassword or ""
|
||||
SubElement(row, "SpecRequest").text = info.SpecRequest or ""
|
||||
|
||||
SubElement(row, "NoSigsMon").text = info.NoSigsMon
|
||||
SubElement(row, "SinceDays").text = str(info.SinceDays)
|
||||
SubElement(row, "SinceHrs").text = str(info.SinceHrs)
|
||||
SubElement(row, "SinceMins").text = str(info.SinceMins)
|
||||
|
||||
SubElement(row, "ResetNosigsIgnored").text = "True" if info.ResetNosigsIgnored else "False"
|
||||
SubElement(row, "ResetNosigsIgnored").text = _bool_text(info.ResetNosigsIgnored)
|
||||
SubElement(row, "ResetNosigsDays").text = str(info.ResetNosigsDays)
|
||||
SubElement(row, "ResetNosigsHrs").text = str(info.ResetNosigsHrs)
|
||||
SubElement(row, "ResetNosigsMins").text = str(info.ResetNosigsMins)
|
||||
|
||||
SubElement(row, "InstallDateTime").text = info.InstallDateTime or ""
|
||||
|
||||
SubElement(row, "PanelName").text = info.PanelName
|
||||
SubElement(row, "PanelSite").text = info.PanelSite
|
||||
SubElement(row, "KeypadLocation").text = info.KeypadLocation or ""
|
||||
SubElement(row, "SPPage").text = info.SPPage or ""
|
||||
|
||||
# Per-key forced fields (from keys.json)
|
||||
SubElement(row, "Installer").text = keyrec.installer_name or ""
|
||||
|
||||
SubElement(row, "UseGlobCallOuts").text = _bool_text(keyrec.use_glob_callouts)
|
||||
SubElement(row, "ShowOnCallOuts").text = _bool_text(keyrec.show_on_callouts)
|
||||
SubElement(row, "GlobCallOuts").text = keyrec.glob_callouts or ""
|
||||
|
||||
SubElement(row, "UseGlobCallOuts2").text = _bool_text(keyrec.use_glob_callouts2)
|
||||
SubElement(row, "ShowOnCallOuts2").text = _bool_text(keyrec.show_on_callouts2)
|
||||
SubElement(row, "GlobCallOuts2").text = keyrec.glob_callouts2 or ""
|
||||
|
||||
SubElement(row, "AltLookup").text = _bool_text(keyrec.alt_lookup)
|
||||
SubElement(row, "AltAlarmNo").text = keyrec.alt_alarm_no or ""
|
||||
SubElement(row, "ConvertType").text = keyrec.convert_type or "None"
|
||||
SubElement(row, "SIGINTERPRET").text = keyrec.siginterpret or "SIADecimal"
|
||||
|
||||
# ClientGroupings from keys.json
|
||||
cgs_el = SubElement(row, "ClientGroupings")
|
||||
if keyrec.client_groupings:
|
||||
for cg in keyrec.client_groupings:
|
||||
cg_el = SubElement(cgs_el, "ClientGrouping")
|
||||
SubElement(cg_el, "Description").text = cg.description
|
||||
SubElement(cg_el, "GroupingTypeDescription").text = cg.grouping_type_description
|
||||
SubElement(cg_el, "GroupingAllowMultiple").text = _bool_text(
|
||||
cg.grouping_allow_multiple
|
||||
)
|
||||
else:
|
||||
# Fallback default if nothing configured (optional)
|
||||
cg_el = SubElement(cgs_el, "ClientGrouping")
|
||||
SubElement(cg_el, "Description").text = "Alarm24"
|
||||
SubElement(cg_el, "GroupingTypeDescription").text = "Alarm24 Tilgang"
|
||||
SubElement(cg_el, "GroupingAllowMultiple").text = "True"
|
||||
|
||||
# Zones
|
||||
zones_el = SubElement(row, "Zones")
|
||||
|
|
@ -288,11 +393,13 @@ def write_client_xml(client_id: int, key_name: str):
|
|||
SubElement(z_el, "Zone_No").text = zid_str
|
||||
SubElement(z_el, "ModuleNo").text = str(z.get("ModuleNo", 0))
|
||||
|
||||
# Users
|
||||
# Users (from API)
|
||||
users_el = SubElement(row, "Users")
|
||||
users = cli.get("users") or {}
|
||||
|
||||
def _sort_user(u: dict):
|
||||
return (u.get("CallOrder", 0), u.get("MobileNoOrder", 0), u.get("UserNo", 0))
|
||||
|
||||
for _, u in sorted(users.items(), key=lambda kv: _sort_user(kv[1])):
|
||||
u_el = SubElement(users_el, "User")
|
||||
SubElement(u_el, "User_Name").text = u.get("User_Name", "")
|
||||
|
|
@ -304,9 +411,18 @@ def write_client_xml(client_id: int, key_name: str):
|
|||
SubElement(u_el, "Instructions").text = u.get("Instructions", "") or ""
|
||||
SubElement(u_el, "CallOrder").text = str(u.get("CallOrder", 0))
|
||||
|
||||
path = _xml_path_for(client_id, key_name)
|
||||
# Extra forced installer user (type "N", UserNo 199)
|
||||
inst_user_el = SubElement(users_el, "User")
|
||||
SubElement(inst_user_el, "User_Name").text = keyrec.installer_name or ""
|
||||
SubElement(inst_user_el, "Email").text = keyrec.installer_email or ""
|
||||
SubElement(inst_user_el, "UserNo").text = "199"
|
||||
SubElement(inst_user_el, "CallOrder").text = "0"
|
||||
SubElement(inst_user_el, "Type").text = "N"
|
||||
|
||||
path = _xml_path_for(client_id, keyrec)
|
||||
try:
|
||||
import xml.dom.minidom as minidom
|
||||
|
||||
tmp_path = path + ".tmp"
|
||||
ElementTree(root).write(tmp_path, encoding="utf-8", xml_declaration=True)
|
||||
with open(tmp_path, "rb") as rf:
|
||||
|
|
@ -318,9 +434,10 @@ def write_client_xml(client_id: int, key_name: str):
|
|||
except Exception:
|
||||
ElementTree(root).write(path, encoding="utf-8", xml_declaration=True)
|
||||
|
||||
|
||||
# ------------ App & logging middleware ------------
|
||||
_load_store()
|
||||
app = FastAPI(title="Client Registry API", version="3.1.0")
|
||||
app = FastAPI(title="Client Registry API", version="3.3.0")
|
||||
|
||||
logging.basicConfig(
|
||||
filename="api_requests.log",
|
||||
|
|
@ -331,6 +448,7 @@ logging.basicConfig(
|
|||
REDACT_KEYS = {"authorization", "password", "secret", "key"}
|
||||
MAX_BODY_CHARS = 4000
|
||||
|
||||
|
||||
def _redact(obj):
|
||||
try:
|
||||
if isinstance(obj, dict):
|
||||
|
|
@ -341,6 +459,7 @@ def _redact(obj):
|
|||
pass
|
||||
return obj
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def log_requests(request: Request, call_next):
|
||||
start = time()
|
||||
|
|
@ -350,8 +469,10 @@ async def log_requests(request: Request, call_next):
|
|||
if method in {"POST", "PUT", "PATCH", "DELETE"}:
|
||||
try:
|
||||
raw = await request.body()
|
||||
|
||||
async def receive():
|
||||
return {"type": "http.request", "body": raw, "more_body": False}
|
||||
|
||||
request._receive = receive
|
||||
|
||||
try:
|
||||
|
|
@ -367,11 +488,16 @@ async def log_requests(request: Request, call_next):
|
|||
|
||||
logging.info(
|
||||
"%s %s - %s - %.3fs - body=%s",
|
||||
method, request.url.path, response.status_code, duration, body_text
|
||||
method,
|
||||
request.url.path,
|
||||
response.status_code,
|
||||
duration,
|
||||
body_text,
|
||||
)
|
||||
return response
|
||||
|
||||
# --------- Routes ---------
|
||||
|
||||
# --------- Routes (unchanged logic, but keyrec now has client_groupings) ---------
|
||||
|
||||
# UPSERT CLIENT (create if missing, update if exists)
|
||||
@app.put("/clients", status_code=200)
|
||||
|
|
@ -384,45 +510,33 @@ def upsert_client(
|
|||
|
||||
logging.info(
|
||||
"AUDIT upsert_client key=%s client_id=%s payload=%s",
|
||||
keyrec.key_name, client_id, payload.model_dump()
|
||||
keyrec.key_name,
|
||||
client_id,
|
||||
payload.model_dump(),
|
||||
)
|
||||
|
||||
info = payload.info.model_dump()
|
||||
|
||||
# Auto-defaults
|
||||
alt = info.get("AltAlarmNo")
|
||||
if alt is None or (isinstance(alt, str) and not alt.strip()):
|
||||
info["AltAlarmNo"] = "SIA1000101"
|
||||
|
||||
pn = info.get("PanelName")
|
||||
if pn is None or (isinstance(pn, str) and not pn.strip()):
|
||||
info["PanelName"] = "STANDARD"
|
||||
|
||||
key = _client_key(client_id)
|
||||
skey = _store_key(client_id, keyrec)
|
||||
|
||||
with _store_lock:
|
||||
clients = _clients_for(keyrec.key_name)
|
||||
existed = key in clients
|
||||
existed = skey in _store["clients"]
|
||||
|
||||
if existed:
|
||||
cli = clients[key]
|
||||
cli = _store["clients"][skey]
|
||||
cli["info"] = info
|
||||
else:
|
||||
cli = {
|
||||
"info": info,
|
||||
"zones": {},
|
||||
"users": {}
|
||||
}
|
||||
clients[key] = cli
|
||||
cli = {"info": info, "zones": {}, "users": {}}
|
||||
_store["clients"][skey] = cli
|
||||
|
||||
if response is not None:
|
||||
response.status_code = status.HTTP_200_OK if existed else status.HTTP_201_CREATED
|
||||
|
||||
_flush_store()
|
||||
write_client_xml(client_id, keyrec.key_name)
|
||||
write_client_xml(client_id, keyrec)
|
||||
|
||||
return {"client_id": client_id}
|
||||
|
||||
|
||||
# PATCH CLIENT INFO (partial replace of info)
|
||||
@app.patch("/clients", status_code=200)
|
||||
def patch_client(
|
||||
|
|
@ -432,41 +546,30 @@ def patch_client(
|
|||
client_id = payload.client_id
|
||||
logging.info(
|
||||
"AUDIT patch_client key=%s client_id=%s payload=%s",
|
||||
keyrec.key_name, client_id, payload.model_dump()
|
||||
keyrec.key_name,
|
||||
client_id,
|
||||
payload.model_dump(),
|
||||
)
|
||||
|
||||
with _store_lock:
|
||||
cli = _require_client_for(keyrec.key_name, client_id)
|
||||
cli = _require_client(client_id, keyrec)
|
||||
|
||||
if payload.info is not None:
|
||||
# Current stored info (must be valid ClientInfo shape)
|
||||
current_info = cli.get("info", {})
|
||||
base = ClientInfo(**current_info).model_dump()
|
||||
|
||||
# Only the fields actually sent in the request
|
||||
updates = payload.info.model_dump(exclude_unset=True)
|
||||
|
||||
# Merge: updates override base
|
||||
merged = {**base, **updates}
|
||||
|
||||
# Auto-defaults AFTER merge
|
||||
alt = merged.get("AltAlarmNo")
|
||||
if alt is None or (isinstance(alt, str) and not alt.strip()):
|
||||
merged["AltAlarmNo"] = "SIA1000101"
|
||||
|
||||
pn = merged.get("PanelName")
|
||||
if pn is None or (isinstance(pn, str) and not pn.strip()):
|
||||
merged["PanelName"] = "STANDARD"
|
||||
|
||||
merged_valid = ClientInfo(**merged).model_dump()
|
||||
cli["info"] = merged_valid
|
||||
|
||||
_flush_store()
|
||||
write_client_xml(client_id, keyrec.key_name)
|
||||
write_client_xml(client_id, keyrec)
|
||||
|
||||
return {"client_id": client_id, **cli}
|
||||
|
||||
# GET CLIENT (header-based)
|
||||
|
||||
# GET CLIENT (with header X-Client-Id, auto-regens XML if missing)
|
||||
@app.get("/clients")
|
||||
def get_client(
|
||||
x_client_id: int = Header(..., alias="X-Client-Id"),
|
||||
|
|
@ -475,72 +578,86 @@ def get_client(
|
|||
client_id = x_client_id
|
||||
logging.info("AUDIT get_client key=%s client_id=%s", keyrec.key_name, client_id)
|
||||
|
||||
cli = _require_client_for(keyrec.key_name, client_id)
|
||||
cli = _require_client(client_id, keyrec)
|
||||
|
||||
xml_path = _xml_path_for(client_id, keyrec.key_name)
|
||||
xml_path = _xml_path_for(client_id, keyrec)
|
||||
if not os.path.exists(xml_path):
|
||||
logging.info("XML missing for client_id=%s (key=%s). Regenerating...", client_id, keyrec.key_name)
|
||||
write_client_xml(client_id, keyrec.key_name)
|
||||
logging.info(
|
||||
"XML missing for client_id=%s (key=%s). Regenerating...",
|
||||
client_id,
|
||||
keyrec.key_name,
|
||||
)
|
||||
write_client_xml(client_id, keyrec)
|
||||
|
||||
return {"client_id": client_id, **cli}
|
||||
|
||||
|
||||
@app.post("/clients/get", status_code=200)
|
||||
def get_client_post(payload: ClientID, keyrec: KeyRecord = Depends(require_api_key)):
|
||||
def get_client_body(payload: ClientID, keyrec: KeyRecord = Depends(require_api_key)):
|
||||
client_id = payload.client_id
|
||||
logging.info("AUDIT get_client (POST) key=%s client_id=%s", keyrec.key_name, client_id)
|
||||
logging.info("AUDIT get_client key=%s client_id=%s", keyrec.key_name, client_id)
|
||||
cli = _require_client(client_id, keyrec)
|
||||
|
||||
cli = _require_client_for(keyrec.key_name, client_id)
|
||||
|
||||
xml_path = _xml_path_for(client_id, keyrec.key_name)
|
||||
xml_path = _xml_path_for(client_id, keyrec)
|
||||
if not os.path.exists(xml_path):
|
||||
logging.info("XML missing for client_id=%s (key=%s). Regenerating...", client_id, keyrec.key_name)
|
||||
write_client_xml(client_id, keyrec.key_name)
|
||||
logging.info(
|
||||
"XML missing for client_id=%s (key=%s). Regenerating...",
|
||||
client_id,
|
||||
keyrec.key_name,
|
||||
)
|
||||
write_client_xml(client_id, keyrec)
|
||||
|
||||
return {"client_id": client_id, **cli}
|
||||
|
||||
# REGEN XML explicitly (for this key only)
|
||||
|
||||
# REGEN XML explicitly
|
||||
@app.post("/clients/regen-xml", status_code=200)
|
||||
def regen_xml(payload: ClientID, keyrec: KeyRecord = Depends(require_api_key)):
|
||||
client_id = payload.client_id
|
||||
logging.info("AUDIT regen_xml key=%s client_id=%s", keyrec.key_name, client_id)
|
||||
_require_client_for(keyrec.key_name, client_id)
|
||||
write_client_xml(client_id, keyrec.key_name)
|
||||
_require_client(client_id, keyrec)
|
||||
write_client_xml(client_id, keyrec)
|
||||
return {"client_id": client_id, "xml": "regenerated"}
|
||||
|
||||
|
||||
# DELETE ENTIRE CLIENT (for this key only)
|
||||
@app.delete("/clients", status_code=204)
|
||||
def delete_client(payload: ClientID, keyrec: KeyRecord = Depends(require_api_key)):
|
||||
client_id = payload.client_id
|
||||
logging.info("AUDIT delete_client key=%s client_id=%s", keyrec.key_name, client_id)
|
||||
key = _client_key(client_id)
|
||||
skey = _store_key(client_id, keyrec)
|
||||
|
||||
with _store_lock:
|
||||
clients = _clients_for(keyrec.key_name)
|
||||
if key not in clients:
|
||||
if skey not in _store["clients"]:
|
||||
raise HTTPException(status_code=404, detail="Client not found")
|
||||
del clients[key]
|
||||
del _store["clients"][skey]
|
||||
_flush_store()
|
||||
|
||||
# Remove XML only for *this* key
|
||||
try:
|
||||
xml_path = _xml_path_for(client_id, keyrec.key_name)
|
||||
xml_path = _xml_path_for(client_id, keyrec)
|
||||
if os.path.exists(xml_path):
|
||||
os.remove(xml_path)
|
||||
except Exception as e:
|
||||
logging.warning("Failed to remove XML for client %s key %s: %s", client_id, keyrec.key_name, e)
|
||||
logging.warning(
|
||||
"Failed to remove XML file for client %s (key=%s): %s",
|
||||
client_id,
|
||||
keyrec.key_name,
|
||||
e,
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
|
||||
# ---- Zones ----
|
||||
@app.get("/clients/zones")
|
||||
def list_zones(
|
||||
x_client_id: int = Header(..., alias="X-Client-Id"),
|
||||
keyrec: KeyRecord = Depends(require_api_key)
|
||||
keyrec: KeyRecord = Depends(require_api_key),
|
||||
):
|
||||
client_id = x_client_id
|
||||
logging.info("AUDIT list_zones key=%s client_id=%s", keyrec.key_name, client_id)
|
||||
|
||||
cli = _require_client_for(keyrec.key_name, client_id)
|
||||
cli = _require_client(client_id, keyrec)
|
||||
zones_obj = cli.get("zones", {})
|
||||
|
||||
zones = [
|
||||
|
|
@ -550,14 +667,14 @@ def list_zones(
|
|||
zones.sort(key=lambda z: z["Zone_No"])
|
||||
return {"client_id": client_id, "zones": zones}
|
||||
|
||||
|
||||
@app.post("/clients/zones/list", status_code=200)
|
||||
def list_zones_post(payload: ClientID, keyrec: KeyRecord = Depends(require_api_key)):
|
||||
def list_zones_body(payload: ClientID, keyrec: KeyRecord = Depends(require_api_key)):
|
||||
client_id = payload.client_id
|
||||
logging.info("AUDIT list_zones (POST) key=%s client_id=%s", keyrec.key_name, client_id)
|
||||
logging.info("AUDIT list_zones key=%s client_id=%s", keyrec.key_name, client_id)
|
||||
|
||||
cli = _require_client_for(keyrec.key_name, client_id)
|
||||
cli = _require_client(client_id, keyrec)
|
||||
zones_obj = cli.get("zones", {})
|
||||
|
||||
zones = [
|
||||
{"Zone_No": int(zid), "Zone_area": data.get("Zone_area"), "ModuleNo": data.get("ModuleNo", 0)}
|
||||
for zid, data in zones_obj.items()
|
||||
|
|
@ -565,78 +682,93 @@ def list_zones_post(payload: ClientID, keyrec: KeyRecord = Depends(require_api_k
|
|||
zones.sort(key=lambda z: z["Zone_No"])
|
||||
return {"client_id": client_id, "zones": zones}
|
||||
|
||||
|
||||
@app.put("/clients/zones", status_code=201)
|
||||
def upsert_zone(payload: ZoneUpdate, keyrec: KeyRecord = Depends(require_api_key)):
|
||||
client_id = payload.client_id
|
||||
zone_id = payload.zone_id
|
||||
logging.info(
|
||||
"AUDIT upsert_zone key=%s client_id=%s zone_id=%s body=%s",
|
||||
keyrec.key_name, client_id, zone_id, payload.model_dump()
|
||||
keyrec.key_name,
|
||||
client_id,
|
||||
zone_id,
|
||||
payload.model_dump(),
|
||||
)
|
||||
|
||||
if not (1 <= zone_id <= 999):
|
||||
raise HTTPException(status_code=422, detail="zone_id must be 1..999")
|
||||
|
||||
with _store_lock:
|
||||
cli = _require_client_for(keyrec.key_name, client_id)
|
||||
cli = _require_client(client_id, keyrec)
|
||||
cli.setdefault("zones", {})
|
||||
cli["zones"][str(zone_id)] = {
|
||||
"Zone_area": payload.Zone_area,
|
||||
"ModuleNo": payload.ModuleNo
|
||||
"ModuleNo": payload.ModuleNo,
|
||||
}
|
||||
_flush_store()
|
||||
write_client_xml(client_id, keyrec.key_name)
|
||||
write_client_xml(client_id, keyrec)
|
||||
|
||||
return {
|
||||
"client_id": client_id,
|
||||
"zone": {"Zone_No": zone_id, "Zone_area": payload.Zone_area, "ModuleNo": payload.ModuleNo}
|
||||
"zone": {"Zone_No": zone_id, "Zone_area": payload.Zone_area, "ModuleNo": payload.ModuleNo},
|
||||
}
|
||||
|
||||
|
||||
@app.delete("/clients/zones", status_code=204)
|
||||
def delete_zone(payload: ZoneDelete, keyrec: KeyRecord = Depends(require_api_key)):
|
||||
client_id = payload.client_id
|
||||
zone_id = payload.zone_id
|
||||
logging.info("AUDIT delete_zone key=%s client_id=%s zone_id=%s",
|
||||
keyrec.key_name, client_id, zone_id)
|
||||
logging.info(
|
||||
"AUDIT delete_zone key=%s client_id=%s zone_id=%s",
|
||||
keyrec.key_name,
|
||||
client_id,
|
||||
zone_id,
|
||||
)
|
||||
|
||||
with _store_lock:
|
||||
cli = _require_client_for(keyrec.key_name, client_id)
|
||||
cli = _require_client(client_id, keyrec)
|
||||
zones = cli.get("zones", {})
|
||||
if str(zone_id) not in zones:
|
||||
raise HTTPException(status_code=404, detail="Zone not found")
|
||||
del zones[str(zone_id)]
|
||||
_flush_store()
|
||||
write_client_xml(client_id, keyrec.key_name)
|
||||
write_client_xml(client_id, keyrec)
|
||||
return
|
||||
|
||||
|
||||
# ---- Users ----
|
||||
@app.get("/clients/users")
|
||||
def list_users(
|
||||
x_client_id: int = Header(..., alias="X-Client-Id"),
|
||||
keyrec: KeyRecord = Depends(require_api_key)
|
||||
keyrec: KeyRecord = Depends(require_api_key),
|
||||
):
|
||||
client_id = x_client_id
|
||||
logging.info("AUDIT list_users key=%s client_id=%s", keyrec.key_name, client_id)
|
||||
|
||||
cli = _require_client_for(keyrec.key_name, client_id)
|
||||
cli = _require_client(client_id, keyrec)
|
||||
users = list(cli.get("users", {}).values())
|
||||
users.sort(key=lambda u: (u.get("CallOrder", 0), u.get("MobileNoOrder", 0), u.get("UserNo", 0)))
|
||||
|
||||
return {"client_id": client_id, "users": users}
|
||||
|
||||
|
||||
@app.get("/clients/users/single")
|
||||
def get_user_header(
|
||||
x_client_id: int = Header(..., alias="X-Client-Id"),
|
||||
x_user_no: int = Header(..., alias="X-User-No"),
|
||||
keyrec: KeyRecord = Depends(require_api_key)
|
||||
keyrec: KeyRecord = Depends(require_api_key),
|
||||
):
|
||||
client_id = x_client_id
|
||||
user_no = x_user_no
|
||||
|
||||
logging.info("AUDIT get_user (header) key=%s client_id=%s user_no=%s",
|
||||
keyrec.key_name, client_id, user_no)
|
||||
logging.info(
|
||||
"AUDIT get_user key=%s client_id=%s user_no=%s",
|
||||
keyrec.key_name,
|
||||
client_id,
|
||||
user_no,
|
||||
)
|
||||
|
||||
cli = _require_client_for(keyrec.key_name, client_id)
|
||||
cli = _require_client(client_id, keyrec)
|
||||
u = cli.get("users", {}).get(str(user_no))
|
||||
|
||||
if not u:
|
||||
|
|
@ -644,49 +776,60 @@ def get_user_header(
|
|||
|
||||
return {"client_id": client_id, "user": u}
|
||||
|
||||
@app.post("/clients/users/list", status_code=200)
|
||||
def list_users_post(payload: ClientID, keyrec: KeyRecord = Depends(require_api_key)):
|
||||
client_id = payload.client_id
|
||||
logging.info("AUDIT list_users (POST) key=%s client_id=%s", keyrec.key_name, client_id)
|
||||
|
||||
cli = _require_client_for(keyrec.key_name, client_id)
|
||||
@app.post("/clients/users/list", status_code=200)
|
||||
def list_users_body(payload: ClientID, keyrec: KeyRecord = Depends(require_api_key)):
|
||||
client_id = payload.client_id
|
||||
logging.info("AUDIT list_users key=%s client_id=%s", keyrec.key_name, client_id)
|
||||
|
||||
cli = _require_client(client_id, keyrec)
|
||||
users = list(cli.get("users", {}).values())
|
||||
users.sort(key=lambda u: (u.get("CallOrder", 0), u.get("MobileNoOrder", 0), u.get("UserNo", 0)))
|
||||
return {"client_id": client_id, "users": users}
|
||||
|
||||
|
||||
@app.post("/clients/users", status_code=201)
|
||||
def add_user(payload: UserWithClient, keyrec: KeyRecord = Depends(require_api_key)):
|
||||
client_id = payload.client_id
|
||||
user = payload.user
|
||||
logging.info(
|
||||
"AUDIT add_user key=%s client_id=%s UserNo=%s user=%s",
|
||||
keyrec.key_name, client_id, user.UserNo, user.model_dump()
|
||||
keyrec.key_name,
|
||||
client_id,
|
||||
user.UserNo,
|
||||
user.model_dump(),
|
||||
)
|
||||
|
||||
with _store_lock:
|
||||
cli = _require_client_for(keyrec.key_name, client_id)
|
||||
cli = _require_client(client_id, keyrec)
|
||||
cli.setdefault("users", {})
|
||||
if str(user.UserNo) in cli["users"]:
|
||||
raise HTTPException(status_code=409, detail="UserNo already exists")
|
||||
cli["users"][str(user.UserNo)] = user.model_dump()
|
||||
_flush_store()
|
||||
write_client_xml(client_id, keyrec.key_name)
|
||||
write_client_xml(client_id, keyrec)
|
||||
|
||||
return {"client_id": client_id, "user": user}
|
||||
|
||||
|
||||
@app.post("/clients/users/get", status_code=200)
|
||||
def get_user(payload: UserKey, keyrec: KeyRecord = Depends(require_api_key)):
|
||||
def get_user_body(payload: UserKey, keyrec: KeyRecord = Depends(require_api_key)):
|
||||
client_id = payload.client_id
|
||||
user_no = payload.user_no
|
||||
logging.info("AUDIT get_user (POST) key=%s client_id=%s user_no=%s",
|
||||
keyrec.key_name, client_id, user_no)
|
||||
logging.info(
|
||||
"AUDIT get_user key=%s client_id=%s user_no=%s",
|
||||
keyrec.key_name,
|
||||
client_id,
|
||||
user_no,
|
||||
)
|
||||
|
||||
cli = _require_client_for(keyrec.key_name, client_id)
|
||||
cli = _require_client(client_id, keyrec)
|
||||
u = cli.get("users", {}).get(str(user_no))
|
||||
if not u:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return {"client_id": client_id, "user": u}
|
||||
|
||||
|
||||
@app.put("/clients/users", status_code=200)
|
||||
def replace_user(payload: UserWithClient, keyrec: KeyRecord = Depends(require_api_key)):
|
||||
client_id = payload.client_id
|
||||
|
|
@ -695,31 +838,39 @@ def replace_user(payload: UserWithClient, keyrec: KeyRecord = Depends(require_ap
|
|||
|
||||
logging.info(
|
||||
"AUDIT replace_user key=%s client_id=%s user_no=%s user=%s",
|
||||
keyrec.key_name, client_id, user_no, user.model_dump()
|
||||
keyrec.key_name,
|
||||
client_id,
|
||||
user_no,
|
||||
user.model_dump(),
|
||||
)
|
||||
|
||||
with _store_lock:
|
||||
cli = _require_client_for(keyrec.key_name, client_id)
|
||||
cli = _require_client(client_id, keyrec)
|
||||
cli.setdefault("users", {})
|
||||
cli["users"][str(user_no)] = user.model_dump()
|
||||
_flush_store()
|
||||
write_client_xml(client_id, keyrec.key_name)
|
||||
write_client_xml(client_id, keyrec)
|
||||
|
||||
return {"client_id": client_id, "user": user}
|
||||
|
||||
|
||||
@app.delete("/clients/users", status_code=204)
|
||||
def delete_user(payload: UserKey, keyrec: KeyRecord = Depends(require_api_key)):
|
||||
client_id = payload.client_id
|
||||
user_no = payload.user_no
|
||||
logging.info("AUDIT delete_user key=%s client_id=%s user_no=%s",
|
||||
keyrec.key_name, client_id, user_no)
|
||||
logging.info(
|
||||
"AUDIT delete_user key=%s client_id=%s user_no=%s",
|
||||
keyrec.key_name,
|
||||
client_id,
|
||||
user_no,
|
||||
)
|
||||
|
||||
with _store_lock:
|
||||
cli = _require_client_for(keyrec.key_name, client_id)
|
||||
cli = _require_client(client_id, keyrec)
|
||||
users = cli.get("users", {})
|
||||
if str(user_no) not in users:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
del users[str(user_no)]
|
||||
_flush_store()
|
||||
write_client_xml(client_id, keyrec.key_name)
|
||||
write_client_xml(client_id, keyrec)
|
||||
return
|
||||
|
|
|
|||
Loading…
Reference in New Issue