Skip to content

Commit 991abd3

Browse files
committed
backend: add endpoint to update groups
1 parent d438986 commit 991abd3

File tree

4 files changed

+295
-0
lines changed

4 files changed

+295
-0
lines changed

backend/src/api_docs.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ async fn main() {
9797
routes::charger::info::charger_info,
9898
routes::grouping::create_grouping::create_grouping,
9999
routes::grouping::delete_grouping::delete_grouping,
100+
routes::grouping::edit_grouping::edit_grouping,
100101
routes::grouping::add_device_to_grouping::add_device_to_grouping,
101102
routes::grouping::remove_device_from_grouping::remove_device_from_grouping,
102103
routes::grouping::get_groupings::get_groupings,
@@ -138,6 +139,8 @@ async fn main() {
138139
routes::grouping::create_grouping::CreateGroupingSchema,
139140
routes::grouping::create_grouping::CreateGroupingResponse,
140141
routes::grouping::delete_grouping::DeleteGroupingSchema,
142+
routes::grouping::edit_grouping::EditGroupingSchema,
143+
routes::grouping::edit_grouping::EditGroupingResponse,
141144
routes::grouping::add_device_to_grouping::AddDeviceToGroupingSchema,
142145
routes::grouping::add_device_to_grouping::AddDeviceToGroupingResponse,
143146
routes::grouping::remove_device_from_grouping::RemoveDeviceFromGroupingSchema,
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/* esp32-remote-access
2+
* Copyright (C) 2025 Frederic Henrichs <[email protected]>
3+
*
4+
* This library is free software; you can redistribute it and/or
5+
* modify it under the terms of the GNU Lesser General Public
6+
* License as published by the Free Software Foundation; either
7+
* version 2 of the License, or (at your option) any later version.
8+
*
9+
* This library is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12+
* Lesser General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public
15+
* License along with this library; if not, write to the
16+
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
17+
* Boston, MA 02111-1307, USA.
18+
*/
19+
20+
use actix_web::{put, web, HttpResponse, Responder};
21+
use db_connector::models::device_groupings::DeviceGrouping;
22+
use diesel::prelude::*;
23+
use diesel::result::Error::NotFound;
24+
use serde::{Deserialize, Serialize};
25+
use utoipa::ToSchema;
26+
27+
use crate::{
28+
error::Error,
29+
utils::{get_connection, parse_uuid, web_block_unpacked},
30+
AppState,
31+
};
32+
33+
#[derive(Serialize, Deserialize, ToSchema)]
34+
pub struct EditGroupingSchema {
35+
pub grouping_id: String,
36+
pub name: String,
37+
}
38+
39+
#[derive(Serialize, Deserialize, ToSchema)]
40+
pub struct EditGroupingResponse {
41+
pub id: String,
42+
pub name: String,
43+
}
44+
45+
/// Edit the name of a device grouping
46+
#[utoipa::path(
47+
context_path = "/grouping",
48+
request_body = EditGroupingSchema,
49+
responses(
50+
(status = 200, description = "Grouping name updated successfully", body = EditGroupingResponse),
51+
(status = 400, description = "Invalid grouping ID or grouping not found"),
52+
(status = 401, description = "Unauthorized - user does not own this grouping"),
53+
(status = 500, description = "Internal server error")
54+
),
55+
security(
56+
("jwt" = [])
57+
)
58+
)]
59+
#[put("/edit")]
60+
pub async fn edit_grouping(
61+
state: web::Data<AppState>,
62+
payload: web::Json<EditGroupingSchema>,
63+
user_id: crate::models::uuid::Uuid,
64+
) -> actix_web::Result<impl Responder> {
65+
use db_connector::schema::device_groupings::dsl as groupings;
66+
67+
let grouping_uuid = parse_uuid(&payload.grouping_id)?;
68+
let new_name = payload.name.clone();
69+
let user_uuid: uuid::Uuid = user_id.into();
70+
let mut conn = get_connection(&state)?;
71+
72+
let updated_grouping = web_block_unpacked(move || {
73+
// First verify the grouping exists and belongs to the user
74+
let grouping: DeviceGrouping = match groupings::device_groupings
75+
.find(grouping_uuid)
76+
.select(DeviceGrouping::as_select())
77+
.get_result(&mut conn)
78+
{
79+
Ok(g) => g,
80+
Err(NotFound) => return Err(Error::ChargerDoesNotExist),
81+
Err(_err) => return Err(Error::InternalError),
82+
};
83+
84+
// Verify ownership
85+
if grouping.user_id != user_uuid {
86+
return Err(Error::Unauthorized);
87+
}
88+
89+
// Update the grouping name
90+
match diesel::update(groupings::device_groupings.find(grouping_uuid))
91+
.set(groupings::name.eq(new_name))
92+
.get_result::<DeviceGrouping>(&mut conn)
93+
{
94+
Ok(g) => Ok(g),
95+
Err(_err) => Err(Error::InternalError),
96+
}
97+
})
98+
.await?;
99+
100+
Ok(HttpResponse::Ok().json(EditGroupingResponse {
101+
id: updated_grouping.id.to_string(),
102+
name: updated_grouping.name,
103+
}))
104+
}
105+
106+
#[cfg(test)]
107+
mod tests {
108+
use super::*;
109+
use actix_web::{cookie::Cookie, test, App};
110+
111+
use crate::{
112+
routes::{
113+
grouping::{configure, test_helpers::*},
114+
user::tests::TestUser,
115+
},
116+
tests::configure as test_configure,
117+
};
118+
119+
#[actix_web::test]
120+
async fn test_edit_grouping() {
121+
let (mut user, _) = TestUser::random().await;
122+
let token = user.login().await;
123+
124+
let grouping = create_test_grouping(token, "Original Name").await;
125+
126+
let app = App::new().configure(test_configure).configure(configure);
127+
let app = test::init_service(app).await;
128+
129+
let new_name = "Updated Name";
130+
let body = EditGroupingSchema {
131+
grouping_id: grouping.id.clone(),
132+
name: new_name.to_string(),
133+
};
134+
135+
let cookie = Cookie::new("access_token", token);
136+
let req = test::TestRequest::put()
137+
.uri("/grouping/edit")
138+
.cookie(cookie)
139+
.set_json(&body)
140+
.to_request();
141+
142+
let resp = test::call_service(&app, req).await;
143+
assert!(resp.status().is_success());
144+
145+
let response: EditGroupingResponse = test::read_body_json(resp).await;
146+
assert_eq!(response.name, new_name);
147+
assert_eq!(response.id, grouping.id);
148+
149+
// Verify the name was updated in the database
150+
let db_grouping = get_grouping_from_db(&grouping.id);
151+
assert!(db_grouping.is_some());
152+
assert_eq!(db_grouping.unwrap().name, new_name);
153+
154+
// Cleanup
155+
delete_test_grouping_from_db(&grouping.id);
156+
}
157+
158+
#[actix_web::test]
159+
async fn test_edit_grouping_unauthorized() {
160+
let (mut user1, _) = TestUser::random().await;
161+
let (mut user2, _) = TestUser::random().await;
162+
let token1 = user1.login().await;
163+
let token2 = user2.login().await;
164+
165+
// User 1 creates a grouping
166+
let grouping = create_test_grouping(token1, "User1 Grouping").await;
167+
let original_name = grouping.name.clone();
168+
169+
let app = App::new().configure(test_configure).configure(configure);
170+
let app = test::init_service(app).await;
171+
172+
// User 2 tries to edit it
173+
let body = EditGroupingSchema {
174+
grouping_id: grouping.id.clone(),
175+
name: "Hacked Name".to_string(),
176+
};
177+
178+
let cookie = Cookie::new("access_token", token2);
179+
let req = test::TestRequest::put()
180+
.uri("/grouping/edit")
181+
.cookie(cookie)
182+
.set_json(&body)
183+
.to_request();
184+
185+
let resp = test::call_service(&app, req).await;
186+
assert_eq!(resp.status().as_u16(), 401); // Unauthorized
187+
188+
// Verify the name was not changed
189+
let db_grouping = get_grouping_from_db(&grouping.id);
190+
assert!(db_grouping.is_some());
191+
assert_eq!(db_grouping.unwrap().name, original_name);
192+
193+
// Cleanup
194+
delete_test_grouping_from_db(&grouping.id);
195+
}
196+
197+
#[actix_web::test]
198+
async fn test_edit_grouping_not_found() {
199+
let (mut user, _) = TestUser::random().await;
200+
let token = user.login().await;
201+
202+
let app = App::new().configure(test_configure).configure(configure);
203+
let app = test::init_service(app).await;
204+
205+
let body = EditGroupingSchema {
206+
grouping_id: uuid::Uuid::new_v4().to_string(),
207+
name: "New Name".to_string(),
208+
};
209+
210+
let cookie = Cookie::new("access_token", token);
211+
let req = test::TestRequest::put()
212+
.uri("/grouping/edit")
213+
.cookie(cookie)
214+
.set_json(&body)
215+
.to_request();
216+
217+
let resp = test::call_service(&app, req).await;
218+
assert_eq!(resp.status().as_u16(), 400); // Bad request (ChargerDoesNotExist returns 400)
219+
}
220+
}

