tuwunel_api/client/account/
change_password.rs1use axum::extract::State;
2use futures::StreamExt;
3use ruma::{
4 OwnedUserId,
5 api::client::{
6 account::change_password,
7 uiaa::{AuthData, EmailIdentity, ThirdpartyIdCredentials},
8 },
9 thirdparty::Medium,
10};
11use tuwunel_core::{Err, Error, Result, info, utils::ReadyExt};
12
13use crate::{ClientIp, Ruma, router::auth_uiaa};
14
15#[tracing::instrument(skip_all, fields(%client), name = "change_password")]
36pub(crate) async fn change_password_route(
37 State(services): State<crate::State>,
38 ClientIp(client): ClientIp,
39 body: Ruma<change_password::v3::Request>,
40) -> Result<change_password::v3::Response> {
41 let sender_user = match (body.sender_user.as_ref(), body.auth.as_ref()) {
42 | (None, Some(AuthData::EmailIdentity(EmailIdentity { thirdparty_id_creds, .. }))) =>
43 redeem_password_reset(services, thirdparty_id_creds).await?,
44 | (None, _) => return Err!(Request(MissingToken("Missing access token."))),
45 | (Some(_), _) => auth_uiaa(&services, &body).await?,
46 };
47
48 services
49 .users
50 .set_password(&sender_user, Some(&body.new_password))
51 .await?;
52
53 if body.logout_devices {
54 services
56 .users
57 .all_device_ids(&sender_user)
58 .ready_filter(|&id| Some(id) != body.sender_device.as_deref())
59 .for_each(|id| services.users.remove_device(&sender_user, id))
60 .await;
61 }
62
63 info!("User {sender_user} changed their password.");
64
65 services
66 .admin
67 .notify(&format!("User {sender_user} changed their password."))
68 .await;
69
70 Ok(change_password::v3::Response {})
71}
72
73#[tracing::instrument(level = "debug", skip_all)]
74async fn redeem_password_reset(
75 services: crate::State,
76 creds: &ThirdpartyIdCredentials,
77) -> Result<OwnedUserId> {
78 let association = match services
79 .threepid
80 .redeem_validated(creds.sid.as_str(), creds.client_secret.as_str())
81 .await
82 {
83 | Ok(association) => association,
84 | Err(error) if error.is_not_found() || matches!(&error, Error::Request(..)) => {
85 return Err!(Request(Forbidden("Invalid email identity proof.")));
86 },
87 | Err(error) => return Err(error),
88 };
89
90 if association.medium != Medium::Email {
91 return Err!(Request(Forbidden("Invalid email identity proof.")));
92 }
93
94 let Some(user_id) = services
95 .threepid
96 .user_id_for_email(&association.address)
97 .await?
98 else {
99 return Err!(Request(Forbidden("Invalid email identity proof.")));
100 };
101
102 Ok(user_id)
103}