diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 82b78fde..565f21d7 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -25,6 +25,9 @@ # # Also see the `[global.well_known]` config section at the very bottom. # +# If `client` is not set under `[global.well_known]`, the server name will be used +# as the base domain for user-facing links (such as password reset links) created by Continuwuity. +# # Examples of delegation: # - https://continuwuity.org/.well-known/matrix/server # - https://continuwuity.org/.well-known/matrix/client diff --git a/src/admin/user/commands.rs b/src/admin/user/commands.rs index 17ea503f..5e03cdb7 100644 --- a/src/admin/user/commands.rs +++ b/src/admin/user/commands.rs @@ -296,6 +296,29 @@ pub(super) async fn reset_password( Ok(()) } +#[admin_command] +pub(super) async fn issue_password_reset_link(&self, username: String) -> Result { + use conduwuit_service::password_reset::{PASSWORD_RESET_PATH, RESET_TOKEN_QUERY_PARAM}; + + let mut reset_url = self + .services + .config + .get_client_domain() + .join(PASSWORD_RESET_PATH) + .unwrap(); + + let user_id = parse_local_user_id(self.services, &username)?; + let token = self.services.password_reset.issue_token(user_id).await?; + reset_url + .query_pairs_mut() + .append_pair(RESET_TOKEN_QUERY_PARAM, &token.token); + + self.write_str(&format!("Password reset link issued for {username}: {reset_url}")) + .await?; + + Ok(()) +} + #[admin_command] pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) -> Result { if self.body.len() < 2 diff --git a/src/admin/user/mod.rs b/src/admin/user/mod.rs index 9bdbf396..f4b26765 100644 --- a/src/admin/user/mod.rs +++ b/src/admin/user/mod.rs @@ -29,6 +29,12 @@ pub enum UserCommand { password: Option, }, + /// Issue a self-service password reset link for a user. + IssuePasswordResetLink { + /// Username of the user who may use the link + username: String, + }, + /// Deactivate a user /// /// User will be removed from all rooms by default. diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index a642f5b7..975dbb29 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -68,6 +68,10 @@ pub struct Config { /// /// Also see the `[global.well_known]` config section at the very bottom. /// + /// If `client` is not set under `[global.well_known]`, the server name will + /// be used as the base domain for user-facing links (such as password + /// reset links) created by Continuwuity. + /// /// Examples of delegation: /// - https://continuwuity.org/.well-known/matrix/server /// - https://continuwuity.org/.well-known/matrix/client diff --git a/src/database/maps.rs b/src/database/maps.rs index 1126f6c5..97222523 100644 --- a/src/database/maps.rs +++ b/src/database/maps.rs @@ -113,7 +113,7 @@ pub(super) static MAPS: &[Descriptor] = &[ ..descriptor::RANDOM_SMALL }, Descriptor { - name: "passwordresettoken_userid", + name: "passwordresettoken_info", ..descriptor::RANDOM_SMALL }, Descriptor { diff --git a/src/service/config/mod.rs b/src/service/config/mod.rs index a8ae8c11..07401845 100644 --- a/src/service/config/mod.rs +++ b/src/service/config/mod.rs @@ -6,6 +6,7 @@ use conduwuit::{ config::{Config, check}, error, implement, }; +use url::Url; use crate::registration_tokens::{ValidToken, ValidTokenSource}; @@ -23,6 +24,18 @@ impl Service { .clone() .map(|token| ValidToken { token, source: ValidTokenSource::Config }) } + + /// Get the base domain to use for user-facing URLs. + #[must_use] + pub fn get_client_domain(&self) -> Url { + self.well_known.client.clone().unwrap_or_else(|| { + let host = self.server_name.host(); + format!("https://{host}") + .as_str() + .try_into() + .expect("server name should be a valid host") + }) + } } #[async_trait] diff --git a/src/service/password_reset/mod.rs b/src/service/password_reset/mod.rs index 4fa2ecaf..44ab2ce3 100644 --- a/src/service/password_reset/mod.rs +++ b/src/service/password_reset/mod.rs @@ -8,6 +8,8 @@ use ruma::OwnedUserId; use crate::{Dep, globals, users}; +pub const PASSWORD_RESET_PATH: &str = "/_continuwuity/password_reset"; +pub const RESET_TOKEN_QUERY_PARAM: &str = "token"; const RESET_TOKEN_LENGTH: usize = 32; pub struct Service { @@ -52,6 +54,10 @@ impl Service { return Err!("Cannot issue a password reset token for remote user {user}"); } + if user == self.services.globals.server_user { + return Err!("Cannot issue a password reset token for the server user"); + } + if self.services.users.origin(&user).await? != "password" { return Err!("Cannot issue a password reset token for non-internal user {user}"); }