koprogo_api/infrastructure/web/handlers/
iot_grid_handlers.rs

1use crate::application::use_cases::boinc_use_cases::SubmitOptimisationTaskDto;
2use crate::infrastructure::web::middleware::AuthenticatedUser;
3use crate::infrastructure::web::AppState;
4use actix_web::{delete, get, post, web, HttpRequest, HttpResponse, Result};
5use serde::Deserialize;
6use uuid::Uuid;
7
8// ─────────────────────────────────────────────────────────────────────────────
9// MQTT Control Endpoints
10// ─────────────────────────────────────────────────────────────────────────────
11
12/// Démarre le listener MQTT Home Assistant.
13///
14/// POST /api/v1/iot/mqtt/start
15/// Requiert rôle: syndic ou superadmin
16#[post("/iot/mqtt/start")]
17pub async fn start_mqtt_listener(
18    state: web::Data<AppState>,
19    _auth: AuthenticatedUser,
20) -> Result<HttpResponse> {
21    match state.mqtt_energy_adapter.start_listening().await {
22        Ok(()) => Ok(HttpResponse::Ok().json(serde_json::json!({
23            "status": "started",
24            "message": "MQTT listener started successfully"
25        }))),
26        Err(e) => Ok(HttpResponse::BadRequest().json(serde_json::json!({
27            "error": e.to_string()
28        }))),
29    }
30}
31
32/// Arrête le listener MQTT Home Assistant.
33///
34/// POST /api/v1/iot/mqtt/stop
35#[post("/iot/mqtt/stop")]
36pub async fn stop_mqtt_listener(
37    state: web::Data<AppState>,
38    _auth: AuthenticatedUser,
39) -> Result<HttpResponse> {
40    match state.mqtt_energy_adapter.stop_listening().await {
41        Ok(()) => Ok(HttpResponse::Ok().json(serde_json::json!({
42            "status": "stopped",
43            "message": "MQTT listener stopped"
44        }))),
45        Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
46            "error": e.to_string()
47        }))),
48    }
49}
50
51/// Statut du listener MQTT.
52///
53/// GET /api/v1/iot/mqtt/status
54#[get("/iot/mqtt/status")]
55pub async fn mqtt_status(
56    state: web::Data<AppState>,
57    _auth: AuthenticatedUser,
58) -> Result<HttpResponse> {
59    let running = state.mqtt_energy_adapter.is_running().await;
60    Ok(HttpResponse::Ok().json(serde_json::json!({
61        "running": running,
62        "topic": std::env::var("MQTT_TOPIC").unwrap_or_else(|_| "koprogo/+/energy/#".to_string())
63    })))
64}
65
66// ─────────────────────────────────────────────────────────────────────────────
67// BOINC Grid Consent Endpoints (GDPR)
68// ─────────────────────────────────────────────────────────────────────────────
69
70#[derive(Deserialize)]
71pub struct ConsentRequest {
72    pub owner_id: Uuid,
73    pub organization_id: Uuid,
74    /// true = accorder, false = révoquer
75    pub granted: bool,
76}
77
78/// Accorder ou révoquer le consentement BOINC (GDPR Art. 7).
79///
80/// POST /api/v1/iot/grid/consent
81///
82/// Body: { "owner_id": "uuid", "organization_id": "uuid", "granted": true|false }
83#[post("/iot/grid/consent")]
84pub async fn update_grid_consent(
85    state: web::Data<AppState>,
86    body: web::Json<ConsentRequest>,
87    req: HttpRequest,
88    _auth: AuthenticatedUser,
89) -> Result<HttpResponse> {
90    let ip = req
91        .connection_info()
92        .realip_remote_addr()
93        .map(|s| s.chars().take(45).collect::<String>());
94
95    if body.granted {
96        match state
97            .boinc_use_cases
98            .grant_consent(body.owner_id, body.organization_id, ip.as_deref())
99            .await
100        {
101            Ok(consent) => Ok(HttpResponse::Ok().json(consent)),
102            Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
103                "error": e
104            }))),
105        }
106    } else {
107        match state.boinc_use_cases.revoke_consent(body.owner_id).await {
108            Ok(()) => Ok(HttpResponse::Ok().json(serde_json::json!({
109                "status": "consent_revoked",
110                "owner_id": body.owner_id
111            }))),
112            Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
113                "error": e
114            }))),
115        }
116    }
117}
118
119/// Récupère le consentement BOINC courant d'un propriétaire.
120///
121/// GET /api/v1/iot/grid/consent/{owner_id}
122#[get("/iot/grid/consent/{owner_id}")]
123pub async fn get_grid_consent(
124    state: web::Data<AppState>,
125    path: web::Path<Uuid>,
126    _auth: AuthenticatedUser,
127) -> Result<HttpResponse> {
128    match state.boinc_use_cases.get_consent(*path).await {
129        Ok(Some(consent)) => Ok(HttpResponse::Ok().json(consent)),
130        Ok(None) => Ok(HttpResponse::Ok().json(serde_json::json!({
131            "owner_id": *path,
132            "granted": false,
133            "message": "No consent record found"
134        }))),
135        Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
136            "error": e
137        }))),
138    }
139}
140
141// ─────────────────────────────────────────────────────────────────────────────
142// BOINC Grid Task Endpoints
143// ─────────────────────────────────────────────────────────────────────────────
144
145/// Soumet une tâche d'optimisation énergétique groupée à BOINC.
146///
147/// POST /api/v1/iot/grid/tasks
148///
149/// Body: { "building_id": "uuid", "owner_id": "uuid", "organization_id": "uuid", "simulation_months": 12 }
150/// Requiert consentement BOINC préalable (GDPR).
151#[post("/iot/grid/tasks")]
152pub async fn submit_grid_task(
153    state: web::Data<AppState>,
154    body: web::Json<SubmitOptimisationTaskDto>,
155    _auth: AuthenticatedUser,
156) -> Result<HttpResponse> {
157    match state
158        .boinc_use_cases
159        .submit_optimisation_task(body.into_inner())
160        .await
161    {
162        Ok(resp) => Ok(HttpResponse::Created().json(resp)),
163        Err(e) if e.contains("not consented") => {
164            Ok(HttpResponse::Forbidden().json(serde_json::json!({
165                "error": e,
166                "hint": "Grant BOINC consent first via POST /iot/grid/consent"
167            })))
168        }
169        Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
170            "error": e
171        }))),
172    }
173}
174
175/// Récupère le statut d'une tâche BOINC.
176///
177/// GET /api/v1/iot/grid/tasks/{task_id}
178#[get("/iot/grid/tasks/{task_id}")]
179pub async fn get_task_status(
180    state: web::Data<AppState>,
181    path: web::Path<String>,
182    _auth: AuthenticatedUser,
183) -> Result<HttpResponse> {
184    match state.boinc_use_cases.poll_task(&path).await {
185        Ok(status) => Ok(HttpResponse::Ok().json(status)),
186        Err(e) if e.contains("not found") => Ok(HttpResponse::NotFound().json(serde_json::json!({
187            "error": e
188        }))),
189        Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
190            "error": e
191        }))),
192    }
193}
194
195/// Annule une tâche BOINC en cours.
196///
197/// DELETE /api/v1/iot/grid/tasks/{task_id}
198#[delete("/iot/grid/tasks/{task_id}")]
199pub async fn cancel_grid_task(
200    state: web::Data<AppState>,
201    path: web::Path<String>,
202    _auth: AuthenticatedUser,
203) -> Result<HttpResponse> {
204    match state.boinc_use_cases.cancel_task(&path).await {
205        Ok(()) => Ok(HttpResponse::Ok().json(serde_json::json!({
206            "status": "cancelled",
207            "task_id": *path
208        }))),
209        Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
210            "error": e
211        }))),
212    }
213}