use axum::{ Extension, Router, extract::{Query, State}, routing::{get, on, post}, }; use conduwuit_core::warn; use conduwuit_service::{mailer::messages, threepid::session::ValidationSessions}; use lettre::{Address, message::Mailbox}; use ruma::{ClientSecret, OwnedClientSecret, OwnedSessionId}; use serde::{Deserialize, Serialize}; use crate::{ WebError, extract::{Expect, PostForm}, form, pages::{ GET_POST, Result, TemplateContext, account::ThreepidQuery, components::{UserCard, form::Form}, }, response, session::{LoginTarget, User}, template, }; pub(crate) fn build() -> Router { Router::new() .route("/change/", on(GET_POST, route_change_email)) .route("/change/validate", get(get_change_email_validate)) .route("/change/delete", post(post_delete_email)) } template! { struct ChangeEmail use "change_email.html.j2" { user_card: UserCard, email: Option, form: Form<'static>, may_remove: bool } } form! { struct ChangeEmailForm { email: Address where { input_type: "email", label: "Email address" } submit: "Change email" } } template! { struct ChangeEmailValidate use "change_email_validate.html.j2" { user_card: UserCard, body: ChangeEmailValidateBody } } template! { struct DeleteEmail use "delete_email.html.j2" { user_card: UserCard } } #[derive(Debug)] enum ChangeEmailValidateBody { ValidationPending { session_id: OwnedSessionId, client_secret: OwnedClientSecret, validation_error: bool, }, Success, } async fn route_change_email( State(services): State, Extension(context): Extension, user: User, PostForm(form): PostForm, ) -> Result { let user_id = user.expect_recent(LoginTarget::ChangeEmail)?; let Some(form) = form else { return response!(ChangeEmail::new( context.clone(), UserCard::for_local_user(&services, user_id.clone()).await, services .threepid .get_email_for_localpart(user_id.localpart()) .await .map(|address| address.to_string()), ChangeEmailForm::build(context), services.threepid.email_requirement().may_remove(), )); }; let client_secret = ClientSecret::new(); let session_id = { let display_name = services.users.displayname(&user_id).await.ok(); match services .threepid .send_validation_email( Mailbox::new(display_name, form.email.clone()), |verification_link| messages::ChangeEmail { server_name: services.globals.server_name().as_str(), user_id: Some(&user_id), verification_link, }, &client_secret, 0, ) .await { | Ok(session_id) => session_id, | Err(err) => { // If we couldn't send an email, generate a random session ID to not give that // away warn!( "Failed to send email change message for {user_id} to {}: {err}", form.email ); ValidationSessions::generate_session_id() }, } }; response!(ChangeEmailValidate::new( context, UserCard::for_local_user(&services, user_id).await, ChangeEmailValidateBody::ValidationPending { session_id, client_secret, validation_error: false } )) } #[derive(Deserialize, Serialize)] struct ChangeEmailQuery { #[serde(flatten)] threepid: ThreepidQuery, } async fn get_change_email_validate( State(services): State, Extension(context): Extension, Expect(Query(ChangeEmailQuery { threepid: ThreepidQuery { client_secret, session_id }, })): Expect>, user: User, ) -> Result { let user_id = user.expect(LoginTarget::ChangeEmail)?; let user_card = UserCard::for_local_user(&services, user_id.clone()).await; if !services.threepid.email_requirement().may_change() { return Err(WebError::Forbidden("You may not change your email address.".to_owned())); } let Ok(session) = services .threepid .get_valid_session(&session_id, &client_secret) .await else { return response!(ChangeEmailValidate::new( context, user_card, ChangeEmailValidateBody::ValidationPending { session_id, client_secret, validation_error: true } )); }; let new_email = session.consume(); if let Err(err) = services .threepid .associate_localpart_email(user_id.localpart(), &new_email) .await { return response!(BadRequest(err.message())); } response!(ChangeEmailValidate::new(context, user_card, ChangeEmailValidateBody::Success)) } async fn post_delete_email( State(services): State, Extension(context): Extension, user: User, ) -> Result { let user_id = user.expect(LoginTarget::ChangeEmail)?; let user_card = UserCard::for_local_user(&services, user_id.clone()).await; if !services.threepid.email_requirement().may_remove() { return Err(WebError::Forbidden("You may not remove your email address.".to_owned())); } let _ = services .threepid .disassociate_localpart_email(user_id.localpart()) .await; response!(DeleteEmail::new(context, user_card)) }