Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/cli/src/commands/manage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ impl Options {
username,
email,
passwordless,
None,
)
.await?;

Expand Down
1 change: 1 addition & 0 deletions crates/cli/src/commands/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ impl Options {
&config.account,
&config.captcha,
&config.oauth,
&config.room_invites,
)?;

// Load and compile the templates
Expand Down
6 changes: 5 additions & 1 deletion crates/cli/src/commands/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ use clap::Parser;
use figment::Figment;
use mas_config::{
AccountConfig, BrandingConfig, CaptchaConfig, ConfigurationSection, ConfigurationSectionExt,
ExperimentalConfig, MatrixConfig, OAuthConfig, PasswordsConfig, TemplatesConfig,
ExperimentalConfig, MatrixConfig, OAuthConfig, PasswordsConfig, RoomInvitesConfig,
TemplatesConfig,
};
use mas_data_model::{Clock, SystemClock};
use rand::SeedableRng;
Expand Down Expand Up @@ -68,6 +69,8 @@ impl Options {
.map_err(anyhow::Error::from_boxed)?;
let oauth_config =
OAuthConfig::extract_or_default(figment).map_err(anyhow::Error::from_boxed)?;
let room_invites_config = RoomInvitesConfig::extract_or_default(figment)
.map_err(anyhow::Error::from_boxed)?;

let now = if stabilise {
DateTime::from_timestamp_secs(1_446_823_992).unwrap()
Expand All @@ -92,6 +95,7 @@ impl Options {
&account_config,
&captcha_config,
&oauth_config,
&room_invites_config,
)?;
let templates = templates_from_config(
&template_config,
Expand Down
2 changes: 2 additions & 0 deletions crates/cli/src/commands/worker.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Copyright 2025, 2026 Element Creations Ltd.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should 2025 be here?

// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
//
Expand Down Expand Up @@ -50,6 +51,7 @@ impl Options {
&config.account,
&config.captcha,
&config.oauth,
&config.room_invites,
)?;

// Load and compile the templates
Expand Down
5 changes: 4 additions & 1 deletion crates/cli/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use anyhow::Context;
use mas_config::{
AccountConfig, BrandingConfig, CaptchaConfig, DatabaseConfig, EmailConfig, EmailSmtpMode,
EmailTransportKind, ExperimentalConfig, HomeserverKind, MatrixConfig, OAuthConfig,
PasswordsConfig, PolicyConfig, TemplatesConfig,
PasswordsConfig, PolicyConfig, RoomInvitesConfig, TemplatesConfig,
};
use mas_context::LogContext;
use mas_data_model::{SessionExpirationConfig, SessionLimitConfig, SiteConfig};
Expand Down Expand Up @@ -205,6 +205,7 @@ pub fn captcha_config_from_config(
}))
}

#[expect(clippy::too_many_arguments)]
pub fn site_config_from_config(
branding_config: &BrandingConfig,
matrix_config: &MatrixConfig,
Expand All @@ -213,6 +214,7 @@ pub fn site_config_from_config(
account_config: &AccountConfig,
captcha_config: &CaptchaConfig,
oauth_config: &OAuthConfig,
room_invites_config: &RoomInvitesConfig,
) -> Result<SiteConfig, anyhow::Error> {
let captcha = captcha_config_from_config(captcha_config)?;
let session_expiration = experimental_config
Expand Down Expand Up @@ -269,6 +271,7 @@ pub fn site_config_from_config(
device_code_grant_enabled: oauth_config.device_code_grant_enabled,
device_code_user_code_auto_fill_enabled: oauth_config
.device_code_user_code_auto_fill_enabled,
room_invites_client_room_url: room_invites_config.client_room_url.clone(),
})
}

