Allow completing user email authentications using an upstream session

This will let us push emails in user registrations using an upstream
session
This commit is contained in:
Quentin Gliech
2025-11-21 19:28:26 +01:00
parent 23c31d0e43
commit ad9f04c8ba
6 changed files with 116 additions and 13 deletions

View File

@@ -817,7 +817,7 @@ impl UserEmailMutations {
let authentication = repo
.user_email()
.complete_authentication(&clock, authentication, &code)
.complete_authentication_with_code(&clock, authentication, &code)
.await?;
// Check the email is not already in use by anyone, including the current user

View File

@@ -200,7 +200,7 @@ pub(crate) async fn post(
};
repo.user_email()
.complete_authentication(&clock, email_authentication, &code)
.complete_authentication_with_code(&clock, email_authentication, &code)
.await?;
repo.save().await?;

View File

@@ -7,8 +7,8 @@
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use mas_data_model::{
BrowserSession, Clock, User, UserEmail, UserEmailAuthentication, UserEmailAuthenticationCode,
UserRegistration,
BrowserSession, Clock, UpstreamOAuthAuthorizationSession, User, UserEmail,
UserEmailAuthentication, UserEmailAuthenticationCode, UserRegistration,
};
use mas_storage::{
Page, Pagination,
@@ -668,7 +668,7 @@ impl UserEmailRepository for PgUserEmailRepository<'_> {
}
#[tracing::instrument(
name = "db.user_email.complete_email_authentication",
name = "db.user_email.complete_email_authentication_with_code",
skip_all,
fields(
db.query.text,
@@ -679,7 +679,7 @@ impl UserEmailRepository for PgUserEmailRepository<'_> {
),
err,
)]
async fn complete_authentication(
async fn complete_authentication_with_code(
&mut self,
clock: &dyn Clock,
mut user_email_authentication: UserEmailAuthentication,
@@ -712,4 +712,49 @@ impl UserEmailRepository for PgUserEmailRepository<'_> {
user_email_authentication.completed_at = Some(completed_at);
Ok(user_email_authentication)
}
#[tracing::instrument(
name = "db.user_email.complete_email_authentication_with_upstream",
skip_all,
fields(
db.query.text,
%user_email_authentication.id,
%user_email_authentication.email,
%upstream_oauth_authorization_session.id,
),
err,
)]
async fn complete_authentication_with_upstream(
&mut self,
clock: &dyn Clock,
mut user_email_authentication: UserEmailAuthentication,
upstream_oauth_authorization_session: &UpstreamOAuthAuthorizationSession,
) -> Result<UserEmailAuthentication, Self::Error> {
// We technically don't use the upstream_oauth_authorization_session here (other
// than recording it in the span), but this is to make sure the caller
// has fetched one before calling this
let completed_at = clock.now();
// We'll assume the caller has checked that completed_at is None, so in case
// they haven't, the update will not affect any rows, which will raise
// an error
let res = sqlx::query!(
r#"
UPDATE user_email_authentications
SET completed_at = $2
WHERE user_email_authentication_id = $1
AND completed_at IS NULL
"#,
Uuid::from(user_email_authentication.id),
completed_at,
)
.traced()
.execute(&mut *self.conn)
.await?;
DatabaseError::ensure_affected_rows(&res, 1)?;
user_email_authentication.completed_at = Some(completed_at);
Ok(user_email_authentication)
}
}

View File

