5 Ревизии

Автор SHA1 Съобщение Дата
  root e1bad36709 api bug fix update 3 преди 3 седмици
  Lorenzo Pollutri 2e891538ae API fails to authenticate 2 преди 3 седмици
  Lorenzo Pollutri cf6a1717fa API fails to authenticate 2 преди 3 седмици
  Lorenzo Pollutri 4b94416ef3 API fails to authenticate преди 3 седмици
  Lorenzo Pollutri 3a8249f2e8 Download maps API преди 1 месец
променени са 4 файла, в които са добавени 211 реда и са изтрити 15 реда
  1. +3
    -3
      .env
  2. +53
    -0
      app.py
  3. +147
    -12
      config_env.py
  4. +8
    -0
      security.py

+ 3
- 3
.env Целия файл

@@ -3,7 +3,7 @@ set -a
set +a

#CLINET SETTINGS
KEYCLOAK_AUDIENCE=account,reslevis-frontend,Fastapi
KEYCLOAK_AUDIENCE=account,$client_keycloak,Fastapi
SECRET=$client_secret

#KEYCLOAK URLS
@@ -14,6 +14,6 @@ KEYCLOAK_JWKS_URL=${KEYCLOAK_PROTOCOL_ENDPOINT}/certs
KEYCLOAK_AUTH_URL=${KEYCLOAK_PROTOCOL_ENDPOINT}/auth
KEYCLOAK_TOKEN_URL=${KEYCLOAK_PROTOCOL_ENDPOINT}/token

#BLE AI infer CSV
CORE_API_URL="$core_url"

BLE_AI_INFER_CSV=/data/service/ble-ai-localizer/data/infer/infer.csv
BLE_AI_META_DIR=/data/service/ble-ai-localizer/data/maps/

+ 53
- 0
app.py Целия файл

@@ -297,6 +297,59 @@ async def get_ble_ai_metadata(floor: Optional[int] = Query(default=None)):
return {"items": items, "count": len(items)}


@app.get("/ble-ai/maps", tags=["BLE-AI"], dependencies=[Depends(get_current_user)])
async def get_ble_ai_maps():
maps_dir = config_env.BLE_AI_MAPS_DIR
if not os.path.isdir(maps_dir):
raise HTTPException(status_code=404, detail="Maps directory not found")

items = []
for file_name in sorted(os.listdir(maps_dir), key=str.lower):
if not file_name.lower().endswith(".png"):
continue

file_path = os.path.join(maps_dir, file_name)
if not os.path.isfile(file_path):
continue

with open(file_path, "rb") as f:
encoded = base64.b64encode(f.read()).decode("ascii")

items.append(
{
"name": file_name,
"mime_type": "image/png",
"content_base64": encoded,
}
)

if not items:
raise HTTPException(status_code=404, detail="No PNG maps found")

return {"items": items, "count": len(items)}


@app.get("/ble-ai/maps/{filename}", tags=["BLE-AI"], dependencies=[Depends(get_current_user)])
async def download_ble_ai_map(filename: str):
maps_dir = config_env.BLE_AI_MAPS_DIR
if not os.path.isdir(maps_dir):
raise HTTPException(status_code=404, detail="Maps directory not found")

safe_name = os.path.basename(filename)
if safe_name != filename or not safe_name.lower().endswith(".png"):
raise HTTPException(status_code=400, detail="Invalid map filename")

file_path = os.path.join(maps_dir, safe_name)
if not os.path.isfile(file_path):
raise HTTPException(status_code=404, detail="Map file not found")

return FileResponse(
path=file_path,
filename=safe_name,
media_type="image/png",
)


@app.get("/openapi.json/", tags=["Documentation"])
async def get_open_api_endpoint():
#async def get_open_api_endpoint(current_user: User = Depends(get_current_active_user)):


+ 147
- 12
config_env.py Целия файл