backend/src/routes/grouping/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
pub mod add_device_to_grouping;
2121
pub mod create_grouping;
2222
pub mod delete_grouping;
23+
pub mod edit_grouping;
2324
pub mod get_groupings;
2425
pub mod remove_device_from_grouping;
2526

@@ -34,6 +35,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
3435
.wrap(JwtMiddleware)
3536
.service(create_grouping::create_grouping)
3637
.service(delete_grouping::delete_grouping)
38+
.service(edit_grouping::edit_grouping)
3739
.service(add_device_to_grouping::add_device_to_grouping)
3840
.service(remove_device_from_grouping::remove_device_from_grouping)
3941
.service(get_groupings::get_groupings);

frontend/src/schema.d.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,23 @@ export interface paths {
371371
patch?: never;
372372
trace?: never;
373373
};
374+
"/grouping/edit": {
375+
parameters: {
376+
query?: never;
377+
header?: never;
378+
path?: never;
379+
cookie?: never;
380+
};
381+
get?: never;
382+
/** Edit the name of a device grouping */
383+
put: operations["edit_grouping"];
384+
post?: never;
385+
delete?: never;
386+
options?: never;
387+
head?: never;
388+
patch?: never;
389+
trace?: never;
390+
};
374391
"/grouping/list": {
375392
parameters: {
376393
query?: never;
@@ -697,6 +714,14 @@ export interface components {
697714
DeleteUserSchema: {
698715
login_key: number[];
699716
};
717+
EditGroupingResponse: {
718+
id: string;
719+
name: string;
720+
};
721+
EditGroupingSchema: {
722+
grouping_id: string;
723+
name: string;
724+
};
700725
GetAuthorizationTokensResponseSchema: {
701726
tokens: components["schemas"]["ResponseAuthorizationToken"][];
702727
};
@@ -1587,6 +1612,51 @@ export interface operations {
15871612
};
15881613
};
15891614
};
1615+
edit_grouping: {
1616+
parameters: {
1617+
query?: never;
1618+
header?: never;
1619+
path?: never;
1620+
cookie?: never;
1621+
};
1622+
requestBody: {
1623+
content: {
1624+
"application/json": components["schemas"]["EditGroupingSchema"];
1625+
};
1626+
};
1627+
responses: {
1628+
/** @description Grouping name updated successfully */
1629+
200: {
1630+
headers: {
1631+
[name: string]: unknown;
1632+
};
1633+
content: {
1634+
"application/json": components["schemas"]["EditGroupingResponse"];
1635+
};
1636+
};
1637+
/** @description Invalid grouping ID or grouping not found */
1638+
400: {
1639+
headers: {
1640+
[name: string]: unknown;
1641+
};
1642+
content?: never;
1643+
};
1644+
/** @description Unauthorized - user does not own this grouping */
1645+
401: {
1646+
headers: {
1647+
[name: string]: unknown;
1648+
};
1649+
content?: never;
1650+
};
1651+
/** @description Internal server error */
1652+
500: {
1653+
headers: {
1654+
[name: string]: unknown;
1655+
};
1656+
content?: never;
1657+
};
1658+
};
1659+
};
15901660
get_groupings: {
15911661
parameters: {
15921662
query?: never;

0 commit comments

Comments
 (0)