feat: Implement a command for issuing password reset links

This commit is contained in:
Ginger 2026-03-03 11:18:48 -05:00
parent 267feb3c09
commit da8833fca4
No known key found for this signature in database
7 changed files with 56 additions and 1 deletions

View file

@ -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

View file

@ -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

View file

@ -29,6 +29,12 @@ pub enum UserCommand {
password: Option<String>,
},
/// 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.

View file

@ -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

View file

@ -113,7 +113,7 @@ pub(super) static MAPS: &[Descriptor] = &[
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "passwordresettoken_userid",
name: "passwordresettoken_info",
..descriptor::RANDOM_SMALL
},
Descriptor {

View file

@ -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]

View file

@ -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}");
}