@@ -1,18 +1,148 @@
#This file reads the .env where the variables should be stored
import os
import re
from pathlib import Path
from dotenv import load_dotenv

load_dotenv()

#Keycloak configuration (look in the .env)
SECRET = os.getenv("SECRET")
KEYCLOAK_AUDIENCE = os.getenv("KEYCLOAK_AUDIENCE")
KEYCLOAK_SERVER = os.getenv("KEYCLOAK_SERVER")
KEYCLOAK_ISSUER = os.getenv("KEYCLOAK_ISSUER")
KEYCLOAK_PROTOCOL_ENDPOINT = os.getenv("KEYCLOAK_PROTOCOL_ENDPOINT")
KEYCLOAK_JWKS_URL = os.getenv("KEYCLOAK_JWKS_URL")
KEYCLOAK_AUTH_URL = os.getenv("KEYCLOAK_AUTH_URL")
KEYCLOAK_TOKEN_URL = os.getenv("KEYCLOAK_TOKEN_URL")

def _load_dotenv_chain() -> None:
# Load local .env first
load_dotenv(override=True)

# Support shell-style source directives in .env, e.g. ". /data/conf/presence/core.conf"
local_env = Path(".env")
if not local_env.exists():
return

for raw_line in local_env.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line.startswith(". "):
target = line[2:].strip().strip('"').strip("'")
elif line.startswith("source "):
target = line[7:].strip().strip('"').strip("'")
else:
continue

if not target:
continue
source_path = Path(target)
if not source_path.is_absolute():
source_path = (local_env.parent / source_path).resolve()
if source_path.exists():
load_dotenv(dotenv_path=source_path, override=True)

# Safety fallback for deployments that keep Keycloak vars in core.conf.
default_core_conf = Path("/data/conf/presence/core.conf")
if default_core_conf.exists():
load_dotenv(dotenv_path=default_core_conf, override=True)


_load_dotenv_chain()

_SHELL_VAR_PATTERN = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)")


def _clean(value: str) -> str:
if value is None:
return ""
text = str(value).strip().strip('"').strip("'")
if not text:
return ""

# Expand ${VAR} and $VAR references using already loaded environment variables.
def _replace(match: re.Match) -> str:
var_name = match.group(1) or match.group(2)
return os.getenv(var_name, "")

return _SHELL_VAR_PATTERN.sub(_replace, text)


def _has_http_scheme(value: str) -> bool:
return value.startswith("http://") or value.startswith("https://")


def _ensure_http_scheme(value: str, default_scheme: str = "https") -> str:
value = _clean(value)
if not value:
return ""
if _has_http_scheme(value):
return value
if value.startswith("/"):
return value
return f"{default_scheme}://{value}"


def _absolute_or_join(value: str, base_url: str) -> str:
value = _clean(value)
if not value:
return ""
if _has_http_scheme(value):
return value
if value.startswith("/"):
if base_url:
return f"{base_url.rstrip('/')}{value}"
return ""
return _ensure_http_scheme(value)


def _env_first(*names: str) -> str:
for name in names:
value = _clean(os.getenv(name))
if value:
return value
return ""


# Keycloak configuration (look in the .env)
SECRET = _env_first("SECRET", "client_secret")
KEYCLOAK_AUDIENCE = _env_first("KEYCLOAK_AUDIENCE", "keycloak_audience")

_raw_keycloak_server = _env_first("KEYCLOAK_SERVER", "keycloak_server")
KEYCLOAK_SERVER = _ensure_http_scheme(_raw_keycloak_server)

_default_realm = _env_first("KEYCLOAK_REALM", "keycloak_realm") or "API.Server.local"

_raw_keycloak_issuer = _env_first("KEYCLOAK_ISSUER", "keycloak_issuer")
if _raw_keycloak_issuer and "${" not in _raw_keycloak_issuer:
KEYCLOAK_ISSUER = _absolute_or_join(_raw_keycloak_issuer, KEYCLOAK_SERVER)
elif KEYCLOAK_SERVER:
KEYCLOAK_ISSUER = f"{KEYCLOAK_SERVER.rstrip('/')}/realms/{_default_realm}"
else:
KEYCLOAK_ISSUER = ""

