Skip to content

Commit 1f678da

Browse files
committed
backend/send_chargelog_to_user: use multipat/form-data
1 parent 65f496a commit 1f678da

File tree

2 files changed

+97
-43
lines changed

2 files changed

+97
-43
lines changed

backend/src/api_docs.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ async fn main() {
148148
routes::check_expiration::TokenType,
149149
models::response_auth_token::ResponseAuthorizationToken,
150150
routes::send_chargelog_to_user::SendChargelogSchema,
151+
routes::send_chargelog_to_user::SendChargelogMetadata,
151152
)),
152153
modifiers(&JwtToken, &RefreshToken)
153154
)]

backend/src/routes/send_chargelog_to_user.rs

Lines changed: 96 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm};
12
use actix_web::{post, web, HttpRequest, HttpResponse, Responder};
23
use askama::Template;
3-
use futures_util::StreamExt;
44
use serde::{Deserialize, Serialize};
5+
use std::io::Read;
56
use utoipa::ToSchema;
67

78
use crate::{
@@ -13,13 +14,20 @@ use crate::{
1314
};
1415

1516
#[derive(Serialize, Deserialize, ToSchema)]
16-
pub struct SendChargelogSchema {
17+
pub struct SendChargelogMetadata {
1718
pub charger_uuid: String,
1819
pub password: String,
1920
pub user_uuid: String,
2021
pub filename: String,
2122
pub display_name: String,
22-
pub chargelog: Vec<u8>,
23+
}
24+
25+
#[derive(ToSchema, MultipartForm)]
26+
pub struct SendChargelogSchema {
27+
#[schema(value_type = SendChargelogMetadata)]
28+
pub json: MpJson<SendChargelogMetadata>,
29+
#[schema(value_type = Vec<u8>, format = "binary", content_media_type = "application/octet-stream")]
30+
pub chargelog: TempFile,
2331
}
2432

2533
#[derive(Template)]
@@ -109,32 +117,21 @@ pub async fn send_chargelog(
109117
req: HttpRequest,
110118
state: web::Data<AppState>,
111119
rate_limiter: web::Data<ChargerRateLimiter>,
112-
mut payload: web::Payload,
120+
form: MultipartForm<SendChargelogSchema>,
113121
#[cfg(not(test))] lang: crate::models::lang::Lang,
114122
) -> actix_web::Result<impl Responder> {
115-
let mut bytes = web::BytesMut::new();
116-
while let Some(chunk) = payload.next().await {
117-
let chunk = chunk.map_err(|_| Error::InternalError)?;
118-
let chunk = chunk
119-
.into_iter()
120-
.filter(|b| *b != b'\r' && *b != b'\n')
121-
.collect::<Vec<u8>>();
122-
bytes.extend_from_slice(&chunk);
123-
}
124-
let payload: SendChargelogSchema = serde_json::from_slice(&bytes).map_err(|err| {
125-
log::error!("Failed to parse payload: {err}");
126-
Error::InvalidPayload
127-
})?;
123+
let SendChargelogSchema { json, chargelog } = form.into_inner();
124+
let metadata = json.into_inner();
128125

129-
rate_limiter.check(payload.charger_uuid.clone(), &req)?;
126+
rate_limiter.check(metadata.charger_uuid.clone(), &req)?;
130127

131-
let charger_id = parse_uuid(&payload.charger_uuid)?;
128+
let charger_id = parse_uuid(&metadata.charger_uuid)?;
132129
let charger = get_charger_from_db(charger_id, &state).await?;
133-
if !password_matches(&payload.password, &charger.password)? {
130+
if !password_matches(&metadata.password, &charger.password)? {
134131
return Err(Error::ChargerCredentialsWrong.into());
135132
}
136133

137-
let user = parse_uuid(&payload.user_uuid)?;
134+
let user = parse_uuid(&metadata.user_uuid)?;
138135
let user = get_user(&state, user).await?;
139136

140137
#[cfg(not(test))]
@@ -158,17 +155,39 @@ pub async fn send_chargelog(
158155
let (body, subject) = render_chargelog_email(
159156
&user.name,
160157
&month,
161-
&payload.filename,
162-
&payload.display_name,
158+
&metadata.filename,
159+
&metadata.display_name,
163160
&lang_str,
164161
)?;
165162

163+
let mut chargelog_file = chargelog.file.reopen().map_err(|err| {
164+
log::error!(
165+
"Failed to reopen chargelog temporary file '{}' for user '{}': {}",
166+
metadata.filename,
167+
user.email,
168+
err
169+
);
170+
Error::InternalError
171+
})?;
172+
let mut chargelog_bytes = Vec::with_capacity(chargelog.size);
173+
chargelog_file
174+
.read_to_end(&mut chargelog_bytes)
175+
.map_err(|err| {
176+
log::error!(
177+
"Failed to read chargelog temporary file '{}' for user '{}': {}",
178+
metadata.filename,
179+
user.email,
180+
err
181+
);
182+
Error::InternalError
183+
})?;
184+
166185
send_email_with_attachment(
167186
&user.email,
168187
&subject,
169188
body,
170-
payload.chargelog.clone(),
171-
&payload.filename,
189+
chargelog_bytes,
190+
&metadata.filename,
172191
&state,
173192
);
174193

@@ -180,6 +199,28 @@ mod tests {
180199
use super::*;
181200
use crate::{routes::user::tests::TestUser, tests::configure};
182201
use actix_web::{test, App};
202+
use serde_json::{json, Value};
203+
204+
fn build_multipart_body(boundary: &str, metadata: &Value, file_bytes: &[u8]) -> Vec<u8> {
205+
let metadata_str = metadata.to_string();
206+
let mut body = Vec::new();
207+
body.extend_from_slice(
208+
format!(
209+
"--{boundary}\r\nContent-Disposition: form-data; name=\"json\"\r\nContent-Type: application/json\r\n\r\n{}\r\n",
210+
metadata_str
211+
)
212+
.as_bytes(),
213+
);
214+
body.extend_from_slice(
215+
format!(
216+
"--{boundary}\r\nContent-Disposition: form-data; name=\"chargelog\"; filename=\"chargelog.pdf\"\r\nContent-Type: application/pdf\r\n\r\n"
217+
)
218+
.as_bytes(),
219+
);
220+
body.extend_from_slice(file_bytes);
221+
body.extend_from_slice(format!("\r\n--{boundary}--\r\n").as_bytes());
222+
body
223+
}
183224

184225
#[actix_web::test]
185226
async fn test_send_chargelog_success() {
@@ -190,21 +231,27 @@ mod tests {
190231
let app = App::new().configure(configure).service(send_chargelog);
191232
let app = test::init_service(app).await;
192233

193-
let payload = SendChargelogSchema {
194-
charger_uuid: charger.uuid.clone(),
195-
password: charger.password.clone(),
196-
user_uuid: crate::routes::user::tests::get_test_uuid(&user.mail)
234+
let metadata = json!({
235+
"charger_uuid": charger.uuid,
236+
"password": charger.password,
237+
"user_uuid": crate::routes::user::tests::get_test_uuid(&user.mail)
197238
.unwrap()
198239
.to_string(),
199-
display_name: "Test Device".to_string(),
200-
filename: "chargelog.pdf".to_string(),
201-
chargelog: vec![1, 2, 3, 4, 5],
202-
};
240+
"display_name": "Test Device",
241+
"filename": "chargelog.pdf"
242+
});
243+
244+
let boundary = "----testboundary";
245+
let body = build_multipart_body(boundary, &metadata, &[1, 2, 3, 4, 5]);
203246

204247
let req = test::TestRequest::post()
205248
.uri("/send_chargelog_to_user")
206249
.append_header(("X-Forwarded-For", "123.123.123.3"))
207-
.set_json(payload)
250+
.append_header((
251+
"Content-Type",
252+
format!("multipart/form-data; boundary={boundary}"),
253+
))
254+
.set_payload(body)
208255
.to_request();
209256
let resp = test::call_service(&app, req).await;
210257
assert_eq!(resp.status(), 200);
@@ -219,21 +266,27 @@ mod tests {
219266
let app = App::new().configure(configure).service(send_chargelog);
220267
let app = test::init_service(app).await;
221268

222-
let payload = SendChargelogSchema {
223-
charger_uuid: charger.uuid.clone(),
224-
password: "wrongpassword".to_string(),
225-
user_uuid: crate::routes::user::tests::get_test_uuid(&user.mail)
269+
let metadata = json!({
270+
"charger_uuid": charger.uuid,
271+
"password": "wrongpassword",
272+
"user_uuid": crate::routes::user::tests::get_test_uuid(&user.mail)
226273
.unwrap()
227274
.to_string(),
228-
display_name: "Test Device".to_string(),
229-
filename: "chargelog.pdf".to_string(),
230-
chargelog: vec![1, 2, 3, 4, 5],
231-
};
275+
"display_name": "Test Device",
276+
"filename": "chargelog.pdf"
277+
});
278+
279+
let boundary = "----testboundary2";
280+
let body = build_multipart_body(boundary, &metadata, &[1, 2, 3, 4, 5]);
232281

233282
let req = test::TestRequest::post()
234283
.uri("/send_chargelog_to_user")
235284
.append_header(("X-Forwarded-For", "123.123.123.3"))
236-
.set_json(payload)
285+
.append_header((
286+
"Content-Type",
287+
format!("multipart/form-data; boundary={boundary}"),
288+
))
289+
.set_payload(body)
237290
.to_request();
238291
let resp = test::call_service(&app, req).await;
239292
assert_eq!(resp.status(), 401);

0 commit comments

Comments
 (0)