mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-10-15 04:31:20 +00:00
Add a passwordless flag to registration tokens
A registration token can now be marked `passwordless`, which will let the user register without picking a password: their identity is established by the email verification code instead. This only adds the column and plumbs it through the storage layer, the admin API and the anonymous `registrationToken` GraphQL query; the registration flow itself is unchanged for now.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// Copyright 2025, 2026 Element Creations Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
@@ -515,6 +516,7 @@ impl Options {
|
||||
expires_at,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Copyright 2025, 2026 Element Creations Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
@@ -231,6 +232,7 @@ pub struct UserRegistrationToken {
|
||||
pub token: String,
|
||||
pub username: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub passwordless: bool,
|
||||
pub usage_limit: Option<u32>,
|
||||
pub times_used: u32,
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
@@ -640,6 +640,10 @@ pub struct UserRegistrationToken {
|
||||
/// can choose their own email.
|
||||
email: Option<String>,
|
||||
|
||||
/// Whether registering with this token skips the password step. The user's
|
||||
/// identity is then established by verifying their email address.
|
||||
passwordless: bool,
|
||||
|
||||
/// Maximum number of times this token can be used
|
||||
usage_limit: Option<u32>,
|
||||
|
||||
@@ -676,6 +680,7 @@ impl UserRegistrationToken {
|
||||
token: token.token,
|
||||
username: token.username,
|
||||
email: token.email,
|
||||
passwordless: token.passwordless,
|
||||
usage_limit: token.usage_limit,
|
||||
times_used: token.times_used,
|
||||
created_at: token.created_at,
|
||||
@@ -706,6 +711,7 @@ impl UserRegistrationToken {
|
||||
valid: true,
|
||||
username: None,
|
||||
email: None,
|
||||
passwordless: false,
|
||||
usage_limit: Some(10),
|
||||
times_used: 5,
|
||||
created_at: DateTime::default(),
|
||||
@@ -720,6 +726,7 @@ impl UserRegistrationToken {
|
||||
valid: false,
|
||||
username: None,
|
||||
email: None,
|
||||
passwordless: false,
|
||||
usage_limit: None,
|
||||
times_used: 0,
|
||||
created_at: DateTime::default(),
|
||||
|
||||
@@ -70,6 +70,11 @@ pub struct Request {
|
||||
/// An email address to impose on the registering user. If set, the user
|
||||
/// cannot choose their own email.
|
||||
email: Option<String>,
|
||||
|
||||
/// Whether registering with this token skips the password step. The user's
|
||||
/// identity is then established by verifying their email address.
|
||||
#[serde(default)]
|
||||
passwordless: bool,
|
||||
}
|
||||
|
||||
pub fn doc(operation: TransformOperation) -> TransformOperation {
|
||||
@@ -115,6 +120,7 @@ pub async fn handler(
|
||||
params.expires_at,
|
||||
params.username,
|
||||
params.email,
|
||||
params.passwordless,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -164,6 +170,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 5,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -209,6 +216,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 1,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -228,6 +236,54 @@ mod tests {
|
||||
"#);
|
||||
}
|
||||
|
||||
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
|
||||
async fn test_create_passwordless(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": "invite_alice",
|
||||
"username": "alice",
|
||||
"email": "alice@example.com",
|
||||
"passwordless": true,
|
||||
}));
|
||||
let response = state.request(request).await;
|
||||
response.assert_status(StatusCode::CREATED);
|
||||
let body: serde_json::Value = response.json();
|
||||
|
||||
assert_json_snapshot!(body, @r#"
|
||||
{
|
||||
"data": {
|
||||
"type": "user-registration_token",
|
||||
"id": "01FSHN9AG0MZAA6S4AF7CTV32E",
|
||||
"attributes": {
|
||||
"token": "invite_alice",
|
||||
"valid": true,
|
||||
"username": "alice",
|
||||
"email": "alice@example.com",
|
||||
"passwordless": true,
|
||||
"usage_limit": null,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
"last_used_at": null,
|
||||
"expires_at": null,
|
||||
"revoked_at": null,
|
||||
"invite_url": "https://example.com/invite/invite_alice"
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
|
||||
}
|
||||
}
|
||||
"#);
|
||||
}
|
||||
|
||||
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
|
||||
async fn test_create_conflict(pool: PgPool) {
|
||||
setup();
|
||||
@@ -255,6 +311,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 5,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
|
||||
@@ -109,6 +109,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -134,6 +135,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 5,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
|
||||
@@ -211,6 +211,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -226,6 +227,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -245,6 +247,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -264,6 +267,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -288,6 +292,7 @@ mod tests {
|
||||
Some(expires_at),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -323,6 +328,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 5,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -348,6 +354,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 1,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -373,6 +380,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -398,6 +406,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -423,6 +432,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 1,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -479,6 +489,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 1,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -504,6 +515,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 1,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -552,6 +564,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 5,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -577,6 +590,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -602,6 +616,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -658,6 +673,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -683,6 +699,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 1,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -731,6 +748,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 5,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -756,6 +774,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 1,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -781,6 +800,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -837,6 +857,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 5,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -885,6 +906,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 1,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -910,6 +932,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -935,6 +958,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -960,6 +984,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 1,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1016,6 +1041,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 1,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1041,6 +1067,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1089,6 +1116,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 5,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1114,6 +1142,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1139,6 +1168,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 1,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1197,6 +1227,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 1,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1253,6 +1284,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 5,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1278,6 +1310,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 1,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1327,6 +1360,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1352,6 +1386,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1401,6 +1436,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 1,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1476,6 +1512,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 5,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1501,6 +1538,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 1,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1526,6 +1564,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1551,6 +1590,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1576,6 +1616,7 @@ mod tests {
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 1,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1639,6 +1680,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 1,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -1664,6 +1706,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
|
||||
@@ -130,6 +130,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -169,6 +170,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -130,6 +130,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -165,6 +166,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 5,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -201,6 +203,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -171,6 +171,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -203,6 +204,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": null,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -246,6 +248,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": null,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -284,6 +287,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -315,6 +319,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -358,6 +363,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": null,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -396,6 +402,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -429,6 +436,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 20,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
@@ -467,6 +475,7 @@ mod tests {
|
||||
Some(state.clock.now() + Duration::days(30)),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -496,6 +505,7 @@ mod tests {
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 5,
|
||||
"times_used": 0,
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
|
||||
@@ -38,6 +38,8 @@ struct RegistrationTokenInfo {
|
||||
username: Option<String>,
|
||||
/// An email imposed by this token, if any.
|
||||
email: Option<String>,
|
||||
/// Whether registering with this token skips the password step.
|
||||
passwordless: bool,
|
||||
}
|
||||
|
||||
/// The result of a username availability check.
|
||||
@@ -206,6 +208,7 @@ impl UserQuery {
|
||||
valid: registration_token.is_valid(state.clock().now()),
|
||||
username: registration_token.username,
|
||||
email: registration_token.email,
|
||||
passwordless: registration_token.passwordless,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
+13
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"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 ",
|
||||
"query": "\n SELECT user_registration_token_id,\n token,\n username,\n email,\n passwordless,\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": [
|
||||
{
|
||||
@@ -25,31 +25,36 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "passwordless",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "usage_limit",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "times_used",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "last_used_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "revoked_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
@@ -64,6 +69,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
@@ -72,5 +78,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "1ba7c2d8929e3c7d9af7afe92015d3a07eb4609827d97b3477b020059c8a2d4f"
|
||||
"hash": "47233b98ede94299e4a119d8accdc9dbfe25790570d3bc2ea07287bf6631bcbd"
|
||||
}
|
||||
+4
-3
@@ -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, username, email)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ",
|
||||
"query": "\n INSERT INTO user_registration_tokens\n (user_registration_token_id, token, usage_limit, created_at, expires_at, username, email, passwordless)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -11,10 +11,11 @@
|
||||
"Timestamptz",
|
||||
"Timestamptz",
|
||||
"Text",
|
||||
"Text"
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8e001a260d28c537f525e35f70e5cc4ea771124820e2f6f8cfc5b958ab21eecc"
|
||||
"hash": "9d38f660331b79c0eaf03ec74c3d90357408c573ba300d1bfe95c9bcd7bd7870"
|
||||
}
|
||||
+13
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"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 ",
|
||||
"query": "\n SELECT user_registration_token_id,\n token,\n username,\n email,\n passwordless,\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": [
|
||||
{
|
||||
@@ -25,31 +25,36 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "passwordless",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "usage_limit",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "times_used",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "last_used_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "revoked_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
@@ -64,6 +69,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
@@ -72,5 +78,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "4cebc83f7699b61dd5704b82f25553eff02d3fb708071a60d2d5f55a35945218"
|
||||
"hash": "a3e55b75ae98f025db44d3d0dfd88382ce4f53810a86d43d2eeddad758ccb870"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
-- 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.
|
||||
|
||||
-- Whether registrations using this token skip the password step entirely
|
||||
ALTER TABLE user_registration_tokens
|
||||
ADD COLUMN passwordless BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -212,6 +212,7 @@ pub enum UserRegistrationTokens {
|
||||
Token,
|
||||
Username,
|
||||
Email,
|
||||
Passwordless,
|
||||
UsageLimit,
|
||||
TimesUsed,
|
||||
CreatedAt,
|
||||
|
||||
@@ -49,6 +49,7 @@ struct UserRegistrationTokenLookup {
|
||||
token: String,
|
||||
username: Option<String>,
|
||||
email: Option<String>,
|
||||
passwordless: bool,
|
||||
usage_limit: Option<i32>,
|
||||
times_used: i32,
|
||||
created_at: DateTime<Utc>,
|
||||
@@ -215,6 +216,7 @@ impl TryFrom<UserRegistrationTokenLookup> for UserRegistrationToken {
|
||||
token: res.token,
|
||||
username: res.username,
|
||||
email: res.email,
|
||||
passwordless: res.passwordless,
|
||||
usage_limit,
|
||||
times_used,
|
||||
created_at: res.created_at,
|
||||
@@ -265,6 +267,13 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> {
|
||||
Expr::col((UserRegistrationTokens::Table, UserRegistrationTokens::Email)),
|
||||
UserRegistrationTokenLookupIden::Email,
|
||||
)
|
||||
.expr_as(
|
||||
Expr::col((
|
||||
UserRegistrationTokens::Table,
|
||||
UserRegistrationTokens::Passwordless,
|
||||
)),
|
||||
UserRegistrationTokenLookupIden::Passwordless,
|
||||
)
|
||||
.expr_as(
|
||||
Expr::col((
|
||||
UserRegistrationTokens::Table,
|
||||
@@ -379,6 +388,7 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> {
|
||||
token,
|
||||
username,
|
||||
email,
|
||||
passwordless,
|
||||
usage_limit,
|
||||
times_used,
|
||||
created_at,
|
||||
@@ -421,6 +431,7 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> {
|
||||
token,
|
||||
username,
|
||||
email,
|
||||
passwordless,
|
||||
usage_limit,
|
||||
times_used,
|
||||
created_at,
|
||||
@@ -461,6 +472,7 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> {
|
||||
expires_at: Option<DateTime<Utc>>,
|
||||
username: Option<String>,
|
||||
email: Option<String>,
|
||||
passwordless: bool,
|
||||
) -> Result<UserRegistrationToken, Self::Error> {
|
||||
let created_at = clock.now();
|
||||
let id = Ulid::from_datetime_with_rng(created_at, rng);
|
||||
@@ -473,8 +485,8 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO user_registration_tokens
|
||||
(user_registration_token_id, token, usage_limit, created_at, expires_at, username, email)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
(user_registration_token_id, token, usage_limit, created_at, expires_at, username, email, passwordless)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
"#,
|
||||
Uuid::from(id),
|
||||
&token,
|
||||
@@ -483,6 +495,7 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> {
|
||||
expires_at,
|
||||
username.as_deref(),
|
||||
email.as_deref(),
|
||||
passwordless,
|
||||
)
|
||||
.traced()
|
||||
.execute(&mut *self.conn)
|
||||
@@ -493,6 +506,7 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> {
|
||||
token,
|
||||
username,
|
||||
email,
|
||||
passwordless,
|
||||
usage_limit,
|
||||
times_used: 0,
|
||||
created_at,
|
||||
@@ -781,6 +795,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -834,6 +849,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -881,6 +897,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -938,6 +955,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -953,6 +971,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -974,6 +993,7 @@ mod tests {
|
||||
Some(past_time),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -989,6 +1009,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -152,6 +152,8 @@ pub trait UserRegistrationTokenRepository: Send + Sync {
|
||||
/// * `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
|
||||
/// * `passwordless`: Whether registrations using this token skip the
|
||||
/// password step
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -166,6 +168,7 @@ pub trait UserRegistrationTokenRepository: Send + Sync {
|
||||
expires_at: Option<DateTime<Utc>>,
|
||||
username: Option<String>,
|
||||
email: Option<String>,
|
||||
passwordless: bool,
|
||||
) -> Result<UserRegistrationToken, Self::Error>;
|
||||
|
||||
/// Increment the usage count of a [`UserRegistrationToken`]
|
||||
@@ -325,6 +328,7 @@ repository_impl!(UserRegistrationTokenRepository:
|
||||
expires_at: Option<DateTime<Utc>>,
|
||||
username: Option<String>,
|
||||
email: Option<String>,
|
||||
passwordless: bool,
|
||||
) -> Result<UserRegistrationToken, Self::Error>;
|
||||
async fn use_token(
|
||||
&mut self,
|
||||
|
||||
@@ -3799,6 +3799,7 @@
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 5,
|
||||
"created_at": "1970-01-01T00:00:00Z",
|
||||
@@ -3824,6 +3825,7 @@
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": null,
|
||||
"times_used": 0,
|
||||
"created_at": "1970-01-01T00:00:00Z",
|
||||
@@ -3887,6 +3889,7 @@
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 5,
|
||||
"created_at": "1970-01-01T00:00:00Z",
|
||||
@@ -3945,6 +3948,7 @@
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 5,
|
||||
"created_at": "1970-01-01T00:00:00Z",
|
||||
@@ -4029,6 +4033,7 @@
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 5,
|
||||
"created_at": "1970-01-01T00:00:00Z",
|
||||
@@ -4105,6 +4110,7 @@
|
||||
"valid": false,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": null,
|
||||
"times_used": 0,
|
||||
"created_at": "1970-01-01T00:00:00Z",
|
||||
@@ -4198,6 +4204,7 @@
|
||||
"valid": true,
|
||||
"username": null,
|
||||
"email": null,
|
||||
"passwordless": false,
|
||||
"usage_limit": 10,
|
||||
"times_used": 5,
|
||||
"created_at": "1970-01-01T00:00:00Z",
|
||||
@@ -7345,6 +7352,10 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"passwordless": {
|
||||
"description": "Whether registering with this token skips the password step. The user's\nidentity is then established by verifying their email address.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"usage_limit": {
|
||||
"description": "Maximum number of times this token can be used",
|
||||
"type": [
|
||||
@@ -7398,6 +7409,7 @@
|
||||
"required": [
|
||||
"token",
|
||||
"valid",
|
||||
"passwordless",
|
||||
"times_used",
|
||||
"created_at",
|
||||
"invite_url"
|
||||
@@ -7444,6 +7456,11 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"passwordless": {
|
||||
"description": "Whether registering with this token skips the password step. The user's\nidentity is then established by verifying their email address.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1298,6 +1298,10 @@ type RegistrationTokenInfo {
|
||||
An email imposed by this token, if any.
|
||||
"""
|
||||
email: String
|
||||
"""
|
||||
Whether registering with this token skips the password step.
|
||||
"""
|
||||
passwordless: Boolean!
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user