1+ use actix_multipart:: form:: { json:: Json as MpJson , tempfile:: TempFile , MultipartForm } ;
12use actix_web:: { post, web, HttpRequest , HttpResponse , Responder } ;
23use askama:: Template ;
3- use futures_util:: StreamExt ;
44use serde:: { Deserialize , Serialize } ;
5+ use std:: io:: Read ;
56use utoipa:: ToSchema ;
67
78use 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 \n Content-Disposition: form-data; name=\" json\" \r \n Content-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 \n Content-Disposition: form-data; name=\" chargelog\" ; filename=\" chargelog.pdf\" \r \n Content-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