Expand Down
13 changes: 13 additions & 0 deletions crates/config/src/sections/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ mod oauth;
mod passwords;
mod policy;
mod rate_limiting;
mod room_invites;
mod secrets;
mod telemetry;
mod templates;
Expand All @@ -48,6 +49,7 @@ pub use self::{
},
policy::PolicyConfig,
rate_limiting::RateLimitingConfig,
room_invites::RoomInvitesConfig,
secrets::SecretsConfig,
telemetry::{
MetricsConfig, MetricsExporterKind, Propagator, TelemetryConfig, TracingConfig,
Expand Down Expand Up @@ -133,6 +135,10 @@ pub struct RootConfig {
#[serde(default, skip_serializing_if = "OAuthConfig::is_default")]
pub oauth: OAuthConfig,

/// Configuration section for inviting email addresses to rooms
#[serde(default, skip_serializing_if = "RoomInvitesConfig::is_default")]
pub room_invites: RoomInvitesConfig,

/// Experimental configuration options
#[serde(default, skip_serializing_if = "ExperimentalConfig::is_default")]
pub experimental: ExperimentalConfig,
Expand All @@ -159,6 +165,7 @@ impl ConfigurationSection for RootConfig {
self.captcha.validate(figment)?;
self.account.validate(figment)?;
self.oauth.validate(figment)?;
self.room_invites.validate(figment)?;
self.experimental.validate(figment)?;

Ok(())
Expand Down Expand Up @@ -192,6 +199,7 @@ impl RootConfig {
captcha: CaptchaConfig::default(),
account: AccountConfig::default(),
oauth: OAuthConfig::default(),
room_invites: RoomInvitesConfig::default(),
experimental: ExperimentalConfig::default(),
})
}
Expand All @@ -216,6 +224,7 @@ impl RootConfig {
captcha: CaptchaConfig::default(),
account: AccountConfig::default(),
oauth: OAuthConfig::default(),
room_invites: RoomInvitesConfig::default(),
experimental: ExperimentalConfig::default(),
}
}
Expand Down Expand Up @@ -262,6 +271,9 @@ pub struct AppConfig {
#[serde(default)]
pub oauth: OAuthConfig,

#[serde(default)]
pub room_invites: RoomInvitesConfig,

#[serde(default)]
pub experimental: ExperimentalConfig,
}
Expand All @@ -284,6 +296,7 @@ impl ConfigurationSection for AppConfig {
self.captcha.validate(figment)?;
self.account.validate(figment)?;
self.oauth.validate(figment)?;
self.room_invites.validate(figment)?;
self.experimental.validate(figment)?;

Ok(())
Expand Down
129 changes: 129 additions & 0 deletions crates/config/src/sections/room_invites.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::Error};
use url::Url;

use crate::ConfigurationSection;

/// Configuration section for inviting email addresses to rooms
#[derive(Clone, Debug, Default, Deserialize, JsonSchema, Serialize)]
pub struct RoomInvitesConfig {
/// The client URL to send invitees to once they are signed in, with
/// `{room_id}` standing for the room ID, e.g.
/// `https://app.element.io/#/room/{room_id}`.
#[serde(skip_serializing_if = "Option::is_none")]
pub client_room_url: Option<String>,
}

impl RoomInvitesConfig {
/// Returns true if the configuration is the default one
pub(crate) fn is_default(&self) -> bool {
self.client_room_url.is_none()
}
}

impl ConfigurationSection for RoomInvitesConfig {
const PATH: Option<&'static str> = Some("room_invites");

fn validate(
&self,
figment: &figment::Figment,
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
let metadata = figment.find_metadata(Self::PATH.unwrap());

let error = |message: &str| -> Box<dyn std::error::Error + Send + Sync + 'static> {
let mut error = figment::error::Error::custom(message);
error.metadata = metadata.cloned();
error.profile = Some(figment::Profile::Default);
error.path = vec![Self::PATH.unwrap().to_owned(), "client_room_url".to_owned()];
error.into()
};

match &self.client_room_url {
Some(url) if !url.contains("{room_id}") => {
return Err(error("must contain `{room_id}`"));
}
Some(url)
if !Url::parse(&url.replace("{room_id}", "room")).is_ok_and(|url| {
matches!(url.scheme(), "http" | "https") && url.has_host()
}) =>
{
return Err(error("must be an absolute HTTP or HTTPS URL"));
}
_ => {}
}

Ok(())
}
}

