koprogo_api/infrastructure/web/handlers/
iot_grid_handlers.rs1use crate::application::use_cases::boinc_use_cases::SubmitOptimisationTaskDto;
2use crate::infrastructure::web::classification_erreurs;
3use crate::infrastructure::web::middleware::scope_guard::verify_owner_org_access;
4use crate::infrastructure::web::middleware::AuthenticatedUser;
5use crate::infrastructure::web::AppState;
6use actix_web::{delete, get, post, web, HttpRequest, HttpResponse, ResponseError, Result};
7use serde::Deserialize;
8use uuid::Uuid;
9
10fn user_ne_peut_pas(auth: &AuthenticatedUser, organisation: uuid::Uuid) -> bool {
16 auth.verify_org_access(organisation).is_err()
17}
18
19#[post("/iot/mqtt/start")]
28pub async fn start_mqtt_listener(
29 state: web::Data<AppState>,
30 auth: AuthenticatedUser,
31) -> Result<HttpResponse> {
32 if !auth.is_superadmin() && auth.role != "syndic" {
40 return Ok(HttpResponse::Forbidden().json(serde_json::json!({
41 "error": "Réservé au syndic et à l'administration de la plateforme"
42 })));
43 }
44
45 match state.mqtt_energy_adapter.start_listening().await {
46 Ok(()) => Ok(HttpResponse::Ok().json(serde_json::json!({
47 "status": "started",
48 "message": "MQTT listener started successfully"
49 }))),
50 Err(e) => Ok(HttpResponse::BadRequest().json(serde_json::json!({
51 "error": e.to_string()
52 }))),
53 }
54}
55
56#[post("/iot/mqtt/stop")]
60pub async fn stop_mqtt_listener(
61 state: web::Data<AppState>,
62 auth: AuthenticatedUser,
63) -> Result<HttpResponse> {
64 if !auth.is_superadmin() && auth.role != "syndic" {
72 return Ok(HttpResponse::Forbidden().json(serde_json::json!({
73 "error": "Réservé au syndic et à l'administration de la plateforme"
74 })));
75 }
76
77 match state.mqtt_energy_adapter.stop_listening().await {
78 Ok(()) => Ok(HttpResponse::Ok().json(serde_json::json!({
79 "status": "stopped",
80 "message": "MQTT listener stopped"
81 }))),
82 Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
83 "error": e.to_string()
84 }))),
85 }
86}
87
88#[get("/iot/mqtt/status")]
92pub async fn mqtt_status(
93 state: web::Data<AppState>,
94 auth: AuthenticatedUser,
95) -> Result<HttpResponse> {
96 if !auth.is_superadmin() {
101 return Ok(HttpResponse::Forbidden().json(serde_json::json!({
102 "error": "Réservé à l'administration de la plateforme"
103 })));
104 }
105
106 let running = state.mqtt_energy_adapter.is_running().await;
107 Ok(HttpResponse::Ok().json(serde_json::json!({
108 "running": running,
109 "topic": std::env::var("MQTT_TOPIC").unwrap_or_else(|_| "koprogo/+/energy/#".to_string())
110 })))
111}
112
113#[derive(Deserialize)]
118pub struct ConsentRequest {
119 pub owner_id: Uuid,
120 pub organization_id: Uuid,
121 pub granted: bool,
123}
124
125#[post("/iot/grid/consent")]
131pub async fn update_grid_consent(
132 state: web::Data<AppState>,
133 body: web::Json<ConsentRequest>,
134 req: HttpRequest,
135 auth: AuthenticatedUser,
136) -> Result<HttpResponse> {
137 if user_ne_peut_pas(&auth, body.organization_id) {
142 return Ok(
143 actix_web::HttpResponse::Forbidden().json(serde_json::json!({
144 "error": "Cette organisation n'est pas la vôtre"
145 })),
146 );
147 }
148
149 let ip = req
150 .connection_info()
151 .realip_remote_addr()
152 .map(|s| s.chars().take(45).collect::<String>());
153
154 if body.granted {
155 match state
156 .boinc_use_cases
157 .grant_consent(body.owner_id, body.organization_id, ip.as_deref())
158 .await
159 {
160 Ok(consent) => Ok(HttpResponse::Ok().json(consent)),
161 Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
162 "error": e
163 }))),
164 }
165 } else {
166 match state.boinc_use_cases.revoke_consent(body.owner_id).await {
167 Ok(()) => Ok(HttpResponse::Ok().json(serde_json::json!({
168 "status": "consent_revoked",
169 "owner_id": body.owner_id
170 }))),
171 Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
172 "error": e
173 }))),
174 }
175 }
176}
177
178#[get("/iot/grid/consent/{owner_id}")]
182pub async fn get_grid_consent(
183 state: web::Data<AppState>,
184 path: web::Path<Uuid>,
185 auth: AuthenticatedUser,
186) -> Result<HttpResponse> {
187 if let Err(err) = verify_owner_org_access(&auth, *path, &state.owner_use_cases).await {
193 return Ok(err.error_response());
194 }
195
196 match state.boinc_use_cases.get_consent(*path).await {
197 Ok(Some(consent)) => Ok(HttpResponse::Ok().json(consent)),
198 Ok(None) => Ok(HttpResponse::Ok().json(serde_json::json!({
199 "owner_id": *path,
200 "granted": false,
201 "message": "No consent record found"
202 }))),
203 Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
204 "error": e
205 }))),
206 }
207}
208
209#[post("/iot/grid/tasks")]
220pub async fn submit_grid_task(
221 state: web::Data<AppState>,
222 body: web::Json<SubmitOptimisationTaskDto>,
223 auth: AuthenticatedUser,
224) -> Result<HttpResponse> {
225 if user_ne_peut_pas(&auth, body.organization_id) {
230 return Ok(
231 actix_web::HttpResponse::Forbidden().json(serde_json::json!({
232 "error": "Cette organisation n'est pas la vôtre"
233 })),
234 );
235 }
236
237 match state
238 .boinc_use_cases
239 .submit_optimisation_task(body.into_inner())
240 .await
241 {
242 Ok(resp) => Ok(HttpResponse::Created().json(resp)),
243 Err(e) if e.contains("not consented") => {
244 Ok(HttpResponse::Forbidden().json(serde_json::json!({
245 "error": e,
246 "hint": "Grant BOINC consent first via POST /iot/grid/consent"
247 })))
248 }
249 Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
250 "error": e
251 }))),
252 }
253}
254
255#[get("/iot/grid/tasks/{task_id}")]
259pub async fn get_task_status(
260 state: web::Data<AppState>,
261 path: web::Path<String>,
262 auth: AuthenticatedUser,
263) -> Result<HttpResponse> {
264 if !auth.is_superadmin() && auth.role != "syndic" {
269 return Ok(
270 actix_web::HttpResponse::Forbidden().json(serde_json::json!({
271 "error": "Réservé au syndic et à l'administration de la plateforme"
272 })),
273 );
274 }
275
276 match state.boinc_use_cases.poll_task(&path).await {
277 Ok(status) => Ok(HttpResponse::Ok().json(status)),
278 Err(e) if classification_erreurs::est_introuvable(&e) => {
279 Ok(HttpResponse::NotFound().json(serde_json::json!({
280 "error": e
281 })))
282 }
283 Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
284 "error": e
285 }))),
286 }
287}
288
289#[delete("/iot/grid/tasks/{task_id}")]
293pub async fn cancel_grid_task(
294 state: web::Data<AppState>,
295 path: web::Path<String>,
296 auth: AuthenticatedUser,
297) -> Result<HttpResponse> {
298 if !auth.is_superadmin() && auth.role != "syndic" {
303 return Ok(
304 actix_web::HttpResponse::Forbidden().json(serde_json::json!({
305 "error": "Réservé au syndic et à l'administration de la plateforme"
306 })),
307 );
308 }
309
310 match state.boinc_use_cases.cancel_task(&path).await {
311 Ok(()) => Ok(HttpResponse::Ok().json(serde_json::json!({
312 "status": "cancelled",
313 "task_id": *path
314 }))),
315 Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
316 "error": e
317 }))),
318 }
319}