commit 6bdc76dd9494c13a72291f5969c9831c5498f7b1
parent 3e9d8a750032c40e26bfa1c91925311c251944fb
Author: Silas Brack <silasbrack@gmail.com>
Date: Sun, 7 Jun 2026 20:11:17 +0200
feat: add resend verification email page
Users who registered but can't login can request a new verification
email via /resend-verification. Link added to the login page.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat:
5 files changed, 82 insertions(+), 1 deletion(-)
diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs
@@ -9,7 +9,7 @@ use serde::Deserialize;
use crate::error::AppError;
use crate::state::AppState;
-use crate::templates::{ApplicationTemplate, LoginTemplate, RegisterTemplate};
+use crate::templates::{ApplicationTemplate, LoginTemplate, RegisterTemplate, ResendVerificationTemplate};
use crate::trailbase::{self, StoredTokens};
const AUTH_COOKIE: &str = "auth_tokens";
@@ -169,6 +169,49 @@ pub async fn register(
}
}
+pub async fn show_resend_verification() -> Result<Html<String>, AppError> {
+ let template = ResendVerificationTemplate {
+ error: None,
+ message: None,
+ };
+ let app = ApplicationTemplate {
+ content: template.render()?,
+ };
+ Ok(Html(app.render()?))
+}
+
+#[derive(Debug, Deserialize)]
+pub struct ResendVerificationForm {
+ pub email: String,
+}
+
+pub async fn resend_verification(
+ State(state): State<AppState>,
+ Form(form): Form<ResendVerificationForm>,
+) -> Result<Html<String>, AppError> {
+ let url = format!("{}api/auth/v1/verify_email/trigger?email={}", state.trailbase_url, url::form_urlencoded::byte_serialize(form.email.as_bytes()).collect::<String>());
+ let response = state.http_client.get(&url).send().await;
+
+ let (message, error) = match response {
+ Ok(resp) if resp.status().is_success() => {
+ (Some("Verification email sent! Check your inbox.".to_string()), None)
+ }
+ Ok(resp) => {
+ let body = resp.text().await.unwrap_or_default();
+ (None, Some(format!("Failed to send: {}", body)))
+ }
+ Err(e) => {
+ (None, Some(format!("Request failed: {}", e)))
+ }
+ };
+
+ let template = ResendVerificationTemplate { error, message };
+ let app = ApplicationTemplate {
+ content: template.render()?,
+ };
+ Ok(Html(app.render()?))
+}
+
pub async fn logout(jar: CookieJar) -> (CookieJar, Redirect) {
let jar = clear_auth_cookie(jar);
(jar, Redirect::to("/"))
diff --git a/src/routes.rs b/src/routes.rs
@@ -86,6 +86,8 @@ pub fn create_router(state: AppState) -> Router {
.route("/register", get(handlers::show_register))
.route("/register", post(handlers::register))
.route("/logout", get(handlers::logout))
+ .route("/resend-verification", get(handlers::show_resend_verification))
+ .route("/resend-verification", post(handlers::resend_verification))
// Static files
.route("/static/style.css", get(serve_style))
diff --git a/src/templates.rs b/src/templates.rs
@@ -131,3 +131,10 @@ pub struct RegisterTemplate {
pub username: String,
pub email: String,
}
+
+#[derive(Template)]
+#[template(path = "resend_verification.html")]
+pub struct ResendVerificationTemplate {
+ pub error: Option<String>,
+ pub message: Option<String>,
+}
diff --git a/templates/login.html b/templates/login.html
@@ -24,4 +24,5 @@
</form>
<p class="auth-link">Don't have an account? <a href="/register">Register</a></p>
+ <p class="auth-link">Registered but can't login? <a href="/resend-verification">Resend verification email</a></p>
</div>
diff --git a/templates/resend_verification.html b/templates/resend_verification.html
@@ -0,0 +1,28 @@
+<div class="auth-page">
+ <h2>Resend Verification Email</h2>
+
+ {% match message %}
+ {% when Some with (msg) %}
+ <div class="success-message">{{ msg }}</div>
+ {% when None %}
+ {% endmatch %}
+
+ {% match error %}
+ {% when Some with (err) %}
+ <div class="error-message">{{ err }}</div>
+ {% when None %}
+ {% endmatch %}
+
+ <form method="post" action="/resend-verification" class="auth-form">
+ <div class="form-group">
+ <label for="email">Email</label>
+ <input type="email" id="email" name="email" required autocomplete="email" />
+ </div>
+
+ <div class="form-actions">
+ <button type="submit">Resend</button>
+ </div>
+ </form>
+
+ <p class="auth-link"><a href="/login">Back to Login</a></p>
+</div>