#[cfg(test)]
mod tests {
// The closures passed to `Jail::expect_with` return `figment::Error`, which is
// large, and we can't change figment's API.
#![expect(clippy::result_large_err)]

use figment::{
Figment, Jail,
providers::{Format, Yaml},
};

use super::*;

fn load(jail: &mut Jail, yaml: &str) -> Result<RoomInvitesConfig, String> {
jail.create_file("config.yaml", yaml)
.map_err(|e| e.to_string())?;
let figment = Figment::new().merge(Yaml::file("config.yaml"));
RoomInvitesConfig::extract(&figment).map_err(|e| e.to_string())
}

#[test]
fn defaults() {
Jail::expect_with(|jail| {
let config = load(jail, "room_invites: {}").unwrap();
assert_eq!(config.client_room_url, None);
assert!(config.is_default());
Ok(())
});
}

#[test]
fn client_room_url_requires_room_id() {
Jail::expect_with(|jail| {
let error = load(
jail,
"room_invites: { client_room_url: 'https://app.example.com/' }",
)
.unwrap_err();
assert!(error.contains("{room_id}"), "{error}");
Ok(())
});
}

#[test]
fn client_room_url_must_be_absolute() {
Jail::expect_with(|jail| {
for url in [
"app.example.com/#/room/{room_id}",
"localhost:8080/#/room/{room_id}",
"ftp://app.example.com/#/room/{room_id}",
"https:/#/room/{room_id}",
] {
let error = load(
jail,
&format!("room_invites: {{ client_room_url: '{url}' }}"),
)
.unwrap_err();
assert!(
error.contains("absolute HTTP or HTTPS URL"),
"{url}: {error}"
);
}
Ok(())
});
}
}
5 changes: 5 additions & 0 deletions crates/data-model/src/site_config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Copyright 2025, 2026 Element Creations Ltd.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should 2025 be here?

// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
//
Expand Down Expand Up @@ -127,4 +128,8 @@ pub struct SiteConfig {
/// `verification_uri_complete` and whether `/link` accepts a `code`
/// query parameter to auto-fill the user code.
pub device_code_user_code_auto_fill_enabled: bool,

/// The client URL to send invitees to, with `{room_id}` standing for the
/// room ID.
pub room_invites_client_room_url: Option<String>,
}
1 change: 1 addition & 0 deletions crates/data-model/src/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ pub struct UserRegistrationToken {
pub username: Option<String>,
pub email: Option<String>,
pub passwordless: bool,
pub room_id: Option<String>,
pub usage_limit: Option<u32>,
pub times_used: u32,
pub created_at: DateTime<Utc>,
Expand Down
1 change: 1 addition & 0 deletions crates/handlers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ tower.workspace = true
tracing.workspace = true
ulid.workspace = true
url.workspace = true
urlencoding.workspace = true
zeroize.workspace = true

mas-axum-utils.workspace = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ pub async fn handler(
params.username,
params.email,
params.passwordless,
None,
)
.await?;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ mod tests {
None,
None,
false,
None,
)
.await
.unwrap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ mod tests {
None,
None,
false,
None,
)
.await
.unwrap();
Expand All @@ -228,6 +229,7 @@ mod tests {
None,
None,
false,
None,
)
.await
.unwrap();
Expand All @@ -248,6 +250,7 @@ mod tests {
None,
None,
false,
None,
)
.await
.unwrap();
Expand All @@ -268,6 +271,7 @@ mod tests {
None,
None,
false,
None,
)
.await
.unwrap();
Expand All @@ -293,6 +297,7 @@ mod tests {
None,
None,
false,
None,
)
.await
.unwrap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ mod tests {
None,
None,
false,
None,
)
.await
.unwrap();
Expand Down Expand Up @@ -171,6 +172,7 @@ mod tests {
None,
None,
false,
None,
)
.await
.unwrap();
Expand Down
Loading
Loading