v.012
This commit is contained in:
parent
fb82b99e0e
commit
9f67d87e9d
|
|
@ -153,7 +153,7 @@
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"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": {
|
"options": {
|
||||||
"raw": {
|
"raw": {
|
||||||
"language": "json"
|
"language": "json"
|
||||||
|
|
@ -186,7 +186,7 @@
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"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": {
|
"options": {
|
||||||
"raw": {
|
"raw": {
|
||||||
"language": "json"
|
"language": "json"
|
||||||
|
|
@ -202,45 +202,9 @@
|
||||||
"clients"
|
"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": [
|
"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": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Create/Update zone",
|
"name": "Create/Update zone",
|
||||||
|
|
@ -255,7 +219,7 @@
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"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": {
|
"options": {
|
||||||
"raw": {
|
"raw": {
|
||||||
"language": "json"
|
"language": "json"
|
||||||
|
|
@ -289,7 +253,7 @@
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"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": {
|
"options": {
|
||||||
"raw": {
|
"raw": {
|
||||||
"language": "json"
|
"language": "json"
|
||||||
|
|
@ -396,7 +360,7 @@
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"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": {
|
"options": {
|
||||||
"raw": {
|
"raw": {
|
||||||
"language": "json"
|
"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": "anders",
|
||||||
{ "key_name": "novus", "key": "_6uOesirJ6QfdACcm2uQIddsstTvXRIP8YjhcKwMuyp9W7mzV2jI1mDisVip1hay5", "enabled": true, "valid_to": "2099-12-31T23:59:59Z" },
|
"key": "_2sW6roe2ZQ4V6Cldo0v295fakHT8vBHqHScfliX445tZuzxDwMRqjPeCE7FDcVVr",
|
||||||
{ "key_name": "futurehome", "key": "_8942WfJxRPA9SiblpR4pD8slkK1jCWlmEJ7XCDmzpJDiG4KlBIbyl5syXxS3RHya", "enabled": true, "valid_to": "2099-01-01T00:00:00Z" }
|
"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
|
from __future__ import annotations
|
||||||
import json, os, threading, re, logging
|
import json, os, threading, re, logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional, List
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, Depends, Header, Request, Response, status
|
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._-]")
|
SAFE_NAME = re.compile(r"[^A-Za-z0-9._-]")
|
||||||
|
|
||||||
|
|
||||||
def _safe_folder(name: str) -> str:
|
def _safe_folder(name: str) -> str:
|
||||||
cleaned = SAFE_NAME.sub("_", name.strip())
|
cleaned = SAFE_NAME.sub("_", name.strip())
|
||||||
return cleaned or "unknown"
|
return cleaned or "unknown"
|
||||||
|
|
||||||
|
|
||||||
# ------------ Config ------------
|
# ------------ Config ------------
|
||||||
DATA_FILE = os.getenv("DATA_FILE", "data.json")
|
DATA_FILE = os.getenv("DATA_FILE", "data.json")
|
||||||
KEY_FILE = os.getenv("KEY_FILE", "keys.json")
|
KEY_FILE = os.getenv("KEY_FILE", "keys.json")
|
||||||
XML_DIR = os.getenv("XML_DIR", "out/clients")
|
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) ------------
|
# ------------ Auth (hot-reloaded key store) ------------
|
||||||
|
|
||||||
|
|
||||||
class KeyRecord(BaseModel):
|
class KeyRecord(BaseModel):
|
||||||
key_name: str
|
key_name: str
|
||||||
key: str
|
key: str
|
||||||
enabled: bool = True
|
enabled: bool = True
|
||||||
valid_to: str # ISO-8601
|
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:
|
class KeyStore:
|
||||||
def __init__(self, path: str):
|
def __init__(self, path: str):
|
||||||
self.path = path
|
self.path = path
|
||||||
|
|
@ -80,39 +117,55 @@ class KeyStore:
|
||||||
raise HTTPException(status_code=401, detail="Token expired")
|
raise HTTPException(status_code=401, detail="Token expired")
|
||||||
return rec
|
return rec
|
||||||
|
|
||||||
|
|
||||||
_key_store = KeyStore(KEY_FILE)
|
_key_store = KeyStore(KEY_FILE)
|
||||||
|
|
||||||
|
|
||||||
def require_api_key(authorization: Optional[str] = Header(None)) -> KeyRecord:
|
def require_api_key(authorization: Optional[str] = Header(None)) -> KeyRecord:
|
||||||
if not authorization or not authorization.startswith("Bearer "):
|
if not authorization or not authorization.startswith("Bearer "):
|
||||||
raise HTTPException(status_code=401, detail="Missing bearer token")
|
raise HTTPException(status_code=401, detail="Missing bearer token")
|
||||||
token = authorization.split(" ", 1)[1].strip()
|
token = authorization.split(" ", 1)[1].strip()
|
||||||
return _key_store.validate_bearer(token)
|
return _key_store.validate_bearer(token)
|
||||||
|
|
||||||
# ------------ Models ------------
|
|
||||||
|
# ------------ Models (client payloads) ------------
|
||||||
|
|
||||||
|
|
||||||
class ClientInfo(BaseModel):
|
class ClientInfo(BaseModel):
|
||||||
|
# From API / client section
|
||||||
Name: str = Field(..., min_length=1, max_length=200)
|
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)
|
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_code: str = Field(..., min_length=1, max_length=20)
|
||||||
area: str = Field(..., min_length=1, max_length=200)
|
area: str = Field(..., min_length=1, max_length=200)
|
||||||
|
|
||||||
AltLookup: bool = True
|
BusPhone: Optional[str] = Field(None, max_length=50)
|
||||||
AltAlarmNo: str | None = None
|
Email: Optional[EmailStr] = None
|
||||||
ConvertType: str = "None"
|
OKPassword: Optional[str] = Field(None, max_length=100)
|
||||||
NoSigsMon: str = "None"
|
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)
|
SinceHrs: int = Field(0, ge=0)
|
||||||
SinceMins: int = Field(0, ge=0)
|
SinceMins: int = Field(30, ge=0)
|
||||||
|
|
||||||
ResetNosigsIgnored: bool = True
|
ResetNosigsIgnored: bool = True
|
||||||
ResetNosigsDays: int = Field(0, ge=0)
|
ResetNosigsDays: int = Field(7, ge=0)
|
||||||
ResetNosigsHrs: int = Field(0, ge=0)
|
ResetNosigsHrs: int = Field(0, ge=0)
|
||||||
ResetNosigsMins: 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"
|
PanelName: str = "Panel Type"
|
||||||
PanelSite: str = "Panel location"
|
PanelSite: str = "Panel location"
|
||||||
|
KeypadLocation: Optional[str] = Field(None, max_length=200)
|
||||||
|
SPPage: Optional[str] = Field(None, max_length=2000)
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
User_Name: str = Field(..., min_length=1, max_length=120)
|
User_Name: str = Field(..., min_length=1, max_length=120)
|
||||||
|
|
@ -130,22 +183,27 @@ class User(BaseModel):
|
||||||
raise ValueError("MobileNo must contain digits")
|
raise ValueError("MobileNo must contain digits")
|
||||||
return v.strip()
|
return v.strip()
|
||||||
|
|
||||||
|
|
||||||
class ClientCreate(BaseModel):
|
class ClientCreate(BaseModel):
|
||||||
client_id: int = Field(..., ge=1)
|
client_id: int = Field(..., ge=1)
|
||||||
info: ClientInfo
|
info: ClientInfo
|
||||||
|
|
||||||
|
|
||||||
class ClientInfoPatch(BaseModel):
|
class ClientInfoPatch(BaseModel):
|
||||||
|
# all optional, for PATCH
|
||||||
Name: Optional[str] = Field(None, min_length=1, max_length=200)
|
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)
|
Location: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||||
|
|
||||||
area_code: Optional[str] = Field(None, min_length=1, max_length=20)
|
area_code: Optional[str] = Field(None, min_length=1, max_length=20)
|
||||||
area: Optional[str] = Field(None, min_length=1, max_length=200)
|
area: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||||
|
|
||||||
AltLookup: Optional[bool] = None
|
BusPhone: Optional[str] = Field(None, max_length=50)
|
||||||
AltAlarmNo: Optional[str] = None
|
Email: Optional[EmailStr] = None
|
||||||
ConvertType: Optional[str] = None
|
OKPassword: Optional[str] = Field(None, max_length=100)
|
||||||
NoSigsMon: Optional[str] = None
|
SpecRequest: Optional[str] = Field(None, max_length=1000)
|
||||||
|
|
||||||
|
NoSigsMon: Optional[str] = None
|
||||||
SinceDays: Optional[int] = Field(None, ge=0)
|
SinceDays: Optional[int] = Field(None, ge=0)
|
||||||
SinceHrs: Optional[int] = Field(None, ge=0)
|
SinceHrs: Optional[int] = Field(None, ge=0)
|
||||||
SinceMins: 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)
|
ResetNosigsHrs: Optional[int] = Field(None, ge=0)
|
||||||
ResetNosigsMins: 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)
|
PanelName: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||||
PanelSite: 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):
|
class ClientPatch(BaseModel):
|
||||||
client_id: int = Field(..., ge=1)
|
client_id: int = Field(..., ge=1)
|
||||||
info: Optional[ClientInfoPatch] = None
|
info: Optional[ClientInfoPatch] = None
|
||||||
|
|
||||||
|
|
||||||
class ClientID(BaseModel):
|
class ClientID(BaseModel):
|
||||||
client_id: int = Field(..., ge=1)
|
client_id: int = Field(..., ge=1)
|
||||||
|
|
||||||
|
|
||||||
class ZoneUpdate(BaseModel):
|
class ZoneUpdate(BaseModel):
|
||||||
client_id: int = Field(..., ge=1)
|
client_id: int = Field(..., ge=1)
|
||||||
zone_id: int = Field(..., ge=1, le=999)
|
zone_id: int = Field(..., ge=1, le=999)
|
||||||
Zone_area: str = Field(..., min_length=1, max_length=200)
|
Zone_area: str = Field(..., min_length=1, max_length=200)
|
||||||
ModuleNo: int = Field(0, ge=0)
|
ModuleNo: int = Field(0, ge=0)
|
||||||
|
|
||||||
|
|
||||||
class ZoneDelete(BaseModel):
|
class ZoneDelete(BaseModel):
|
||||||
client_id: int = Field(..., ge=1)
|
client_id: int = Field(..., ge=1)
|
||||||
zone_id: int = Field(..., ge=1, le=999)
|
zone_id: int = Field(..., ge=1, le=999)
|
||||||
|
|
||||||
|
|
||||||
class UserWithClient(BaseModel):
|
class UserWithClient(BaseModel):
|
||||||
client_id: int = Field(..., ge=1)
|
client_id: int = Field(..., ge=1)
|
||||||
user: User
|
user: User
|
||||||
|
|
||||||
|
|
||||||
class UserKey(BaseModel):
|
class UserKey(BaseModel):
|
||||||
client_id: int = Field(..., ge=1)
|
client_id: int = Field(..., ge=1)
|
||||||
user_no: int = Field(..., ge=1, le=999)
|
user_no: int = Field(..., ge=1, le=999)
|
||||||
|
|
||||||
|
|
||||||
# ------------ In-memory store + persistence ------------
|
# ------------ In-memory store + persistence ------------
|
||||||
|
# Store is namespaced per API key: "key_name:client_id"
|
||||||
|
|
||||||
|
|
||||||
_store_lock = threading.Lock()
|
_store_lock = threading.Lock()
|
||||||
# Multi-tenant: {"keys": { key_name: { "clients": { client_id_str: {...} } } } }
|
_store: Dict = {"clients": {}}
|
||||||
_store: Dict = {"keys": {}}
|
|
||||||
|
|
||||||
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():
|
def _load_store():
|
||||||
global _store
|
global _store
|
||||||
if os.path.exists(DATA_FILE):
|
if os.path.exists(DATA_FILE):
|
||||||
with open(DATA_FILE, "r", encoding="utf-8") as f:
|
with open(DATA_FILE, "r", encoding="utf-8") as f:
|
||||||
data = json.load(f)
|
_store = 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": {}}
|
|
||||||
else:
|
else:
|
||||||
_store = {"keys": {}}
|
|
||||||
_flush_store()
|
_flush_store()
|
||||||
|
|
||||||
|
|
||||||
def _flush_store():
|
def _flush_store():
|
||||||
tmp = DATA_FILE + ".tmp"
|
tmp = DATA_FILE + ".tmp"
|
||||||
with open(tmp, "w", encoding="utf-8") as f:
|
with open(tmp, "w", encoding="utf-8") as f:
|
||||||
json.dump(_store, f, ensure_ascii=False, indent=2)
|
json.dump(_store, f, ensure_ascii=False, indent=2)
|
||||||
os.replace(tmp, DATA_FILE)
|
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 ------------
|
# ------------ XML writer ------------
|
||||||
|
|
||||||
|
|
||||||
def _ensure_dir(path: str):
|
def _ensure_dir(path: str):
|
||||||
os.makedirs(path, exist_ok=True)
|
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)
|
_ensure_dir(client_base)
|
||||||
return os.path.join(client_base, f"{client_id}.xml")
|
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
|
from xml.etree.ElementTree import Element, SubElement, ElementTree
|
||||||
clients = _clients_for(key_name)
|
|
||||||
key = _client_key(client_id)
|
skey = _store_key(client_id, keyrec)
|
||||||
cli = clients.get(key)
|
cli = _store["clients"].get(skey)
|
||||||
if not cli:
|
if not cli:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -255,28 +317,71 @@ def write_client_xml(client_id: int, key_name: str):
|
||||||
|
|
||||||
root = Element("Clients")
|
root = Element("Clients")
|
||||||
row = SubElement(root, "Row")
|
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, "Name").text = info.Name
|
||||||
|
SubElement(row, "Alias").text = info.Alias or ""
|
||||||
SubElement(row, "Location").text = info.Location
|
SubElement(row, "Location").text = info.Location
|
||||||
SubElement(row, "loc2").text = f"{info.area_code} {info.area}".strip()
|
SubElement(row, "loc2").text = f"{info.area_code} {info.area}".strip()
|
||||||
|
|
||||||
SubElement(row, "AltLookup").text = "True" if info.AltLookup else "False"
|
SubElement(row, "BusPhone").text = info.BusPhone or ""
|
||||||
SubElement(row, "AltAlarmNo").text = info.AltAlarmNo or ""
|
SubElement(row, "Email").text = info.Email or ""
|
||||||
SubElement(row, "ConvertType").text = info.ConvertType
|
SubElement(row, "OKPassword").text = info.OKPassword or ""
|
||||||
SubElement(row, "NoSigsMon").text = info.NoSigsMon
|
SubElement(row, "SpecRequest").text = info.SpecRequest or ""
|
||||||
|
|
||||||
|
SubElement(row, "NoSigsMon").text = info.NoSigsMon
|
||||||
SubElement(row, "SinceDays").text = str(info.SinceDays)
|
SubElement(row, "SinceDays").text = str(info.SinceDays)
|
||||||
SubElement(row, "SinceHrs").text = str(info.SinceHrs)
|
SubElement(row, "SinceHrs").text = str(info.SinceHrs)
|
||||||
SubElement(row, "SinceMins").text = str(info.SinceMins)
|
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, "ResetNosigsDays").text = str(info.ResetNosigsDays)
|
||||||
SubElement(row, "ResetNosigsHrs").text = str(info.ResetNosigsHrs)
|
SubElement(row, "ResetNosigsHrs").text = str(info.ResetNosigsHrs)
|
||||||
SubElement(row, "ResetNosigsMins").text = str(info.ResetNosigsMins)
|
SubElement(row, "ResetNosigsMins").text = str(info.ResetNosigsMins)
|
||||||
|
|
||||||
|
SubElement(row, "InstallDateTime").text = info.InstallDateTime or ""
|
||||||
|
|
||||||
SubElement(row, "PanelName").text = info.PanelName
|
SubElement(row, "PanelName").text = info.PanelName
|
||||||
SubElement(row, "PanelSite").text = info.PanelSite
|
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
|
||||||
zones_el = SubElement(row, "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, "Zone_No").text = zid_str
|
||||||
SubElement(z_el, "ModuleNo").text = str(z.get("ModuleNo", 0))
|
SubElement(z_el, "ModuleNo").text = str(z.get("ModuleNo", 0))
|
||||||
|
|
||||||
# Users
|
# Users (from API)
|
||||||
users_el = SubElement(row, "Users")
|
users_el = SubElement(row, "Users")
|
||||||
users = cli.get("users") or {}
|
users = cli.get("users") or {}
|
||||||
|
|
||||||
def _sort_user(u: dict):
|
def _sort_user(u: dict):
|
||||||
return (u.get("CallOrder", 0), u.get("MobileNoOrder", 0), u.get("UserNo", 0))
|
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])):
|
for _, u in sorted(users.items(), key=lambda kv: _sort_user(kv[1])):
|
||||||
u_el = SubElement(users_el, "User")
|
u_el = SubElement(users_el, "User")
|
||||||
SubElement(u_el, "User_Name").text = u.get("User_Name", "")
|
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, "Instructions").text = u.get("Instructions", "") or ""
|
||||||
SubElement(u_el, "CallOrder").text = str(u.get("CallOrder", 0))
|
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:
|
try:
|
||||||
import xml.dom.minidom as minidom
|
import xml.dom.minidom as minidom
|
||||||
|
|
||||||
tmp_path = path + ".tmp"
|
tmp_path = path + ".tmp"
|
||||||
ElementTree(root).write(tmp_path, encoding="utf-8", xml_declaration=True)
|
ElementTree(root).write(tmp_path, encoding="utf-8", xml_declaration=True)
|
||||||
with open(tmp_path, "rb") as rf:
|
with open(tmp_path, "rb") as rf:
|
||||||
|
|
@ -318,9 +434,10 @@ def write_client_xml(client_id: int, key_name: str):
|
||||||
except Exception:
|
except Exception:
|
||||||
ElementTree(root).write(path, encoding="utf-8", xml_declaration=True)
|
ElementTree(root).write(path, encoding="utf-8", xml_declaration=True)
|
||||||
|
|
||||||
|
|
||||||
# ------------ App & logging middleware ------------
|
# ------------ App & logging middleware ------------
|
||||||
_load_store()
|
_load_store()
|
||||||
app = FastAPI(title="Client Registry API", version="3.1.0")
|
app = FastAPI(title="Client Registry API", version="3.3.0")
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
filename="api_requests.log",
|
filename="api_requests.log",
|
||||||
|
|
@ -331,6 +448,7 @@ logging.basicConfig(
|
||||||
REDACT_KEYS = {"authorization", "password", "secret", "key"}
|
REDACT_KEYS = {"authorization", "password", "secret", "key"}
|
||||||
MAX_BODY_CHARS = 4000
|
MAX_BODY_CHARS = 4000
|
||||||
|
|
||||||
|
|
||||||
def _redact(obj):
|
def _redact(obj):
|
||||||
try:
|
try:
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
|
|
@ -341,6 +459,7 @@ def _redact(obj):
|
||||||
pass
|
pass
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def log_requests(request: Request, call_next):
|
async def log_requests(request: Request, call_next):
|
||||||
start = time()
|
start = time()
|
||||||
|
|
@ -350,8 +469,10 @@ async def log_requests(request: Request, call_next):
|
||||||
if method in {"POST", "PUT", "PATCH", "DELETE"}:
|
if method in {"POST", "PUT", "PATCH", "DELETE"}:
|
||||||
try:
|
try:
|
||||||
raw = await request.body()
|
raw = await request.body()
|
||||||
|
|
||||||
async def receive():
|
async def receive():
|
||||||
return {"type": "http.request", "body": raw, "more_body": False}
|
return {"type": "http.request", "body": raw, "more_body": False}
|
||||||
|
|
||||||
request._receive = receive
|
request._receive = receive
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -367,11 +488,16 @@ async def log_requests(request: Request, call_next):
|
||||||
|
|
||||||
logging.info(
|
logging.info(
|
||||||
"%s %s - %s - %.3fs - body=%s",
|
"%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
|
return response
|
||||||
|
|
||||||
# --------- Routes ---------
|
|
||||||
|
# --------- Routes (unchanged logic, but keyrec now has client_groupings) ---------
|
||||||
|
|
||||||
# UPSERT CLIENT (create if missing, update if exists)
|
# UPSERT CLIENT (create if missing, update if exists)
|
||||||
@app.put("/clients", status_code=200)
|
@app.put("/clients", status_code=200)
|
||||||
|
|
@ -384,45 +510,33 @@ def upsert_client(
|
||||||
|
|
||||||
logging.info(
|
logging.info(
|
||||||
"AUDIT upsert_client key=%s client_id=%s payload=%s",
|
"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()
|
info = payload.info.model_dump()
|
||||||
|
skey = _store_key(client_id, keyrec)
|
||||||
# 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)
|
|
||||||
|
|
||||||
with _store_lock:
|
with _store_lock:
|
||||||
clients = _clients_for(keyrec.key_name)
|
existed = skey in _store["clients"]
|
||||||
existed = key in clients
|
|
||||||
|
|
||||||
if existed:
|
if existed:
|
||||||
cli = clients[key]
|
cli = _store["clients"][skey]
|
||||||
cli["info"] = info
|
cli["info"] = info
|
||||||
else:
|
else:
|
||||||
cli = {
|
cli = {"info": info, "zones": {}, "users": {}}
|
||||||
"info": info,
|
_store["clients"][skey] = cli
|
||||||
"zones": {},
|
|
||||||
"users": {}
|
|
||||||
}
|
|
||||||
clients[key] = cli
|
|
||||||
|
|
||||||
if response is not None:
|
if response is not None:
|
||||||
response.status_code = status.HTTP_200_OK if existed else status.HTTP_201_CREATED
|
response.status_code = status.HTTP_200_OK if existed else status.HTTP_201_CREATED
|
||||||
|
|
||||||
_flush_store()
|
_flush_store()
|
||||||
write_client_xml(client_id, keyrec.key_name)
|
write_client_xml(client_id, keyrec)
|
||||||
|
|
||||||
return {"client_id": client_id}
|
return {"client_id": client_id}
|
||||||
|
|
||||||
|
|
||||||
# PATCH CLIENT INFO (partial replace of info)
|
# PATCH CLIENT INFO (partial replace of info)
|
||||||
@app.patch("/clients", status_code=200)
|
@app.patch("/clients", status_code=200)
|
||||||
def patch_client(
|
def patch_client(
|
||||||
|
|
@ -432,41 +546,30 @@ def patch_client(
|
||||||
client_id = payload.client_id
|
client_id = payload.client_id
|
||||||
logging.info(
|
logging.info(
|
||||||
"AUDIT patch_client key=%s client_id=%s payload=%s",
|
"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:
|
with _store_lock:
|
||||||
cli = _require_client_for(keyrec.key_name, client_id)
|
cli = _require_client(client_id, keyrec)
|
||||||
|
|
||||||
if payload.info is not None:
|
if payload.info is not None:
|
||||||
# Current stored info (must be valid ClientInfo shape)
|
|
||||||
current_info = cli.get("info", {})
|
current_info = cli.get("info", {})
|
||||||
base = ClientInfo(**current_info).model_dump()
|
base = ClientInfo(**current_info).model_dump()
|
||||||
|
|
||||||
# Only the fields actually sent in the request
|
|
||||||
updates = payload.info.model_dump(exclude_unset=True)
|
updates = payload.info.model_dump(exclude_unset=True)
|
||||||
|
|
||||||
# Merge: updates override base
|
|
||||||
merged = {**base, **updates}
|
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()
|
merged_valid = ClientInfo(**merged).model_dump()
|
||||||
cli["info"] = merged_valid
|
cli["info"] = merged_valid
|
||||||
|
|
||||||
_flush_store()
|
_flush_store()
|
||||||
write_client_xml(client_id, keyrec.key_name)
|
write_client_xml(client_id, keyrec)
|
||||||
|
|
||||||
return {"client_id": client_id, **cli}
|
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")
|
@app.get("/clients")
|
||||||
def get_client(
|
def get_client(
|
||||||
x_client_id: int = Header(..., alias="X-Client-Id"),
|
x_client_id: int = Header(..., alias="X-Client-Id"),
|
||||||
|
|
@ -475,72 +578,86 @@ def get_client(
|
||||||
client_id = x_client_id
|
client_id = x_client_id
|
||||||
logging.info("AUDIT get_client 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_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):
|
if not os.path.exists(xml_path):
|
||||||
logging.info("XML missing for client_id=%s (key=%s). Regenerating...", client_id, keyrec.key_name)
|
logging.info(
|
||||||
write_client_xml(client_id, keyrec.key_name)
|
"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}
|
return {"client_id": client_id, **cli}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/clients/get", status_code=200)
|
@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
|
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)
|
||||||
|
|
||||||
xml_path = _xml_path_for(client_id, keyrec.key_name)
|
|
||||||
if not os.path.exists(xml_path):
|
if not os.path.exists(xml_path):
|
||||||
logging.info("XML missing for client_id=%s (key=%s). Regenerating...", client_id, keyrec.key_name)
|
logging.info(
|
||||||
write_client_xml(client_id, keyrec.key_name)
|
"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}
|
return {"client_id": client_id, **cli}
|
||||||
|
|
||||||
# REGEN XML explicitly (for this key only)
|
|
||||||
|
# REGEN XML explicitly
|
||||||
@app.post("/clients/regen-xml", status_code=200)
|
@app.post("/clients/regen-xml", status_code=200)
|
||||||
def regen_xml(payload: ClientID, keyrec: KeyRecord = Depends(require_api_key)):
|
def regen_xml(payload: ClientID, keyrec: KeyRecord = Depends(require_api_key)):
|
||||||
client_id = payload.client_id
|
client_id = payload.client_id
|
||||||
logging.info("AUDIT regen_xml key=%s client_id=%s", keyrec.key_name, 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)
|
_require_client(client_id, keyrec)
|
||||||
write_client_xml(client_id, keyrec.key_name)
|
write_client_xml(client_id, keyrec)
|
||||||
return {"client_id": client_id, "xml": "regenerated"}
|
return {"client_id": client_id, "xml": "regenerated"}
|
||||||
|
|
||||||
|
|
||||||
# DELETE ENTIRE CLIENT (for this key only)
|
# DELETE ENTIRE CLIENT (for this key only)
|
||||||
@app.delete("/clients", status_code=204)
|
@app.delete("/clients", status_code=204)
|
||||||
def delete_client(payload: ClientID, keyrec: KeyRecord = Depends(require_api_key)):
|
def delete_client(payload: ClientID, keyrec: KeyRecord = Depends(require_api_key)):
|
||||||
client_id = payload.client_id
|
client_id = payload.client_id
|
||||||
logging.info("AUDIT delete_client key=%s client_id=%s", keyrec.key_name, 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:
|
with _store_lock:
|
||||||
clients = _clients_for(keyrec.key_name)
|
if skey not in _store["clients"]:
|
||||||
if key not in clients:
|
|
||||||
raise HTTPException(status_code=404, detail="Client not found")
|
raise HTTPException(status_code=404, detail="Client not found")
|
||||||
del clients[key]
|
del _store["clients"][skey]
|
||||||
_flush_store()
|
_flush_store()
|
||||||
|
|
||||||
# Remove XML only for *this* key
|
|
||||||
try:
|
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):
|
if os.path.exists(xml_path):
|
||||||
os.remove(xml_path)
|
os.remove(xml_path)
|
||||||
except Exception as e:
|
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
|
return
|
||||||
|
|
||||||
|
|
||||||
# ---- Zones ----
|
# ---- Zones ----
|
||||||
@app.get("/clients/zones")
|
@app.get("/clients/zones")
|
||||||
def list_zones(
|
def list_zones(
|
||||||
x_client_id: int = Header(..., alias="X-Client-Id"),
|
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
|
client_id = x_client_id
|
||||||
logging.info("AUDIT list_zones 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_obj = cli.get("zones", {})
|
||||||
|
|
||||||
zones = [
|
zones = [
|
||||||
|
|
@ -550,14 +667,14 @@ def list_zones(
|
||||||
zones.sort(key=lambda z: z["Zone_No"])
|
zones.sort(key=lambda z: z["Zone_No"])
|
||||||
return {"client_id": client_id, "zones": zones}
|
return {"client_id": client_id, "zones": zones}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/clients/zones/list", status_code=200)
|
@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
|
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_obj = cli.get("zones", {})
|
||||||
|
|
||||||
zones = [
|
zones = [
|
||||||
{"Zone_No": int(zid), "Zone_area": data.get("Zone_area"), "ModuleNo": data.get("ModuleNo", 0)}
|
{"Zone_No": int(zid), "Zone_area": data.get("Zone_area"), "ModuleNo": data.get("ModuleNo", 0)}
|
||||||
for zid, data in zones_obj.items()
|
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"])
|
zones.sort(key=lambda z: z["Zone_No"])
|
||||||
return {"client_id": client_id, "zones": zones}
|
return {"client_id": client_id, "zones": zones}
|
||||||
|
|
||||||
|
|
||||||
@app.put("/clients/zones", status_code=201)
|
@app.put("/clients/zones", status_code=201)
|
||||||
def upsert_zone(payload: ZoneUpdate, keyrec: KeyRecord = Depends(require_api_key)):
|
def upsert_zone(payload: ZoneUpdate, keyrec: KeyRecord = Depends(require_api_key)):
|
||||||
client_id = payload.client_id
|
client_id = payload.client_id
|
||||||
zone_id = payload.zone_id
|
zone_id = payload.zone_id
|
||||||
logging.info(
|
logging.info(
|
||||||
"AUDIT upsert_zone key=%s client_id=%s zone_id=%s body=%s",
|
"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):
|
if not (1 <= zone_id <= 999):
|
||||||
raise HTTPException(status_code=422, detail="zone_id must be 1..999")
|
raise HTTPException(status_code=422, detail="zone_id must be 1..999")
|
||||||
|
|
||||||
with _store_lock:
|
with _store_lock:
|
||||||
cli = _require_client_for(keyrec.key_name, client_id)
|
cli = _require_client(client_id, keyrec)
|
||||||
cli.setdefault("zones", {})
|
cli.setdefault("zones", {})
|
||||||
cli["zones"][str(zone_id)] = {
|
cli["zones"][str(zone_id)] = {
|
||||||
"Zone_area": payload.Zone_area,
|
"Zone_area": payload.Zone_area,
|
||||||
"ModuleNo": payload.ModuleNo
|
"ModuleNo": payload.ModuleNo,
|
||||||
}
|
}
|
||||||
_flush_store()
|
_flush_store()
|
||||||
write_client_xml(client_id, keyrec.key_name)
|
write_client_xml(client_id, keyrec)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"client_id": client_id,
|
"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)
|
@app.delete("/clients/zones", status_code=204)
|
||||||
def delete_zone(payload: ZoneDelete, keyrec: KeyRecord = Depends(require_api_key)):
|
def delete_zone(payload: ZoneDelete, keyrec: KeyRecord = Depends(require_api_key)):
|
||||||
client_id = payload.client_id
|
client_id = payload.client_id
|
||||||
zone_id = payload.zone_id
|
zone_id = payload.zone_id
|
||||||
logging.info("AUDIT delete_zone key=%s client_id=%s zone_id=%s",
|
logging.info(
|
||||||
keyrec.key_name, client_id, zone_id)
|
"AUDIT delete_zone key=%s client_id=%s zone_id=%s",
|
||||||
|
keyrec.key_name,
|
||||||
|
client_id,
|
||||||
|
zone_id,
|
||||||
|
)
|
||||||
|
|
||||||
with _store_lock:
|
with _store_lock:
|
||||||
cli = _require_client_for(keyrec.key_name, client_id)
|
cli = _require_client(client_id, keyrec)
|
||||||
zones = cli.get("zones", {})
|
zones = cli.get("zones", {})
|
||||||
if str(zone_id) not in zones:
|
if str(zone_id) not in zones:
|
||||||
raise HTTPException(status_code=404, detail="Zone not found")
|
raise HTTPException(status_code=404, detail="Zone not found")
|
||||||
del zones[str(zone_id)]
|
del zones[str(zone_id)]
|
||||||
_flush_store()
|
_flush_store()
|
||||||
write_client_xml(client_id, keyrec.key_name)
|
write_client_xml(client_id, keyrec)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
# ---- Users ----
|
# ---- Users ----
|
||||||
@app.get("/clients/users")
|
@app.get("/clients/users")
|
||||||
def list_users(
|
def list_users(
|
||||||
x_client_id: int = Header(..., alias="X-Client-Id"),
|
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
|
client_id = x_client_id
|
||||||
logging.info("AUDIT list_users key=%s client_id=%s", keyrec.key_name, 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 = list(cli.get("users", {}).values())
|
||||||
users.sort(key=lambda u: (u.get("CallOrder", 0), u.get("MobileNoOrder", 0), u.get("UserNo", 0)))
|
users.sort(key=lambda u: (u.get("CallOrder", 0), u.get("MobileNoOrder", 0), u.get("UserNo", 0)))
|
||||||
|
|
||||||
return {"client_id": client_id, "users": users}
|
return {"client_id": client_id, "users": users}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/clients/users/single")
|
@app.get("/clients/users/single")
|
||||||
def get_user_header(
|
def get_user_header(
|
||||||
x_client_id: int = Header(..., alias="X-Client-Id"),
|
x_client_id: int = Header(..., alias="X-Client-Id"),
|
||||||
x_user_no: int = Header(..., alias="X-User-No"),
|
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
|
client_id = x_client_id
|
||||||
user_no = x_user_no
|
user_no = x_user_no
|
||||||
|
|
||||||
logging.info("AUDIT get_user (header) key=%s client_id=%s user_no=%s",
|
logging.info(
|
||||||
keyrec.key_name, client_id, user_no)
|
"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))
|
u = cli.get("users", {}).get(str(user_no))
|
||||||
|
|
||||||
if not u:
|
if not u:
|
||||||
|
|
@ -644,49 +776,60 @@ def get_user_header(
|
||||||
|
|
||||||
return {"client_id": client_id, "user": u}
|
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 = list(cli.get("users", {}).values())
|
||||||
users.sort(key=lambda u: (u.get("CallOrder", 0), u.get("MobileNoOrder", 0), u.get("UserNo", 0)))
|
users.sort(key=lambda u: (u.get("CallOrder", 0), u.get("MobileNoOrder", 0), u.get("UserNo", 0)))
|
||||||
return {"client_id": client_id, "users": users}
|
return {"client_id": client_id, "users": users}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/clients/users", status_code=201)
|
@app.post("/clients/users", status_code=201)
|
||||||
def add_user(payload: UserWithClient, keyrec: KeyRecord = Depends(require_api_key)):
|
def add_user(payload: UserWithClient, keyrec: KeyRecord = Depends(require_api_key)):
|
||||||
client_id = payload.client_id
|
client_id = payload.client_id
|
||||||
user = payload.user
|
user = payload.user
|
||||||
logging.info(
|
logging.info(
|
||||||
"AUDIT add_user key=%s client_id=%s UserNo=%s user=%s",
|
"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:
|
with _store_lock:
|
||||||
cli = _require_client_for(keyrec.key_name, client_id)
|
cli = _require_client(client_id, keyrec)
|
||||||
cli.setdefault("users", {})
|
cli.setdefault("users", {})
|
||||||
if str(user.UserNo) in cli["users"]:
|
if str(user.UserNo) in cli["users"]:
|
||||||
raise HTTPException(status_code=409, detail="UserNo already exists")
|
raise HTTPException(status_code=409, detail="UserNo already exists")
|
||||||
cli["users"][str(user.UserNo)] = user.model_dump()
|
cli["users"][str(user.UserNo)] = user.model_dump()
|
||||||
_flush_store()
|
_flush_store()
|
||||||
write_client_xml(client_id, keyrec.key_name)
|
write_client_xml(client_id, keyrec)
|
||||||
|
|
||||||
return {"client_id": client_id, "user": user}
|
return {"client_id": client_id, "user": user}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/clients/users/get", status_code=200)
|
@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
|
client_id = payload.client_id
|
||||||
user_no = payload.user_no
|
user_no = payload.user_no
|
||||||
logging.info("AUDIT get_user (POST) key=%s client_id=%s user_no=%s",
|
logging.info(
|
||||||
keyrec.key_name, client_id, user_no)
|
"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))
|
u = cli.get("users", {}).get(str(user_no))
|
||||||
if not u:
|
if not u:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
return {"client_id": client_id, "user": u}
|
return {"client_id": client_id, "user": u}
|
||||||
|
|
||||||
|
|
||||||
@app.put("/clients/users", status_code=200)
|
@app.put("/clients/users", status_code=200)
|
||||||
def replace_user(payload: UserWithClient, keyrec: KeyRecord = Depends(require_api_key)):
|
def replace_user(payload: UserWithClient, keyrec: KeyRecord = Depends(require_api_key)):
|
||||||
client_id = payload.client_id
|
client_id = payload.client_id
|
||||||
|
|
@ -695,31 +838,39 @@ def replace_user(payload: UserWithClient, keyrec: KeyRecord = Depends(require_ap
|
||||||
|
|
||||||
logging.info(
|
logging.info(
|
||||||
"AUDIT replace_user key=%s client_id=%s user_no=%s user=%s",
|
"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:
|
with _store_lock:
|
||||||
cli = _require_client_for(keyrec.key_name, client_id)
|
cli = _require_client(client_id, keyrec)
|
||||||
cli.setdefault("users", {})
|
cli.setdefault("users", {})
|
||||||
cli["users"][str(user_no)] = user.model_dump()
|
cli["users"][str(user_no)] = user.model_dump()
|
||||||
_flush_store()
|
_flush_store()
|
||||||
write_client_xml(client_id, keyrec.key_name)
|
write_client_xml(client_id, keyrec)
|
||||||
|
|
||||||
return {"client_id": client_id, "user": user}
|
return {"client_id": client_id, "user": user}
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/clients/users", status_code=204)
|
@app.delete("/clients/users", status_code=204)
|
||||||
def delete_user(payload: UserKey, keyrec: KeyRecord = Depends(require_api_key)):
|
def delete_user(payload: UserKey, keyrec: KeyRecord = Depends(require_api_key)):
|
||||||
client_id = payload.client_id
|
client_id = payload.client_id
|
||||||
user_no = payload.user_no
|
user_no = payload.user_no
|
||||||
logging.info("AUDIT delete_user key=%s client_id=%s user_no=%s",
|
logging.info(
|
||||||
keyrec.key_name, client_id, user_no)
|
"AUDIT delete_user key=%s client_id=%s user_no=%s",
|
||||||
|
keyrec.key_name,
|
||||||
|
client_id,
|
||||||
|
user_no,
|
||||||
|
)
|
||||||
|
|
||||||
with _store_lock:
|
with _store_lock:
|
||||||
cli = _require_client_for(keyrec.key_name, client_id)
|
cli = _require_client(client_id, keyrec)
|
||||||
users = cli.get("users", {})
|
users = cli.get("users", {})
|
||||||
if str(user_no) not in users:
|
if str(user_no) not in users:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
del users[str(user_no)]
|
del users[str(user_no)]
|
||||||
_flush_store()
|
_flush_store()
|
||||||
write_client_xml(client_id, keyrec.key_name)
|
write_client_xml(client_id, keyrec)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue