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:
Quentin Gliech
2026-09-10 18:53:59 +02:00
parent f34d679fca
commit 2c08dfb304
20 changed files with 219 additions and 19 deletions
+2
View File
@@ -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?;
+2
View File
@@ -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>,
+7
View File
@@ -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,
}))
}
+1
View File
@@ -61,6 +61,7 @@ mod tests {
None,
None,
None,
false,
)
.await
.unwrap();
@@ -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"
}
@@ -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"
}
@@ -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;
+1
View File
@@ -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,
+17
View File
@@ -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
}
}
},
+4
View File
@@ -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!
}
"""