diff --git a/crates/cli/src/commands/manage.rs b/crates/cli/src/commands/manage.rs index 91716d3d3..0a40742ca 100644 --- a/crates/cli/src/commands/manage.rs +++ b/crates/cli/src/commands/manage.rs @@ -510,7 +510,15 @@ impl Options { // Create the token let registration_token = repo .user_registration_token() - .add(&mut rng, &clock, token_str, usage_limit, expires_at) + .add( + &mut rng, + &clock, + token_str, + usage_limit, + expires_at, + None, + None, + ) .await?; repo.into_inner().commit().await?; diff --git a/crates/data-model/src/users.rs b/crates/data-model/src/users.rs index eaa245d62..6ab4c2311 100644 --- a/crates/data-model/src/users.rs +++ b/crates/data-model/src/users.rs @@ -230,6 +230,8 @@ pub struct UserRegistrationPassword { pub struct UserRegistrationToken { pub id: Ulid, pub token: String, + pub username: Option, + pub email: Option, pub usage_limit: Option, pub times_used: u32, pub created_at: DateTime, diff --git a/crates/handlers/src/admin/v1/user_registration_tokens/add.rs b/crates/handlers/src/admin/v1/user_registration_tokens/add.rs index 111d40cd9..4b9c3cfe0 100644 --- a/crates/handlers/src/admin/v1/user_registration_tokens/add.rs +++ b/crates/handlers/src/admin/v1/user_registration_tokens/add.rs @@ -28,7 +28,7 @@ use crate::{ #[aide(output_with = "Json")] pub enum RouteError { #[error("A registration token with the same token already exists")] - Conflict(mas_data_model::UserRegistrationToken), + Conflict(Box), #[error(transparent)] Internal(Box), @@ -61,6 +61,14 @@ pub struct Request { /// When the token expires. If not provided, the token never expires. expires_at: Option>, + + /// A username to impose on the registering user. If set, the user cannot + /// choose their own username. + username: Option, + + /// An email address to impose on the registering user. If set, the user + /// cannot choose their own email. + email: Option, } pub fn doc(operation: TransformOperation) -> TransformOperation { @@ -92,7 +100,7 @@ pub async fn handler( // See if we have an existing token with the same token let existing_token = repo.user_registration_token().find_by_token(&token).await?; if let Some(existing_token) = existing_token { - return Err(RouteError::Conflict(existing_token)); + return Err(RouteError::Conflict(Box::new(existing_token))); } let registration_token = repo @@ -103,6 +111,8 @@ pub async fn handler( token, params.usage_limit, params.expires_at, + params.username, + params.email, ) .await?; diff --git a/crates/handlers/src/admin/v1/user_registration_tokens/get.rs b/crates/handlers/src/admin/v1/user_registration_tokens/get.rs index 187c19032..828ad849f 100644 --- a/crates/handlers/src/admin/v1/user_registration_tokens/get.rs +++ b/crates/handlers/src/admin/v1/user_registration_tokens/get.rs @@ -104,6 +104,8 @@ mod tests { "test_token_123".to_owned(), Some(5), None, + None, + None, ) .await .unwrap(); diff --git a/crates/handlers/src/admin/v1/user_registration_tokens/list.rs b/crates/handlers/src/admin/v1/user_registration_tokens/list.rs index 26e925401..cbbd119fd 100644 --- a/crates/handlers/src/admin/v1/user_registration_tokens/list.rs +++ b/crates/handlers/src/admin/v1/user_registration_tokens/list.rs @@ -206,6 +206,8 @@ mod tests { "token_unused".to_owned(), Some(10), None, + None, + None, ) .await .unwrap(); @@ -219,6 +221,8 @@ mod tests { "token_used".to_owned(), Some(10), None, + None, + None, ) .await .unwrap(); @@ -236,6 +240,8 @@ mod tests { "token_revoked".to_owned(), Some(10), None, + None, + None, ) .await .unwrap(); @@ -253,6 +259,8 @@ mod tests { "token_used_revoked".to_owned(), Some(10), None, + None, + None, ) .await .unwrap(); @@ -275,6 +283,8 @@ mod tests { "token_expired".to_owned(), Some(5), Some(expires_at), + None, + None, ) .await .unwrap(); diff --git a/crates/handlers/src/admin/v1/user_registration_tokens/revoke.rs b/crates/handlers/src/admin/v1/user_registration_tokens/revoke.rs index b2632d4b6..8a23dbf58 100644 --- a/crates/handlers/src/admin/v1/user_registration_tokens/revoke.rs +++ b/crates/handlers/src/admin/v1/user_registration_tokens/revoke.rs @@ -125,6 +125,8 @@ mod tests { "test_token_456".to_owned(), Some(5), None, + None, + None, ) .await .unwrap(); @@ -162,6 +164,8 @@ mod tests { "test_token_789".to_owned(), None, None, + None, + None, ) .await .unwrap(); diff --git a/crates/handlers/src/admin/v1/user_registration_tokens/unrevoke.rs b/crates/handlers/src/admin/v1/user_registration_tokens/unrevoke.rs index 212b7cdf9..fb862b030 100644 --- a/crates/handlers/src/admin/v1/user_registration_tokens/unrevoke.rs +++ b/crates/handlers/src/admin/v1/user_registration_tokens/unrevoke.rs @@ -125,6 +125,8 @@ mod tests { "test_token_456".to_owned(), Some(5), None, + None, + None, ) .await .unwrap(); @@ -191,6 +193,8 @@ mod tests { "test_token_789".to_owned(), None, None, + None, + None, ) .await .unwrap(); diff --git a/crates/handlers/src/admin/v1/user_registration_tokens/update.rs b/crates/handlers/src/admin/v1/user_registration_tokens/update.rs index 06ad8283e..f262fded9 100644 --- a/crates/handlers/src/admin/v1/user_registration_tokens/update.rs +++ b/crates/handlers/src/admin/v1/user_registration_tokens/update.rs @@ -166,6 +166,8 @@ mod tests { "test_update_expiry".to_owned(), None, None, + None, + None, ) .await .unwrap(); @@ -271,6 +273,8 @@ mod tests { "test_update_limit".to_owned(), Some(5), None, + None, + None, ) .await .unwrap(); @@ -375,6 +379,8 @@ mod tests { "test_update_multiple".to_owned(), None, None, + None, + None, ) .await .unwrap(); @@ -441,6 +447,8 @@ mod tests { "test_update_none".to_owned(), Some(5), Some(state.clock.now() + Duration::days(30)), + None, + None, ) .await .unwrap(); diff --git a/crates/storage-pg/.sqlx/query-fca331753aeccddbad96d06fc9d066dcefebe978a7af477bb6b55faa1d31e9b1.json b/crates/storage-pg/.sqlx/query-1ba7c2d8929e3c7d9af7afe92015d3a07eb4609827d97b3477b020059c8a2d4f.json similarity index 62% rename from crates/storage-pg/.sqlx/query-fca331753aeccddbad96d06fc9d066dcefebe978a7af477bb6b55faa1d31e9b1.json rename to crates/storage-pg/.sqlx/query-1ba7c2d8929e3c7d9af7afe92015d3a07eb4609827d97b3477b020059c8a2d4f.json index c5e2c6953..4d4086abd 100644 --- a/crates/storage-pg/.sqlx/query-fca331753aeccddbad96d06fc9d066dcefebe978a7af477bb6b55faa1d31e9b1.json +++ b/crates/storage-pg/.sqlx/query-1ba7c2d8929e3c7d9af7afe92015d3a07eb4609827d97b3477b020059c8a2d4f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT user_registration_token_id,\n token,\n usage_limit,\n times_used,\n created_at,\n last_used_at,\n expires_at,\n revoked_at\n FROM user_registration_tokens\n WHERE token = $1\n ", + "query": "\n SELECT user_registration_token_id,\n token,\n username,\n email,\n usage_limit,\n times_used,\n created_at,\n last_used_at,\n expires_at,\n revoked_at\n FROM user_registration_tokens\n WHERE token = $1\n ", "describe": { "columns": [ { @@ -15,31 +15,41 @@ }, { "ordinal": 2, + "name": "username", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "email", + "type_info": "Text" + }, + { + "ordinal": 4, "name": "usage_limit", "type_info": "Int4" }, { - "ordinal": 3, + "ordinal": 5, "name": "times_used", "type_info": "Int4" }, { - "ordinal": 4, + "ordinal": 6, "name": "created_at", "type_info": "Timestamptz" }, { - "ordinal": 5, + "ordinal": 7, "name": "last_used_at", "type_info": "Timestamptz" }, { - "ordinal": 6, + "ordinal": 8, "name": "expires_at", "type_info": "Timestamptz" }, { - "ordinal": 7, + "ordinal": 9, "name": "revoked_at", "type_info": "Timestamptz" } @@ -53,6 +63,8 @@ false, false, true, + true, + true, false, false, true, @@ -60,5 +72,5 @@ true ] }, - "hash": "fca331753aeccddbad96d06fc9d066dcefebe978a7af477bb6b55faa1d31e9b1" + "hash": "1ba7c2d8929e3c7d9af7afe92015d3a07eb4609827d97b3477b020059c8a2d4f" } diff --git a/crates/storage-pg/.sqlx/query-42ae3db131c3237ecbcc57451a09b72a72bbbda53d0f03e20c1471bc120c492b.json b/crates/storage-pg/.sqlx/query-42ae3db131c3237ecbcc57451a09b72a72bbbda53d0f03e20c1471bc120c492b.json new file mode 100644 index 000000000..356858c90 --- /dev/null +++ b/crates/storage-pg/.sqlx/query-42ae3db131c3237ecbcc57451a09b72a72bbbda53d0f03e20c1471bc120c492b.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE user_registration_tokens\n SET username = $2\n WHERE user_registration_token_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "42ae3db131c3237ecbcc57451a09b72a72bbbda53d0f03e20c1471bc120c492b" +} diff --git a/crates/storage-pg/.sqlx/query-d0355d4e98bec6120f17d8cf81ac8c30ed19e9cebd0c8e7c7918b1c3ca0e3cba.json b/crates/storage-pg/.sqlx/query-4cebc83f7699b61dd5704b82f25553eff02d3fb708071a60d2d5f55a35945218.json similarity index 61% rename from crates/storage-pg/.sqlx/query-d0355d4e98bec6120f17d8cf81ac8c30ed19e9cebd0c8e7c7918b1c3ca0e3cba.json rename to crates/storage-pg/.sqlx/query-4cebc83f7699b61dd5704b82f25553eff02d3fb708071a60d2d5f55a35945218.json index 2e20ac5ca..e3f531461 100644 --- a/crates/storage-pg/.sqlx/query-d0355d4e98bec6120f17d8cf81ac8c30ed19e9cebd0c8e7c7918b1c3ca0e3cba.json +++ b/crates/storage-pg/.sqlx/query-4cebc83f7699b61dd5704b82f25553eff02d3fb708071a60d2d5f55a35945218.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT user_registration_token_id,\n token,\n usage_limit,\n times_used,\n created_at,\n last_used_at,\n expires_at,\n revoked_at\n FROM user_registration_tokens\n WHERE user_registration_token_id = $1\n ", + "query": "\n SELECT user_registration_token_id,\n token,\n username,\n email,\n usage_limit,\n times_used,\n created_at,\n last_used_at,\n expires_at,\n revoked_at\n FROM user_registration_tokens\n WHERE user_registration_token_id = $1\n ", "describe": { "columns": [ { @@ -15,31 +15,41 @@ }, { "ordinal": 2, + "name": "username", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "email", + "type_info": "Text" + }, + { + "ordinal": 4, "name": "usage_limit", "type_info": "Int4" }, { - "ordinal": 3, + "ordinal": 5, "name": "times_used", "type_info": "Int4" }, { - "ordinal": 4, + "ordinal": 6, "name": "created_at", "type_info": "Timestamptz" }, { - "ordinal": 5, + "ordinal": 7, "name": "last_used_at", "type_info": "Timestamptz" }, { - "ordinal": 6, + "ordinal": 8, "name": "expires_at", "type_info": "Timestamptz" }, { - "ordinal": 7, + "ordinal": 9, "name": "revoked_at", "type_info": "Timestamptz" } @@ -53,6 +63,8 @@ false, false, true, + true, + true, false, false, true, @@ -60,5 +72,5 @@ true ] }, - "hash": "d0355d4e98bec6120f17d8cf81ac8c30ed19e9cebd0c8e7c7918b1c3ca0e3cba" + "hash": "4cebc83f7699b61dd5704b82f25553eff02d3fb708071a60d2d5f55a35945218" } diff --git a/crates/storage-pg/.sqlx/query-89edaec8661e435c3b71bb9b995cd711eb78a4d39608e897432d6124cd135938.json b/crates/storage-pg/.sqlx/query-8e001a260d28c537f525e35f70e5cc4ea771124820e2f6f8cfc5b958ab21eecc.json similarity index 57% rename from crates/storage-pg/.sqlx/query-89edaec8661e435c3b71bb9b995cd711eb78a4d39608e897432d6124cd135938.json rename to crates/storage-pg/.sqlx/query-8e001a260d28c537f525e35f70e5cc4ea771124820e2f6f8cfc5b958ab21eecc.json index f04a39a7f..b0f713498 100644 --- a/crates/storage-pg/.sqlx/query-89edaec8661e435c3b71bb9b995cd711eb78a4d39608e897432d6124cd135938.json +++ b/crates/storage-pg/.sqlx/query-8e001a260d28c537f525e35f70e5cc4ea771124820e2f6f8cfc5b958ab21eecc.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO user_registration_tokens\n (user_registration_token_id, token, usage_limit, created_at, expires_at)\n VALUES ($1, $2, $3, $4, $5)\n ", + "query": "\n INSERT INTO user_registration_tokens\n (user_registration_token_id, token, usage_limit, created_at, expires_at, username, email)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ", "describe": { "columns": [], "parameters": { @@ -9,10 +9,12 @@ "Text", "Int4", "Timestamptz", - "Timestamptz" + "Timestamptz", + "Text", + "Text" ] }, "nullable": [] }, - "hash": "89edaec8661e435c3b71bb9b995cd711eb78a4d39608e897432d6124cd135938" + "hash": "8e001a260d28c537f525e35f70e5cc4ea771124820e2f6f8cfc5b958ab21eecc" } diff --git a/crates/storage-pg/.sqlx/query-e73b3604e554d17a8d2672c1f32b80274c6062889c2ba650049694bb4f7b2aa7.json b/crates/storage-pg/.sqlx/query-e73b3604e554d17a8d2672c1f32b80274c6062889c2ba650049694bb4f7b2aa7.json new file mode 100644 index 000000000..1f2b0e2d9 --- /dev/null +++ b/crates/storage-pg/.sqlx/query-e73b3604e554d17a8d2672c1f32b80274c6062889c2ba650049694bb4f7b2aa7.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE user_registration_tokens\n SET email = $2\n WHERE user_registration_token_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "e73b3604e554d17a8d2672c1f32b80274c6062889c2ba650049694bb4f7b2aa7" +} diff --git a/crates/storage-pg/migrations/20260327120000_registration_token_username_email.sql b/crates/storage-pg/migrations/20260327120000_registration_token_username_email.sql new file mode 100644 index 000000000..ec6c056cb --- /dev/null +++ b/crates/storage-pg/migrations/20260327120000_registration_token_username_email.sql @@ -0,0 +1,10 @@ +-- 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. + +-- Add optional username and email columns to registration tokens, +-- allowing admins to provision tokens that pre-fill these fields. +ALTER TABLE "user_registration_tokens" + ADD COLUMN "username" TEXT, + ADD COLUMN "email" TEXT; diff --git a/crates/storage-pg/src/iden.rs b/crates/storage-pg/src/iden.rs index 0a9871046..ec522a2d9 100644 --- a/crates/storage-pg/src/iden.rs +++ b/crates/storage-pg/src/iden.rs @@ -210,6 +210,8 @@ pub enum UserRegistrationTokens { Table, UserRegistrationTokenId, Token, + Username, + Email, UsageLimit, TimesUsed, CreatedAt, diff --git a/crates/storage-pg/src/user/registration_token.rs b/crates/storage-pg/src/user/registration_token.rs index 464afbb13..c1c53645e 100644 --- a/crates/storage-pg/src/user/registration_token.rs +++ b/crates/storage-pg/src/user/registration_token.rs @@ -1,4 +1,4 @@ -// Copyright 2026 Element Creations Ltd. +// Copyright 2025, 2026 Element Creations Ltd. // Copyright 2025 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial @@ -47,6 +47,8 @@ impl<'c> PgUserRegistrationTokenRepository<'c> { struct UserRegistrationTokenLookup { user_registration_token_id: Uuid, token: String, + username: Option, + email: Option, usage_limit: Option, times_used: i32, created_at: DateTime, @@ -211,6 +213,8 @@ impl TryFrom for UserRegistrationToken { Ok(UserRegistrationToken { id, token: res.token, + username: res.username, + email: res.email, usage_limit, times_used, created_at: res.created_at, @@ -250,6 +254,17 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> { Expr::col((UserRegistrationTokens::Table, UserRegistrationTokens::Token)), UserRegistrationTokenLookupIden::Token, ) + .expr_as( + Expr::col(( + UserRegistrationTokens::Table, + UserRegistrationTokens::Username, + )), + UserRegistrationTokenLookupIden::Username, + ) + .expr_as( + Expr::col((UserRegistrationTokens::Table, UserRegistrationTokens::Email)), + UserRegistrationTokenLookupIden::Email, + ) .expr_as( Expr::col(( UserRegistrationTokens::Table, @@ -362,6 +377,8 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> { r#" SELECT user_registration_token_id, token, + username, + email, usage_limit, times_used, created_at, @@ -402,6 +419,8 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> { r#" SELECT user_registration_token_id, token, + username, + email, usage_limit, times_used, created_at, @@ -440,6 +459,8 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> { token: String, usage_limit: Option, expires_at: Option>, + username: Option, + email: Option, ) -> Result { let created_at = clock.now(); let id = Ulid::from_datetime_with_rng(created_at, rng); @@ -452,14 +473,16 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> { sqlx::query!( r#" INSERT INTO user_registration_tokens - (user_registration_token_id, token, usage_limit, created_at, expires_at) - VALUES ($1, $2, $3, $4, $5) + (user_registration_token_id, token, usage_limit, created_at, expires_at, username, email) + VALUES ($1, $2, $3, $4, $5, $6, $7) "#, Uuid::from(id), &token, usage_limit_i32, created_at, expires_at, + username.as_deref(), + email.as_deref(), ) .traced() .execute(&mut *self.conn) @@ -468,6 +491,8 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> { Ok(UserRegistrationToken { id, token, + username, + email, usage_limit, times_used: 0, created_at, @@ -657,6 +682,74 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> { Ok(token) } + + #[tracing::instrument( + name = "db.user_registration_token.set_username", + skip_all, + fields( + db.query.text, + user_registration_token.id = %token.id, + ), + err, + )] + async fn set_username( + &mut self, + mut token: UserRegistrationToken, + username: Option, + ) -> Result { + let res = sqlx::query!( + r#" + UPDATE user_registration_tokens + SET username = $2 + WHERE user_registration_token_id = $1 + "#, + Uuid::from(token.id), + username.as_deref(), + ) + .traced() + .execute(&mut *self.conn) + .await?; + + DatabaseError::ensure_affected_rows(&res, 1)?; + + token.username = username; + + Ok(token) + } + + #[tracing::instrument( + name = "db.user_registration_token.set_email", + skip_all, + fields( + db.query.text, + user_registration_token.id = %token.id, + ), + err, + )] + async fn set_email( + &mut self, + mut token: UserRegistrationToken, + email: Option, + ) -> Result { + let res = sqlx::query!( + r#" + UPDATE user_registration_tokens + SET email = $2 + WHERE user_registration_token_id = $1 + "#, + Uuid::from(token.id), + email.as_deref(), + ) + .traced() + .execute(&mut *self.conn) + .await?; + + DatabaseError::ensure_affected_rows(&res, 1)?; + + token.email = email; + + Ok(token) + } } #[cfg(test)] @@ -680,7 +773,15 @@ mod tests { // Create a token let token = repo .user_registration_token() - .add(&mut rng, &clock, "test_token".to_owned(), None, None) + .add( + &mut rng, + &clock, + "test_token".to_owned(), + None, + None, + None, + None, + ) .await .unwrap(); @@ -725,7 +826,15 @@ mod tests { // Create a token without expiry let token = repo .user_registration_token() - .add(&mut rng, &clock, "test_token_expiry".to_owned(), None, None) + .add( + &mut rng, + &clock, + "test_token_expiry".to_owned(), + None, + None, + None, + None, + ) .await .unwrap(); @@ -764,7 +873,15 @@ mod tests { // Create a token without usage limit let token = repo .user_registration_token() - .add(&mut rng, &clock, "test_token_limit".to_owned(), None, None) + .add( + &mut rng, + &clock, + "test_token_limit".to_owned(), + None, + None, + None, + None, + ) .await .unwrap(); @@ -813,14 +930,30 @@ mod tests { // 1. A regular token let _token1 = repo .user_registration_token() - .add(&mut rng, &clock, "token1".to_owned(), None, None) + .add( + &mut rng, + &clock, + "token1".to_owned(), + None, + None, + None, + None, + ) .await .unwrap(); // 2. A token that has been used let token2 = repo .user_registration_token() - .add(&mut rng, &clock, "token2".to_owned(), None, None) + .add( + &mut rng, + &clock, + "token2".to_owned(), + None, + None, + None, + None, + ) .await .unwrap(); let token2 = repo @@ -833,14 +966,30 @@ mod tests { let past_time = clock.now() - Duration::days(1); let token3 = repo .user_registration_token() - .add(&mut rng, &clock, "token3".to_owned(), None, Some(past_time)) + .add( + &mut rng, + &clock, + "token3".to_owned(), + None, + Some(past_time), + None, + None, + ) .await .unwrap(); // 4. A token that is revoked let token4 = repo .user_registration_token() - .add(&mut rng, &clock, "token4".to_owned(), None, None) + .add( + &mut rng, + &clock, + "token4".to_owned(), + None, + None, + None, + None, + ) .await .unwrap(); let token4 = repo diff --git a/crates/storage/src/user/registration_token.rs b/crates/storage/src/user/registration_token.rs index db0df9e57..ca591f0de 100644 --- a/crates/storage/src/user/registration_token.rs +++ b/crates/storage/src/user/registration_token.rs @@ -150,10 +150,13 @@ pub trait UserRegistrationTokenRepository: Send + Sync { /// * `token`: The token string /// * `usage_limit`: Optional limit on how many times the token can be used /// * `expires_at`: Optional expiration time for the token + /// * `username`: Optional username to impose on the registering user + /// * `email`: Optional email to impose on the registering user /// /// # Errors /// /// Returns [`Self::Error`] if the underlying repository fails + #[expect(clippy::too_many_arguments)] async fn add( &mut self, rng: &mut (dyn Rng + Send), @@ -161,6 +164,8 @@ pub trait UserRegistrationTokenRepository: Send + Sync { token: String, usage_limit: Option, expires_at: Option>, + username: Option, + email: Option, ) -> Result; /// Increment the usage count of a [`UserRegistrationToken`] @@ -244,6 +249,38 @@ pub trait UserRegistrationTokenRepository: Send + Sync { usage_limit: Option, ) -> Result; + /// Set or clear the username imposed by this [`UserRegistrationToken`] + /// + /// # Parameters + /// + /// * `token`: The [`UserRegistrationToken`] to update + /// * `username`: The new username, or `None` to clear it + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn set_username( + &mut self, + token: UserRegistrationToken, + username: Option, + ) -> Result; + + /// Set or clear the email imposed by this [`UserRegistrationToken`] + /// + /// # Parameters + /// + /// * `token`: The [`UserRegistrationToken`] to update + /// * `email`: The new email, or `None` to clear it + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn set_email( + &mut self, + token: UserRegistrationToken, + email: Option, + ) -> Result; + /// List [`UserRegistrationToken`]s based on the provided filter /// /// Returns a list of matching [`UserRegistrationToken`]s @@ -286,6 +323,8 @@ repository_impl!(UserRegistrationTokenRepository: token: String, usage_limit: Option, expires_at: Option>, + username: Option, + email: Option, ) -> Result; async fn use_token( &mut self, @@ -311,6 +350,16 @@ repository_impl!(UserRegistrationTokenRepository: token: UserRegistrationToken, usage_limit: Option, ) -> Result; + async fn set_username( + &mut self, + token: UserRegistrationToken, + username: Option, + ) -> Result; + async fn set_email( + &mut self, + token: UserRegistrationToken, + email: Option, + ) -> Result; async fn list( &mut self, filter: UserRegistrationTokenFilter, diff --git a/docs/api/spec.json b/docs/api/spec.json index 8f4cbbce4..ef2580265 100644 --- a/docs/api/spec.json +++ b/docs/api/spec.json @@ -7389,6 +7389,20 @@ "null" ], "format": "date-time" + }, + "username": { + "description": "A username to impose on the registering user. If set, the user cannot\nchoose their own username.", + "type": [ + "string", + "null" + ] + }, + "email": { + "description": "An email address to impose on the registering user. If set, the user\ncannot choose their own email.", + "type": [ + "string", + "null" + ] } } },