feat(auth): verification emails carry clickable link from web_base_url
ZhenYi committed
Modified lib/auth/src/services/email.rs lib/auth/src/services/email.rs +50 −2 View lib/auth/src/services/email.rs
@@ -289,7 +289,17 @@
289289 EmailVerificationPurpose::AddEmail => "Add your email",
290290 };
291291 let minutes = self.config().auth.verify_code_ttl_secs / 60;
292 let html = crate::templates::mail_verify_code(&artifacts.code, minutes, subject);
292 let link = build_verify_link(
293 &self.config().web.web_base_url,
294 &lower_email,
295 &artifacts.temp_key.value,
296 );
297 let html = crate::templates::mail_verify_code(
298 &artifacts.code,
299 minutes,
300 subject,
301 link.as_deref(),
302 );
293303 let body = format!("Your verification code is: {}", artifacts.code);
294304 let sent = self
295305 .send_email(email_row.email.as_str(), subject, &body, html.0.as_str())
@@ -338,7 +348,17 @@
338348 .issue_code(user.uid, &lower_email, EmailVerificationPurpose::Signup)
339349 .await?;
340350 let minutes = self.config().auth.verify_code_ttl_secs / 60;
341 let html = crate::templates::mail_verify_code(&artifacts.code, minutes, "Recover your account");
351 let link = build_verify_link(
352 &self.config().web.web_base_url,
353 &lower_email,
354 &artifacts.temp_key.value,
355 );
356 let html = crate::templates::mail_verify_code(
357 &artifacts.code,
358 minutes,
359 "Recover your account",
360 link.as_deref(),
361 );
342362 let body = format!("Recover your account with code: {}", artifacts.code);
343363 let sent = self
344364 .send_email(
@@ -574,4 +594,32 @@
574594 active.update(db.write()).await?;
575595 Ok(())
576596 }
597}
598
599pub(super) fn build_verify_link(base_url: &str, email: &str, temp_id: &str) -> Option<String> {
600 if base_url.is_empty() {
601 return None;
602 }
603 let base = base_url.trim_end_matches('/');
604 Some(format!(
605 "{base}/auth/verify-email?email={}&temp_id={}",
606 percent_encode(email),
607 percent_encode(temp_id),
608 ))
609}
610
611fn percent_encode(input: &str) -> String {
612 let mut out = String::with_capacity(input.len());
613 for &b in input.as_bytes() {
614 match b {
615 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
616 out.push(b as char);
617 }
618 _ => {
619 out.push('%');
620 out.push_str(&format!("{b:02X}"));
621 }
622 }
623 }
624 out
577625}
No newline at end of file
Modified lib/auth/src/services/register.rs lib/auth/src/services/register.rs +12 −1 View lib/auth/src/services/register.rs
@@ -7,6 +7,7 @@
77use uuid::Uuid;
88
99use super::AuthService;
10use super::email::build_verify_link;
1011use crate::share::crypto::{hash_password, validate_password};
1112use crate::errors::AuthError;
1213use crate::share::types::TempKey;
@@ -147,7 +148,17 @@
147148 .issue_code(uid, &lower_email, models::users::user_email_verification_codes::EmailVerificationPurpose::Signup)
148149 .await?;
149150 let minutes = self.config().auth.verify_code_ttl_secs / 60;
150 let html = crate::templates::mail_verify_code(&artifacts.code, minutes, "Verify your account");
151 let link = build_verify_link(
152 &self.config().web.web_base_url,
153 &lower_email,
154 &artifacts.temp_key.value,
155 );
156 let html = crate::templates::mail_verify_code(
157 &artifacts.code,
158 minutes,
159 "Verify your account",
160 link.as_deref(),
161 );
151162 let text = format!("Verify your account with code: {}", artifacts.code);
152163 if let Err(error) = self
153164 .send_email(&email, "Verify your account", &text, html.0.as_str())
Modified lib/auth/src/templates/mod.rs lib/auth/src/templates/mod.rs +7 −2 View lib/auth/src/templates/mod.rs
@@ -4,8 +4,13 @@
44pub(crate) use common::page_shell;
55use axum::response::Html;
66
7pub fn mail_verify_code(code: &str, minutes: u64, purpose: &str) -> Html<String> {
8 Html(email::templates::mail_verify_code(code, minutes, purpose).into_string())
7pub fn mail_verify_code(
8 code: &str,
9 minutes: u64,
10 purpose: &str,
11 link: Option<&str>,
12) -> Html<String> {
13 Html(email::templates::mail_verify_code(code, minutes, purpose, link).into_string())
914}
1015
1116pub fn mail_reset_link(email: &str, token: &str, temp_id: &str, base_url: &str) -> Html<String> {
Modified lib/email/src/templates/reset_link.rs lib/email/src/templates/reset_link.rs +1 −1 View lib/email/src/templates/reset_link.rs
@@ -5,7 +5,7 @@
55
66pub fn mail_reset_link(email: &str, token: &str, temp_id: &str, base_url: &str) -> Markup {
77 let link = append_query(
8 &url_join(base_url, "/reset-password"),
8 &url_join(base_url, "/auth/reset-password"),
99 &[("email", email), ("token", token), ("temp_id", temp_id)],
1010 );
1111 let body = html! {
Modified lib/email/src/templates/verify_code.rs lib/email/src/templates/verify_code.rs +28 −3 View lib/email/src/templates/verify_code.rs
@@ -1,9 +1,14 @@
11use maud::html;
22use maud::Markup;
33
4use super::mail_shell;
4use super::{append_query, mail_shell, url_join};
55
6pub fn mail_verify_code(code: &str, minutes: u64, purpose: &str) -> Markup {
6pub fn mail_verify_code(
7 code: &str,
8 minutes: u64,
9 purpose: &str,
10 link: Option<&str>,
11) -> Markup {
712 let body = html! {
813 h1 style="font-size:24px;font-weight:700;margin:0 0 8px;" { (purpose) }
914 p style="color:#374151;margin:0 0 8px;" {
@@ -12,6 +17,15 @@
1217 " minutes."
1318 }
1419 div class="code" { (code) }
20 @if let Some(url) = link {
21 p style="margin:24px 0 0;" {
22 a href=(url) class="btn" { "Verify email" }
23 }
24 p class="muted" style="margin:16px 0 0;" {
25 "If the button doesn't work, copy this link into your browser:"
26 }
27 p style="word-break:break-all;font-size:13px;color:#2563eb;margin:4px 0 0;" { (url) }
28 }
1529 p class="muted" style="margin:16px 0 0;" {
1630 "If you didn't request this, you can safely ignore this email."
1731 }
@@ -32,5 +46,16 @@
3246 .get("purpose")
3347 .and_then(|v| v.as_str())
3448 .unwrap_or("Verification");
35 mail_verify_code(code, minutes, purpose)
49 let link = build_link(vars);
50 mail_verify_code(code, minutes, purpose, link.as_deref())
51}
52
53fn build_link(vars: &serde_json::Value) -> Option<String> {
54 let base_url = vars.get("base_url").and_then(|v| v.as_str())?;
55 let email = vars.get("email").and_then(|v| v.as_str())?;
56 let temp_id = vars.get("temp_id").and_then(|v| v.as_str())?;
57 Some(append_query(
58 &url_join(base_url, "/auth/verify-email"),
59 &[("email", email), ("temp_id", temp_id)],
60 ))
3661}
No newline at end of file