123 lines
4.0 KiB
Python
123 lines
4.0 KiB
Python
import logging
|
|
from datetime import datetime
|
|
|
|
from flask import Flask, jsonify, request
|
|
|
|
import config
|
|
|
|
# --- Stockage ---------------------------------------------------------------
|
|
if config.STORE_BACKEND == "firestore":
|
|
from store_firestore import FirestoreStore as Store
|
|
store = Store(project_id=config.PROJECT_ID) # noqa: F401 (voir config)
|
|
else:
|
|
from store_file import FileStore as Store
|
|
store = Store(config.DATA_DIR)
|
|
|
|
# --- Logging ----------------------------------------------------------------
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
|
|
log = logging.getLogger("iot-api")
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
|
# --- Aide : réponses génériques ---------------------------------
|
|
def err(status, code):
|
|
return jsonify({"error": code}), status
|
|
|
|
|
|
# --- Authentification dispositif ------------------------------
|
|
def auth_device(headers):
|
|
"""Retourne (device_id, error) — résout l'identité du device ou renvoie l'erreur."""
|
|
device_id = headers.get("X-Device-ID")
|
|
secret = headers.get("X-Device-Secret")
|
|
|
|
if not device_id:
|
|
return None, err(401, "unauthorized")
|
|
entry = config.DEVICE_SECRETS.get(device_id)
|
|
if not entry or entry["secret"] != secret:
|
|
log.warning("ingest refused bad-secret device=%s", device_id)
|
|
return None, err(401, "unauthorized")
|
|
|
|
return device_id, None
|
|
|
|
|
|
# --- Authentification utilisateur (mode démo ; Firebase en extension) --------
|
|
def auth_user(headers):
|
|
"""Retourne (username, error). Mode démo : token = nom d'utilisateur."""
|
|
auth = headers.get("Authorization", "")
|
|
if not auth.startswith("Bearer "):
|
|
return None, err(401, "unauthorized")
|
|
token = auth[len("Bearer "):]
|
|
if token not in config.DEMO_USERS:
|
|
return None, err(401, "unauthorized")
|
|
return token, None
|
|
|
|
|
|
# --- Validation payload ------------------------------------------
|
|
def validate_payload(payload, device_id):
|
|
"""Valide le payload. Retourne (measure, error)."""
|
|
if not isinstance(payload, dict):
|
|
return None, err(400, "invalid-payload")
|
|
|
|
pid = payload.get("device_id")
|
|
if pid != device_id:
|
|
# L'identité déclarée dans le body doit correspondre à l'en-tête.
|
|
return None, err(400, "invalid-payload")
|
|
|
|
ts = payload.get("timestamp")
|
|
try:
|
|
datetime.fromisoformat(ts)
|
|
except (TypeError, ValueError):
|
|
return None, err(400, "invalid-payload")
|
|
|
|
light = payload.get("light_value")
|
|
if not isinstance(light, int):
|
|
return None, err(400, "invalid-payload")
|
|
if not (config.LIGHT_MIN <= light <= config.LIGHT_MAX):
|
|
log.warning(
|
|
"ingest refused invalid-payload device=%s (light out of range)",
|
|
device_id,
|
|
)
|
|
return None, err(400, "invalid-payload")
|
|
|
|
return {"device_id": pid, "timestamp": ts, "light_value": light}, None
|
|
|
|
|
|
# --- Endpoints ----------------------------------------------------------------
|
|
@app.post("/api/ingest")
|
|
def ingest():
|
|
device_id, aerr = auth_device(request.headers)
|
|
if aerr is not None:
|
|
return aerr
|
|
|
|
payload = request.get_json(silent=True)
|
|
measure, verr = validate_payload(payload, device_id)
|
|
if verr is not None:
|
|
return verr
|
|
|
|
store.add_measure(measure)
|
|
log.info("ingest ok device=%s", device_id)
|
|
return jsonify({"ok": True}), 201
|
|
|
|
|
|
@app.get("/api/measures")
|
|
def list_measures():
|
|
username, aerr = auth_user(request.headers)
|
|
if aerr is not None:
|
|
return aerr
|
|
|
|
device_id = request.args.get("device_id", "")
|
|
|
|
# Autorisation SUR LA RESSOURCE: l'utilisateur ne lit
|
|
# que les devices dont il est propriétaire.
|
|
if device_id not in config.DEMO_USERS[username]["devices"]:
|
|
log.warning("measures refused user=%s device=%s", username, device_id)
|
|
return err(403, "forbidden")
|
|
|
|
measures = store.list_measures(device_id)
|
|
log.info("measures ok user=%s device=%s", username, device_id)
|
|
return jsonify(measures)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host=config.HOST, port=config.PORT) |