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)