Skip to content

Commit fab7241

Browse files
committed
backend: add route to send chargelog to a specific user.
1 parent 5524480 commit fab7241

File tree

4 files changed

+173
-0
lines changed

4 files changed

+173
-0
lines changed

backend/src/api_docs.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ async fn main() {
106106
routes::user::delete::delete_user,
107107
routes::check_expiration::check_expiration,
108108
routes::management::management,
109+
routes::send_chargelog_to_user::send_chargelog,
109110
),
110111
components(schemas(
111112
routes::auth::login::LoginSchema,
@@ -143,6 +144,7 @@ async fn main() {
143144
routes::check_expiration::CheckExpirationRequest,
144145
routes::check_expiration::TokenType,
145146
models::response_auth_token::ResponseAuthorizationToken,
147+
routes::send_chargelog_to_user::SendChargelogSchema,
146148
)),
147149
modifiers(&JwtToken, &RefreshToken)
148150
)]

backend/src/routes/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub mod selfdestruct;
2525
pub mod state;
2626
pub mod static_files;
2727
pub mod user;
28+
pub mod send_chargelog_to_user;
2829

2930
use actix_web::web::{self, scope};
3031

@@ -37,6 +38,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
3738
cfg.configure(static_files::configure);
3839

3940
cfg.service(management::management);
41+
cfg.service(send_chargelog_to_user::send_chargelog);
4042
cfg.service(selfdestruct::selfdestruct);
4143
cfg.service(check_expiration::check_expiration);
4244

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
use actix_web::{post, web, HttpRequest, HttpResponse, Responder};
2+
use serde::{Deserialize, Serialize};
3+
use utoipa::ToSchema;
4+
5+
use crate::{
6+
error::Error,
7+
rate_limit::ChargerRateLimiter,
8+
routes::charger::add::{get_charger_from_db, password_matches},
9+
utils::{parse_uuid, send_email_with_attachment},
10+
AppState,
11+
};
12+
13+
#[derive(Serialize, Deserialize, ToSchema)]
14+
pub struct SendChargelogSchema {
15+
pub charger_uuid: String,
16+
pub password: String,
17+
pub user_email: String,
18+
pub chargelog: Vec<u8>, // binary data
19+
}
20+
21+
#[utoipa::path(
22+
request_body = SendChargelogSchema,
23+
responses(
24+
(status = 200, description = "Chargelog sent via email"),
25+
(status = 401, description = "Invalid charger credentials or rate limit exceeded"),
26+
(status = 500, description = "Internal server error"),
27+
)
28+
)]
29+
#[post("/send_chargelog_to_user")]
30+
pub async fn send_chargelog(
31+
req: HttpRequest,
32+
state: web::Data<AppState>,
33+
rate_limiter: web::Data<ChargerRateLimiter>,
34+
payload: web::Json<SendChargelogSchema>,
35+
) -> actix_web::Result<impl Responder> {
36+
rate_limiter.check(payload.charger_uuid.clone(), &req)?;
37+
38+
let charger_id = parse_uuid(&payload.charger_uuid)?;
39+
let charger = get_charger_from_db(charger_id, &state).await?;
40+
if !password_matches(&payload.password, &charger.password)? {
41+
return Err(Error::ChargerCredentialsWrong.into());
42+
}
43+
44+
let subject = "Your Charger Log";
45+
let body = "Attached is your requested chargelog.".to_string();
46+
send_email_with_attachment(
47+
&payload.user_email,
48+
subject,
49+
body,
50+
payload.chargelog.clone(),
51+
"chargelog.bin",
52+
&state,
53+
);
54+
55+
Ok(HttpResponse::Ok())
56+
}
57+
58+
#[cfg(test)]
59+
mod tests {
60+
use super::*;
61+
use actix_web::{test, App};
62+
use crate::{routes::user::tests::TestUser, tests::configure};
63+
64+
#[actix_web::test]
65+
async fn test_send_chargelog_success() {
66+
let (mut user, _mail) = TestUser::random().await;
67+
user.login().await;
68+
let charger = user.add_random_charger().await;
69+
70+
let app = App::new().configure(configure).service(send_chargelog);
71+
let app = test::init_service(app).await;
72+
73+
let payload = SendChargelogSchema {
74+
charger_uuid: charger.uuid.clone(),
75+
password: charger.password.clone(),
76+
user_email: user.mail.clone(),
77+
chargelog: vec![1, 2, 3, 4, 5],
78+
};
79+
80+
let req = test::TestRequest::post()
81+
.uri("/send_chargelog_to_user")
82+
.append_header(("X-Forwarded-For", "123.123.123.3"))
83+
.set_json(payload)
84+
.to_request();
85+
let resp = test::call_service(&app, req).await;
86+
assert_eq!(resp.status(), 200);
87+
}
88+
89+
#[actix_web::test]
90+
async fn test_send_chargelog_invalid_password() {
91+
let (mut user, _mail) = TestUser::random().await;
92+
user.login().await;
93+
let charger = user.add_random_charger().await;
94+
95+
let app = App::new().configure(configure).service(send_chargelog);
96+
let app = test::init_service(app).await;
97+
98+
let payload = SendChargelogSchema {
99+
charger_uuid: charger.uuid.clone(),
100+
password: "wrongpassword".to_string(),
101+
user_email: user.mail.clone(),
102+
chargelog: vec![1, 2, 3, 4, 5],
103+
};
104+
105+
let req = test::TestRequest::post()
106+
.uri("/send_chargelog_to_user")
107+
.append_header(("X-Forwarded-For", "123.123.123.3"))
108+
.set_json(payload)
109+
.to_request();
110+
let resp = test::call_service(&app, req).await;
111+
assert_eq!(resp.status(), 401);
112+
}
113+
}

