| @@ -154,6 +154,7 @@ async def local_then_core(request: Request, call_next): | |||||
| internal_core_proxy_paths = { | internal_core_proxy_paths = { | ||||
| "/reslevis/updateAlarm", | "/reslevis/updateAlarm", | ||||
| "/reslevis/updateCoreSettings", | "/reslevis/updateCoreSettings", | ||||
| "/reslevis/updateUserPreferences", | |||||
| } | } | ||||
| # only proxy CRUD for Reslevis (change prefix or methods if needed) | # only proxy CRUD for Reslevis (change prefix or methods if needed) | ||||
| if ( | if ( | ||||
| @@ -27,4 +27,5 @@ TRACK_JSON_PATH = DATA_DIR / "tracks.json" | |||||
| TRACKER_ZONE_JSON_PATH = DATA_DIR / "tracker_zone.json" | TRACKER_ZONE_JSON_PATH = DATA_DIR / "tracker_zone.json" | ||||
| SETTING_JSON_PATH = DATA_DIR / "settings.json" | SETTING_JSON_PATH = DATA_DIR / "settings.json" | ||||
| GUI_CONFIG_JSON_PATH = DATA_DIR / "gui_config.json" | GUI_CONFIG_JSON_PATH = DATA_DIR / "gui_config.json" | ||||
| USER_PREFERENCES_JSON_PATH = DATA_DIR / "user_preferences.json" | |||||
| @@ -0,0 +1,88 @@ | |||||
| import json | |||||
| from datetime import datetime, timezone | |||||
| from typing import Any, Dict, List, Optional | |||||
| from uuid import NAMESPACE_URL, uuid5 | |||||
| from fastapi.encoders import jsonable_encoder | |||||
| from schemas.reslevis import UserPreferencesUpdateItem | |||||
| from .config import USER_PREFERENCES_JSON_PATH | |||||
| from .gateway import _LockedFile, _atomic_write, _norm_str | |||||
| def _model_to_dict(model: Any) -> Dict[str, Any]: | |||||
| if hasattr(model, "model_dump"): | |||||
| return model.model_dump(exclude_unset=True, exclude_none=True) | |||||
| return model.dict(exclude_unset=True, exclude_none=True) | |||||
| class UserPreferencesJsonRepository: | |||||
| def __init__(self, json_path: str = USER_PREFERENCES_JSON_PATH): | |||||
| self.path = json_path | |||||
| def _read_all(self) -> List[Dict[str, Any]]: | |||||
| with _LockedFile(self.path, "r") as fp: | |||||
| try: | |||||
| fp.seek(0) | |||||
| data = fp.read().strip() | |||||
| return json.loads(data) if data else [] | |||||
| except json.JSONDecodeError: | |||||
| return [] | |||||
| def _write_all(self, rows: List[Dict[str, Any]]) -> None: | |||||
| payload = json.dumps(rows, ensure_ascii=False, indent=2) | |||||
| _atomic_write(self.path, payload) | |||||
| def _index_by_uid(self, rows: List[Dict[str, Any]], uid: str) -> Optional[int]: | |||||
| target = _norm_str(uid) | |||||
| for i, row in enumerate(rows): | |||||
| if _norm_str(row.get("uid")) == target: | |||||
| return i | |||||
| return None | |||||
| def _default_for_uid(self, uid: str) -> Dict[str, Any]: | |||||
| return { | |||||
| "id": str(uuid5(NAMESPACE_URL, f"reslevis:user-preferences:{uid}")), | |||||
| "uid": uid, | |||||
| "language": "it", | |||||
| "tables": {}, | |||||
| "updated_at": None, | |||||
| } | |||||
| def get(self, uid: str) -> Dict[str, Any]: | |||||
| rows = self._read_all() | |||||
| idx = self._index_by_uid(rows, uid) | |||||
| if idx is None: | |||||
| return self._default_for_uid(uid) | |||||
| obj = self._default_for_uid(uid) | |||||
| obj.update(rows[idx]) | |||||
| obj["uid"] = uid | |||||
| obj["tables"] = obj.get("tables") or {} | |||||
| obj["language"] = obj.get("language") or "it" | |||||
| return obj | |||||
| def update(self, uid: str, item: UserPreferencesUpdateItem) -> Dict[str, Any]: | |||||
| rows = self._read_all() | |||||
| idx = self._index_by_uid(rows, uid) | |||||
| current = self._default_for_uid(uid) if idx is None else self.get(uid) | |||||
| payload = jsonable_encoder(_model_to_dict(item)) | |||||
| if "language" in payload and payload["language"] is not None: | |||||
| current["language"] = payload["language"] | |||||
| if "tables" in payload and payload["tables"] is not None: | |||||
| tables = current.get("tables") or {} | |||||
| for table_name, table_preferences in payload["tables"].items(): | |||||
| tables[table_name] = table_preferences or {"hiddenColumns": {}} | |||||
| current["tables"] = tables | |||||
| current["uid"] = uid | |||||
| current["updated_at"] = datetime.now(timezone.utc).isoformat() | |||||
| if idx is None: | |||||
| rows.append(current) | |||||
| else: | |||||
| rows[idx] = current | |||||
| self._write_all(rows) | |||||
| return current | |||||
| @@ -24,6 +24,8 @@ from schemas.reslevis import ( | |||||
| TrackerZoneItem, | TrackerZoneItem, | ||||
| SettingItem, | SettingItem, | ||||
| GuiConfigItem, | GuiConfigItem, | ||||
| UserPreferencesItem, | |||||
| UserPreferencesUpdateItem, | |||||
| CoreSettingsItem, | CoreSettingsItem, | ||||
| CoreSettingsUpdateItem, | CoreSettingsUpdateItem, | ||||
| ) | ) | ||||
| @@ -37,6 +39,7 @@ from logica_reslevis.tracker import TrackerJsonRepository | |||||
| from logica_reslevis.operator import OperatorJsonRepository | from logica_reslevis.operator import OperatorJsonRepository | ||||
| from logica_reslevis.setting import SettingJsonRepository | from logica_reslevis.setting import SettingJsonRepository | ||||
| from logica_reslevis.gui_config import GuiConfigJsonRepository | from logica_reslevis.gui_config import GuiConfigJsonRepository | ||||
| from logica_reslevis.user_preferences import UserPreferencesJsonRepository | |||||
| from logica_reslevis.subject import SubjectJsonRepository | from logica_reslevis.subject import SubjectJsonRepository | ||||
| from logica_reslevis.alarm import AlarmJsonRepository | from logica_reslevis.alarm import AlarmJsonRepository | ||||
| from logica_reslevis.track import TrackJsonRepository | from logica_reslevis.track import TrackJsonRepository | ||||
| @@ -94,6 +97,7 @@ track_repo = TrackJsonRepository() | |||||
| tracker_zone_repo = TrackerZoneJsonRepository() | tracker_zone_repo = TrackerZoneJsonRepository() | ||||
| setting_repo = SettingJsonRepository() | setting_repo = SettingJsonRepository() | ||||
| gui_config_repo = GuiConfigJsonRepository() | gui_config_repo = GuiConfigJsonRepository() | ||||
| user_preferences_repo = UserPreferencesJsonRepository() | |||||
| def _none_if_empty(v): | def _none_if_empty(v): | ||||
| return None if v in ("", None, 0, "0") else v | return None if v in ("", None, 0, "0") else v | ||||
| @@ -155,6 +159,18 @@ def _normalize_zone(row: dict) -> dict: | |||||
| row["groups"] = _uuid_list(row.get("groups")) | row["groups"] = _uuid_list(row.get("groups")) | ||||
| return row | return row | ||||
| def _uid_from_claims(claims: dict) -> str: | |||||
| uid = ( | |||||
| claims.get("preferred_username") | |||||
| or claims.get("username") | |||||
| or claims.get("email") | |||||
| or claims.get("sub") | |||||
| ) | |||||
| if not uid: | |||||
| raise HTTPException(status_code=401, detail="User identity not found in token") | |||||
| return str(uid) | |||||
| CORE_GET_SYNC = { | CORE_GET_SYNC = { | ||||
| "/reslevis/getGateways": (gateway_repo, _normalize_gateway), | "/reslevis/getGateways": (gateway_repo, _normalize_gateway), | ||||
| "/reslevis/getZones": (zone_repo, _normalize_zone), | "/reslevis/getZones": (zone_repo, _normalize_zone), | ||||
| @@ -604,6 +620,29 @@ def getGuiConfigs(): | |||||
| return gui_config_repo.list() | return gui_config_repo.list() | ||||
| @router.get( | |||||
| "/getUserPreferences", | |||||
| response_model=UserPreferencesItem, | |||||
| tags=["Reslevis"], | |||||
| ) | |||||
| def getUserPreferences(current_user: dict = Depends(get_current_user)): | |||||
| uid = _uid_from_claims(current_user) | |||||
| return user_preferences_repo.get(uid) | |||||
| @router.put( | |||||
| "/updateUserPreferences", | |||||
| response_model=UserPreferencesItem, | |||||
| tags=["Reslevis"], | |||||
| ) | |||||
| def updateUserPreferences( | |||||
| item: UserPreferencesUpdateItem, | |||||
| current_user: dict = Depends(get_current_user), | |||||
| ): | |||||
| uid = _uid_from_claims(current_user) | |||||
| return user_preferences_repo.update(uid, item) | |||||
| @router.post("/postGuiConfig", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) | @router.post("/postGuiConfig", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) | ||||
| def postGuiConfig(item: GuiConfigItem): | def postGuiConfig(item: GuiConfigItem): | ||||
| gui_config_repo.add(item) | gui_config_repo.add(item) | ||||
| @@ -1,9 +1,10 @@ | |||||
| from pydantic import BaseModel | |||||
| from pydantic import BaseModel, Field | |||||
| from typing import Optional | from typing import Optional | ||||
| from uuid import UUID | from uuid import UUID | ||||
| from typing import Optional, Union, Literal | from typing import Optional, Union, Literal | ||||
| from typing import List | from typing import List | ||||
| from typing import Tuple | from typing import Tuple | ||||
| from typing import Dict | |||||
| class BuildingItem(BaseModel): | class BuildingItem(BaseModel): | ||||
| id: UUID | id: UUID | ||||
| @@ -113,6 +114,24 @@ class GuiConfigItem(BaseModel): | |||||
| role: Optional[Literal["developer", "administrator", "user"]] = None | role: Optional[Literal["developer", "administrator", "user"]] = None | ||||
| debug: Optional[bool] = None | debug: Optional[bool] = None | ||||
| class TablePreferenceItem(BaseModel): | |||||
| hiddenColumns: Dict[str, bool] = Field(default_factory=dict) | |||||
| class UserPreferencesItem(BaseModel): | |||||
| id: Optional[UUID] = None | |||||
| uid: str | |||||
| language: Literal["it", "en"] = "it" | |||||
| tables: Dict[str, TablePreferenceItem] = Field(default_factory=dict) | |||||
| updated_at: Optional[str] = None | |||||
| class UserPreferencesUpdateItem(BaseModel): | |||||
| language: Optional[Literal["it", "en"]] = None | |||||
| tables: Optional[Dict[str, TablePreferenceItem]] = None | |||||
| class OperatorItem(BaseModel): | class OperatorItem(BaseModel): | ||||
| id: UUID | id: UUID | ||||
| name: str | name: str | ||||