_raw_keycloak_protocol = _env_first("KEYCLOAK_PROTOCOL_ENDPOINT", "keycloak_protocol_endpoint")
if _raw_keycloak_protocol and "${" not in _raw_keycloak_protocol:
KEYCLOAK_PROTOCOL_ENDPOINT = _absolute_or_join(_raw_keycloak_protocol, KEYCLOAK_SERVER)
elif KEYCLOAK_ISSUER:
KEYCLOAK_PROTOCOL_ENDPOINT = f"{KEYCLOAK_ISSUER.rstrip('/')}/protocol/openid-connect"
else:
KEYCLOAK_PROTOCOL_ENDPOINT = ""

_raw_jwks = _env_first("KEYCLOAK_JWKS_URL", "keycloak_jwks_url")
if _raw_jwks and "${" not in _raw_jwks:
KEYCLOAK_JWKS_URL = _absolute_or_join(_raw_jwks, KEYCLOAK_SERVER)
elif KEYCLOAK_PROTOCOL_ENDPOINT:
KEYCLOAK_JWKS_URL = f"{KEYCLOAK_PROTOCOL_ENDPOINT.rstrip('/')}/certs"
else:
KEYCLOAK_JWKS_URL = ""

_raw_auth = _env_first("KEYCLOAK_AUTH_URL", "keycloak_auth_url")
if _raw_auth and "${" not in _raw_auth:
KEYCLOAK_AUTH_URL = _absolute_or_join(_raw_auth, KEYCLOAK_SERVER)
elif KEYCLOAK_PROTOCOL_ENDPOINT:
KEYCLOAK_AUTH_URL = f"{KEYCLOAK_PROTOCOL_ENDPOINT.rstrip('/')}/auth"
else:
KEYCLOAK_AUTH_URL = ""

_raw_token = _env_first("KEYCLOAK_TOKEN_URL", "keycloak_token_url")
if _raw_token and "${" not in _raw_token:
KEYCLOAK_TOKEN_URL = _absolute_or_join(_raw_token, KEYCLOAK_SERVER)
elif KEYCLOAK_PROTOCOL_ENDPOINT:
KEYCLOAK_TOKEN_URL = f"{KEYCLOAK_PROTOCOL_ENDPOINT.rstrip('/')}/token"
else:
KEYCLOAK_TOKEN_URL = ""

CORE_API_URL = os.getenv("CORE_API_URL", "http://localhost:1902")

MQTT_HOST = os.getenv("MQTT_HOST", "192.168.1.101")
@@ -32,3 +162,8 @@ BLE_AI_META_DIR = os.getenv(
"/data/service/ble-ai-localizer/data/maps/",
)

BLE_AI_MAPS_DIR = os.getenv(
"BLE_AI_MAPS_DIR",
"/data/service/ble-ai-localizer/data/maps",
)


+ 8
- 0
security.py Целия файл

@@ -39,6 +39,14 @@ http_bearer = HTTPBearer(auto_error=True)

async def _get_jwks() -> Dict[str, Any]:
global _cached_jwks
if not KEYCLOAK_JWKS_URL or not (
KEYCLOAK_JWKS_URL.startswith("http://") or KEYCLOAK_JWKS_URL.startswith("https://")
):
logger.error("_get_jwks: invalid KEYCLOAK_JWKS_URL=%r", KEYCLOAK_JWKS_URL)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Invalid Keycloak JWKS URL configuration",
)
if _cached_jwks is None:
logger.info(f"_get_jwks: cache miss, fetching from {KEYCLOAK_JWKS_URL}")
resp = await _http.get(KEYCLOAK_JWKS_URL)


Зареждане…
Отказ
Запис