backend/src/utils.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,62 @@ pub fn send_email(email: &str, subject: &str, body: String, state: &web::Data<Ap
194194
}
195195
}
196196

197+
/// Send an email with a binary attachment (chargelog)
198+
pub fn send_email_with_attachment(
199+
email: &str,
200+
subject: &str,
201+
body: String,
202+
attachment_data: Vec<u8>,
203+
attachment_filename: &str,
204+
state: &web::Data<AppState>,
205+
) {
206+
#[cfg(not(test))]
207+
{
208+
if let Some(ref mailer) = state.mailer {
209+
let multipart = lettre::message::MultiPart::mixed()
210+
.singlepart(
211+
lettre::message::SinglePart::builder()
212+
.header(lettre::message::header::ContentType::TEXT_HTML)
213+
.body(body),
214+
)
215+
.singlepart(
216+
lettre::message::Attachment::new(attachment_filename.to_string())
217+
.body(attachment_data, lettre::message::header::ContentType::parse("application/octet-stream").unwrap()),
218+
);
219+
220+
let email = lettre::Message::builder()
221+
.from(
222+
format!("{} <{}>", state.sender_name, state.sender_email)
223+
.parse()
224+
.unwrap(),
225+
)
226+
.to(email.parse().unwrap())
227+
.subject(subject)
228+
.multipart(multipart)
229+
.unwrap();
230+
231+
match mailer.send(&email) {
232+
Ok(_) => log::info!("Email with attachment sent successfully!"),
233+
Err(e) => log::error!("Could not send email: {e:?}"),
234+
}
235+
} else {
236+
log::error!("No mailer configured, email not sent");
237+
}
238+
}
239+
240+
#[cfg(test)]
241+
{
242+
let _ = body;
243+
let _ = state;
244+
let _ = attachment_data;
245+
let _ = attachment_filename;
246+
println!(
247+
"Test mode: Email would be sent to {} with subject '{}' and attachment {}",
248+
email, subject, attachment_filename
249+
);
250+
}
251+
}
252+
197253
pub async fn update_charger_state_change(charger_id: uuid::Uuid, state: web::Data<AppState>) {
198254
let Ok(mut conn) = get_connection(&state) else {
199255
log::error!("Failed to get database connection for updating charger state change");

0 commit comments

Comments
 (0)