@@ -488,7 +488,7 @@ async fn test_user_email_repo_authentications(pool: PgPool) {
// Complete the authentication
let authentication = repo
.user_email()
.complete_authentication(&clock, authentication, &code)
.complete_authentication_with_code(&clock, authentication, &code)
.await
.unwrap();
@@ -514,7 +514,7 @@ async fn test_user_email_repo_authentications(pool: PgPool) {
// Completing a second time should fail
let res = repo
.user_email()
.complete_authentication(&clock, authentication, &code)
.complete_authentication_with_code(&clock, authentication, &code)
.await;
assert!(res.is_err());
}

View File

@@ -6,8 +6,8 @@
use async_trait::async_trait;
use mas_data_model::{
BrowserSession, Clock, User, UserEmail, UserEmailAuthentication, UserEmailAuthenticationCode,
UserRegistration,
BrowserSession, Clock, UpstreamOAuthAuthorizationSession, User, UserEmail,
UserEmailAuthentication, UserEmailAuthenticationCode, UserRegistration,
};
use rand_core::RngCore;
use ulid::Ulid;
@@ -306,12 +306,34 @@ pub trait UserEmailRepository: Send + Sync {
/// # Errors
///
/// Returns an error if the underlying repository fails
async fn complete_authentication(
async fn complete_authentication_with_code(
&mut self,
clock: &dyn Clock,
authentication: UserEmailAuthentication,
code: &UserEmailAuthenticationCode,
) -> Result<UserEmailAuthentication, Self::Error>;
/// Complete a [`UserEmailAuthentication`] by using the given upstream oauth
/// authorization session
///
/// Returns the completed [`UserEmailAuthentication`]
///
/// # Parameters
///
/// * `clock`: The clock to use to generate timestamps
/// * `authentication`: The [`UserEmailAuthentication`] to complete
/// * `upstream_oauth_authorization_session`: The
/// [`UpstreamOAuthAuthorizationSession`] to use
///
/// # Errors
///
/// Returns an error if the underlying repository fails
async fn complete_authentication_with_upstream(
&mut self,
clock: &dyn Clock,
authentication: UserEmailAuthentication,
upstream_oauth_authorization_session: &UpstreamOAuthAuthorizationSession,
) -> Result<UserEmailAuthentication, Self::Error>;
}
repository_impl!(UserEmailRepository:
@@ -374,10 +396,17 @@ repository_impl!(UserEmailRepository:
code: &str,
) -> Result<Option<UserEmailAuthenticationCode>, Self::Error>;
async fn complete_authentication(
async fn complete_authentication_with_code(
&mut self,
clock: &dyn Clock,
authentication: UserEmailAuthentication,
code: &UserEmailAuthenticationCode,
) -> Result<UserEmailAuthentication, Self::Error>;
async fn complete_authentication_with_upstream(
&mut self,
clock: &dyn Clock,
authentication: UserEmailAuthentication,
upstream_oauth_authorization_session: &UpstreamOAuthAuthorizationSession,
) -> Result<UserEmailAuthentication, Self::Error>;
);

View File

@@ -6,7 +6,10 @@
use std::net::IpAddr;
use async_trait::async_trait;
use mas_data_model::{Clock, UserEmailAuthentication, UserRegistration, UserRegistrationToken};
use mas_data_model::{
Clock, UpstreamOAuthAuthorizationSession, UserEmailAuthentication, UserRegistration,
UserRegistrationToken,
};
use rand_core::RngCore;
use ulid::Ulid;
use url::Url;
@@ -157,6 +160,27 @@ pub trait UserRegistrationRepository: Send + Sync {
user_registration_token: &UserRegistrationToken,
) -> Result<UserRegistration, Self::Error>;
/// Set an [`UpstreamOAuthAuthorizationSession`] to associate with a
/// [`UserRegistration`]
///
/// Returns the updated [`UserRegistration`]
///
/// # Parameters
///
/// * `user_registration`: The [`UserRegistration`] to update
/// * `upstream_oauth_authorization_session`: The
/// [`UpstreamOAuthAuthorizationSession`] to set
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails or if the
/// registration is already completed
async fn set_upstream_oauth_authorization_session(
&mut self,
user_registration: UserRegistration,
upstream_oauth_authorization_session: &UpstreamOAuthAuthorizationSession,
) -> Result<UserRegistration, Self::Error>;
/// Complete a [`UserRegistration`]
///
/// Returns the updated [`UserRegistration`]
@@ -214,6 +238,11 @@ repository_impl!(UserRegistrationRepository:
user_registration: UserRegistration,
user_registration_token: &UserRegistrationToken,
) -> Result<UserRegistration, Self::Error>;
async fn set_upstream_oauth_authorization_session(
&mut self,
user_registration: UserRegistration,
upstream_oauth_authorization_session: &UpstreamOAuthAuthorizationSession,
) -> Result<UserRegistration, Self::Error>;
async fn complete(
&mut self,
clock: &dyn Clock,