Expose the invite URL and the pinned username and email in the admin API

Registration tokens now report the `/register?token=…` deep link alongside the
`username` and `email` they were issued for, and the create endpoint accepts
both. All the token endpoints now take the `UrlBuilder` so they can build that
link, and `Register` carries the token in its query string.
This commit is contained in:
Quentin Gliech committed 2026-10-13 14:02:33 +02:00
1 parent e08fa11467
commit 7cdf85e279
11 files changed
+383 -82

No files matched your search

+28 -1
View File
@@ -15,6 +15,7 @@ use mas_data_model::{
session::{PersonalSession as DataModelPersonalSession, PersonalSessionOwner},
},
};
use mas_router::UrlBuilder;
use oauth2_types::requests::GrantType;
use schemars::JsonSchema;
use serde::Serialize;
@@ -631,6 +632,14 @@ pub struct UserRegistrationToken {
/// Whether the token is valid
valid: bool,
/// A username imposed on the registering user. If null, the user can
/// choose their own username.
username: Option<String>,
/// An email address imposed on the registering user. If null, the user
/// can choose their own email.
email: Option<String>,
/// Maximum number of times this token can be used
usage_limit: Option<u32>,
@@ -648,20 +657,32 @@ pub struct UserRegistrationToken {
/// When the token was revoked. If null, the token is not revoked.
revoked_at: Option<DateTime<Utc>>,
/// URL to share with the user to register with this token
invite_url: Url,
}
impl UserRegistrationToken {
pub fn new(token: mas_data_model::UserRegistrationToken, now: DateTime<Utc>) -> Self {
pub fn new(
token: mas_data_model::UserRegistrationToken,
now: DateTime<Utc>,
url_builder: &UrlBuilder,
) -> Self {
let invite_url = url_builder
.absolute_url_for(&mas_router::Register::default().with_token(token.token.clone()));
Self {
id: token.id,
valid: token.is_valid(now),
token: token.token,
username: token.username,
email: token.email,
usage_limit: token.usage_limit,
times_used: token.times_used,
created_at: token.created_at,
last_used_at: token.last_used_at,
expires_at: token.expires_at,
revoked_at: token.revoked_at,
invite_url,
}
}
}
@@ -683,23 +704,29 @@ impl UserRegistrationToken {
id: Ulid::from_bytes([0x01; 16]),
token: "abc123def456".to_owned(),
valid: true,
username: None,
email: None,
usage_limit: Some(10),
times_used: 5,
created_at: DateTime::default(),
last_used_at: Some(DateTime::default()),
expires_at: Some(DateTime::default() + chrono::Duration::days(30)),
revoked_at: None,
invite_url: Url::parse("https://example.com/register?token=abc123def456").unwrap(),
},
Self {
id: Ulid::from_bytes([0x02; 16]),
token: "xyz789abc012".to_owned(),
valid: false,
username: None,
email: None,
usage_limit: None,
times_used: 0,
created_at: DateTime::default(),
last_used_at: None,
expires_at: None,
revoked_at: Some(DateTime::default()),
invite_url: Url::parse("https://example.com/register?token=xyz789abc012").unwrap(),
},
]
}
+2
View File
@@ -15,6 +15,7 @@ use axum::extract::{FromRef, FromRequestParts};
use mas_data_model::{AppVersion, BoxRng, SiteConfig};
use mas_matrix::HomeserverConnection;
use mas_policy::PolicyFactory;
use mas_router::UrlBuilder;
use super::call_context::CallContext;
use crate::passwords::PasswordManager;
@@ -41,6 +42,7 @@ where
SiteConfig: FromRef<S>,
AppVersion: FromRef<S>,
Arc<PolicyFactory>: FromRef<S>,
UrlBuilder: FromRef<S>,
BoxRng: FromRequestParts<S>,
CallContext: FromRequestParts<S>,
{
@@ -6,11 +6,12 @@
// Please see LICENSE files in the repository root for full details.
use aide::{NoApi, OperationIo, transform::TransformOperation};
use axum::{Json, response::IntoResponse};
use axum::{Json, extract::State, response::IntoResponse};
use chrono::{DateTime, Utc};
use hyper::StatusCode;
use mas_axum_utils::record_error;
use mas_data_model::BoxRng;
use mas_router::UrlBuilder;
use rand::distr::{Alphanumeric, SampleString};
use schemars::JsonSchema;
use serde::Deserialize;
@@ -89,6 +90,7 @@ pub async fn handler(
CallContext {
mut repo, clock, ..
}: CallContext,
State(url_builder): State<UrlBuilder>,
NoApi(mut rng): NoApi<BoxRng>,
Json(params): Json<Request>,
) -> Result<(StatusCode, Json<SingleResponse<UserRegistrationToken>>), RouteError> {
@@ -123,6 +125,7 @@ pub async fn handler(
Json(SingleResponse::new_canonical(UserRegistrationToken::new(
registration_token,
clock.now(),
&url_builder,
))),
))
}
@@ -159,12 +162,15 @@ mod tests {
"attributes": {
"token": "test_token_123",
"valid": true,
"username": null,
"email": null,
"usage_limit": 5,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=test_token_123"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -177,6 +183,27 @@ mod tests {
"#);
}
/// The token travels in the query string of the invite URL, so one with
/// reserved characters comes back percent-encoded
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_create_invite_url_encoding(pool: PgPool) {
setup();
let mut state = TestState::from_pool(pool).await.unwrap();
let token = state.token_with_scope("urn:mas:admin").await;
let request = Request::post("/api/admin/v1/user-registration-tokens")
.bearer(&token)
.json(serde_json::json!({ "token": "a token&more" }));
let response = state.request(request).await;
response.assert_status(StatusCode::CREATED);
let body: serde_json::Value = response.json();
assert_eq!(
body["data"]["attributes"]["invite_url"],
"https://example.com/register?token=a+token%26more"
);
}
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_create_auto_token(pool: PgPool) {
setup();
@@ -201,12 +228,15 @@ mod tests {
"attributes": {
"token": "42oTpLoieH5I",
"valid": true,
"username": null,
"email": null,
"usage_limit": 1,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=42oTpLoieH5I"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0QMGC989M0XSFVF2X"
@@ -244,12 +274,15 @@ mod tests {
"attributes": {
"token": "test_token_123",
"valid": true,
"username": null,
"email": null,
"usage_limit": 5,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=test_token_123"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -1,3 +1,4 @@
// Copyright 2025, 2026 Element Creations Ltd.
// Copyright 2025 New Vector Ltd.
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
@@ -5,9 +6,10 @@
// Please see LICENSE files in the repository root for full details.
use aide::{OperationIo, transform::TransformOperation};
use axum::{Json, response::IntoResponse};
use axum::{Json, extract::State, response::IntoResponse};
use hyper::StatusCode;
use mas_axum_utils::record_error;
use mas_router::UrlBuilder;
use ulid::Ulid;
use crate::{
@@ -67,6 +69,7 @@ pub async fn handler(
CallContext {
mut repo, clock, ..
}: CallContext,
State(url_builder): State<UrlBuilder>,
id: UlidPathParam,
) -> Result<Json<SingleResponse<UserRegistrationToken>>, RouteError> {
let token = repo
@@ -76,7 +79,7 @@ pub async fn handler(
.ok_or(RouteError::NotFound(*id))?;
Ok(Json(SingleResponse::new_canonical(
UserRegistrationToken::new(token, clock.now()),
UserRegistrationToken::new(token, clock.now(), &url_builder),
)))
}
@@ -129,12 +132,15 @@ mod tests {
"attributes": {
"token": "test_token_123",
"valid": true,
"username": null,
"email": null,
"usage_limit": 5,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=test_token_123"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -1,3 +1,4 @@
// Copyright 2025, 2026 Element Creations Ltd.
// Copyright 2025 New Vector Ltd.
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
@@ -5,11 +6,12 @@
// Please see LICENSE files in the repository root for full details.
use aide::{OperationIo, transform::TransformOperation};
use axum::{Json, response::IntoResponse};
use axum::{Json, extract::State, response::IntoResponse};
use axum_extra::extract::{Query, QueryRejection};
use axum_macros::FromRequestParts;
use hyper::StatusCode;
use mas_axum_utils::record_error;
use mas_router::UrlBuilder;
use mas_storage::{Page, user::UserRegistrationTokenFilter};
use schemars::JsonSchema;
use serde::Deserialize;
@@ -135,6 +137,7 @@ pub async fn handler(
CallContext {
mut repo, clock, ..
}: CallContext,
State(url_builder): State<UrlBuilder>,
Pagination(pagination, include_count): Pagination,
params: FilterParams,
) -> Result<Json<PaginatedResponse<UserRegistrationToken>>, RouteError> {
@@ -165,7 +168,7 @@ pub async fn handler(
.user_registration_token()
.list(filter, pagination)
.await?
.map(|token| UserRegistrationToken::new(token, now));
.map(|token| UserRegistrationToken::new(token, now, &url_builder));
let count = repo.user_registration_token().count(filter).await?;
PaginatedResponse::for_page(page, pagination, Some(count), &base)
}
@@ -174,7 +177,7 @@ pub async fn handler(
.user_registration_token()
.list(filter, pagination)
.await?
.map(|token| UserRegistrationToken::new(token, now));
.map(|token| UserRegistrationToken::new(token, now, &url_builder));
PaginatedResponse::for_page(page, pagination, None, &base)
}
IncludeCount::Only => {
@@ -318,12 +321,15 @@ mod tests {
"attributes": {
"token": "token_expired",
"valid": false,
"username": null,
"email": null,
"usage_limit": 5,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": "2022-01-15T14:40:00Z",
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_expired"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z"
@@ -340,12 +346,15 @@ mod tests {
"attributes": {
"token": "token_used",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 1,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": "2022-01-16T14:40:00Z",
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_used"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6"
@@ -362,12 +371,15 @@ mod tests {
"attributes": {
"token": "token_revoked",
"valid": false,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": "2022-01-16T14:40:00Z"
"revoked_at": "2022-01-16T14:40:00Z",
"invite_url": "https://example.com/register?token=token_revoked"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC"
@@ -384,12 +396,15 @@ mod tests {
"attributes": {
"token": "token_unused",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_unused"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -406,12 +421,15 @@ mod tests {
"attributes": {
"token": "token_used_revoked",
"valid": false,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 1,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": "2022-01-16T14:40:00Z",
"expires_at": null,
"revoked_at": "2022-01-16T14:40:00Z"
"revoked_at": "2022-01-16T14:40:00Z",
"invite_url": "https://example.com/register?token=token_used_revoked"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN"
@@ -459,12 +477,15 @@ mod tests {
"attributes": {
"token": "token_used",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 1,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": "2022-01-16T14:40:00Z",
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_used"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6"
@@ -481,12 +502,15 @@ mod tests {
"attributes": {
"token": "token_used_revoked",
"valid": false,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 1,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": "2022-01-16T14:40:00Z",
"expires_at": null,
"revoked_at": "2022-01-16T14:40:00Z"
"revoked_at": "2022-01-16T14:40:00Z",
"invite_url": "https://example.com/register?token=token_used_revoked"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN"
@@ -526,12 +550,15 @@ mod tests {
"attributes": {
"token": "token_expired",
"valid": false,
"username": null,
"email": null,
"usage_limit": 5,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": "2022-01-15T14:40:00Z",
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_expired"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z"
@@ -548,12 +575,15 @@ mod tests {
"attributes": {
"token": "token_revoked",
"valid": false,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": "2022-01-16T14:40:00Z"
"revoked_at": "2022-01-16T14:40:00Z",
"invite_url": "https://example.com/register?token=token_revoked"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC"
@@ -570,12 +600,15 @@ mod tests {
"attributes": {
"token": "token_unused",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_unused"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -623,12 +656,15 @@ mod tests {
"attributes": {
"token": "token_revoked",
"valid": false,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": "2022-01-16T14:40:00Z"
"revoked_at": "2022-01-16T14:40:00Z",
"invite_url": "https://example.com/register?token=token_revoked"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC"
@@ -645,12 +681,15 @@ mod tests {
"attributes": {
"token": "token_used_revoked",
"valid": false,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 1,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": "2022-01-16T14:40:00Z",
"expires_at": null,
"revoked_at": "2022-01-16T14:40:00Z"
"revoked_at": "2022-01-16T14:40:00Z",
"invite_url": "https://example.com/register?token=token_used_revoked"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN"
@@ -690,12 +729,15 @@ mod tests {
"attributes": {
"token": "token_expired",
"valid": false,
"username": null,
"email": null,
"usage_limit": 5,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": "2022-01-15T14:40:00Z",
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_expired"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z"
@@ -712,12 +754,15 @@ mod tests {
"attributes": {
"token": "token_used",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 1,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": "2022-01-16T14:40:00Z",
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_used"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6"
@@ -734,12 +779,15 @@ mod tests {
"attributes": {
"token": "token_unused",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_unused"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -787,12 +835,15 @@ mod tests {
"attributes": {
"token": "token_expired",
"valid": false,
"username": null,
"email": null,
"usage_limit": 5,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": "2022-01-15T14:40:00Z",
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_expired"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z"
@@ -832,12 +883,15 @@ mod tests {
"attributes": {
"token": "token_used",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 1,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": "2022-01-16T14:40:00Z",
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_used"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6"
@@ -854,12 +908,15 @@ mod tests {
"attributes": {
"token": "token_revoked",
"valid": false,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": "2022-01-16T14:40:00Z"
"revoked_at": "2022-01-16T14:40:00Z",
"invite_url": "https://example.com/register?token=token_revoked"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC"
@@ -876,12 +933,15 @@ mod tests {
"attributes": {
"token": "token_unused",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_unused"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -898,12 +958,15 @@ mod tests {
"attributes": {
"token": "token_used_revoked",
"valid": false,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 1,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": "2022-01-16T14:40:00Z",
"expires_at": null,
"revoked_at": "2022-01-16T14:40:00Z"
"revoked_at": "2022-01-16T14:40:00Z",
"invite_url": "https://example.com/register?token=token_used_revoked"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN"
@@ -951,12 +1014,15 @@ mod tests {
"attributes": {
"token": "token_used",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 1,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": "2022-01-16T14:40:00Z",
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_used"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6"
@@ -973,12 +1039,15 @@ mod tests {
"attributes": {
"token": "token_unused",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_unused"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -1018,12 +1087,15 @@ mod tests {
"attributes": {
"token": "token_expired",
"valid": false,
"username": null,
"email": null,
"usage_limit": 5,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": "2022-01-15T14:40:00Z",
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_expired"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z"
@@ -1040,12 +1112,15 @@ mod tests {
"attributes": {
"token": "token_revoked",
"valid": false,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": "2022-01-16T14:40:00Z"
"revoked_at": "2022-01-16T14:40:00Z",
"invite_url": "https://example.com/register?token=token_revoked"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC"
@@ -1062,12 +1137,15 @@ mod tests {
"attributes": {
"token": "token_used_revoked",
"valid": false,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 1,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": "2022-01-16T14:40:00Z",
"expires_at": null,
"revoked_at": "2022-01-16T14:40:00Z"
"revoked_at": "2022-01-16T14:40:00Z",
"invite_url": "https://example.com/register?token=token_used_revoked"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN"
@@ -1117,12 +1195,15 @@ mod tests {
"attributes": {
"token": "token_used_revoked",
"valid": false,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 1,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": "2022-01-16T14:40:00Z",
"expires_at": null,
"revoked_at": "2022-01-16T14:40:00Z"
"revoked_at": "2022-01-16T14:40:00Z",
"invite_url": "https://example.com/register?token=token_used_revoked"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN"
@@ -1170,12 +1251,15 @@ mod tests {
"attributes": {
"token": "token_expired",
"valid": false,
"username": null,
"email": null,
"usage_limit": 5,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": "2022-01-15T14:40:00Z",
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_expired"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z"
@@ -1192,12 +1276,15 @@ mod tests {
"attributes": {
"token": "token_used",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 1,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": "2022-01-16T14:40:00Z",
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_used"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6"
@@ -1238,12 +1325,15 @@ mod tests {
"attributes": {
"token": "token_revoked",
"valid": false,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": "2022-01-16T14:40:00Z"
"revoked_at": "2022-01-16T14:40:00Z",
"invite_url": "https://example.com/register?token=token_revoked"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC"
@@ -1260,12 +1350,15 @@ mod tests {
"attributes": {
"token": "token_unused",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_unused"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -1306,12 +1399,15 @@ mod tests {
"attributes": {
"token": "token_used_revoked",
"valid": false,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 1,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": "2022-01-16T14:40:00Z",
"expires_at": null,
"revoked_at": "2022-01-16T14:40:00Z"
"revoked_at": "2022-01-16T14:40:00Z",
"invite_url": "https://example.com/register?token=token_used_revoked"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN"
@@ -1378,12 +1474,15 @@ mod tests {
"attributes": {
"token": "token_expired",
"valid": false,
"username": null,
"email": null,
"usage_limit": 5,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": "2022-01-15T14:40:00Z",
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_expired"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z"
@@ -1400,12 +1499,15 @@ mod tests {
"attributes": {
"token": "token_used",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 1,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": "2022-01-16T14:40:00Z",
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_used"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6"
@@ -1422,12 +1524,15 @@ mod tests {
"attributes": {
"token": "token_revoked",
"valid": false,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": "2022-01-16T14:40:00Z"
"revoked_at": "2022-01-16T14:40:00Z",
"invite_url": "https://example.com/register?token=token_revoked"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC"
@@ -1444,12 +1549,15 @@ mod tests {
"attributes": {
"token": "token_unused",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_unused"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -1466,12 +1574,15 @@ mod tests {
"attributes": {
"token": "token_used_revoked",
"valid": false,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 1,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": "2022-01-16T14:40:00Z",
"expires_at": null,
"revoked_at": "2022-01-16T14:40:00Z"
"revoked_at": "2022-01-16T14:40:00Z",
"invite_url": "https://example.com/register?token=token_used_revoked"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN"
@@ -1526,12 +1637,15 @@ mod tests {
"attributes": {
"token": "token_used",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 1,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": "2022-01-16T14:40:00Z",
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_used"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6"
@@ -1548,12 +1662,15 @@ mod tests {
"attributes": {
"token": "token_unused",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=token_unused"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -1,3 +1,4 @@
// Copyright 2025, 2026 Element Creations Ltd.
// Copyright 2025 New Vector Ltd.
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
@@ -5,9 +6,10 @@
// Please see LICENSE files in the repository root for full details.
use aide::{OperationIo, transform::TransformOperation};
use axum::{Json, response::IntoResponse};
use axum::{Json, extract::State, response::IntoResponse};
use hyper::StatusCode;
use mas_axum_utils::record_error;
use mas_router::UrlBuilder;
use ulid::Ulid;
use crate::{
@@ -76,6 +78,7 @@ pub async fn handler(
CallContext {
mut repo, clock, ..
}: CallContext,
State(url_builder): State<UrlBuilder>,
id: UlidPathParam,
) -> Result<Json<SingleResponse<UserRegistrationToken>>, RouteError> {
let id = *id;
@@ -96,7 +99,7 @@ pub async fn handler(
repo.save().await?;
Ok(Json(SingleResponse::new(
UserRegistrationToken::new(token, clock.now()),
UserRegistrationToken::new(token, clock.now(), &url_builder),
format!("/api/admin/v1/user-registration-tokens/{id}/revoke"),
)))
}
@@ -1,3 +1,4 @@
// Copyright 2025, 2026 Element Creations Ltd.
// Copyright 2025 New Vector Ltd.
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
@@ -5,9 +6,10 @@
// Please see LICENSE files in the repository root for full details.
use aide::{OperationIo, transform::TransformOperation};
use axum::{Json, response::IntoResponse};
use axum::{Json, extract::State, response::IntoResponse};
use hyper::StatusCode;
use mas_axum_utils::record_error;
use mas_router::UrlBuilder;
use ulid::Ulid;
use crate::{
@@ -76,6 +78,7 @@ pub async fn handler(
CallContext {
mut repo, clock, ..
}: CallContext,
State(url_builder): State<UrlBuilder>,
id: UlidPathParam,
) -> Result<Json<SingleResponse<UserRegistrationToken>>, RouteError> {
let id = *id;
@@ -96,7 +99,7 @@ pub async fn handler(
repo.save().await?;
Ok(Json(SingleResponse::new(
UserRegistrationToken::new(token, clock.now()),
UserRegistrationToken::new(token, clock.now(), &url_builder),
format!("/api/admin/v1/user-registration-tokens/{id}/unrevoke"),
)))
}
@@ -160,12 +163,15 @@ mod tests {
"attributes": {
"token": "test_token_456",
"valid": true,
"username": null,
"email": null,
"usage_limit": 5,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=test_token_456"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -1,3 +1,4 @@
// Copyright 2025, 2026 Element Creations Ltd.
// Copyright 2025 New Vector Ltd.
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
@@ -5,10 +6,11 @@
// Please see LICENSE files in the repository root for full details.
use aide::{OperationIo, transform::TransformOperation};
use axum::{Json, response::IntoResponse};
use axum::{Json, extract::State, response::IntoResponse};
use chrono::{DateTime, Utc};
use hyper::StatusCode;
use mas_axum_utils::record_error;
use mas_router::UrlBuilder;
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer};
use ulid::Ulid;
@@ -103,6 +105,7 @@ pub async fn handler(
CallContext {
mut repo, clock, ..
}: CallContext,
State(url_builder): State<UrlBuilder>,
id: UlidPathParam,
Json(request): Json<Request>,
) -> Result<Json<SingleResponse<UserRegistrationToken>>, RouteError> {
@@ -134,7 +137,7 @@ pub async fn handler(
repo.save().await?;
Ok(Json(SingleResponse::new(
UserRegistrationToken::new(token, clock.now()),
UserRegistrationToken::new(token, clock.now(), &url_builder),
format!("/api/admin/v1/user-registration-tokens/{id}"),
)))
}
@@ -198,12 +201,15 @@ mod tests {
"attributes": {
"token": "test_update_expiry",
"valid": true,
"username": null,
"email": null,
"usage_limit": null,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": "2022-02-15T14:40:00Z",
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=test_update_expiry"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -238,12 +244,15 @@ mod tests {
"attributes": {
"token": "test_update_expiry",
"valid": true,
"username": null,
"email": null,
"usage_limit": null,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=test_update_expiry"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -304,12 +313,15 @@ mod tests {
"attributes": {
"token": "test_update_limit",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=test_update_limit"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -344,12 +356,15 @@ mod tests {
"attributes": {
"token": "test_update_limit",
"valid": true,
"username": null,
"email": null,
"usage_limit": null,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=test_update_limit"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -412,12 +427,15 @@ mod tests {
"attributes": {
"token": "test_update_multiple",
"valid": true,
"username": null,
"email": null,
"usage_limit": 20,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": "2022-02-15T14:40:00Z",
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=test_update_multiple"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
@@ -476,12 +494,15 @@ mod tests {
"attributes": {
"token": "test_update_none",
"valid": true,
"username": null,
"email": null,
"usage_limit": 5,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": "2022-02-15T14:40:00Z",
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=test_update_none"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
+34 -12
View File
@@ -274,6 +274,17 @@ impl SimpleRoute for Logout {
/// `GET|POST /register`
#[derive(Default, Debug, Clone)]
pub struct Register {
query: RegisterQuery,
}
/// The query parameters of the registration page
#[derive(Default, Debug, Clone, Serialize)]
pub struct RegisterQuery {
/// The registration token an invite link deep-links with
#[serde(skip_serializing_if = "Option::is_none")]
token: Option<String>,
#[serde(flatten)]
post_auth_action: Option<PostAuthAction>,
}
@@ -281,32 +292,38 @@ impl Register {
#[must_use]
pub fn and_then(action: PostAuthAction) -> Self {
Self {
post_auth_action: Some(action),
query: RegisterQuery {
token: None,
post_auth_action: Some(action),
},
}
}
#[must_use]
pub fn and_continue_grant(data: Ulid) -> Self {
Self {
post_auth_action: Some(PostAuthAction::continue_grant(data)),
}
Self::and_then(PostAuthAction::continue_grant(data))
}
#[must_use]
pub fn and_continue_compat_sso_login(data: Ulid) -> Self {
Self {
post_auth_action: Some(PostAuthAction::continue_compat_sso_login(data)),
}
Self::and_then(PostAuthAction::continue_compat_sso_login(data))
}
/// Deep-link to the registration page with a registration token
#[must_use]
pub fn with_token(mut self, token: String) -> Self {
self.query.token = Some(token);
self
}
/// Get a reference to the reauth's post auth action.
#[must_use]
pub fn post_auth_action(&self) -> Option<&PostAuthAction> {
self.post_auth_action.as_ref()
self.query.post_auth_action.as_ref()
}
pub fn go_next(&self, url_builder: &UrlBuilder) -> axum::response::Redirect {
match &self.post_auth_action {
match &self.query.post_auth_action {
Some(action) => action.go_next(url_builder),
None => url_builder.redirect(&Index),
}
@@ -314,20 +331,25 @@ impl Register {
}
impl Route for Register {
type Query = PostAuthAction;
type Query = RegisterQuery;
fn route() -> &'static str {
"/register"
}
fn query(&self) -> Option<&Self::Query> {
self.post_auth_action.as_ref()
Some(&self.query)
}
}
impl From<Option<PostAuthAction>> for Register {
fn from(post_auth_action: Option<PostAuthAction>) -> Self {
Self { post_auth_action }
Self {
query: RegisterQuery {
token: None,
post_auth_action,
},
}
}
}
+23
View File
@@ -1,3 +1,4 @@
// Copyright 2025, 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
//
@@ -32,6 +33,28 @@ mod tests {
);
}
#[test]
fn test_register_urls() {
assert_eq!(
Register::default().path_and_query(),
Cow::Borrowed("/register")
);
assert_eq!(
Register::default()
.with_token("a token&more".to_owned())
.path_and_query(),
Cow::Borrowed("/register?token=a+token%26more")
);
assert_eq!(
Register::and_continue_grant(Ulid::nil())
.with_token("invite".to_owned())
.path_and_query(),
Cow::Borrowed(
"/register?token=invite&kind=continue_authorization_grant&id=00000000000000000000000000"
)
);
}
#[test]
fn test_absolute_urls() {
let base = Url::try_from("https://example.com/").unwrap();
+49 -8
View File
@@ -3797,12 +3797,15 @@
"attributes": {
"token": "abc123def456",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 5,
"created_at": "1970-01-01T00:00:00Z",
"last_used_at": "1970-01-01T00:00:00Z",
"expires_at": "1970-01-31T00:00:00Z",
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=abc123def456"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01040G2081040G2081040G2081"
@@ -3819,12 +3822,15 @@
"attributes": {
"token": "xyz789abc012",
"valid": false,
"username": null,
"email": null,
"usage_limit": null,
"times_used": 0,
"created_at": "1970-01-01T00:00:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": "1970-01-01T00:00:00Z"
"revoked_at": "1970-01-01T00:00:00Z",
"invite_url": "https://example.com/register?token=xyz789abc012"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/02081040G2081040G2081040G2"
@@ -3879,12 +3885,15 @@
"attributes": {
"token": "abc123def456",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 5,
"created_at": "1970-01-01T00:00:00Z",
"last_used_at": "1970-01-01T00:00:00Z",
"expires_at": "1970-01-31T00:00:00Z",
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=abc123def456"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01040G2081040G2081040G2081"
@@ -3934,12 +3943,15 @@
"attributes": {
"token": "abc123def456",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 5,
"created_at": "1970-01-01T00:00:00Z",
"last_used_at": "1970-01-01T00:00:00Z",
"expires_at": "1970-01-31T00:00:00Z",
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=abc123def456"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01040G2081040G2081040G2081"
@@ -4015,12 +4027,15 @@
"attributes": {
"token": "abc123def456",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 5,
"created_at": "1970-01-01T00:00:00Z",
"last_used_at": "1970-01-01T00:00:00Z",
"expires_at": "1970-01-31T00:00:00Z",
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=abc123def456"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01040G2081040G2081040G2081"
@@ -4088,12 +4103,15 @@
"attributes": {
"token": "xyz789abc012",
"valid": false,
"username": null,
"email": null,
"usage_limit": null,
"times_used": 0,
"created_at": "1970-01-01T00:00:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": "1970-01-01T00:00:00Z"
"revoked_at": "1970-01-01T00:00:00Z",
"invite_url": "https://example.com/register?token=xyz789abc012"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/02081040G2081040G2081040G2"
@@ -4178,12 +4196,15 @@
"attributes": {
"token": "abc123def456",
"valid": true,
"username": null,
"email": null,
"usage_limit": 10,
"times_used": 5,
"created_at": "1970-01-01T00:00:00Z",
"last_used_at": "1970-01-01T00:00:00Z",
"expires_at": "1970-01-31T00:00:00Z",
"revoked_at": null
"revoked_at": null,
"invite_url": "https://example.com/register?token=abc123def456"
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01040G2081040G2081040G2081"
@@ -7310,6 +7331,20 @@
"description": "Whether the token is valid",
"type": "boolean"
},
"username": {
"description": "A username imposed on the registering user. If null, the user can\nchoose their own username.",
"type": [
"string",
"null"
]
},
"email": {
"description": "An email address imposed on the registering user. If null, the user\ncan choose their own email.",
"type": [
"string",
"null"
]
},
"usage_limit": {
"description": "Maximum number of times this token can be used",
"type": [
@@ -7353,13 +7388,19 @@
"null"
],
"format": "date-time"
},
"invite_url": {
"description": "URL to share with the user to register with this token",
"type": "string",
"format": "uri"
}
},
"required": [
"token",
"valid",
"times_used",
"created_at"
"created_at",
"invite_url"
]
},
"AddUserRegistrationTokenRequest": {