Skip to content

Commit 81259df

Browse files
committed
backend: add route to resend verification token
1 parent 1c47b5f commit 81259df

File tree

3 files changed

+171
-0
lines changed

3 files changed

+171
-0
lines changed

backend/src/api_docs.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ async fn main() {
8686
routes::auth::get_login_salt::get_login_salt,
8787
routes::auth::recovery::recovery,
8888
routes::auth::start_recovery::start_recovery,
89+
routes::auth::resend_verification::resend_verification,
8990
routes::charger::add::add,
9091
routes::charger::allow_user::allow_user,
9192
routes::charger::remove::remove,
@@ -112,6 +113,7 @@ async fn main() {
112113
routes::auth::login::LoginSchema,
113114
routes::auth::register::RegisterSchema,
114115
routes::auth::recovery::RecoverySchema,
116+
routes::auth::resend_verification::ResendSchema,
115117
routes::charger::add::AddChargerSchema,
116118
routes::charger::add::ChargerSchema,
117119
routes::charger::add::Keys,

backend/src/routes/auth/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ pub mod recovery;
2727
pub mod register;
2828
pub mod start_recovery;
2929
pub mod verify;
30+
pub mod resend_verification;
3031

3132
pub const VERIFICATION_EXPIRATION_DAYS: u64 = 1;
3233

3334
pub fn configure(cfg: &mut ServiceConfig) {
3435
let scope = web::scope("/auth")
3536
.service(register::register)
37+
.service(resend_verification::resend_verification)
3638
.service(verify::verify)
3739
.service(get_login_salt::get_login_salt)
3840
.service(generate_salt::generate_salt)
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
use actix_web::{post, web, HttpResponse, Responder};
2+
use askama::Template;
3+
use chrono::Days;
4+
use diesel::prelude::*;
5+
use serde::{Deserialize, Serialize};
6+
use utoipa::ToSchema;
7+
8+
use crate::{error::Error, routes::auth::VERIFICATION_EXPIRATION_DAYS, utils::{get_connection, web_block_unpacked}, AppState};
9+
10+
use db_connector::models::{users::User, verification::Verification};
11+
12+
#[derive(Template)]
13+
#[template(path = "email_verification_en.html")]
14+
pub struct VerifyEmailENTemplate<'a> {
15+
pub name: &'a str,
16+
pub link: &'a str,
17+
}
18+
19+
#[derive(Template)]
20+
#[template(path = "email_verification_de.html")]
21+
pub struct VerifyEmailDETemplate<'a> {
22+
pub name: &'a str,
23+
pub link: &'a str,
24+
}
25+
26+
#[derive(Deserialize, ToSchema, Serialize)]
27+
pub struct ResendSchema {
28+
pub email: String,
29+
}
30+
31+
#[allow(unused)]
32+
fn send_verification_mail(
33+
name: String,
34+
id: Verification,
35+
email: String,
36+
state: web::Data<AppState>,
37+
lang: String,
38+
) -> Result<(), actix_web::Error> {
39+
let link = format!("{}/api/auth/verify?id={}", state.frontend_url, id.id);
40+
41+
let (body, subject) = match lang.as_str() {
42+
"de" | "de-DE" => {
43+
let template = VerifyEmailDETemplate { name: &name, link: &link };
44+
match template.render() { Ok(body) => (body, "Email verifizieren"), Err(e) => { log::error!("Failed to render German verification email template for user '{name}': {e}"); return Err(Error::InternalError.into()); } }
45+
}
46+
_ => {
47+
let template = VerifyEmailENTemplate { name: &name, link: &link };
48+
match template.render() { Ok(body) => (body, "Verify email"), Err(e) => { log::error!("Failed to render English verification email template for user '{name}': {e}"); return Err(Error::InternalError.into()); } }
49+
}
50+
};
51+
52+
crate::utils::send_email(&email, subject, body, &state);
53+
Ok(())
54+
}
55+
56+
/// Resend a verification email if user exists and not verified yet.
57+
#[utoipa::path(
58+
context_path = "/auth",
59+
responses(
60+
(status = 200, description = "Verification email resent (or already verified but hidden)."),
61+
(status = 404, description = "User not found")
62+
)
63+
)]
64+
#[post("/resend_verification")]
65+
pub async fn resend_verification(
66+
state: web::Data<AppState>,
67+
data: web::Json<ResendSchema>,
68+
#[cfg(not(test))] lang: crate::models::lang::Lang,
69+
) -> actix_web::Result<impl Responder> {
70+
use db_connector::schema::users::dsl as u_dsl;
71+
72+
73+
let mut conn = get_connection(&state)?;
74+
let user_email = data.email.to_lowercase();
75+
76+
// Load user
77+
let db_user: User = web_block_unpacked(move || {
78+
match u_dsl::users
79+
.filter(u_dsl::email.eq(&user_email))
80+
.select(User::as_select())
81+
.get_result(&mut conn) {
82+
Ok(u) => Ok(u),
83+
Err(diesel::result::Error::NotFound) => Err(Error::UserDoesNotExist),
84+
Err(_) => Err(Error::InternalError)
85+
}
86+
}).await?;
87+
88+
if db_user.email_verified { // silently return success
89+
return Ok(HttpResponse::Ok());
90+
}
91+
92+
let mut conn = get_connection(&state)?;
93+
94+
// (Re)create verification token (delete old first if present)
95+
let user_id = db_user.id;
96+
web_block_unpacked(move || {
97+
use db_connector::schema::verification::dsl::*;
98+
// remove old tokens
99+
let _ = diesel::delete(verification.filter(user.eq(user_id))).execute(&mut conn);
100+
101+
let exp = chrono::Utc::now().checked_add_days(Days::new(VERIFICATION_EXPIRATION_DAYS)).ok_or(Error::InternalError)?;
102+
103+
let verify = Verification { id: uuid::Uuid::new_v4(), user: user_id, expiration: exp.naive_utc() };
104+
diesel::insert_into(verification).values(&verify).execute(&mut conn).map_err(|_| Error::InternalError)?;
105+
Ok(verify)
106+
}).await.and_then(|_verify| {
107+
#[cfg(not(test))]
108+
{
109+
let user_name = db_user.name.clone();
110+
let lang: String = lang.into();
111+
let state_cpy = state.clone();
112+
let email_cpy = data.email.clone();
113+
std::thread::spawn(move || {
114+
if let Err(e) = send_verification_mail(user_name, _verify, email_cpy, state_cpy, lang) {
115+
log::error!("Failed to resend verification mail: {e:?}");
116+
}
117+
});
118+
}
119+
Ok(())
120+
})?;
121+
122+
Ok(HttpResponse::Ok())
123+
}
124+
125+
#[cfg(test)]
126+
mod tests {
127+
use super::*;
128+
use actix_web::{test, App};
129+
use crate::tests::configure;
130+
use crate::routes::auth::register::tests::{create_user, delete_user};
131+
use crate::routes::auth::verify::tests::fast_verify;
132+
133+
#[actix_web::test]
134+
async fn test_resend_unverified() {
135+
let mail = "[email protected]";
136+
create_user(mail).await;
137+
let app = App::new().configure(configure).service(resend_verification);
138+
let app = test::init_service(app).await;
139+
let req = test::TestRequest::post().uri("/resend_verification").set_json(&ResendSchema{ email: mail.to_string() }).to_request();
140+
let resp = test::call_service(&app, req).await;
141+
assert!(resp.status().is_success());
142+
delete_user(mail);
143+
}
144+
145+
#[actix_web::test]
146+
async fn test_resend_verified() {
147+
let mail = "[email protected]";
148+
create_user(mail).await;
149+
fast_verify(mail);
150+
let app = App::new().configure(configure).service(resend_verification);
151+
let app = test::init_service(app).await;
152+
let req = test::TestRequest::post().uri("/resend_verification").set_json(&ResendSchema{ email: mail.to_string() }).to_request();
153+
let resp = test::call_service(&app, req).await;
154+
assert!(resp.status().is_success());
155+
delete_user(mail);
156+
}
157+
158+
#[actix_web::test]
159+
async fn test_resend_missing() {
160+
let mail = "[email protected]";
161+
let app = App::new().configure(configure).service(resend_verification);
162+
let app = test::init_service(app).await;
163+
let req = test::TestRequest::post().uri("/resend_verification").set_json(&ResendSchema{ email: mail.to_string() }).to_request();
164+
let resp = test::call_service(&app, req).await;
165+
assert_eq!(resp.status().as_u16(), 400); // mapped from UserDoesNotExist
166+
}
167+
}

0 commit comments

Comments
 (0)