koprogo_api/infrastructure/web/handlers/
iot_grid_handlers.rs1use 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#[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#[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#[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#[derive(Deserialize)]
71pub struct ConsentRequest {
72 pub owner_id: Uuid,
73 pub organization_id: Uuid,
74 pub granted: bool,
76}
77
78#[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#[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#[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#[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#[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}