mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-09-17 03:44:55 +00:00
Data model and storage layer for storing user registrations
This commit is contained in:
@@ -48,6 +48,6 @@ pub use self::{
|
||||
users::{
|
||||
Authentication, AuthenticationMethod, BrowserSession, Password, User, UserEmail,
|
||||
UserEmailAuthentication, UserEmailAuthenticationCode, UserRecoverySession,
|
||||
UserRecoveryTicket,
|
||||
UserRecoveryTicket, UserRegistration, UserRegistrationPassword,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ use chrono::{DateTime, Utc};
|
||||
use rand::Rng;
|
||||
use serde::Serialize;
|
||||
use ulid::Ulid;
|
||||
use url::Url;
|
||||
|
||||
use crate::UserAgent;
|
||||
|
||||
@@ -112,6 +113,7 @@ impl UserRecoveryTicket {
|
||||
pub struct UserEmailAuthentication {
|
||||
pub id: Ulid,
|
||||
pub user_session_id: Option<Ulid>,
|
||||
pub user_registration_id: Option<Ulid>,
|
||||
pub email: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
@@ -192,3 +194,24 @@ impl UserEmail {
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct UserRegistrationPassword {
|
||||
pub hashed_password: String,
|
||||
pub version: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct UserRegistration {
|
||||
pub id: Ulid,
|
||||
pub username: String,
|
||||
pub display_name: Option<String>,
|
||||
pub terms_url: Option<Url>,
|
||||
pub email_authentication_id: Option<Ulid>,
|
||||
pub password: Option<UserRegistrationPassword>,
|
||||
pub post_auth_action: Option<serde_json::Value>,
|
||||
pub ip_address: Option<IpAddr>,
|
||||
pub user_agent: Option<UserAgent>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
Generated
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO user_email_authentications\n ( user_email_authentication_id\n , user_registration_id\n , email\n , created_at\n )\n VALUES ($1, $2, $3, $4)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0e1bce56e15751d82a622d532b279bfc50e22cb12ddf7495c7b0fedca61f9421"
|
||||
}
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE user_registrations\n SET terms_url = $2\n WHERE user_registration_id = $1 AND completed_at IS NULL\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "188a4aeef5a8b4bf3230c7176ded64d52804848df378dc74f8f54ec4404e094e"
|
||||
}
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE user_registrations\n SET email_authentication_id = $2\n WHERE user_registration_id = $1 AND completed_at IS NULL\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4968c60adef69c7215a7efe2021baffb050b2f475ae106155c2e2f210a81191a"
|
||||
}
|
||||
Generated
+88
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT user_registration_id\n , ip_address as \"ip_address: IpAddr\"\n , user_agent\n , post_auth_action\n , username\n , display_name\n , terms_url\n , email_authentication_id\n , hashed_password\n , hashed_password_version\n , created_at\n , completed_at\n FROM user_registrations\n WHERE user_registration_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "user_registration_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "ip_address: IpAddr",
|
||||
"type_info": "Inet"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "user_agent",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "post_auth_action",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "username",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "display_name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "terms_url",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "email_authentication_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "hashed_password",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "hashed_password_version",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "completed_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "6772b17585f26365e70ec3e342100c6890d2d63f54f1306e1bb95ca6ca123777"
|
||||
}
|
||||
+10
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT user_email_authentication_id\n , user_session_id\n , email\n , created_at\n , completed_at\n FROM user_email_authentications\n WHERE user_email_authentication_id = $1\n ",
|
||||
"query": "\n SELECT user_email_authentication_id\n , user_session_id\n , user_registration_id\n , email\n , created_at\n , completed_at\n FROM user_email_authentications\n WHERE user_email_authentication_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -15,16 +15,21 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "user_registration_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"ordinal": 4,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 5,
|
||||
"name": "completed_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
@@ -37,10 +42,11 @@
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "7fd19dac2c15091e7f8bd85531d2b99d8a42cc89fe7bb6e9411a886f68e38628"
|
||||
"hash": "7e367e416d18fcf9b227bf053421410b4b7b4af441f0a138c5421d1111cb9f79"
|
||||
}
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE user_registrations\n SET completed_at = $2\n WHERE user_registration_id = $1 AND completed_at IS NULL\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "83d1b0720dfde3209d77f1142aa19359913b8a934ca8a642b7bb43c9a7a58a6d"
|
||||
}
|
||||
Generated
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO user_registrations\n ( user_registration_id\n , ip_address\n , user_agent\n , post_auth_action\n , username\n , created_at\n )\n VALUES ($1, $2, $3, $4, $5, $6)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Inet",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8d240d72d651f59d53bed7380710038e9d00492b1e282237c0ec0e03bc36a9c0"
|
||||
}
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE user_registrations\n SET display_name = $2\n WHERE user_registration_id = $1 AND completed_at IS NULL\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8f5ce493e8b8473ba03d5263915a8b231f9e7c211ab83487536008e48316c269"
|
||||
}
|
||||
Generated
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE user_registrations\n SET hashed_password = $2, hashed_password_version = $3\n WHERE user_registration_id = $1 AND completed_at IS NULL\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b60d34f4d250c12f75dba10491c1337d69aebad12be6fbfbdde91e34083ba4ed"
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- Add a table for storing user registrations
|
||||
CREATE TABLE "user_registrations" (
|
||||
"user_registration_id" UUID PRIMARY KEY,
|
||||
|
||||
-- The IP address of the user agent, if any
|
||||
"ip_address" INET,
|
||||
|
||||
-- The user agent string of the user agent, if any
|
||||
"user_agent" TEXT,
|
||||
|
||||
-- The post auth action to execute after the registration, if any
|
||||
"post_auth_action" JSONB,
|
||||
|
||||
-- The username the user asked for
|
||||
"username" TEXT NOT NULL,
|
||||
|
||||
-- The display name the user asked for
|
||||
"display_name" TEXT,
|
||||
|
||||
-- The URL to the terms of service at the time of registration
|
||||
"terms_url" TEXT,
|
||||
|
||||
-- The ID of the email authentication session
|
||||
"email_authentication_id" UUID
|
||||
REFERENCES "user_email_authentications" ("user_email_authentication_id")
|
||||
ON DELETE SET NULL,
|
||||
|
||||
-- The hashed password of the user
|
||||
"hashed_password" TEXT,
|
||||
-- The scheme version used to hash the password
|
||||
"hashed_password_version" INTEGER,
|
||||
|
||||
-- When the object was created
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
|
||||
-- When the registration was completed
|
||||
"completed_at" TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
-- Allow using user email authentications for user registrations
|
||||
ALTER TABLE "user_email_authentications"
|
||||
ADD COLUMN "user_registration_id" UUID
|
||||
REFERENCES "user_registrations" ("user_registration_id")
|
||||
ON DELETE CASCADE;
|
||||
@@ -49,7 +49,8 @@ use crate::{
|
||||
},
|
||||
user::{
|
||||
PgBrowserSessionRepository, PgUserEmailRepository, PgUserPasswordRepository,
|
||||
PgUserRecoveryRepository, PgUserRepository, PgUserTermsRepository,
|
||||
PgUserRecoveryRepository, PgUserRegistrationRepository, PgUserRepository,
|
||||
PgUserTermsRepository,
|
||||
},
|
||||
DatabaseError,
|
||||
};
|
||||
@@ -191,6 +192,12 @@ where
|
||||
Box::new(PgUserTermsRepository::new(self.conn.as_mut()))
|
||||
}
|
||||
|
||||
fn user_registration<'c>(
|
||||
&'c mut self,
|
||||
) -> Box<dyn mas_storage::user::UserRegistrationRepository<Error = Self::Error> + 'c> {
|
||||
Box::new(PgUserRegistrationRepository::new(self.conn.as_mut()))
|
||||
}
|
||||
|
||||
fn browser_session<'c>(
|
||||
&'c mut self,
|
||||
) -> Box<dyn BrowserSessionRepository<Error = Self::Error> + 'c> {
|
||||
|
||||
@@ -8,6 +8,7 @@ use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use mas_data_model::{
|
||||
BrowserSession, User, UserEmail, UserEmailAuthentication, UserEmailAuthenticationCode,
|
||||
UserRegistration,
|
||||
};
|
||||
use mas_storage::{
|
||||
user::{UserEmailFilter, UserEmailRepository},
|
||||
@@ -66,6 +67,7 @@ impl From<UserEmailLookup> for UserEmail {
|
||||
struct UserEmailAuthenticationLookup {
|
||||
user_email_authentication_id: Uuid,
|
||||
user_session_id: Option<Uuid>,
|
||||
user_registration_id: Option<Uuid>,
|
||||
email: String,
|
||||
created_at: DateTime<Utc>,
|
||||
completed_at: Option<DateTime<Utc>>,
|
||||
@@ -76,6 +78,7 @@ impl From<UserEmailAuthenticationLookup> for UserEmailAuthentication {
|
||||
UserEmailAuthentication {
|
||||
id: value.user_email_authentication_id.into(),
|
||||
user_session_id: value.user_session_id.map(Ulid::from),
|
||||
user_registration_id: value.user_registration_id.map(Ulid::from),
|
||||
email: value.email,
|
||||
created_at: value.created_at,
|
||||
completed_at: value.completed_at,
|
||||
@@ -427,6 +430,59 @@ impl UserEmailRepository for PgUserEmailRepository<'_> {
|
||||
Ok(UserEmailAuthentication {
|
||||
id,
|
||||
user_session_id: Some(session.id),
|
||||
user_registration_id: None,
|
||||
email,
|
||||
created_at,
|
||||
completed_at: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "db.user_email.add_authentication_for_registration",
|
||||
skip_all,
|
||||
fields(
|
||||
db.query.text,
|
||||
%user_registration.id,
|
||||
user_email_authentication.id,
|
||||
user_email_authentication.email = email,
|
||||
),
|
||||
err,
|
||||
)]
|
||||
async fn add_authentication_for_registration(
|
||||
&mut self,
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
email: String,
|
||||
user_registration: &UserRegistration,
|
||||
) -> Result<UserEmailAuthentication, Self::Error> {
|
||||
let created_at = clock.now();
|
||||
let id = Ulid::from_datetime_with_source(created_at.into(), rng);
|
||||
tracing::Span::current()
|
||||
.record("user_email_authentication.id", tracing::field::display(id));
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO user_email_authentications
|
||||
( user_email_authentication_id
|
||||
, user_registration_id
|
||||
, email
|
||||
, created_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
"#,
|
||||
Uuid::from(id),
|
||||
Uuid::from(user_registration.id),
|
||||
&email,
|
||||
created_at,
|
||||
)
|
||||
.traced()
|
||||
.execute(&mut *self.conn)
|
||||
.await?;
|
||||
|
||||
Ok(UserEmailAuthentication {
|
||||
id,
|
||||
user_session_id: None,
|
||||
user_registration_id: Some(user_registration.id),
|
||||
email,
|
||||
created_at,
|
||||
completed_at: None,
|
||||
@@ -509,6 +565,7 @@ impl UserEmailRepository for PgUserEmailRepository<'_> {
|
||||
r#"
|
||||
SELECT user_email_authentication_id
|
||||
, user_session_id
|
||||
, user_registration_id
|
||||
, email
|
||||
, created_at
|
||||
, completed_at
|
||||
|
||||
@@ -31,6 +31,7 @@ use crate::{
|
||||
mod email;
|
||||
mod password;
|
||||
mod recovery;
|
||||
mod registration;
|
||||
mod session;
|
||||
mod terms;
|
||||
|
||||
@@ -39,8 +40,8 @@ mod tests;
|
||||
|
||||
pub use self::{
|
||||
email::PgUserEmailRepository, password::PgUserPasswordRepository,
|
||||
recovery::PgUserRecoveryRepository, session::PgBrowserSessionRepository,
|
||||
terms::PgUserTermsRepository,
|
||||
recovery::PgUserRecoveryRepository, registration::PgUserRegistrationRepository,
|
||||
session::PgBrowserSessionRepository, terms::PgUserTermsRepository,
|
||||
};
|
||||
|
||||
/// An implementation of [`UserRepository`] for a PostgreSQL connection
|
||||
|
||||
@@ -0,0 +1,819 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
use std::net::IpAddr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use mas_data_model::{
|
||||
UserAgent, UserEmailAuthentication, UserRegistration, UserRegistrationPassword,
|
||||
};
|
||||
use mas_storage::{user::UserRegistrationRepository, Clock};
|
||||
use rand::RngCore;
|
||||
use sqlx::PgConnection;
|
||||
use ulid::Ulid;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{DatabaseError, DatabaseInconsistencyError, ExecuteExt as _};
|
||||
|
||||
/// An implementation of [`UserRegistrationRepository`] for a PostgreSQL
|
||||
/// connection
|
||||
pub struct PgUserRegistrationRepository<'c> {
|
||||
conn: &'c mut PgConnection,
|
||||
}
|
||||
|
||||
impl<'c> PgUserRegistrationRepository<'c> {
|
||||
/// Create a new [`PgUserRegistrationRepository`] from an active PostgreSQL
|
||||
/// connection
|
||||
pub fn new(conn: &'c mut PgConnection) -> Self {
|
||||
Self { conn }
|
||||
}
|
||||
}
|
||||
|
||||
struct UserRegistrationLookup {
|
||||
user_registration_id: Uuid,
|
||||
ip_address: Option<IpAddr>,
|
||||
user_agent: Option<String>,
|
||||
post_auth_action: Option<serde_json::Value>,
|
||||
username: String,
|
||||
display_name: Option<String>,
|
||||
terms_url: Option<String>,
|
||||
email_authentication_id: Option<Uuid>,
|
||||
hashed_password: Option<String>,
|
||||
hashed_password_version: Option<i32>,
|
||||
created_at: DateTime<Utc>,
|
||||
completed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl TryFrom<UserRegistrationLookup> for UserRegistration {
|
||||
type Error = DatabaseInconsistencyError;
|
||||
|
||||
fn try_from(value: UserRegistrationLookup) -> Result<Self, Self::Error> {
|
||||
let id = Ulid::from(value.user_registration_id);
|
||||
let user_agent = value.user_agent.map(UserAgent::parse);
|
||||
|
||||
let password = match (value.hashed_password, value.hashed_password_version) {
|
||||
(Some(hashed_password), Some(version)) => {
|
||||
let version = version.try_into().map_err(|e| {
|
||||
DatabaseInconsistencyError::on("user_registrations")
|
||||
.column("hashed_password_version")
|
||||
.row(id)
|
||||
.source(e)
|
||||
})?;
|
||||
|
||||
Some(UserRegistrationPassword {
|
||||
hashed_password,
|
||||
version,
|
||||
})
|
||||
}
|
||||
(None, None) => None,
|
||||
_ => {
|
||||
return Err(DatabaseInconsistencyError::on("user_registrations")
|
||||
.column("hashed_password")
|
||||
.row(id));
|
||||
}
|
||||
};
|
||||
|
||||
let terms_url = value
|
||||
.terms_url
|
||||
.map(|u| u.parse())
|
||||
.transpose()
|
||||
.map_err(|e| {
|
||||
DatabaseInconsistencyError::on("user_registrations")
|
||||
.column("terms_url")
|
||||
.row(id)
|
||||
.source(e)
|
||||
})?;
|
||||
|
||||
Ok(UserRegistration {
|
||||
id,
|
||||
ip_address: value.ip_address,
|
||||
user_agent,
|
||||
post_auth_action: value.post_auth_action,
|
||||
username: value.username,
|
||||
display_name: value.display_name,
|
||||
terms_url,
|
||||
email_authentication_id: value.email_authentication_id.map(Ulid::from),
|
||||
password,
|
||||
created_at: value.created_at,
|
||||
completed_at: value.completed_at,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserRegistrationRepository for PgUserRegistrationRepository<'_> {
|
||||
type Error = DatabaseError;
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "db.user_registration.lookup",
|
||||
skip_all,
|
||||
fields(
|
||||
db.query.text,
|
||||
user_registration.id = %id,
|
||||
),
|
||||
err,
|
||||
)]
|
||||
async fn lookup(&mut self, id: Ulid) -> Result<Option<UserRegistration>, Self::Error> {
|
||||
let res = sqlx::query_as!(
|
||||
UserRegistrationLookup,
|
||||
r#"
|
||||
SELECT user_registration_id
|
||||
, ip_address as "ip_address: IpAddr"
|
||||
, user_agent
|
||||
, post_auth_action
|
||||
, username
|
||||
, display_name
|
||||
, terms_url
|
||||
, email_authentication_id
|
||||
, hashed_password
|
||||
, hashed_password_version
|
||||
, created_at
|
||||
, completed_at
|
||||
FROM user_registrations
|
||||
WHERE user_registration_id = $1
|
||||
"#,
|
||||
Uuid::from(id),
|
||||
)
|
||||
.traced()
|
||||
.fetch_optional(&mut *self.conn)
|
||||
.await?;
|
||||
|
||||
let Some(res) = res else { return Ok(None) };
|
||||
|
||||
Ok(Some(res.try_into()?))
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "db.user_registration.add",
|
||||
skip_all,
|
||||
fields(
|
||||
db.query.text,
|
||||
user_registration.id,
|
||||
),
|
||||
err,
|
||||
)]
|
||||
async fn add(
|
||||
&mut self,
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
username: String,
|
||||
ip_address: Option<IpAddr>,
|
||||
user_agent: Option<UserAgent>,
|
||||
post_auth_action: Option<serde_json::Value>,
|
||||
) -> Result<UserRegistration, Self::Error> {
|
||||
let created_at = clock.now();
|
||||
let id = Ulid::from_datetime_with_source(created_at.into(), rng);
|
||||
tracing::Span::current().record("user_registration.id", tracing::field::display(id));
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO user_registrations
|
||||
( user_registration_id
|
||||
, ip_address
|
||||
, user_agent
|
||||
, post_auth_action
|
||||
, username
|
||||
, created_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
"#,
|
||||
Uuid::from(id),
|
||||
ip_address as Option<IpAddr>,
|
||||
user_agent.as_deref(),
|
||||
post_auth_action,
|
||||
username,
|
||||
created_at,
|
||||
)
|
||||
.traced()
|
||||
.execute(&mut *self.conn)
|
||||
.await?;
|
||||
|
||||
Ok(UserRegistration {
|
||||
id,
|
||||
ip_address,
|
||||
user_agent,
|
||||
post_auth_action,
|
||||
created_at,
|
||||
completed_at: None,
|
||||
username,
|
||||
display_name: None,
|
||||
terms_url: None,
|
||||
email_authentication_id: None,
|
||||
password: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "db.user_registration.set_display_name",
|
||||
skip_all,
|
||||
fields(
|
||||
db.query.text,
|
||||
user_registration.id = %user_registration.id,
|
||||
user_registration.display_name = display_name,
|
||||
),
|
||||
err,
|
||||
)]
|
||||
async fn set_display_name(
|
||||
&mut self,
|
||||
mut user_registration: UserRegistration,
|
||||
display_name: String,
|
||||
) -> Result<UserRegistration, Self::Error> {
|
||||
let res = sqlx::query!(
|
||||
r#"
|
||||
UPDATE user_registrations
|
||||
SET display_name = $2
|
||||
WHERE user_registration_id = $1 AND completed_at IS NULL
|
||||
"#,
|
||||
Uuid::from(user_registration.id),
|
||||
display_name,
|
||||
)
|
||||
.traced()
|
||||
.execute(&mut *self.conn)
|
||||
.await?;
|
||||
|
||||
DatabaseError::ensure_affected_rows(&res, 1)?;
|
||||
|
||||
user_registration.display_name = Some(display_name);
|
||||
|
||||
Ok(user_registration)
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "db.user_registration.set_terms_url",
|
||||
skip_all,
|
||||
fields(
|
||||
db.query.text,
|
||||
user_registration.id = %user_registration.id,
|
||||
user_registration.terms_url = %terms_url,
|
||||
),
|
||||
err,
|
||||
)]
|
||||
async fn set_terms_url(
|
||||
&mut self,
|
||||
mut user_registration: UserRegistration,
|
||||
terms_url: Url,
|
||||
) -> Result<UserRegistration, Self::Error> {
|
||||
let res = sqlx::query!(
|
||||
r#"
|
||||
UPDATE user_registrations
|
||||
SET terms_url = $2
|
||||
WHERE user_registration_id = $1 AND completed_at IS NULL
|
||||
"#,
|
||||
Uuid::from(user_registration.id),
|
||||
terms_url.as_str(),
|
||||
)
|
||||
.traced()
|
||||
.execute(&mut *self.conn)
|
||||
.await?;
|
||||
|
||||
DatabaseError::ensure_affected_rows(&res, 1)?;
|
||||
|
||||
user_registration.terms_url = Some(terms_url);
|
||||
|
||||
Ok(user_registration)
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "db.user_registration.set_email_authentication",
|
||||
skip_all,
|
||||
fields(
|
||||
db.query.text,
|
||||
%user_registration.id,
|
||||
%user_email_authentication.id,
|
||||
%user_email_authentication.email,
|
||||
),
|
||||
err,
|
||||
)]
|
||||
async fn set_email_authentication(
|
||||
&mut self,
|
||||
mut user_registration: UserRegistration,
|
||||
user_email_authentication: &UserEmailAuthentication,
|
||||
) -> Result<UserRegistration, Self::Error> {
|
||||
let res = sqlx::query!(
|
||||
r#"
|
||||
UPDATE user_registrations
|
||||
SET email_authentication_id = $2
|
||||
WHERE user_registration_id = $1 AND completed_at IS NULL
|
||||
"#,
|
||||
Uuid::from(user_registration.id),
|
||||
Uuid::from(user_email_authentication.id),
|
||||
)
|
||||
.traced()
|
||||
.execute(&mut *self.conn)
|
||||
.await?;
|
||||
|
||||
DatabaseError::ensure_affected_rows(&res, 1)?;
|
||||
|
||||
user_registration.email_authentication_id = Some(user_email_authentication.id);
|
||||
|
||||
Ok(user_registration)
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "db.user_registration.set_password",
|
||||
skip_all,
|
||||
fields(
|
||||
db.query.text,
|
||||
user_registration.id = %user_registration.id,
|
||||
user_registration.hashed_password = hashed_password,
|
||||
user_registration.hashed_password_version = version,
|
||||
),
|
||||
err,
|
||||
)]
|
||||
async fn set_password(
|
||||
&mut self,
|
||||
mut user_registration: UserRegistration,
|
||||
hashed_password: String,
|
||||
version: u16,
|
||||
) -> Result<UserRegistration, Self::Error> {
|
||||
let res = sqlx::query!(
|
||||
r#"
|
||||
UPDATE user_registrations
|
||||
SET hashed_password = $2, hashed_password_version = $3
|
||||
WHERE user_registration_id = $1 AND completed_at IS NULL
|
||||
"#,
|
||||
Uuid::from(user_registration.id),
|
||||
hashed_password,
|
||||
i32::from(version),
|
||||
)
|
||||
.traced()
|
||||
.execute(&mut *self.conn)
|
||||
.await?;
|
||||
|
||||
DatabaseError::ensure_affected_rows(&res, 1)?;
|
||||
|
||||
user_registration.password = Some(UserRegistrationPassword {
|
||||
hashed_password,
|
||||
version,
|
||||
});
|
||||
|
||||
Ok(user_registration)
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "db.user_registration.complete",
|
||||
skip_all,
|
||||
fields(
|
||||
db.query.text,
|
||||
user_registration.id = %user_registration.id,
|
||||
),
|
||||
err,
|
||||
)]
|
||||
async fn complete(
|
||||
&mut self,
|
||||
clock: &dyn Clock,
|
||||
mut user_registration: UserRegistration,
|
||||
) -> Result<UserRegistration, Self::Error> {
|
||||
let completed_at = clock.now();
|
||||
let res = sqlx::query!(
|
||||
r#"
|
||||
UPDATE user_registrations
|
||||
SET completed_at = $2
|
||||
WHERE user_registration_id = $1 AND completed_at IS NULL
|
||||
"#,
|
||||
Uuid::from(user_registration.id),
|
||||
completed_at,
|
||||
)
|
||||
.traced()
|
||||
.execute(&mut *self.conn)
|
||||
.await?;
|
||||
|
||||
DatabaseError::ensure_affected_rows(&res, 1)?;
|
||||
|
||||
user_registration.completed_at = Some(completed_at);
|
||||
|
||||
Ok(user_registration)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
use mas_data_model::{UserAgent, UserRegistrationPassword};
|
||||
use mas_storage::{clock::MockClock, Clock};
|
||||
use rand::SeedableRng;
|
||||
use rand_chacha::ChaChaRng;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::PgRepository;
|
||||
|
||||
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||
async fn test_create_lookup_complete(pool: PgPool) {
|
||||
let mut rng = ChaChaRng::seed_from_u64(42);
|
||||
let clock = MockClock::default();
|
||||
|
||||
let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
|
||||
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.add(&mut rng, &clock, "alice".to_owned(), None, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(registration.created_at, clock.now());
|
||||
assert_eq!(registration.completed_at, None);
|
||||
assert_eq!(registration.username, "alice");
|
||||
assert_eq!(registration.display_name, None);
|
||||
assert_eq!(registration.terms_url, None);
|
||||
assert_eq!(registration.email_authentication_id, None);
|
||||
assert_eq!(registration.password, None);
|
||||
assert_eq!(registration.user_agent, None);
|
||||
assert_eq!(registration.ip_address, None);
|
||||
assert_eq!(registration.post_auth_action, None);
|
||||
|
||||
let lookup = repo
|
||||
.user_registration()
|
||||
.lookup(registration.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(lookup.id, registration.id);
|
||||
assert_eq!(lookup.created_at, registration.created_at);
|
||||
assert_eq!(lookup.completed_at, registration.completed_at);
|
||||
assert_eq!(lookup.username, registration.username);
|
||||
assert_eq!(lookup.display_name, registration.display_name);
|
||||
assert_eq!(lookup.terms_url, registration.terms_url);
|
||||
assert_eq!(
|
||||
lookup.email_authentication_id,
|
||||
registration.email_authentication_id
|
||||
);
|
||||
assert_eq!(lookup.password, registration.password);
|
||||
assert_eq!(lookup.user_agent, registration.user_agent);
|
||||
assert_eq!(lookup.ip_address, registration.ip_address);
|
||||
assert_eq!(lookup.post_auth_action, registration.post_auth_action);
|
||||
|
||||
// Mark the registration as completed
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.complete(&clock, registration)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(registration.completed_at, Some(clock.now()));
|
||||
|
||||
// Lookup the registration again
|
||||
let lookup = repo
|
||||
.user_registration()
|
||||
.lookup(registration.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(lookup.completed_at, registration.completed_at);
|
||||
|
||||
// Do it again, it should fail
|
||||
let res = repo
|
||||
.user_registration()
|
||||
.complete(&clock, registration)
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||
async fn test_create_useragent_ipaddress(pool: PgPool) {
|
||||
let mut rng = ChaChaRng::seed_from_u64(42);
|
||||
let clock = MockClock::default();
|
||||
|
||||
let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
|
||||
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.add(
|
||||
&mut rng,
|
||||
&clock,
|
||||
"alice".to_owned(),
|
||||
Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
|
||||
Some(UserAgent::parse("Mozilla/5.0".to_owned())),
|
||||
Some(serde_json::json!({"action": "continue_compat_sso_login", "id": "01FSHN9AG0MKGTBNZ16RDR3PVY"})),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
registration.user_agent,
|
||||
Some(UserAgent::parse("Mozilla/5.0".to_owned()))
|
||||
);
|
||||
assert_eq!(
|
||||
registration.ip_address,
|
||||
Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)))
|
||||
);
|
||||
assert_eq!(
|
||||
registration.post_auth_action,
|
||||
Some(
|
||||
serde_json::json!({"action": "continue_compat_sso_login", "id": "01FSHN9AG0MKGTBNZ16RDR3PVY"})
|
||||
)
|
||||
);
|
||||
|
||||
let lookup = repo
|
||||
.user_registration()
|
||||
.lookup(registration.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(lookup.user_agent, registration.user_agent);
|
||||
assert_eq!(lookup.ip_address, registration.ip_address);
|
||||
assert_eq!(lookup.post_auth_action, registration.post_auth_action);
|
||||
}
|
||||
|
||||
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||
async fn test_set_display_name(pool: PgPool) {
|
||||
let mut rng = ChaChaRng::seed_from_u64(42);
|
||||
let clock = MockClock::default();
|
||||
|
||||
let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
|
||||
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.add(&mut rng, &clock, "alice".to_owned(), None, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(registration.display_name, None);
|
||||
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.set_display_name(registration, "Alice".to_owned())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(registration.display_name, Some("Alice".to_owned()));
|
||||
|
||||
let lookup = repo
|
||||
.user_registration()
|
||||
.lookup(registration.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(lookup.display_name, registration.display_name);
|
||||
|
||||
// Setting it again should work
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.set_display_name(registration, "Bob".to_owned())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(registration.display_name, Some("Bob".to_owned()));
|
||||
|
||||
let lookup = repo
|
||||
.user_registration()
|
||||
.lookup(registration.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(lookup.display_name, registration.display_name);
|
||||
|
||||
// Can't set it once completed
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.complete(&clock, registration)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let res = repo
|
||||
.user_registration()
|
||||
.set_display_name(registration, "Charlie".to_owned())
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||
async fn test_set_terms_url(pool: PgPool) {
|
||||
let mut rng = ChaChaRng::seed_from_u64(42);
|
||||
let clock = MockClock::default();
|
||||
|
||||
let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
|
||||
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.add(&mut rng, &clock, "alice".to_owned(), None, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(registration.terms_url, None);
|
||||
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.set_terms_url(registration, "https://example.com/terms".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
registration.terms_url,
|
||||
Some("https://example.com/terms".parse().unwrap())
|
||||
);
|
||||
|
||||
let lookup = repo
|
||||
.user_registration()
|
||||
.lookup(registration.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(lookup.terms_url, registration.terms_url);
|
||||
|
||||
// Setting it again should work
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.set_terms_url(registration, "https://example.com/terms2".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
registration.terms_url,
|
||||
Some("https://example.com/terms2".parse().unwrap())
|
||||
);
|
||||
|
||||
let lookup = repo
|
||||
.user_registration()
|
||||
.lookup(registration.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(lookup.terms_url, registration.terms_url);
|
||||
|
||||
// Can't set it once completed
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.complete(&clock, registration)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let res = repo
|
||||
.user_registration()
|
||||
.set_terms_url(registration, "https://example.com/terms3".parse().unwrap())
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||
async fn test_set_email_authentication(pool: PgPool) {
|
||||
let mut rng = ChaChaRng::seed_from_u64(42);
|
||||
let clock = MockClock::default();
|
||||
|
||||
let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
|
||||
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.add(&mut rng, &clock, "alice".to_owned(), None, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(registration.email_authentication_id, None);
|
||||
|
||||
let authentication = repo
|
||||
.user_email()
|
||||
.add_authentication_for_registration(
|
||||
&mut rng,
|
||||
&clock,
|
||||
"alice@example.com".to_owned(),
|
||||
®istration,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.set_email_authentication(registration, &authentication)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
registration.email_authentication_id,
|
||||
Some(authentication.id)
|
||||
);
|
||||
|
||||
let lookup = repo
|
||||
.user_registration()
|
||||
.lookup(registration.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
lookup.email_authentication_id,
|
||||
registration.email_authentication_id
|
||||
);
|
||||
|
||||
// Setting it again should work
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.set_email_authentication(registration, &authentication)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
registration.email_authentication_id,
|
||||
Some(authentication.id)
|
||||
);
|
||||
|
||||
let lookup = repo
|
||||
.user_registration()
|
||||
.lookup(registration.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
lookup.email_authentication_id,
|
||||
registration.email_authentication_id
|
||||
);
|
||||
|
||||
// Can't set it once completed
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.complete(&clock, registration)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let res = repo
|
||||
.user_registration()
|
||||
.set_email_authentication(registration, &authentication)
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||
async fn test_set_password(pool: PgPool) {
|
||||
let mut rng = ChaChaRng::seed_from_u64(42);
|
||||
let clock = MockClock::default();
|
||||
|
||||
let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
|
||||
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.add(&mut rng, &clock, "alice".to_owned(), None, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(registration.password, None);
|
||||
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.set_password(registration, "fakehashedpassword".to_owned(), 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
registration.password,
|
||||
Some(UserRegistrationPassword {
|
||||
hashed_password: "fakehashedpassword".to_owned(),
|
||||
version: 1,
|
||||
})
|
||||
);
|
||||
|
||||
let lookup = repo
|
||||
.user_registration()
|
||||
.lookup(registration.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(lookup.password, registration.password);
|
||||
|
||||
// Setting it again should work
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.set_password(registration, "fakehashedpassword2".to_owned(), 2)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
registration.password,
|
||||
Some(UserRegistrationPassword {
|
||||
hashed_password: "fakehashedpassword2".to_owned(),
|
||||
version: 2,
|
||||
})
|
||||
);
|
||||
|
||||
let lookup = repo
|
||||
.user_registration()
|
||||
.lookup(registration.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(lookup.password, registration.password);
|
||||
|
||||
// Can't set it once completed
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.complete(&clock, registration)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let res = repo
|
||||
.user_registration()
|
||||
.set_password(registration, "fakehashedpassword3".to_owned(), 3)
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ use crate::{
|
||||
},
|
||||
user::{
|
||||
BrowserSessionRepository, UserEmailRepository, UserPasswordRepository,
|
||||
UserRecoveryRepository, UserRepository, UserTermsRepository,
|
||||
UserRecoveryRepository, UserRegistrationRepository, UserRepository, UserTermsRepository,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -129,6 +129,11 @@ pub trait RepositoryAccess: Send {
|
||||
fn user_recovery<'c>(&'c mut self)
|
||||
-> Box<dyn UserRecoveryRepository<Error = Self::Error> + 'c>;
|
||||
|
||||
/// Get an [`UserRegistrationRepository`]
|
||||
fn user_registration<'c>(
|
||||
&'c mut self,
|
||||
) -> Box<dyn UserRegistrationRepository<Error = Self::Error> + 'c>;
|
||||
|
||||
/// Get an [`UserTermsRepository`]
|
||||
fn user_terms<'c>(&'c mut self) -> Box<dyn UserTermsRepository<Error = Self::Error> + 'c>;
|
||||
|
||||
@@ -224,8 +229,8 @@ mod impls {
|
||||
UpstreamOAuthSessionRepository,
|
||||
},
|
||||
user::{
|
||||
BrowserSessionRepository, UserEmailRepository, UserPasswordRepository, UserRepository,
|
||||
UserTermsRepository,
|
||||
BrowserSessionRepository, UserEmailRepository, UserPasswordRepository,
|
||||
UserRegistrationRepository, UserRepository, UserTermsRepository,
|
||||
},
|
||||
MapErr, Repository, RepositoryTransaction,
|
||||
};
|
||||
@@ -316,6 +321,15 @@ mod impls {
|
||||
Box::new(MapErr::new(self.inner.user_recovery(), &mut self.mapper))
|
||||
}
|
||||
|
||||
fn user_registration<'c>(
|
||||
&'c mut self,
|
||||
) -> Box<dyn UserRegistrationRepository<Error = Self::Error> + 'c> {
|
||||
Box::new(MapErr::new(
|
||||
self.inner.user_registration(),
|
||||
&mut self.mapper,
|
||||
))
|
||||
}
|
||||
|
||||
fn user_terms<'c>(&'c mut self) -> Box<dyn UserTermsRepository<Error = Self::Error> + 'c> {
|
||||
Box::new(MapErr::new(self.inner.user_terms(), &mut self.mapper))
|
||||
}
|
||||
@@ -468,6 +482,12 @@ mod impls {
|
||||
(**self).user_recovery()
|
||||
}
|
||||
|
||||
fn user_registration<'c>(
|
||||
&'c mut self,
|
||||
) -> Box<dyn UserRegistrationRepository<Error = Self::Error> + 'c> {
|
||||
(**self).user_registration()
|
||||
}
|
||||
|
||||
fn user_terms<'c>(&'c mut self) -> Box<dyn UserTermsRepository<Error = Self::Error> + 'c> {
|
||||
(**self).user_terms()
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
use async_trait::async_trait;
|
||||
use mas_data_model::{
|
||||
BrowserSession, User, UserEmail, UserEmailAuthentication, UserEmailAuthenticationCode,
|
||||
UserRegistration,
|
||||
};
|
||||
use rand_core::RngCore;
|
||||
use ulid::Ulid;
|
||||
@@ -184,6 +185,27 @@ pub trait UserEmailRepository: Send + Sync {
|
||||
session: &BrowserSession,
|
||||
) -> Result<UserEmailAuthentication, Self::Error>;
|
||||
|
||||
/// Add a new [`UserEmailAuthentication`] for a [`UserRegistration`]
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `rng`: The random number generator to use
|
||||
/// * `clock`: The clock to use
|
||||
/// * `email`: The email address to add
|
||||
/// * `registration`: The [`UserRegistration`] for which to add the
|
||||
/// [`UserEmailAuthentication`]
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the underlying repository fails
|
||||
async fn add_authentication_for_registration(
|
||||
&mut self,
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
email: String,
|
||||
registration: &UserRegistration,
|
||||
) -> Result<UserEmailAuthentication, Self::Error>;
|
||||
|
||||
/// Add a new [`UserEmailAuthenticationCode`] for a
|
||||
/// [`UserEmailAuthentication`]
|
||||
///
|
||||
@@ -289,6 +311,14 @@ repository_impl!(UserEmailRepository:
|
||||
session: &BrowserSession,
|
||||
) -> Result<UserEmailAuthentication, Self::Error>;
|
||||
|
||||
async fn add_authentication_for_registration(
|
||||
&mut self,
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
email: String,
|
||||
registration: &UserRegistration,
|
||||
) -> Result<UserEmailAuthentication, Self::Error>;
|
||||
|
||||
async fn add_authentication_code(
|
||||
&mut self,
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::{repository_impl, Clock, Page, Pagination};
|
||||
mod email;
|
||||
mod password;
|
||||
mod recovery;
|
||||
mod registration;
|
||||
mod session;
|
||||
mod terms;
|
||||
|
||||
@@ -23,6 +24,7 @@ pub use self::{
|
||||
email::{UserEmailFilter, UserEmailRepository},
|
||||
password::UserPasswordRepository,
|
||||
recovery::UserRecoveryRepository,
|
||||
registration::UserRegistrationRepository,
|
||||
session::{BrowserSessionFilter, BrowserSessionRepository},
|
||||
terms::UserTermsRepository,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
use std::net::IpAddr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use mas_data_model::{UserAgent, UserEmailAuthentication, UserRegistration};
|
||||
use rand_core::RngCore;
|
||||
use ulid::Ulid;
|
||||
use url::Url;
|
||||
|
||||
use crate::{repository_impl, Clock};
|
||||
|
||||
/// A [`UserRegistrationRepository`] helps interacting with [`UserRegistration`]
|
||||
/// saved in the storage backend
|
||||
#[async_trait]
|
||||
pub trait UserRegistrationRepository: Send + Sync {
|
||||
/// The error type returned by the repository
|
||||
type Error;
|
||||
|
||||
/// Lookup a [`UserRegistration`] by its ID
|
||||
///
|
||||
/// Returns `None` if no [`UserRegistration`] was found
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `id`: The ID of the [`UserRegistration`] to lookup
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Self::Error`] if the underlying repository fails
|
||||
async fn lookup(&mut self, id: Ulid) -> Result<Option<UserRegistration>, Self::Error>;
|
||||
|
||||
/// Create a new [`UserRegistration`] session
|
||||
///
|
||||
/// Returns the newly created [`UserRegistration`]
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `rng`: The random number generator to use
|
||||
/// * `clock`: The clock used to generate timestamps
|
||||
/// * `username`: The username of the user
|
||||
/// * `ip_address`: The IP address of the user agent, if any
|
||||
/// * `user_agent`: The user agent of the user agent, if any
|
||||
/// * `post_auth_action`: The post auth action to execute after the
|
||||
/// registration, if any
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Self::Error`] if the underlying repository fails
|
||||
async fn add(
|
||||
&mut self,
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
username: String,
|
||||
ip_address: Option<IpAddr>,
|
||||
user_agent: Option<UserAgent>,
|
||||
post_auth_action: Option<serde_json::Value>,
|
||||
) -> Result<UserRegistration, Self::Error>;
|
||||
|
||||
/// Set the display name of a [`UserRegistration`]
|
||||
///
|
||||
/// Returns the updated [`UserRegistration`]
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `user_registration`: The [`UserRegistration`] to update
|
||||
/// * `display_name`: The display name to set
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Self::Error`] if the underlying repository fails or if the
|
||||
/// registration is already completed
|
||||
async fn set_display_name(
|
||||
&mut self,
|
||||
user_registration: UserRegistration,
|
||||
display_name: String,
|
||||
) -> Result<UserRegistration, Self::Error>;
|
||||
|
||||
/// Set the terms URL of a [`UserRegistration`]
|
||||
///
|
||||
/// Returns the updated [`UserRegistration`]
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `user_registration`: The [`UserRegistration`] to update
|
||||
/// * `terms_url`: The terms URL to set
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Self::Error`] if the underlying repository fails or if the
|
||||
/// registration is already completed
|
||||
async fn set_terms_url(
|
||||
&mut self,
|
||||
user_registration: UserRegistration,
|
||||
terms_url: Url,
|
||||
) -> Result<UserRegistration, Self::Error>;
|
||||
|
||||
/// Set the email authentication code of a [`UserRegistration`]
|
||||
///
|
||||
/// Returns the updated [`UserRegistration`]
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `user_registration`: The [`UserRegistration`] to update
|
||||
/// * `email_authentication`: The [`UserEmailAuthentication`] to set
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Self::Error`] if the underlying repository fails or if the
|
||||
/// registration is already completed
|
||||
async fn set_email_authentication(
|
||||
&mut self,
|
||||
user_registration: UserRegistration,
|
||||
email_authentication: &UserEmailAuthentication,
|
||||
) -> Result<UserRegistration, Self::Error>;
|
||||
|
||||
/// Set the password of a [`UserRegistration`]
|
||||
///
|
||||
/// Returns the updated [`UserRegistration`]
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `user_registration`: The [`UserRegistration`] to update
|
||||
/// * `hashed_password`: The hashed password to set
|
||||
/// * `version`: The version of the hashing scheme
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Self::Error`] if the underlying repository fails or if the
|
||||
/// registration is already completed
|
||||
async fn set_password(
|
||||
&mut self,
|
||||
user_registration: UserRegistration,
|
||||
hashed_password: String,
|
||||
version: u16,
|
||||
) -> Result<UserRegistration, Self::Error>;
|
||||
|
||||
/// Complete a [`UserRegistration`]
|
||||
///
|
||||
/// Returns the updated [`UserRegistration`]
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `clock`: The clock used to generate timestamps
|
||||
/// * `user_registration`: The [`UserRegistration`] to complete
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Self::Error`] if the underlying repository fails or if the
|
||||
/// registration is already completed
|
||||
async fn complete(
|
||||
&mut self,
|
||||
clock: &dyn Clock,
|
||||
user_registration: UserRegistration,
|
||||
) -> Result<UserRegistration, Self::Error>;
|
||||
}
|
||||
|
||||
repository_impl!(UserRegistrationRepository:
|
||||
async fn lookup(&mut self, id: Ulid) -> Result<Option<UserRegistration>, Self::Error>;
|
||||
async fn add(
|
||||
&mut self,
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
username: String,
|
||||
ip_address: Option<IpAddr>,
|
||||
user_agent: Option<UserAgent>,
|
||||
post_auth_action: Option<serde_json::Value>,
|
||||
) -> Result<UserRegistration, Self::Error>;
|
||||
async fn set_display_name(
|
||||
&mut self,
|
||||
user_registration: UserRegistration,
|
||||
display_name: String,
|
||||
) -> Result<UserRegistration, Self::Error>;
|
||||
async fn set_terms_url(
|
||||
&mut self,
|
||||
user_registration: UserRegistration,
|
||||
terms_url: Url,
|
||||
) -> Result<UserRegistration, Self::Error>;
|
||||
async fn set_email_authentication(
|
||||
&mut self,
|
||||
user_registration: UserRegistration,
|
||||
email_authentication: &UserEmailAuthentication,
|
||||
) -> Result<UserRegistration, Self::Error>;
|
||||
async fn set_password(
|
||||
&mut self,
|
||||
user_registration: UserRegistration,
|
||||
hashed_password: String,
|
||||
version: u16,
|
||||
) -> Result<UserRegistration, Self::Error>;
|
||||
async fn complete(
|
||||
&mut self,
|
||||
clock: &dyn Clock,
|
||||
user_registration: UserRegistration,
|
||||
) -> Result<UserRegistration, Self::Error>;
|
||||
);
|
||||
Reference in New Issue
Block a user