diff --git a/crates/cli/src/commands/manage.rs b/crates/cli/src/commands/manage.rs index a92f35af..d46130e2 100644 --- a/crates/cli/src/commands/manage.rs +++ b/crates/cli/src/commands/manage.rs @@ -20,9 +20,7 @@ use mas_router::UrlBuilder; use mas_storage::{ oauth2::client::{insert_client_from_config, lookup_client, truncate_clients}, upstream_oauth2::UpstreamOAuthProviderRepository, - user::{ - add_user_password, lookup_user_by_username, lookup_user_email, mark_user_email_as_verified, - }, + user::{add_user_password, UserEmailRepository, UserRepository}, Clock, Repository, }; use oauth2_types::scope::Scope; @@ -202,7 +200,9 @@ impl Options { let password_manager = password_manager_from_config(&passwords_config).await?; let mut txn = pool.begin().await?; - let user = lookup_user_by_username(&mut txn, username) + let user = txn + .user() + .find_by_username(username) .await? .context("User not found")?; @@ -232,13 +232,18 @@ impl Options { let pool = database_from_config(&config).await?; let mut txn = pool.begin().await?; - let user = lookup_user_by_username(&mut txn, username) + let user = txn + .user() + .find_by_username(username) .await? .context("User not found")?; - let email = lookup_user_email(&mut txn, &user, email) + + let email = txn + .user_email() + .find(&user, email) .await? .context("Email not found")?; - let email = mark_user_email_as_verified(&mut txn, &clock, email).await?; + let email = txn.user_email().mark_as_verified(&clock, email).await?; txn.commit().await?; info!(?email, "Email marked as verified"); diff --git a/crates/data-model/src/users.rs b/crates/data-model/src/users.rs index 4d9c884a..995535d7 100644 --- a/crates/data-model/src/users.rs +++ b/crates/data-model/src/users.rs @@ -22,7 +22,7 @@ pub struct User { pub id: Ulid, pub username: String, pub sub: String, - pub primary_email: Option, + pub primary_user_email_id: Option, } impl User { @@ -32,7 +32,7 @@ impl User { id: Ulid::from_datetime_with_source(now.into(), rng), username: "john".to_owned(), sub: "123-456".to_owned(), - primary_email: None, + primary_user_email_id: None, }] } } @@ -89,6 +89,7 @@ impl BrowserSession { #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct UserEmail { pub id: Ulid, + pub user_id: Ulid, pub email: String, pub created_at: DateTime, pub confirmed_at: Option>, @@ -100,12 +101,14 @@ impl UserEmail { vec![ Self { id: Ulid::from_datetime_with_source(now.into(), rng), + user_id: Ulid::from_datetime_with_source(now.into(), rng), email: "alice@example.com".to_owned(), created_at: now, confirmed_at: Some(now), }, Self { id: Ulid::from_datetime_with_source(now.into(), rng), + user_id: Ulid::from_datetime_with_source(now.into(), rng), email: "bob@example.com".to_owned(), created_at: now, confirmed_at: None, @@ -124,7 +127,7 @@ pub enum UserEmailVerificationState { #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct UserEmailVerification { pub id: Ulid, - pub email: UserEmail, + pub user_email_id: Ulid, pub code: String, pub created_at: DateTime, pub state: UserEmailVerificationState, @@ -152,8 +155,8 @@ impl UserEmailVerification { .into_iter() .map(move |email| Self { id: Ulid::from_datetime_with_source(now.into(), &mut rng), + user_email_id: email.id, code: "123456".to_owned(), - email, created_at: now - Duration::minutes(10), state: state.clone(), }) diff --git a/crates/graphql/src/lib.rs b/crates/graphql/src/lib.rs index 8f3ef321..f01d8370 100644 --- a/crates/graphql/src/lib.rs +++ b/crates/graphql/src/lib.rs @@ -31,7 +31,8 @@ use async_graphql::{ Context, Description, EmptyMutation, EmptySubscription, ID, }; use mas_storage::{ - upstream_oauth2::UpstreamOAuthProviderRepository, Repository, UpstreamOAuthLinkRepository, + upstream_oauth2::UpstreamOAuthProviderRepository, user::UserEmailRepository, Repository, + UpstreamOAuthLinkRepository, }; use model::CreationEvent; use sqlx::PgPool; @@ -154,8 +155,11 @@ impl RootQuery { let Some(session) = session else { return Ok(None) }; let current_user = session.user; - let user_email = - mas_storage::user::lookup_user_email_by_id(&mut conn, ¤t_user, id).await?; + let user_email = conn + .user_email() + .lookup(id) + .await? + .filter(|e| e.user_id == current_user.id); Ok(user_email.map(UserEmail)) } diff --git a/crates/graphql/src/model/upstream_oauth.rs b/crates/graphql/src/model/upstream_oauth.rs index 2de6f2f7..249a0928 100644 --- a/crates/graphql/src/model/upstream_oauth.rs +++ b/crates/graphql/src/model/upstream_oauth.rs @@ -15,7 +15,9 @@ use anyhow::Context as _; use async_graphql::{Context, Object, ID}; use chrono::{DateTime, Utc}; -use mas_storage::{upstream_oauth2::UpstreamOAuthProviderRepository, Repository}; +use mas_storage::{ + upstream_oauth2::UpstreamOAuthProviderRepository, user::UserRepository, Repository, +}; use sqlx::PgPool; use super::{NodeType, User}; @@ -120,7 +122,10 @@ impl UpstreamOAuth2Link { // Fetch on-the-fly let database = ctx.data::()?; let mut conn = database.acquire().await?; - mas_storage::user::lookup_user(&mut conn, *user_id).await? + conn.user() + .lookup(*user_id) + .await? + .context("User not found")? } else { return Ok(None); }; diff --git a/crates/graphql/src/model/users.rs b/crates/graphql/src/model/users.rs index 01fcfb0e..58119c09 100644 --- a/crates/graphql/src/model/users.rs +++ b/crates/graphql/src/model/users.rs @@ -17,7 +17,7 @@ use async_graphql::{ Context, Description, Object, ID, }; use chrono::{DateTime, Utc}; -use mas_storage::{Repository, UpstreamOAuthLinkRepository}; +use mas_storage::{user::UserEmailRepository, Repository, UpstreamOAuthLinkRepository}; use sqlx::PgPool; use super::{ @@ -54,8 +54,14 @@ impl User { } /// Primary email address of the user. - async fn primary_email(&self) -> Option { - self.0.primary_email.clone().map(UserEmail) + async fn primary_email( + &self, + ctx: &Context<'_>, + ) -> Result, async_graphql::Error> { + let database = ctx.data::()?; + let mut conn = database.acquire().await?; + + Ok(conn.user_email().get_primary(&self.0).await?.map(UserEmail)) } /// Get the list of compatibility SSO logins, chronologically sorted @@ -182,18 +188,17 @@ impl User { .map(|x: OpaqueCursor| x.extract_for_type(NodeType::UserEmail)) .transpose()?; - let (has_previous_page, has_next_page, edges) = - mas_storage::user::get_paginated_user_emails( - &mut conn, &self.0, before_id, after_id, first, last, - ) + let page = conn + .user_email() + .list_paginated(&self.0, before_id, after_id, first, last) .await?; let mut connection = Connection::with_additional_fields( - has_previous_page, - has_next_page, + page.has_previous_page, + page.has_next_page, UserEmailsPagination(self.0.clone()), ); - connection.edges.extend(edges.into_iter().map(|u| { + connection.edges.extend(page.edges.into_iter().map(|u| { Edge::new( OpaqueCursor(NodeCursor(NodeType::UserEmail, u.id)), UserEmail(u), @@ -339,9 +344,9 @@ pub struct UserEmailsPagination(mas_data_model::User); #[Object] impl UserEmailsPagination { /// Identifies the total count of items in the connection. - async fn total_count(&self, ctx: &Context<'_>) -> Result { + async fn total_count(&self, ctx: &Context<'_>) -> Result { let mut conn = ctx.data::()?.acquire().await?; - let count = mas_storage::user::count_user_emails(&mut conn, &self.0).await?; + let count = conn.user_email().count(&self.0).await?; Ok(count) } } diff --git a/crates/handlers/src/compat/login.rs b/crates/handlers/src/compat/login.rs index 6d5ca825..dd4d4742 100644 --- a/crates/handlers/src/compat/login.rs +++ b/crates/handlers/src/compat/login.rs @@ -21,8 +21,8 @@ use mas_storage::{ add_compat_access_token, add_compat_refresh_token, get_compat_sso_login_by_token, mark_compat_sso_login_as_exchanged, start_compat_session, }, - user::{add_user_password, lookup_user_by_username, lookup_user_password}, - Clock, + user::{add_user_password, lookup_user_password, UserRepository}, + Clock, Repository, }; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, skip_serializing_none, DurationMilliSeconds}; @@ -314,7 +314,9 @@ async fn user_password_login( let (clock, mut rng) = crate::clock_and_rng(); // Find the user - let user = lookup_user_by_username(&mut *txn, &username) + let user = txn + .user() + .find_by_username(&username) .await? .ok_or(RouteError::UserNotFound)?; diff --git a/crates/handlers/src/compat/login_sso_complete.rs b/crates/handlers/src/compat/login_sso_complete.rs index e0416cd1..49790842 100644 --- a/crates/handlers/src/compat/login_sso_complete.rs +++ b/crates/handlers/src/compat/login_sso_complete.rs @@ -80,14 +80,8 @@ pub async fn get( return Ok((cookie_jar, url).into_response()); }; - // TODO: make that more generic - if session - .user - .primary_email - .as_ref() - .and_then(|e| e.confirmed_at) - .is_none() - { + // TODO: make that more generic, check that the email has been confirmed + if session.user.primary_user_email_id.is_none() { let destination = mas_router::AccountAddEmail::default() .and_then(PostAuthAction::continue_compat_sso_login(id)); return Ok((cookie_jar, destination.go()).into_response()); @@ -149,13 +143,7 @@ pub async fn post( }; // TODO: make that more generic - if session - .user - .primary_email - .as_ref() - .and_then(|e| e.confirmed_at) - .is_none() - { + if session.user.primary_user_email_id.is_none() { let destination = mas_router::AccountAddEmail::default() .and_then(PostAuthAction::continue_compat_sso_login(id)); return Ok((cookie_jar, destination.go()).into_response()); diff --git a/crates/handlers/src/oauth2/userinfo.rs b/crates/handlers/src/oauth2/userinfo.rs index 0369739d..225870ad 100644 --- a/crates/handlers/src/oauth2/userinfo.rs +++ b/crates/handlers/src/oauth2/userinfo.rs @@ -28,6 +28,7 @@ use mas_jose::{ }; use mas_keystore::Keystore; use mas_router::UrlBuilder; +use mas_storage::{user::UserEmailRepository, Repository}; use oauth2_types::scope; use serde::Serialize; use serde_with::skip_serializing_none; @@ -66,6 +67,7 @@ pub enum RouteError { } impl_from_error_for_route!(sqlx::Error); +impl_from_error_for_route!(mas_storage::DatabaseError); impl_from_error_for_route!(mas_keystore::WrongAlgorithmError); impl_from_error_for_route!(mas_jose::jwt::JwtSignatureError); @@ -92,19 +94,19 @@ pub async fn get( let session = user_authorization.protected(&mut conn).await?; let user = session.browser_session.user; - let mut user_info = UserInfo { - sub: user.sub, - username: user.username, - email: None, - email_verified: None, + + let user_email = if session.scope.contains(&scope::EMAIL) { + conn.user_email().get_primary(&user).await? + } else { + None }; - if session.scope.contains(&scope::EMAIL) { - if let Some(email) = user.primary_email { - user_info.email_verified = Some(email.confirmed_at.is_some()); - user_info.email = Some(email.email); - } - } + let user_info = UserInfo { + sub: user.sub.clone(), + username: user.username.clone(), + email_verified: user_email.as_ref().map(|u| u.confirmed_at.is_some()), + email: user_email.map(|u| u.email), + }; if let Some(alg) = session.client.userinfo_signed_response_alg { let key = key_store diff --git a/crates/handlers/src/upstream_oauth2/link.rs b/crates/handlers/src/upstream_oauth2/link.rs index c01d9799..dbd06059 100644 --- a/crates/handlers/src/upstream_oauth2/link.rs +++ b/crates/handlers/src/upstream_oauth2/link.rs @@ -26,7 +26,7 @@ use mas_axum_utils::{ use mas_keystore::Encrypter; use mas_storage::{ upstream_oauth2::UpstreamOAuthSessionRepository, - user::{add_user, authenticate_session_with_upstream, lookup_user, start_session}, + user::{authenticate_session_with_upstream, start_session, UserRepository}, Repository, UpstreamOAuthLinkRepository, }; use mas_templates::{ @@ -51,6 +51,10 @@ pub(crate) enum RouteError { #[error("Session not found")] SessionNotFound, + /// Couldn't find the user + #[error("User not found")] + UserNotFound, + /// Session was already consumed #[error("Session already consumed")] SessionConsumed, @@ -157,7 +161,11 @@ pub(crate) async fn get( // Session already linked, but link doesn't match the currently // logged user. Suggest logging out of the current user // and logging in with the new one - let user = lookup_user(&mut txn, user_id).await?; + let user = txn + .user() + .lookup(user_id) + .await? + .ok_or(RouteError::UserNotFound)?; let ctx = UpstreamExistingLinkContext::new(user) .with_session(user_session) @@ -177,7 +185,11 @@ pub(crate) async fn get( (None, Some(user_id)) => { // Session linked, but user not logged in: do the login - let user = lookup_user(&mut txn, user_id).await?; + let user = txn + .user() + .lookup(user_id) + .await? + .ok_or(RouteError::UserNotFound)?; let ctx = UpstreamExistingLinkContext::new(user).with_csrf(csrf_token.form_value()); @@ -250,12 +262,17 @@ pub(crate) async fn post( } (None, Some(user_id), FormData::Login) => { - let user = lookup_user(&mut txn, user_id).await?; + let user = txn + .user() + .lookup(user_id) + .await? + .ok_or(RouteError::UserNotFound)?; + start_session(&mut txn, &mut rng, &clock, user).await? } (None, None, FormData::Register { username }) => { - let user = add_user(&mut txn, &mut rng, &clock, &username).await?; + let user = txn.user().add(&mut rng, &clock, username).await?; txn.upstream_oauth_link() .associate_to_user(&link, &user) .await?; diff --git a/crates/handlers/src/views/account/emails/add.rs b/crates/handlers/src/views/account/emails/add.rs index 06fe7e06..c7cd2767 100644 --- a/crates/handlers/src/views/account/emails/add.rs +++ b/crates/handlers/src/views/account/emails/add.rs @@ -24,7 +24,7 @@ use mas_axum_utils::{ use mas_email::Mailer; use mas_keystore::Encrypter; use mas_router::Route; -use mas_storage::user::add_user_email; +use mas_storage::{user::UserEmailRepository, Repository}; use mas_templates::{EmailAddContext, TemplateContext, Templates}; use serde::Deserialize; use sqlx::PgPool; @@ -88,7 +88,11 @@ pub(crate) async fn post( return Ok((cookie_jar, login.go()).into_response()); }; - let user_email = add_user_email(&mut txn, &mut rng, &clock, &session.user, form.email).await?; + let user_email = txn + .user_email() + .add(&mut rng, &clock, &session.user, form.email) + .await?; + let next = mas_router::AccountVerifyEmail::new(user_email.id); let next = if let Some(action) = query.post_auth_action { next.and_then(action) diff --git a/crates/handlers/src/views/account/emails/mod.rs b/crates/handlers/src/views/account/emails/mod.rs index 061e360c..e6e1e341 100644 --- a/crates/handlers/src/views/account/emails/mod.rs +++ b/crates/handlers/src/views/account/emails/mod.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use anyhow::{anyhow, Context}; use axum::{ extract::{Form, State}, response::{Html, IntoResponse, Response}, @@ -27,17 +28,11 @@ use mas_data_model::{BrowserSession, User, UserEmail}; use mas_email::Mailer; use mas_keystore::Encrypter; use mas_router::Route; -use mas_storage::{ - user::{ - add_user_email, add_user_email_verification_code, get_user_email, get_user_emails, - remove_user_email, set_user_email_as_primary, - }, - Clock, -}; +use mas_storage::{user::UserEmailRepository, Clock, Repository}; use mas_templates::{AccountEmailsContext, EmailVerificationContext, TemplateContext, Templates}; use rand::{distributions::Uniform, Rng}; use serde::Deserialize; -use sqlx::{PgExecutor, PgPool}; +use sqlx::{PgConnection, PgPool}; use tracing::info; pub mod add; @@ -79,11 +74,11 @@ async fn render( templates: Templates, session: BrowserSession, cookie_jar: PrivateCookieJar, - executor: impl PgExecutor<'_>, + conn: &mut PgConnection, ) -> Result { let (csrf_token, cookie_jar) = cookie_jar.csrf_token(clock.now(), rng); - let emails = get_user_emails(executor, &session.user).await?; + let emails = conn.user_email().all(&session.user).await?; let ctx = AccountEmailsContext::new(emails) .with_session(session) @@ -96,7 +91,7 @@ async fn render( async fn start_email_verification( mailer: &Mailer, - executor: impl PgExecutor<'_>, + conn: &mut PgConnection, mut rng: impl Rng + Send, clock: &Clock, user: &User, @@ -108,15 +103,10 @@ async fn start_email_verification( let address: Address = user_email.email.parse()?; - let verification = add_user_email_verification_code( - executor, - &mut rng, - clock, - user_email, - Duration::hours(8), - code, - ) - .await?; + let verification = conn + .user_email() + .add_verification_code(&mut rng, clock, &user_email, Duration::hours(8), code) + .await?; // And send the verification email let mailbox = Mailbox::new(Some(user.username.clone()), address); @@ -126,7 +116,7 @@ async fn start_email_verification( mailer.send_verification_email(mailbox, &context).await?; info!( - email.id = %verification.email.id, + email.id = %user_email.id, "Verification email sent" ); Ok(()) @@ -157,49 +147,65 @@ pub(crate) async fn post( match form { ManagementForm::Add { email } => { - let user_email = - add_user_email(&mut txn, &mut rng, &clock, &session.user, email).await?; - let next = mas_router::AccountVerifyEmail::new(user_email.id); - start_email_verification( - &mailer, - &mut txn, - &mut rng, - &clock, - &session.user, - user_email, - ) - .await?; + let email = txn + .user_email() + .add(&mut rng, &clock, &session.user, email) + .await?; + + let next = mas_router::AccountVerifyEmail::new(email.id); + start_email_verification(&mailer, &mut txn, &mut rng, &clock, &session.user, email) + .await?; txn.commit().await?; return Ok((cookie_jar, next.go()).into_response()); } ManagementForm::ResendConfirmation { id } => { let id = id.parse()?; - let user_email = get_user_email(&mut txn, &session.user, id).await?; - let next = mas_router::AccountVerifyEmail::new(user_email.id); - start_email_verification( - &mailer, - &mut txn, - &mut rng, - &clock, - &session.user, - user_email, - ) - .await?; + let email = txn + .user_email() + .lookup(id) + .await? + .context("Email not found")?; + + if email.user_id != session.user.id { + return Err(anyhow!("Email not found").into()); + } + + let next = mas_router::AccountVerifyEmail::new(email.id); + start_email_verification(&mailer, &mut txn, &mut rng, &clock, &session.user, email) + .await?; txn.commit().await?; return Ok((cookie_jar, next.go()).into_response()); } ManagementForm::Remove { id } => { let id = id.parse()?; - let email = get_user_email(&mut txn, &session.user, id).await?; - remove_user_email(&mut txn, email).await?; + let email = txn + .user_email() + .lookup(id) + .await? + .context("Email not found")?; + + if email.user_id != session.user.id { + return Err(anyhow!("Email not found").into()); + } + + txn.user_email().remove(email).await?; } ManagementForm::SetPrimary { id } => { let id = id.parse()?; - let email = get_user_email(&mut txn, &session.user, id).await?; - set_user_email_as_primary(&mut txn, &email).await?; - session.user.primary_email = Some(email); + let email = txn + .user_email() + .lookup(id) + .await? + .context("Email not found")?; + + if email.user_id != session.user.id { + return Err(anyhow!("Email not found").into()); + } + + txn.user_email().set_as_primary(&email).await?; + session.user.primary_user_email_id = Some(email.id); } }; diff --git a/crates/handlers/src/views/account/emails/verify.rs b/crates/handlers/src/views/account/emails/verify.rs index 0ce6503a..1192743e 100644 --- a/crates/handlers/src/views/account/emails/verify.rs +++ b/crates/handlers/src/views/account/emails/verify.rs @@ -24,13 +24,7 @@ use mas_axum_utils::{ }; use mas_keystore::Encrypter; use mas_router::Route; -use mas_storage::{ - user::{ - consume_email_verification, lookup_user_email_by_id, lookup_user_email_verification_code, - mark_user_email_as_verified, set_user_email_as_primary, - }, - Clock, -}; +use mas_storage::{user::UserEmailRepository, Clock, Repository}; use mas_templates::{EmailVerificationPageContext, TemplateContext, Templates}; use serde::Deserialize; use sqlx::PgPool; @@ -65,8 +59,11 @@ pub(crate) async fn get( return Ok((cookie_jar, login.go()).into_response()); }; - let user_email = lookup_user_email_by_id(&mut conn, &session.user, id) + let user_email = conn + .user_email() + .lookup(id) .await? + .filter(|u| u.user_id == session.user.id) .context("Could not find user email")?; if user_email.confirmed_at.is_some() { @@ -106,23 +103,31 @@ pub(crate) async fn post( return Ok((cookie_jar, login.go()).into_response()); }; - let email = lookup_user_email_by_id(&mut txn, &session.user, id) + let user_email = txn + .user_email() + .lookup(id) .await? + .filter(|u| u.user_id == session.user.id) .context("Could not find user email")?; - if session.user.primary_email.is_none() { - set_user_email_as_primary(&mut txn, &email).await?; - } - - // TODO: make those 8 hours configurable - let verification = lookup_user_email_verification_code(&mut txn, &clock, email, &form.code) + let verification = txn + .user_email() + .find_verification_code(&clock, &user_email, &form.code) .await? .context("Invalid code")?; // TODO: display nice errors if the code was already consumed or expired - let verification = consume_email_verification(&mut txn, &clock, verification).await?; + txn.user_email() + .consume_verification_code(&clock, verification) + .await?; - let _email = mark_user_email_as_verified(&mut txn, &clock, verification.email).await?; + if session.user.primary_user_email_id.is_none() { + txn.user_email().set_as_primary(&user_email).await?; + } + + txn.user_email() + .mark_as_verified(&clock, user_email) + .await?; txn.commit().await?; diff --git a/crates/handlers/src/views/account/mod.rs b/crates/handlers/src/views/account/mod.rs index 87eec096..07a70898 100644 --- a/crates/handlers/src/views/account/mod.rs +++ b/crates/handlers/src/views/account/mod.rs @@ -23,7 +23,10 @@ use axum_extra::extract::PrivateCookieJar; use mas_axum_utils::{csrf::CsrfExt, FancyError, SessionInfoExt}; use mas_keystore::Encrypter; use mas_router::Route; -use mas_storage::user::{count_active_sessions, get_user_emails}; +use mas_storage::{ + user::{count_active_sessions, UserEmailRepository}, + Repository, +}; use mas_templates::{AccountContext, TemplateContext, Templates}; use sqlx::PgPool; @@ -49,7 +52,7 @@ pub(crate) async fn get( let active_sessions = count_active_sessions(&mut conn, &session.user).await?; - let emails = get_user_emails(&mut conn, &session.user).await?; + let emails = conn.user_email().all(&session.user).await?; let ctx = AccountContext::new(active_sessions, emails) .with_session(session) diff --git a/crates/handlers/src/views/login.rs b/crates/handlers/src/views/login.rs index fd54175d..5ba76b72 100644 --- a/crates/handlers/src/views/login.rs +++ b/crates/handlers/src/views/login.rs @@ -26,8 +26,8 @@ use mas_keystore::Encrypter; use mas_storage::{ upstream_oauth2::UpstreamOAuthProviderRepository, user::{ - add_user_password, authenticate_session_with_password, lookup_user_by_username, - lookup_user_password, start_session, + add_user_password, authenticate_session_with_password, lookup_user_password, start_session, + UserRepository, }, Clock, Repository, }; @@ -130,8 +130,6 @@ pub(crate) async fn post( return Ok((cookie_jar, Html(content)).into_response()); } - lookup_user_by_username(&mut conn, &form.username).await?; - match login( password_manager, &mut conn, @@ -175,7 +173,9 @@ async fn login( ) -> Result { // XXX: we're loosing the error context here // First, lookup the user - let user = lookup_user_by_username(&mut *conn, username) + let user = conn + .user() + .find_by_username(username) .await .map_err(|_e| FormError::Internal)? .ok_or(FormError::InvalidCredentials)?; diff --git a/crates/handlers/src/views/register.rs b/crates/handlers/src/views/register.rs index 9a12efac..01dc2116 100644 --- a/crates/handlers/src/views/register.rs +++ b/crates/handlers/src/views/register.rs @@ -31,9 +31,12 @@ use mas_email::Mailer; use mas_keystore::Encrypter; use mas_policy::PolicyFactory; use mas_router::Route; -use mas_storage::user::{ - add_user, add_user_email, add_user_email_verification_code, add_user_password, - authenticate_session_with_password, start_session, username_exists, +use mas_storage::{ + user::{ + add_user_password, authenticate_session_with_password, start_session, UserEmailRepository, + UserRepository, + }, + Repository, }; use mas_templates::{ EmailVerificationContext, FieldError, FormError, RegisterContext, RegisterFormField, @@ -114,7 +117,7 @@ pub(crate) async fn post( if form.username.is_empty() { state.add_error_on_field(RegisterFormField::Username, FieldError::Required); - } else if username_exists(&mut txn, &form.username).await? { + } else if txn.user().exists(&form.username).await? { state.add_error_on_field(RegisterFormField::Username, FieldError::Exists); } @@ -185,7 +188,7 @@ pub(crate) async fn post( return Ok((cookie_jar, Html(content)).into_response()); } - let user = add_user(&mut txn, &mut rng, &clock, &form.username).await?; + let user = txn.user().add(&mut rng, &clock, form.username).await?; let password = Zeroizing::new(form.password.into_bytes()); let (version, hashed_password) = password_manager.hash(&mut rng, password).await?; let user_password = add_user_password( @@ -199,7 +202,10 @@ pub(crate) async fn post( ) .await?; - let user_email = add_user_email(&mut txn, &mut rng, &clock, &user, form.email).await?; + let user_email = txn + .user_email() + .add(&mut rng, &clock, &user, form.email) + .await?; // First, generate a code let range = Uniform::::from(0..1_000_000); @@ -208,15 +214,10 @@ pub(crate) async fn post( let address: Address = user_email.email.parse()?; - let verification = add_user_email_verification_code( - &mut txn, - &mut rng, - &clock, - user_email, - Duration::hours(8), - code, - ) - .await?; + let verification = txn + .user_email() + .add_verification_code(&mut rng, &clock, &user_email, Duration::hours(8), code) + .await?; // And send the verification email let mailbox = Mailbox::new(Some(user.username.clone()), address); @@ -225,8 +226,7 @@ pub(crate) async fn post( mailer.send_verification_email(mailbox, &context).await?; - let next = mas_router::AccountVerifyEmail::new(verification.email.id) - .and_maybe(query.post_auth_action); + let next = mas_router::AccountVerifyEmail::new(user_email.id).and_maybe(query.post_auth_action); let mut session = start_session(&mut txn, &mut rng, &clock, user).await?; authenticate_session_with_password(&mut txn, &mut rng, &clock, &mut session, &user_password) diff --git a/crates/storage/sqlx-data.json b/crates/storage/sqlx-data.json index 8167fef3..3191a9db 100644 --- a/crates/storage/sqlx-data.json +++ b/crates/storage/sqlx-data.json @@ -1,5 +1,103 @@ { "db": "PostgreSQL", + "03bc4a14e97e011fec04e5788a967e04838cf978984254ecfd2c8b8a979da1c8": { + "describe": { + "columns": [ + { + "name": "oauth2_access_token_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "oauth2_access_token", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "oauth2_access_token_created_at", + "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_access_token_expires_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_session_id!", + "ordinal": 4, + "type_info": "Uuid" + }, + { + "name": "oauth2_client_id!", + "ordinal": 5, + "type_info": "Uuid" + }, + { + "name": "scope!", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "user_session_id!", + "ordinal": 7, + "type_info": "Uuid" + }, + { + "name": "user_session_created_at!", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "user_id!", + "ordinal": 9, + "type_info": "Uuid" + }, + { + "name": "user_username!", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "user_primary_user_email_id", + "ordinal": 11, + "type_info": "Uuid" + }, + { + "name": "user_session_last_authentication_id?", + "ordinal": 12, + "type_info": "Uuid" + }, + { + "name": "user_session_last_authentication_created_at?", + "ordinal": 13, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "\n SELECT at.oauth2_access_token_id\n , at.access_token AS \"oauth2_access_token\"\n , at.created_at AS \"oauth2_access_token_created_at\"\n , at.expires_at AS \"oauth2_access_token_expires_at\"\n , os.oauth2_session_id AS \"oauth2_session_id!\"\n , os.oauth2_client_id AS \"oauth2_client_id!\"\n , os.scope AS \"scope!\"\n , us.user_session_id AS \"user_session_id!\"\n , us.created_at AS \"user_session_created_at!\"\n , u.user_id AS \"user_id!\"\n , u.username AS \"user_username!\"\n , u.primary_user_email_id AS \"user_primary_user_email_id\"\n , usa.user_session_authentication_id AS \"user_session_last_authentication_id?\"\n , usa.created_at AS \"user_session_last_authentication_created_at?\"\n\n FROM oauth2_access_tokens at\n INNER JOIN oauth2_sessions os\n USING (oauth2_session_id)\n INNER JOIN user_sessions us\n USING (user_session_id)\n INNER JOIN users u\n USING (user_id)\n LEFT JOIN user_session_authentications usa\n USING (user_session_id)\n\n WHERE at.access_token = $1\n AND at.revoked_at IS NULL\n AND os.finished_at IS NULL\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " + }, "05b50b7ae0109063c50fe70e83635a31920e44a7fbaa2b4f07552ba2f83a28d7": { "describe": { "columns": [ @@ -116,7 +214,7 @@ }, "query": "\n SELECT\n c.oauth2_client_id,\n c.encrypted_client_secret,\n ARRAY(\n SELECT redirect_uri\n FROM oauth2_client_redirect_uris r\n WHERE r.oauth2_client_id = c.oauth2_client_id\n ) AS \"redirect_uris!\",\n c.grant_type_authorization_code,\n c.grant_type_refresh_token,\n c.client_name,\n c.logo_uri,\n c.client_uri,\n c.policy_uri,\n c.tos_uri,\n c.jwks_uri,\n c.jwks,\n c.id_token_signed_response_alg,\n c.userinfo_signed_response_alg,\n c.token_endpoint_auth_method,\n c.token_endpoint_auth_signing_alg,\n c.initiate_login_uri\n FROM oauth2_clients c\n\n WHERE c.oauth2_client_id = $1\n " }, - "0b49cde0b7b79f79ec261502ab89bcffa81f9f5ed2f922a41b1718274b9e3073": { + "08d7df347c806ef14b6d0fb031cab041d79ba48528420160e23286369db7af35": { "describe": { "columns": [ { @@ -125,28 +223,71 @@ "type_info": "Uuid" }, { - "name": "user_username", + "name": "username", "ordinal": 1, "type_info": "Text" }, { - "name": "user_email_id?", + "name": "primary_user_email_id", "ordinal": 2, "type_info": "Uuid" }, { - "name": "user_email?", + "name": "created_at", + "ordinal": 3, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + true, + false + ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "\n SELECT user_id\n , username\n , primary_user_email_id\n , created_at\n FROM users\n WHERE user_id = $1\n " + }, + "09d995295b2e4f180181ec96023b1e524ddae9098694eedc4dcce857e3095c0e": { + "describe": { + "columns": [ + { + "name": "user_session_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "user_session_created_at", + "ordinal": 1, + "type_info": "Timestamptz" + }, + { + "name": "user_id", + "ordinal": 2, + "type_info": "Uuid" + }, + { + "name": "user_username", "ordinal": 3, "type_info": "Text" }, { - "name": "user_email_created_at?", + "name": "user_primary_user_email_id", "ordinal": 4, - "type_info": "Timestamptz" + "type_info": "Uuid" }, { - "name": "user_email_confirmed_at?", + "name": "last_authentication_id?", "ordinal": 5, + "type_info": "Uuid" + }, + { + "name": "last_authd_at?", + "ordinal": 6, "type_info": "Timestamptz" } ], @@ -155,29 +296,17 @@ false, false, false, + true, false, - true + false ], "parameters": { "Left": [ - "Text" + "Uuid" ] } }, - "query": "\n SELECT\n u.user_id,\n u.username AS user_username,\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM users u\n\n LEFT JOIN user_emails ue\n USING (user_id)\n\n WHERE u.username = $1\n " - }, - "1166343ad1563cb66ab387368f67320a53c34edf388bdb991359ebdf324497d5": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Uuid", - "Timestamptz" - ] - } - }, - "query": "\n UPDATE user_emails\n SET confirmed_at = $2\n WHERE user_email_id = $1\n " + "query": "\n SELECT s.user_session_id\n , s.created_at AS \"user_session_created_at\"\n , u.user_id\n , u.username AS \"user_username\"\n , u.primary_user_email_id AS \"user_primary_user_email_id\"\n , a.user_session_authentication_id AS \"last_authentication_id?\"\n , a.created_at AS \"last_authd_at?\"\n FROM user_sessions s\n INNER JOIN users u\n USING (user_id)\n LEFT JOIN user_session_authentications a\n USING (user_session_id)\n WHERE s.user_session_id = $1 AND s.finished_at IS NULL\n ORDER BY a.created_at DESC\n LIMIT 1\n " }, "154e2e4488ff87e09163698750b56a43127cee4e1392785416a586d40a4d9b21": { "describe": { @@ -239,56 +368,99 @@ }, "query": "\n SELECT\n upstream_oauth_provider_id,\n issuer,\n scope,\n client_id,\n encrypted_client_secret,\n token_endpoint_signing_alg,\n token_endpoint_auth_method,\n created_at\n FROM upstream_oauth_providers\n " }, - "1eb6d13e75d8f526c2785749a020731c18012f03e07995213acd38ab560ce497": { + "16a1c5fe5a4c5481212560d79d589b550dfefe7480c5ee4febcbfaaa01ee93a4": { "describe": { - "columns": [], - "nullable": [], + "columns": [ + { + "name": "compat_sso_login_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "compat_sso_login_token", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "compat_sso_login_redirect_uri", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "compat_sso_login_created_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "compat_sso_login_fulfilled_at", + "ordinal": 4, + "type_info": "Timestamptz" + }, + { + "name": "compat_sso_login_exchanged_at", + "ordinal": 5, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_id?", + "ordinal": 6, + "type_info": "Uuid" + }, + { + "name": "compat_session_created_at?", + "ordinal": 7, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_finished_at?", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_device_id?", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "user_id?", + "ordinal": 10, + "type_info": "Uuid" + }, + { + "name": "user_username?", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "user_primary_user_email_id?", + "ordinal": 12, + "type_info": "Uuid" + } + ], + "nullable": [ + false, + false, + false, + false, + true, + true, + false, + false, + true, + false, + false, + false, + true + ], "parameters": { "Left": [ - "Uuid", - "Uuid", - "Timestamptz" + "Text" ] } }, - "query": "\n INSERT INTO user_session_authentications\n (user_session_authentication_id, user_session_id, created_at)\n VALUES ($1, $2, $3)\n " + "query": "\n SELECT\n cl.compat_sso_login_id,\n cl.login_token AS \"compat_sso_login_token\",\n cl.redirect_uri AS \"compat_sso_login_redirect_uri\",\n cl.created_at AS \"compat_sso_login_created_at\",\n cl.fulfilled_at AS \"compat_sso_login_fulfilled_at\",\n cl.exchanged_at AS \"compat_sso_login_exchanged_at\",\n cs.compat_session_id AS \"compat_session_id?\",\n cs.created_at AS \"compat_session_created_at?\",\n cs.finished_at AS \"compat_session_finished_at?\",\n cs.device_id AS \"compat_session_device_id?\",\n u.user_id AS \"user_id?\",\n u.username AS \"user_username?\",\n u.primary_user_email_id AS \"user_primary_user_email_id?\"\n FROM compat_sso_logins cl\n LEFT JOIN compat_sessions cs\n USING (compat_session_id)\n LEFT JOIN users u\n USING (user_id)\n WHERE cl.login_token = $1\n " }, - "1ee5cecfafd4726a4ebc08da8a34c09178e6e1e072581c8fca9d3d76967792cb": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Uuid", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Timestamptz" - ] - } - }, - "query": "\n INSERT INTO upstream_oauth_providers (\n upstream_oauth_provider_id,\n issuer,\n scope,\n token_endpoint_auth_method,\n token_endpoint_signing_alg,\n client_id,\n encrypted_client_secret,\n created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n " - }, - "2153118b364a33582e7f598acce3789fcb8d938948a819b15cf0b6d37edf58b2": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Uuid", - "Uuid", - "Text", - "Timestamptz", - "Timestamptz" - ] - } - }, - "query": "\n INSERT INTO compat_access_tokens\n (compat_access_token_id, compat_session_id, access_token, created_at, expires_at)\n VALUES ($1, $2, $3, $4, $5)\n " - }, - "24d6154b138a5e9105b996d6447e45a5c208e157f6583b4220cf58813d6f436c": { + "1a5e0d1d88065bb4e7f790942856d1d94ecdb30a7007f3277ca3f7cbdabd4dff": { "describe": { "columns": [ { @@ -407,33 +579,18 @@ "type_info": "Text" }, { - "name": "user_session_last_authentication_id?", + "name": "user_primary_user_email_id?", "ordinal": 23, "type_info": "Uuid" }, { - "name": "user_session_last_authentication_created_at?", + "name": "user_session_last_authentication_id?", "ordinal": 24, - "type_info": "Timestamptz" - }, - { - "name": "user_email_id?", - "ordinal": 25, "type_info": "Uuid" }, { - "name": "user_email?", - "ordinal": 26, - "type_info": "Text" - }, - { - "name": "user_email_created_at?", - "ordinal": 27, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at?", - "ordinal": 28, + "name": "user_session_last_authentication_created_at?", + "ordinal": 25, "type_info": "Timestamptz" } ], @@ -461,6 +618,223 @@ false, false, false, + true, + false, + false + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "\n SELECT\n og.oauth2_authorization_grant_id,\n og.created_at AS oauth2_authorization_grant_created_at,\n og.cancelled_at AS oauth2_authorization_grant_cancelled_at,\n og.fulfilled_at AS oauth2_authorization_grant_fulfilled_at,\n og.exchanged_at AS oauth2_authorization_grant_exchanged_at,\n og.scope AS oauth2_authorization_grant_scope,\n og.state AS oauth2_authorization_grant_state,\n og.redirect_uri AS oauth2_authorization_grant_redirect_uri,\n og.response_mode AS oauth2_authorization_grant_response_mode,\n og.nonce AS oauth2_authorization_grant_nonce,\n og.max_age AS oauth2_authorization_grant_max_age,\n og.oauth2_client_id AS oauth2_client_id,\n og.authorization_code AS oauth2_authorization_grant_code,\n og.response_type_code AS oauth2_authorization_grant_response_type_code,\n og.response_type_id_token AS oauth2_authorization_grant_response_type_id_token,\n og.code_challenge AS oauth2_authorization_grant_code_challenge,\n og.code_challenge_method AS oauth2_authorization_grant_code_challenge_method,\n og.requires_consent AS oauth2_authorization_grant_requires_consent,\n os.oauth2_session_id AS \"oauth2_session_id?\",\n us.user_session_id AS \"user_session_id?\",\n us.created_at AS \"user_session_created_at?\",\n u.user_id AS \"user_id?\",\n u.username AS \"user_username?\",\n u.primary_user_email_id AS \"user_primary_user_email_id?\",\n usa.user_session_authentication_id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\"\n FROM\n oauth2_authorization_grants og\n LEFT JOIN oauth2_sessions os\n USING (oauth2_session_id)\n LEFT JOIN user_sessions us\n USING (user_session_id)\n LEFT JOIN users u\n USING (user_id)\n LEFT JOIN user_session_authentications usa\n USING (user_session_id)\n\n WHERE og.authorization_code = $1\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " + }, + "1b448fe73e12bef622b75857e4c9b257c9529ca18da7f63d127e63184f4bc94b": { + "describe": { + "columns": [ + { + "name": "oauth2_authorization_grant_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "oauth2_authorization_grant_created_at", + "ordinal": 1, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_authorization_grant_cancelled_at", + "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_authorization_grant_fulfilled_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_authorization_grant_exchanged_at", + "ordinal": 4, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_authorization_grant_scope", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "oauth2_authorization_grant_state", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "oauth2_authorization_grant_redirect_uri", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "oauth2_authorization_grant_response_mode", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "oauth2_authorization_grant_nonce", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "oauth2_authorization_grant_max_age", + "ordinal": 10, + "type_info": "Int4" + }, + { + "name": "oauth2_client_id", + "ordinal": 11, + "type_info": "Uuid" + }, + { + "name": "oauth2_authorization_grant_code", + "ordinal": 12, + "type_info": "Text" + }, + { + "name": "oauth2_authorization_grant_response_type_code", + "ordinal": 13, + "type_info": "Bool" + }, + { + "name": "oauth2_authorization_grant_response_type_id_token", + "ordinal": 14, + "type_info": "Bool" + }, + { + "name": "oauth2_authorization_grant_code_challenge", + "ordinal": 15, + "type_info": "Text" + }, + { + "name": "oauth2_authorization_grant_code_challenge_method", + "ordinal": 16, + "type_info": "Text" + }, + { + "name": "oauth2_authorization_grant_requires_consent", + "ordinal": 17, + "type_info": "Bool" + }, + { + "name": "oauth2_session_id?", + "ordinal": 18, + "type_info": "Uuid" + }, + { + "name": "user_session_id?", + "ordinal": 19, + "type_info": "Uuid" + }, + { + "name": "user_session_created_at?", + "ordinal": 20, + "type_info": "Timestamptz" + }, + { + "name": "user_id?", + "ordinal": 21, + "type_info": "Uuid" + }, + { + "name": "user_username?", + "ordinal": 22, + "type_info": "Text" + }, + { + "name": "user_primary_user_email_id?", + "ordinal": 23, + "type_info": "Uuid" + }, + { + "name": "user_session_last_authentication_id?", + "ordinal": 24, + "type_info": "Uuid" + }, + { + "name": "user_session_last_authentication_created_at?", + "ordinal": 25, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + true, + true, + true, + false, + true, + false, + false, + true, + true, + false, + true, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + true, + false, + false + ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "\n SELECT\n og.oauth2_authorization_grant_id,\n og.created_at AS oauth2_authorization_grant_created_at,\n og.cancelled_at AS oauth2_authorization_grant_cancelled_at,\n og.fulfilled_at AS oauth2_authorization_grant_fulfilled_at,\n og.exchanged_at AS oauth2_authorization_grant_exchanged_at,\n og.scope AS oauth2_authorization_grant_scope,\n og.state AS oauth2_authorization_grant_state,\n og.redirect_uri AS oauth2_authorization_grant_redirect_uri,\n og.response_mode AS oauth2_authorization_grant_response_mode,\n og.nonce AS oauth2_authorization_grant_nonce,\n og.max_age AS oauth2_authorization_grant_max_age,\n og.oauth2_client_id AS oauth2_client_id,\n og.authorization_code AS oauth2_authorization_grant_code,\n og.response_type_code AS oauth2_authorization_grant_response_type_code,\n og.response_type_id_token AS oauth2_authorization_grant_response_type_id_token,\n og.code_challenge AS oauth2_authorization_grant_code_challenge,\n og.code_challenge_method AS oauth2_authorization_grant_code_challenge_method,\n og.requires_consent AS oauth2_authorization_grant_requires_consent,\n os.oauth2_session_id AS \"oauth2_session_id?\",\n us.user_session_id AS \"user_session_id?\",\n us.created_at AS \"user_session_created_at?\",\n u.user_id AS \"user_id?\",\n u.username AS \"user_username?\",\n u.primary_user_email_id AS \"user_primary_user_email_id?\",\n usa.user_session_authentication_id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\"\n FROM\n oauth2_authorization_grants og\n LEFT JOIN oauth2_sessions os\n USING (oauth2_session_id)\n LEFT JOIN user_sessions us\n USING (user_session_id)\n LEFT JOIN users u\n USING (user_id)\n LEFT JOIN user_session_authentications usa\n USING (user_session_id)\n\n WHERE og.oauth2_authorization_grant_id = $1\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " + }, + "1d372f36c382ab16264cea54537af3544ea6d6d75d10b432b07dbd0dadd2fa4e": { + "describe": { + "columns": [ + { + "name": "user_email_confirmation_code_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "user_email_id", + "ordinal": 1, + "type_info": "Uuid" + }, + { + "name": "code", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "expires_at", + "ordinal": 4, + "type_info": "Timestamptz" + }, + { + "name": "consumed_at", + "ordinal": 5, + "type_info": "Timestamptz" + } + ], + "nullable": [ false, false, false, @@ -470,11 +844,61 @@ ], "parameters": { "Left": [ - "Text" + "Text", + "Uuid" ] } }, - "query": "\n SELECT\n og.oauth2_authorization_grant_id,\n og.created_at AS oauth2_authorization_grant_created_at,\n og.cancelled_at AS oauth2_authorization_grant_cancelled_at,\n og.fulfilled_at AS oauth2_authorization_grant_fulfilled_at,\n og.exchanged_at AS oauth2_authorization_grant_exchanged_at,\n og.scope AS oauth2_authorization_grant_scope,\n og.state AS oauth2_authorization_grant_state,\n og.redirect_uri AS oauth2_authorization_grant_redirect_uri,\n og.response_mode AS oauth2_authorization_grant_response_mode,\n og.nonce AS oauth2_authorization_grant_nonce,\n og.max_age AS oauth2_authorization_grant_max_age,\n og.oauth2_client_id AS oauth2_client_id,\n og.authorization_code AS oauth2_authorization_grant_code,\n og.response_type_code AS oauth2_authorization_grant_response_type_code,\n og.response_type_id_token AS oauth2_authorization_grant_response_type_id_token,\n og.code_challenge AS oauth2_authorization_grant_code_challenge,\n og.code_challenge_method AS oauth2_authorization_grant_code_challenge_method,\n og.requires_consent AS oauth2_authorization_grant_requires_consent,\n os.oauth2_session_id AS \"oauth2_session_id?\",\n us.user_session_id AS \"user_session_id?\",\n us.created_at AS \"user_session_created_at?\",\n u.user_id AS \"user_id?\",\n u.username AS \"user_username?\",\n usa.user_session_authentication_id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM\n oauth2_authorization_grants og\n LEFT JOIN oauth2_sessions os\n USING (oauth2_session_id)\n LEFT JOIN user_sessions us\n USING (user_session_id)\n LEFT JOIN users u\n USING (user_id)\n LEFT JOIN user_session_authentications usa\n USING (user_session_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n\n WHERE og.authorization_code = $1\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " + "query": "\n SELECT user_email_confirmation_code_id\n , user_email_id\n , code\n , created_at\n , expires_at\n , consumed_at\n FROM user_email_confirmation_codes\n WHERE code = $1\n AND user_email_id = $2\n " + }, + "1eb6d13e75d8f526c2785749a020731c18012f03e07995213acd38ab560ce497": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO user_session_authentications\n (user_session_authentication_id, user_session_id, created_at)\n VALUES ($1, $2, $3)\n " + }, + "1ee5cecfafd4726a4ebc08da8a34c09178e6e1e072581c8fca9d3d76967792cb": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO upstream_oauth_providers (\n upstream_oauth_provider_id,\n issuer,\n scope,\n token_endpoint_auth_method,\n token_endpoint_signing_alg,\n client_id,\n encrypted_client_secret,\n created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n " + }, + "2153118b364a33582e7f598acce3789fcb8d938948a819b15cf0b6d37edf58b2": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Timestamptz", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO compat_access_tokens\n (compat_access_token_id, compat_session_id, access_token, created_at, expires_at)\n VALUES ($1, $2, $3, $4, $5)\n " }, "262bee715889dc3e608639549600a131e641951ff979634e7c97afc74bbc1605": { "describe": { @@ -507,119 +931,6 @@ }, "query": "\n INSERT INTO oauth2_clients\n (oauth2_client_id,\n encrypted_client_secret,\n grant_type_authorization_code,\n grant_type_refresh_token,\n token_endpoint_auth_method,\n jwks,\n jwks_uri)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7)\n " }, - "27a729b229491d179391b19b634f07291312bd238380c5a7ea0f60e9b71dfb14": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Uuid", - "Text", - "Timestamptz" - ] - } - }, - "query": "\n INSERT INTO users (user_id, username, created_at)\n VALUES ($1, $2, $3)\n " - }, - "2e581d57db471b96091860cd0252361d16332deeffabab0dace405ead55324be": { - "describe": { - "columns": [ - { - "name": "compat_access_token_id", - "ordinal": 0, - "type_info": "Uuid" - }, - { - "name": "compat_access_token", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "compat_access_token_created_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "compat_access_token_expires_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_id", - "ordinal": 4, - "type_info": "Uuid" - }, - { - "name": "compat_session_created_at", - "ordinal": 5, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_finished_at", - "ordinal": 6, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_device_id", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "user_id!", - "ordinal": 8, - "type_info": "Uuid" - }, - { - "name": "user_username!", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "user_email_id?", - "ordinal": 10, - "type_info": "Uuid" - }, - { - "name": "user_email?", - "ordinal": 11, - "type_info": "Text" - }, - { - "name": "user_email_created_at?", - "ordinal": 12, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at?", - "ordinal": 13, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - false, - true, - false, - false, - true, - false, - false, - false, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Text", - "Timestamptz" - ] - } - }, - "query": "\n SELECT\n ct.compat_access_token_id,\n ct.access_token AS \"compat_access_token\",\n ct.created_at AS \"compat_access_token_created_at\",\n ct.expires_at AS \"compat_access_token_expires_at\",\n cs.compat_session_id,\n cs.created_at AS \"compat_session_created_at\",\n cs.finished_at AS \"compat_session_finished_at\",\n cs.device_id AS \"compat_session_device_id\",\n u.user_id AS \"user_id!\",\n u.username AS \"user_username!\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n\n FROM compat_access_tokens ct\n INNER JOIN compat_sessions cs\n USING (compat_session_id)\n INNER JOIN users u\n USING (user_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n\n WHERE ct.access_token = $1\n AND (ct.expires_at < $2 OR ct.expires_at IS NULL)\n AND cs.finished_at IS NULL \n " - }, "2e756fe7be50128c0acc5f79df3a084230e9ca13cd45bd0858f97e59da20006e": { "describe": { "columns": [], @@ -649,133 +960,17 @@ }, "query": "\n INSERT INTO compat_refresh_tokens\n (compat_refresh_token_id, compat_session_id,\n compat_access_token_id, refresh_token, created_at)\n VALUES ($1, $2, $3, $4, $5)\n " }, - "3a19b087ae9e4dab770f102de1cb62628525fc72c7b052e1c146161ab088c02b": { + "3d66f3121b11ce923b9c60609b510a8ca899640e78cc8f5b03168622928ffe94": { "describe": { - "columns": [ - { - "name": "user_email_id", - "ordinal": 0, - "type_info": "Uuid" - }, - { - "name": "user_email", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "user_email_created_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at", - "ordinal": 3, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Uuid", - "Text" - ] - } - }, - "query": "\n SELECT\n ue.user_email_id,\n ue.email AS \"user_email\",\n ue.created_at AS \"user_email_created_at\",\n ue.confirmed_at AS \"user_email_confirmed_at\"\n FROM user_emails ue\n\n WHERE ue.user_id = $1\n AND ue.email = $2\n " - }, - "3df0838b660466f69ee681337fe6753133748defb715e53c8381badcc3e8bca9": { - "describe": { - "columns": [ - { - "name": "user_session_id", - "ordinal": 0, - "type_info": "Uuid" - }, - { - "name": "user_id", - "ordinal": 1, - "type_info": "Uuid" - }, - { - "name": "username", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "created_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "last_authentication_id?", - "ordinal": 4, - "type_info": "Uuid" - }, - { - "name": "last_authd_at?", - "ordinal": 5, - "type_info": "Timestamptz" - }, - { - "name": "user_email_id?", - "ordinal": 6, - "type_info": "Uuid" - }, - { - "name": "user_email?", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "user_email_created_at?", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at?", - "ordinal": 9, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - true - ], + "columns": [], + "nullable": [], "parameters": { "Left": [ "Uuid" ] } }, - "query": "\n SELECT\n s.user_session_id,\n u.user_id,\n u.username,\n s.created_at,\n a.user_session_authentication_id AS \"last_authentication_id?\",\n a.created_at AS \"last_authd_at?\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM user_sessions s\n INNER JOIN users u\n USING (user_id)\n LEFT JOIN user_session_authentications a\n USING (user_session_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n WHERE s.user_session_id = $1 AND s.finished_at IS NULL\n ORDER BY a.created_at DESC\n LIMIT 1\n " - }, - "3e8f862ed05ce3e58c181ac6e0bd71e0a6a88419611af6f4117d14d9c36cb1ef": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Uuid", - "Uuid", - "Text", - "Timestamptz" - ] - } - }, - "query": "\n INSERT INTO user_emails (user_email_id, user_id, email, created_at)\n VALUES ($1, $2, $3, $4)\n " + "query": "\n DELETE FROM user_emails\n WHERE user_email_id = $1\n " }, "4187907bfc770b2c76f741671d5e672f5c35eed7c9a9e57ff52888b1768a5ed6": { "describe": { @@ -821,102 +1016,36 @@ }, "query": "\n SELECT\n upstream_oauth_link_id,\n upstream_oauth_provider_id,\n user_id,\n subject,\n created_at\n FROM upstream_oauth_links\n WHERE upstream_oauth_link_id = $1\n " }, - "42bfb0de5bbea2d580f1ff2322255731a4a5655ba80fc2dba0b55a0add8c55c0": { + "4192c1144c0ea530cf1aa77993a38e94cd5cf8b5c42cb037efb7917c6fc44a1d": { "describe": { "columns": [ { - "name": "compat_sso_login_id", + "name": "user_email_id", "ordinal": 0, "type_info": "Uuid" }, { - "name": "compat_sso_login_token", + "name": "user_id", "ordinal": 1, - "type_info": "Text" + "type_info": "Uuid" }, { - "name": "compat_sso_login_redirect_uri", + "name": "email", "ordinal": 2, "type_info": "Text" }, { - "name": "compat_sso_login_created_at", + "name": "created_at", "ordinal": 3, "type_info": "Timestamptz" }, { - "name": "compat_sso_login_fulfilled_at", + "name": "confirmed_at", "ordinal": 4, "type_info": "Timestamptz" - }, - { - "name": "compat_sso_login_exchanged_at", - "ordinal": 5, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_id?", - "ordinal": 6, - "type_info": "Uuid" - }, - { - "name": "compat_session_created_at?", - "ordinal": 7, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_finished_at?", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_device_id?", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "user_id?", - "ordinal": 10, - "type_info": "Uuid" - }, - { - "name": "user_username?", - "ordinal": 11, - "type_info": "Text" - }, - { - "name": "user_email_id?", - "ordinal": 12, - "type_info": "Uuid" - }, - { - "name": "user_email?", - "ordinal": 13, - "type_info": "Text" - }, - { - "name": "user_email_created_at?", - "ordinal": 14, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at?", - "ordinal": 15, - "type_info": "Timestamptz" } ], "nullable": [ - false, - false, - false, - false, - true, - true, - false, - false, - true, - false, - false, false, false, false, @@ -929,7 +1058,7 @@ ] } }, - "query": "\n SELECT\n cl.compat_sso_login_id,\n cl.login_token AS \"compat_sso_login_token\",\n cl.redirect_uri AS \"compat_sso_login_redirect_uri\",\n cl.created_at AS \"compat_sso_login_created_at\",\n cl.fulfilled_at AS \"compat_sso_login_fulfilled_at\",\n cl.exchanged_at AS \"compat_sso_login_exchanged_at\",\n cs.compat_session_id AS \"compat_session_id?\",\n cs.created_at AS \"compat_session_created_at?\",\n cs.finished_at AS \"compat_session_finished_at?\",\n cs.device_id AS \"compat_session_device_id?\",\n u.user_id AS \"user_id?\",\n u.username AS \"user_username?\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM compat_sso_logins cl\n LEFT JOIN compat_sessions cs\n USING (compat_session_id)\n LEFT JOIN users u\n USING (user_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n WHERE cl.compat_sso_login_id = $1\n " + "query": "\n SELECT user_email_id\n , user_id\n , email\n , created_at\n , confirmed_at\n FROM user_emails\n\n WHERE user_email_id = $1\n " }, "43a5cafbdc8037e9fb779812a0793cf0859902aa0dc8d25d4c33d231d3d1118b": { "describe": { @@ -982,122 +1111,6 @@ }, "query": "\n UPDATE oauth2_authorization_grants AS og\n SET\n oauth2_session_id = os.oauth2_session_id,\n fulfilled_at = os.created_at\n FROM oauth2_sessions os\n WHERE\n og.oauth2_authorization_grant_id = $1\n AND os.oauth2_session_id = $2\n RETURNING fulfilled_at AS \"fulfilled_at!: DateTime\"\n " }, - "4f8ec19f3f1bfe0268fe102a24e5a9fa542e77eccbebdce65e6deb1c197adf36": { - "describe": { - "columns": [ - { - "name": "oauth2_access_token_id", - "ordinal": 0, - "type_info": "Uuid" - }, - { - "name": "oauth2_access_token", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "oauth2_access_token_created_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_access_token_expires_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_session_id!", - "ordinal": 4, - "type_info": "Uuid" - }, - { - "name": "oauth2_client_id!", - "ordinal": 5, - "type_info": "Uuid" - }, - { - "name": "scope!", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "user_session_id!", - "ordinal": 7, - "type_info": "Uuid" - }, - { - "name": "user_session_created_at!", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "user_id!", - "ordinal": 9, - "type_info": "Uuid" - }, - { - "name": "user_username!", - "ordinal": 10, - "type_info": "Text" - }, - { - "name": "user_session_last_authentication_id?", - "ordinal": 11, - "type_info": "Uuid" - }, - { - "name": "user_session_last_authentication_created_at?", - "ordinal": 12, - "type_info": "Timestamptz" - }, - { - "name": "user_email_id?", - "ordinal": 13, - "type_info": "Uuid" - }, - { - "name": "user_email?", - "ordinal": 14, - "type_info": "Text" - }, - { - "name": "user_email_created_at?", - "ordinal": 15, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at?", - "ordinal": 16, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT\n at.oauth2_access_token_id,\n at.access_token AS \"oauth2_access_token\",\n at.created_at AS \"oauth2_access_token_created_at\",\n at.expires_at AS \"oauth2_access_token_expires_at\",\n os.oauth2_session_id AS \"oauth2_session_id!\",\n os.oauth2_client_id AS \"oauth2_client_id!\",\n os.scope AS \"scope!\",\n us.user_session_id AS \"user_session_id!\",\n us.created_at AS \"user_session_created_at!\",\n u.user_id AS \"user_id!\",\n u.username AS \"user_username!\",\n usa.user_session_authentication_id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n\n FROM oauth2_access_tokens at\n INNER JOIN oauth2_sessions os\n USING (oauth2_session_id)\n INNER JOIN user_sessions us\n USING (user_session_id)\n INNER JOIN users u\n USING (user_id)\n LEFT JOIN user_session_authentications usa\n USING (user_session_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n\n WHERE at.access_token = $1\n AND at.revoked_at IS NULL\n AND os.finished_at IS NULL\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " - }, "51158bfcaa1a8d8e051bffe7c5ba0369bf53fb162f7622626054e89e68fc07bd": { "describe": { "columns": [ @@ -1119,6 +1132,104 @@ }, "query": "\n SELECT scope_token\n FROM oauth2_consents\n WHERE user_id = $1 AND oauth2_client_id = $2\n " }, + "51bf417d259989d1228ba86fa11432e9428dece97b79e93f13921d0a510a9428": { + "describe": { + "columns": [ + { + "name": "compat_refresh_token_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "compat_refresh_token", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "compat_refresh_token_created_at", + "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "compat_access_token_id", + "ordinal": 3, + "type_info": "Uuid" + }, + { + "name": "compat_access_token", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "compat_access_token_created_at", + "ordinal": 5, + "type_info": "Timestamptz" + }, + { + "name": "compat_access_token_expires_at", + "ordinal": 6, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_id", + "ordinal": 7, + "type_info": "Uuid" + }, + { + "name": "compat_session_created_at", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_finished_at", + "ordinal": 9, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_device_id", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "user_id", + "ordinal": 11, + "type_info": "Uuid" + }, + { + "name": "user_username!", + "ordinal": 12, + "type_info": "Text" + }, + { + "name": "user_primary_user_email_id", + "ordinal": 13, + "type_info": "Uuid" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + false, + true, + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "\n SELECT\n cr.compat_refresh_token_id,\n cr.refresh_token AS \"compat_refresh_token\",\n cr.created_at AS \"compat_refresh_token_created_at\",\n ct.compat_access_token_id,\n ct.access_token AS \"compat_access_token\",\n ct.created_at AS \"compat_access_token_created_at\",\n ct.expires_at AS \"compat_access_token_expires_at\",\n cs.compat_session_id,\n cs.created_at AS \"compat_session_created_at\",\n cs.finished_at AS \"compat_session_finished_at\",\n cs.device_id AS \"compat_session_device_id\",\n u.user_id,\n u.username AS \"user_username!\",\n u.primary_user_email_id AS \"user_primary_user_email_id\"\n\n FROM compat_refresh_tokens cr\n INNER JOIN compat_sessions cs\n USING (compat_session_id)\n INNER JOIN compat_access_tokens ct\n USING (compat_access_token_id)\n INNER JOIN users u\n USING (user_id)\n\n WHERE cr.refresh_token = $1\n AND cr.consumed_at IS NULL\n AND cs.finished_at IS NULL\n " + }, "559a486756d08d101eb7188ef6637b9d24c024d056795b8121f7f04a7f9db6a3": { "describe": { "columns": [ @@ -1140,56 +1251,6 @@ }, "query": "\n UPDATE compat_sessions cs\n SET finished_at = $2\n FROM compat_access_tokens ca\n WHERE ca.access_token = $1\n AND ca.compat_session_id = cs.compat_session_id\n AND cs.finished_at IS NULL\n RETURNING cs.compat_session_id\n " }, - "59439585536bb4e547a6cf58a8bc6ac735f29c225bcbeac7d371f09166789a73": { - "describe": { - "columns": [ - { - "name": "user_id", - "ordinal": 0, - "type_info": "Uuid" - }, - { - "name": "user_username", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "user_email_id?", - "ordinal": 2, - "type_info": "Uuid" - }, - { - "name": "user_email?", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "user_email_created_at?", - "ordinal": 4, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at?", - "ordinal": 5, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Uuid" - ] - } - }, - "query": "\n SELECT\n u.user_id,\n u.username AS user_username,\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM users u\n\n LEFT JOIN user_emails ue\n USING (user_id)\n\n WHERE u.user_id = $1\n " - }, "5b5d5c82da37c6f2d8affacfb02119965c04d1f2a9cc53dbf5bd4c12584969a0": { "describe": { "columns": [], @@ -1202,44 +1263,6 @@ }, "query": "\n DELETE FROM oauth2_access_tokens\n WHERE expires_at < $1\n " }, - "5ccde09ee3fe43e7b492d73fa67708b5dcb2b7496c4d05bcfcf0ea63c7576d48": { - "describe": { - "columns": [ - { - "name": "user_email_id", - "ordinal": 0, - "type_info": "Uuid" - }, - { - "name": "user_email", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "user_email_created_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at", - "ordinal": 3, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Uuid" - ] - } - }, - "query": "\n SELECT\n ue.user_email_id,\n ue.email AS \"user_email\",\n ue.created_at AS \"user_email_created_at\",\n ue.confirmed_at AS \"user_email_confirmed_at\"\n FROM user_emails ue\n\n WHERE ue.user_id = $1\n\n ORDER BY ue.email ASC\n " - }, "5f6b7e38ef9bc3b39deabba277d0255fb8cfb2adaa65f47b78a8fac11d8c91c3": { "describe": { "columns": [], @@ -1415,210 +1438,6 @@ }, "query": "\n UPDATE oauth2_access_tokens\n SET revoked_at = $2\n WHERE oauth2_access_token_id = $1\n " }, - "7262f81a335a984c4051383d2ede7455ff65ed90fbd3151d625f8a21fd26cb05": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Uuid", - "Uuid", - "Text", - "Timestamptz", - "Timestamptz" - ] - } - }, - "query": "\n INSERT INTO user_email_confirmation_codes\n (user_email_confirmation_code_id, user_email_id, code, created_at, expires_at)\n VALUES ($1, $2, $3, $4, $5)\n " - }, - "75a16693cabdf57012f741e789b19d0a0f96fcd1e41bb2af92f2991b722cc9f1": { - "describe": { - "columns": [ - { - "name": "oauth2_authorization_grant_id", - "ordinal": 0, - "type_info": "Uuid" - }, - { - "name": "oauth2_authorization_grant_created_at", - "ordinal": 1, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_authorization_grant_cancelled_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_authorization_grant_fulfilled_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_authorization_grant_exchanged_at", - "ordinal": 4, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_authorization_grant_scope", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_state", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_redirect_uri", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_response_mode", - "ordinal": 8, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_nonce", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_max_age", - "ordinal": 10, - "type_info": "Int4" - }, - { - "name": "oauth2_client_id", - "ordinal": 11, - "type_info": "Uuid" - }, - { - "name": "oauth2_authorization_grant_code", - "ordinal": 12, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_response_type_code", - "ordinal": 13, - "type_info": "Bool" - }, - { - "name": "oauth2_authorization_grant_response_type_id_token", - "ordinal": 14, - "type_info": "Bool" - }, - { - "name": "oauth2_authorization_grant_code_challenge", - "ordinal": 15, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_code_challenge_method", - "ordinal": 16, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_requires_consent", - "ordinal": 17, - "type_info": "Bool" - }, - { - "name": "oauth2_session_id?", - "ordinal": 18, - "type_info": "Uuid" - }, - { - "name": "user_session_id?", - "ordinal": 19, - "type_info": "Uuid" - }, - { - "name": "user_session_created_at?", - "ordinal": 20, - "type_info": "Timestamptz" - }, - { - "name": "user_id?", - "ordinal": 21, - "type_info": "Uuid" - }, - { - "name": "user_username?", - "ordinal": 22, - "type_info": "Text" - }, - { - "name": "user_session_last_authentication_id?", - "ordinal": 23, - "type_info": "Uuid" - }, - { - "name": "user_session_last_authentication_created_at?", - "ordinal": 24, - "type_info": "Timestamptz" - }, - { - "name": "user_email_id?", - "ordinal": 25, - "type_info": "Uuid" - }, - { - "name": "user_email?", - "ordinal": 26, - "type_info": "Text" - }, - { - "name": "user_email_created_at?", - "ordinal": 27, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at?", - "ordinal": 28, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - true, - true, - true, - false, - true, - false, - false, - true, - true, - false, - true, - false, - false, - true, - true, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Uuid" - ] - } - }, - "query": "\n SELECT\n og.oauth2_authorization_grant_id,\n og.created_at AS oauth2_authorization_grant_created_at,\n og.cancelled_at AS oauth2_authorization_grant_cancelled_at,\n og.fulfilled_at AS oauth2_authorization_grant_fulfilled_at,\n og.exchanged_at AS oauth2_authorization_grant_exchanged_at,\n og.scope AS oauth2_authorization_grant_scope,\n og.state AS oauth2_authorization_grant_state,\n og.redirect_uri AS oauth2_authorization_grant_redirect_uri,\n og.response_mode AS oauth2_authorization_grant_response_mode,\n og.nonce AS oauth2_authorization_grant_nonce,\n og.max_age AS oauth2_authorization_grant_max_age,\n og.oauth2_client_id AS oauth2_client_id,\n og.authorization_code AS oauth2_authorization_grant_code,\n og.response_type_code AS oauth2_authorization_grant_response_type_code,\n og.response_type_id_token AS oauth2_authorization_grant_response_type_id_token,\n og.code_challenge AS oauth2_authorization_grant_code_challenge,\n og.code_challenge_method AS oauth2_authorization_grant_code_challenge_method,\n og.requires_consent AS oauth2_authorization_grant_requires_consent,\n os.oauth2_session_id AS \"oauth2_session_id?\",\n us.user_session_id AS \"user_session_id?\",\n us.created_at AS \"user_session_created_at?\",\n u.user_id AS \"user_id?\",\n u.username AS \"user_username?\",\n usa.user_session_authentication_id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM\n oauth2_authorization_grants og\n LEFT JOIN oauth2_sessions os\n USING (oauth2_session_id)\n LEFT JOIN user_sessions us\n USING (user_session_id)\n LEFT JOIN users u\n USING (user_id)\n LEFT JOIN user_session_authentications usa\n USING (user_session_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n\n WHERE og.oauth2_authorization_grant_id = $1\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " - }, "7756a60c36a64a259f7450d6eb77ee92303638ca374a63f23ac4944ccf9f4436": { "describe": { "columns": [ @@ -1748,185 +1567,6 @@ }, "query": "\n UPDATE upstream_oauth_links\n SET user_id = $1\n WHERE upstream_oauth_link_id = $2\n " }, - "7cf5ae665b15ba78b01bb1dfa304150a89fd7203f4ee15b0753cb2143049a3dc": { - "describe": { - "columns": [ - { - "name": "oauth2_refresh_token_id", - "ordinal": 0, - "type_info": "Uuid" - }, - { - "name": "oauth2_refresh_token", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "oauth2_refresh_token_created_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_access_token_id?", - "ordinal": 3, - "type_info": "Uuid" - }, - { - "name": "oauth2_access_token?", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "oauth2_access_token_created_at?", - "ordinal": 5, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_access_token_expires_at?", - "ordinal": 6, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_session_id!", - "ordinal": 7, - "type_info": "Uuid" - }, - { - "name": "oauth2_client_id!", - "ordinal": 8, - "type_info": "Uuid" - }, - { - "name": "oauth2_session_scope!", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "user_session_id!", - "ordinal": 10, - "type_info": "Uuid" - }, - { - "name": "user_session_created_at!", - "ordinal": 11, - "type_info": "Timestamptz" - }, - { - "name": "user_id!", - "ordinal": 12, - "type_info": "Uuid" - }, - { - "name": "user_username!", - "ordinal": 13, - "type_info": "Text" - }, - { - "name": "user_session_last_authentication_id?", - "ordinal": 14, - "type_info": "Uuid" - }, - { - "name": "user_session_last_authentication_created_at?", - "ordinal": 15, - "type_info": "Timestamptz" - }, - { - "name": "user_email_id?", - "ordinal": 16, - "type_info": "Uuid" - }, - { - "name": "user_email?", - "ordinal": 17, - "type_info": "Text" - }, - { - "name": "user_email_created_at?", - "ordinal": 18, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at?", - "ordinal": 19, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT\n rt.oauth2_refresh_token_id,\n rt.refresh_token AS oauth2_refresh_token,\n rt.created_at AS oauth2_refresh_token_created_at,\n at.oauth2_access_token_id AS \"oauth2_access_token_id?\",\n at.access_token AS \"oauth2_access_token?\",\n at.created_at AS \"oauth2_access_token_created_at?\",\n at.expires_at AS \"oauth2_access_token_expires_at?\",\n os.oauth2_session_id AS \"oauth2_session_id!\",\n os.oauth2_client_id AS \"oauth2_client_id!\",\n os.scope AS \"oauth2_session_scope!\",\n us.user_session_id AS \"user_session_id!\",\n us.created_at AS \"user_session_created_at!\",\n u.user_id AS \"user_id!\",\n u.username AS \"user_username!\",\n usa.user_session_authentication_id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM oauth2_refresh_tokens rt\n INNER JOIN oauth2_sessions os\n USING (oauth2_session_id)\n LEFT JOIN oauth2_access_tokens at\n USING (oauth2_access_token_id)\n INNER JOIN user_sessions us\n USING (user_session_id)\n INNER JOIN users u\n USING (user_id)\n LEFT JOIN user_session_authentications usa\n USING (user_session_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n\n WHERE rt.refresh_token = $1\n AND rt.consumed_at IS NULL\n AND rt.revoked_at IS NULL\n AND us.finished_at IS NULL\n AND os.finished_at IS NULL\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " - }, - "7d600dd15e9dac72c8071c854799fc2ac69777ade5e2d7d2d944b0dedf8ecdf8": { - "describe": { - "columns": [ - { - "name": "user_email_confirmation_code_id", - "ordinal": 0, - "type_info": "Uuid" - }, - { - "name": "code", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "created_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "expires_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "consumed_at", - "ordinal": 4, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Text", - "Uuid" - ] - } - }, - "query": "\n SELECT\n ec.user_email_confirmation_code_id,\n ec.code,\n ec.created_at,\n ec.expires_at,\n ec.consumed_at\n FROM user_email_confirmation_codes ec\n WHERE ec.code = $1\n AND ec.user_email_id = $2\n " - }, "7e3247e35ecf5335f0656c53bcde27264a9efb8dccb6246344950614f487dcaf": { "describe": { "columns": [], @@ -1940,17 +1580,43 @@ }, "query": "\n UPDATE compat_access_tokens\n SET expires_at = $2\n WHERE compat_access_token_id = $1\n " }, - "819d6472e5bcbd83a83f3a7680e8dc88e77f3970d6beddcf54e8416c880bd496": { + "836fb7567d84057fa7f1edaab834c21a158a5762fe220b6bfacd6576be6c613c": { "describe": { - "columns": [], - "nullable": [], + "columns": [ + { + "name": "user_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "username", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "primary_user_email_id", + "ordinal": 2, + "type_info": "Uuid" + }, + { + "name": "created_at", + "ordinal": 3, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + true, + false + ], "parameters": { "Left": [ - "Uuid" + "Text" ] } }, - "query": "\n UPDATE users\n SET primary_user_email_id = user_emails.user_email_id\n FROM user_emails\n WHERE user_emails.user_email_id = $1\n AND users.user_id = user_emails.user_id\n " + "query": "\n SELECT user_id\n , username\n , primary_user_email_id\n , created_at\n FROM users\n WHERE username = $1\n " }, "874e677f82c221c5bb621c12f293bcef4e70c68c87ec003fcd475bcb994b5a4c": { "describe": { @@ -1965,26 +1631,6 @@ }, "query": "\n UPDATE oauth2_refresh_tokens\n SET consumed_at = $2\n WHERE oauth2_refresh_token_id = $1\n " }, - "89e0d338348588831a7a810763a1901073f7a7cb81d51c18bb987a5be10c1202": { - "describe": { - "columns": [ - { - "name": "count", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - null - ], - "parameters": { - "Left": [ - "Uuid" - ] - } - }, - "query": "\n SELECT COUNT(*)\n FROM user_emails ue\n WHERE ue.user_id = $1\n " - }, "8f7a9fb1f24c24f8dbc3c193df2a742c9ac730ab958587b67297de2d4b843863": { "describe": { "columns": [ @@ -2047,6 +1693,159 @@ }, "query": "\n SELECT\n upstream_oauth_provider_id,\n issuer,\n scope,\n client_id,\n encrypted_client_secret,\n token_endpoint_signing_alg,\n token_endpoint_auth_method,\n created_at\n FROM upstream_oauth_providers\n WHERE upstream_oauth_provider_id = $1\n " }, + "90b5512c0c9dc3b3eb6500056cc72f9993216d9b553c2e33a7edec26ffb0fc59": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Timestamptz" + ] + } + }, + "query": "\n UPDATE user_emails\n SET confirmed_at = $2\n WHERE user_email_id = $1\n " + }, + "90fe32cb9c88a262a682c0db700fef7d69d6ce0be1f930d9f16c50b921a8b819": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO user_emails (user_email_id, user_id, email, created_at)\n VALUES ($1, $2, $3, $4)\n " + }, + "921d77c194609615a7e9a6fd806e9cc17a7927e3e5deb58f3917ceeb9ab4dede": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Timestamptz" + ] + } + }, + "query": "\n UPDATE user_email_confirmation_codes\n SET consumed_at = $2\n WHERE user_email_confirmation_code_id = $1\n " + }, + "94fd96446b237c87bd6bf741f3c42b37ee751b87b7fcc459602bdf8c46962443": { + "describe": { + "columns": [ + { + "name": "exists!", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "\n SELECT EXISTS(\n SELECT 1 FROM users WHERE username = $1\n ) AS \"exists!\"\n " + }, + "976ac2435784128eab195c8e6b9bd6e8d7b3a9142c2a34538de03817a3c94e99": { + "describe": { + "columns": [ + { + "name": "compat_sso_login_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "compat_sso_login_token", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "compat_sso_login_redirect_uri", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "compat_sso_login_created_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "compat_sso_login_fulfilled_at", + "ordinal": 4, + "type_info": "Timestamptz" + }, + { + "name": "compat_sso_login_exchanged_at", + "ordinal": 5, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_id?", + "ordinal": 6, + "type_info": "Uuid" + }, + { + "name": "compat_session_created_at?", + "ordinal": 7, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_finished_at?", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_device_id?", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "user_id?", + "ordinal": 10, + "type_info": "Uuid" + }, + { + "name": "user_username?", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "user_primary_user_email_id?", + "ordinal": 12, + "type_info": "Uuid" + } + ], + "nullable": [ + false, + false, + false, + false, + true, + true, + false, + false, + true, + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "\n SELECT\n cl.compat_sso_login_id,\n cl.login_token AS \"compat_sso_login_token\",\n cl.redirect_uri AS \"compat_sso_login_redirect_uri\",\n cl.created_at AS \"compat_sso_login_created_at\",\n cl.fulfilled_at AS \"compat_sso_login_fulfilled_at\",\n cl.exchanged_at AS \"compat_sso_login_exchanged_at\",\n cs.compat_session_id AS \"compat_session_id?\",\n cs.created_at AS \"compat_session_created_at?\",\n cs.finished_at AS \"compat_session_finished_at?\",\n cs.device_id AS \"compat_session_device_id?\",\n u.user_id AS \"user_id?\",\n u.username AS \"user_username?\",\n u.primary_user_email_id AS \"user_primary_user_email_id?\"\n FROM compat_sso_logins cl\n LEFT JOIN compat_sessions cs\n USING (compat_session_id)\n LEFT JOIN users u\n USING (user_id)\n WHERE cl.compat_sso_login_id = $1\n " + }, "99f5f9eb0adc5ec120ed8194cbf6a8545155bef09e6d94d92fb67fd1b14d4f28": { "describe": { "columns": [], @@ -2137,6 +1936,50 @@ }, "query": "\n SELECT up.user_password_id\n , up.hashed_password\n , up.version\n , up.upgraded_from_id\n , up.created_at\n FROM user_passwords up\n WHERE up.user_id = $1\n ORDER BY up.created_at DESC\n LIMIT 1\n " }, + "a300fe99c95679c5664646a6a525c0491829e97db45f3234483872ed38436322": { + "describe": { + "columns": [ + { + "name": "user_email_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "user_id", + "ordinal": 1, + "type_info": "Uuid" + }, + { + "name": "email", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "confirmed_at", + "ordinal": 4, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "\n SELECT user_email_id\n , user_id\n , email\n , created_at\n , confirmed_at\n FROM user_emails\n\n WHERE user_id = $1\n\n ORDER BY email ASC\n " + }, "a5a7dad633396e087239d5629092e4a305908ffce9c2610db07372f719070546": { "describe": { "columns": [], @@ -2149,137 +1992,7 @@ }, "query": "\n UPDATE oauth2_authorization_grants AS og\n SET\n requires_consent = 'f'\n WHERE\n og.oauth2_authorization_grant_id = $1\n " }, - "a8117b4dd167167b477fb4ebda52789e376defbdc67f3d9093aa06308b2f856e": { - "describe": { - "columns": [ - { - "name": "compat_sso_login_id", - "ordinal": 0, - "type_info": "Uuid" - }, - { - "name": "compat_sso_login_token", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "compat_sso_login_redirect_uri", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "compat_sso_login_created_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "compat_sso_login_fulfilled_at", - "ordinal": 4, - "type_info": "Timestamptz" - }, - { - "name": "compat_sso_login_exchanged_at", - "ordinal": 5, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_id?", - "ordinal": 6, - "type_info": "Uuid" - }, - { - "name": "compat_session_created_at?", - "ordinal": 7, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_finished_at?", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_device_id?", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "user_id?", - "ordinal": 10, - "type_info": "Uuid" - }, - { - "name": "user_username?", - "ordinal": 11, - "type_info": "Text" - }, - { - "name": "user_email_id?", - "ordinal": 12, - "type_info": "Uuid" - }, - { - "name": "user_email?", - "ordinal": 13, - "type_info": "Text" - }, - { - "name": "user_email_created_at?", - "ordinal": 14, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at?", - "ordinal": 15, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - false, - false, - true, - true, - false, - false, - true, - false, - false, - false, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT\n cl.compat_sso_login_id,\n cl.login_token AS \"compat_sso_login_token\",\n cl.redirect_uri AS \"compat_sso_login_redirect_uri\",\n cl.created_at AS \"compat_sso_login_created_at\",\n cl.fulfilled_at AS \"compat_sso_login_fulfilled_at\",\n cl.exchanged_at AS \"compat_sso_login_exchanged_at\",\n cs.compat_session_id AS \"compat_session_id?\",\n cs.created_at AS \"compat_session_created_at?\",\n cs.finished_at AS \"compat_session_finished_at?\",\n cs.device_id AS \"compat_session_device_id?\",\n u.user_id AS \"user_id?\",\n u.username AS \"user_username?\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM compat_sso_logins cl\n LEFT JOIN compat_sessions cs\n USING (compat_session_id)\n LEFT JOIN users u\n USING (user_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n WHERE cl.login_token = $1\n " - }, - "af77bad7259175464c5ad57f9662571c17b29552ebb70e4b6022584b41bdff0d": { - "describe": { - "columns": [ - { - "name": "exists!", - "ordinal": 0, - "type_info": "Bool" - } - ], - "nullable": [ - null - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT EXISTS(\n SELECT 1 FROM users WHERE username = $1\n ) AS \"exists!\"\n " - }, - "b5b955169ebe6c399e53b74627c11c8219c0736ef2b5b6b44be568a35fd5389f": { + "aff08a8caabeb62f4929e6e901e7ca7c55e284c18c5c1d1e78821dd9bc961412": { "describe": { "columns": [ { @@ -2288,18 +2001,23 @@ "type_info": "Uuid" }, { - "name": "user_email", + "name": "user_id", "ordinal": 1, + "type_info": "Uuid" + }, + { + "name": "email", + "ordinal": 2, "type_info": "Text" }, { - "name": "user_email_created_at", - "ordinal": 2, + "name": "created_at", + "ordinal": 3, "type_info": "Timestamptz" }, { - "name": "user_email_confirmed_at", - "ordinal": 3, + "name": "confirmed_at", + "ordinal": 4, "type_info": "Timestamptz" } ], @@ -2307,16 +2025,47 @@ false, false, false, + false, true ], "parameters": { "Left": [ "Uuid", - "Uuid" + "Text" ] } }, - "query": "\n SELECT\n ue.user_email_id,\n ue.email AS \"user_email\",\n ue.created_at AS \"user_email_created_at\",\n ue.confirmed_at AS \"user_email_confirmed_at\"\n FROM user_emails ue\n\n WHERE ue.user_id = $1\n AND ue.user_email_id = $2\n " + "query": "\n SELECT user_email_id\n , user_id\n , email\n , created_at\n , confirmed_at\n FROM user_emails\n\n WHERE user_id = $1 AND email = $2\n " + }, + "b26ae7dd28f8a756b55a76e80cdedd7be9ba26435ea4a914421483f8ed832537": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO users (user_id, username, created_at)\n VALUES ($1, $2, $3)\n " + }, + "b515bbfb331e46acd3c0219f09223cc5d8d31cb41287e693dcb82c6e199f7991": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Timestamptz", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO user_email_confirmation_codes\n (user_email_confirmation_code_id, user_email_id, code, created_at, expires_at)\n VALUES ($1, $2, $3, $4, $5)\n " }, "b9875a270f7e753e48075ccae233df6e24a91775ceb877735508c1d5b2300d64": { "describe": { @@ -2348,6 +2097,18 @@ }, "query": "\n INSERT INTO oauth2_sessions\n (oauth2_session_id, user_session_id, oauth2_client_id, scope, created_at)\n SELECT\n $1,\n $2,\n og.oauth2_client_id,\n og.scope,\n $3\n FROM\n oauth2_authorization_grants og\n WHERE\n og.oauth2_authorization_grant_id = $4\n " }, + "bd1f6daa5fa1b10250c01f8b3fbe451646a9ceeefa6f72b9c4e29b6d05f17641": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "\n UPDATE users\n SET primary_user_email_id = user_emails.user_email_id\n FROM user_emails\n WHERE user_emails.user_email_id = $1\n AND users.user_id = user_emails.user_id\n " + }, "bd7a4a008851f3f6d7591e3463e4369cee08820af57dcd3faf95f8e9be82857d": { "describe": { "columns": [], @@ -2365,122 +2126,6 @@ }, "query": "\n INSERT INTO user_passwords\n (user_password_id, user_id, hashed_password, version, upgraded_from_id, created_at)\n VALUES ($1, $2, $3, $4, $5, $6)\n " }, - "c52c911bf39ada298bfdc4526028f1b29fdcb6f557b288bb7ea2472b160c8698": { - "describe": { - "columns": [ - { - "name": "compat_refresh_token_id", - "ordinal": 0, - "type_info": "Uuid" - }, - { - "name": "compat_refresh_token", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "compat_refresh_token_created_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "compat_access_token_id", - "ordinal": 3, - "type_info": "Uuid" - }, - { - "name": "compat_access_token", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "compat_access_token_created_at", - "ordinal": 5, - "type_info": "Timestamptz" - }, - { - "name": "compat_access_token_expires_at", - "ordinal": 6, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_id", - "ordinal": 7, - "type_info": "Uuid" - }, - { - "name": "compat_session_created_at", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_finished_at", - "ordinal": 9, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_device_id", - "ordinal": 10, - "type_info": "Text" - }, - { - "name": "user_id", - "ordinal": 11, - "type_info": "Uuid" - }, - { - "name": "user_username!", - "ordinal": 12, - "type_info": "Text" - }, - { - "name": "user_email_id?", - "ordinal": 13, - "type_info": "Uuid" - }, - { - "name": "user_email?", - "ordinal": 14, - "type_info": "Text" - }, - { - "name": "user_email_created_at?", - "ordinal": 15, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at?", - "ordinal": 16, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - false, - true, - false, - false, - false, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT\n cr.compat_refresh_token_id,\n cr.refresh_token AS \"compat_refresh_token\",\n cr.created_at AS \"compat_refresh_token_created_at\",\n ct.compat_access_token_id,\n ct.access_token AS \"compat_access_token\",\n ct.created_at AS \"compat_access_token_created_at\",\n ct.expires_at AS \"compat_access_token_expires_at\",\n cs.compat_session_id,\n cs.created_at AS \"compat_session_created_at\",\n cs.finished_at AS \"compat_session_finished_at\",\n cs.device_id AS \"compat_session_device_id\",\n u.user_id,\n u.username AS \"user_username!\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n\n FROM compat_refresh_tokens cr\n INNER JOIN compat_sessions cs\n USING (compat_session_id)\n INNER JOIN compat_access_tokens ct\n USING (compat_access_token_id)\n INNER JOIN users u\n USING (user_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n\n WHERE cr.refresh_token = $1\n AND cr.consumed_at IS NULL\n AND cs.finished_at IS NULL\n " - }, "c88376abdba124ff0487a9a69d2345c7d69d7394f355111ec369cfa6d45fb40f": { "describe": { "columns": [], @@ -2559,6 +2204,142 @@ }, "query": "\n INSERT INTO oauth2_clients\n (oauth2_client_id,\n encrypted_client_secret,\n grant_type_authorization_code,\n grant_type_refresh_token,\n client_name,\n logo_uri,\n client_uri,\n policy_uri,\n tos_uri,\n jwks_uri,\n jwks,\n id_token_signed_response_alg,\n userinfo_signed_response_alg,\n token_endpoint_auth_method,\n token_endpoint_auth_signing_alg,\n initiate_login_uri)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)\n " }, + "d023d7346ec1f32da9459db3c39dffd8a4e3d4e91cdf096928de4517d3f8c622": { + "describe": { + "columns": [ + { + "name": "oauth2_refresh_token_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "oauth2_refresh_token", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "oauth2_refresh_token_created_at", + "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_access_token_id?", + "ordinal": 3, + "type_info": "Uuid" + }, + { + "name": "oauth2_access_token?", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "oauth2_access_token_created_at?", + "ordinal": 5, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_access_token_expires_at?", + "ordinal": 6, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_session_id!", + "ordinal": 7, + "type_info": "Uuid" + }, + { + "name": "oauth2_client_id!", + "ordinal": 8, + "type_info": "Uuid" + }, + { + "name": "oauth2_session_scope!", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "user_session_id!", + "ordinal": 10, + "type_info": "Uuid" + }, + { + "name": "user_session_created_at!", + "ordinal": 11, + "type_info": "Timestamptz" + }, + { + "name": "user_id!", + "ordinal": 12, + "type_info": "Uuid" + }, + { + "name": "user_username!", + "ordinal": 13, + "type_info": "Text" + }, + { + "name": "user_primary_user_email_id", + "ordinal": 14, + "type_info": "Uuid" + }, + { + "name": "user_session_last_authentication_id?", + "ordinal": 15, + "type_info": "Uuid" + }, + { + "name": "user_session_last_authentication_created_at?", + "ordinal": 16, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "\n SELECT\n rt.oauth2_refresh_token_id,\n rt.refresh_token AS oauth2_refresh_token,\n rt.created_at AS oauth2_refresh_token_created_at,\n at.oauth2_access_token_id AS \"oauth2_access_token_id?\",\n at.access_token AS \"oauth2_access_token?\",\n at.created_at AS \"oauth2_access_token_created_at?\",\n at.expires_at AS \"oauth2_access_token_expires_at?\",\n os.oauth2_session_id AS \"oauth2_session_id!\",\n os.oauth2_client_id AS \"oauth2_client_id!\",\n os.scope AS \"oauth2_session_scope!\",\n us.user_session_id AS \"user_session_id!\",\n us.created_at AS \"user_session_created_at!\",\n u.user_id AS \"user_id!\",\n u.username AS \"user_username!\",\n u.primary_user_email_id AS \"user_primary_user_email_id\",\n usa.user_session_authentication_id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\"\n FROM oauth2_refresh_tokens rt\n INNER JOIN oauth2_sessions os\n USING (oauth2_session_id)\n LEFT JOIN oauth2_access_tokens at\n USING (oauth2_access_token_id)\n INNER JOIN user_sessions us\n USING (user_session_id)\n INNER JOIN users u\n USING (user_id)\n LEFT JOIN user_session_authentications usa\n USING (user_session_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n\n WHERE rt.refresh_token = $1\n AND rt.consumed_at IS NULL\n AND rt.revoked_at IS NULL\n AND us.finished_at IS NULL\n AND os.finished_at IS NULL\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " + }, + "d12a513b81b3ef658eae1f0a719933323f28c6ee260b52cafe337dd3d19e865c": { + "describe": { + "columns": [ + { + "name": "count", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "\n SELECT COUNT(*)\n FROM user_emails\n WHERE user_id = $1\n " + }, "d1738c27339b81f0844da4bd9b040b9b07a91aa4d9b199b98f24c9cee5709b2b": { "describe": { "columns": [], @@ -2574,19 +2355,6 @@ }, "query": "\n INSERT INTO compat_sso_logins\n (compat_sso_login_id, login_token, redirect_uri, created_at)\n VALUES ($1, $2, $3, $4)\n " }, - "d55a321e8935f4effda29d9620a0f622125cb38472785049ee21c2616a6bd068": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Uuid", - "Timestamptz" - ] - } - }, - "query": "\n UPDATE user_email_confirmation_codes\n SET consumed_at = $2\n WHERE user_email_confirmation_code_id = $1\n " - }, "d8677b3b6ee594c230fad98c1aa1c6e3d983375bf5b701c7b52468e7f906abf9": { "describe": { "columns": [], @@ -2603,18 +2371,6 @@ }, "query": "\n INSERT INTO oauth2_refresh_tokens\n (oauth2_refresh_token_id, oauth2_session_id, oauth2_access_token_id,\n refresh_token, created_at)\n VALUES\n ($1, $2, $3, $4, $5)\n " }, - "e16ac9f75be25ef6873f1851e916df3ea730422409decc0344f7f05ce3c3841f": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Uuid" - ] - } - }, - "query": "\n DELETE FROM user_emails\n WHERE user_emails.user_email_id = $1\n " - }, "e446e37d48c8838ef2e0d0fd82f8f7b04893c84ad46747cdf193ebd83755ceb2": { "describe": { "columns": [], @@ -2673,5 +2429,86 @@ } }, "query": "\n SELECT\n upstream_oauth_link_id,\n upstream_oauth_provider_id,\n user_id,\n subject,\n created_at\n FROM upstream_oauth_links\n WHERE upstream_oauth_provider_id = $1\n AND subject = $2\n " + }, + "f624e1bdbff4e97b300362d1bbd86035e4a0fdd8ffe16c3bfb9bc451ba60851b": { + "describe": { + "columns": [ + { + "name": "compat_access_token_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "compat_access_token", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "compat_access_token_created_at", + "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "compat_access_token_expires_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_id", + "ordinal": 4, + "type_info": "Uuid" + }, + { + "name": "compat_session_created_at", + "ordinal": 5, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_finished_at", + "ordinal": 6, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_device_id", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "user_id!", + "ordinal": 8, + "type_info": "Uuid" + }, + { + "name": "user_username!", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "user_primary_user_email_id", + "ordinal": 10, + "type_info": "Uuid" + } + ], + "nullable": [ + false, + false, + false, + true, + false, + false, + true, + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Text", + "Timestamptz" + ] + } + }, + "query": "\n SELECT\n ct.compat_access_token_id,\n ct.access_token AS \"compat_access_token\",\n ct.created_at AS \"compat_access_token_created_at\",\n ct.expires_at AS \"compat_access_token_expires_at\",\n cs.compat_session_id,\n cs.created_at AS \"compat_session_created_at\",\n cs.finished_at AS \"compat_session_finished_at\",\n cs.device_id AS \"compat_session_device_id\",\n u.user_id AS \"user_id!\",\n u.username AS \"user_username!\",\n u.primary_user_email_id AS \"user_primary_user_email_id\"\n\n FROM compat_access_tokens ct\n INNER JOIN compat_sessions cs\n USING (compat_session_id)\n INNER JOIN users u\n USING (user_id)\n\n WHERE ct.access_token = $1\n AND (ct.expires_at < $2 OR ct.expires_at IS NULL)\n AND cs.finished_at IS NULL \n " } } \ No newline at end of file diff --git a/crates/storage/src/compat.rs b/crates/storage/src/compat.rs index 9737fcb9..4708e91b 100644 --- a/crates/storage/src/compat.rs +++ b/crates/storage/src/compat.rs @@ -15,7 +15,7 @@ use chrono::{DateTime, Duration, Utc}; use mas_data_model::{ CompatAccessToken, CompatRefreshToken, CompatSession, CompatSsoLogin, CompatSsoLoginState, - Device, User, UserEmail, + Device, User, }; use rand::Rng; use sqlx::{Acquire, PgExecutor, Postgres, QueryBuilder}; @@ -40,10 +40,7 @@ struct CompatAccessTokenLookup { compat_session_device_id: String, user_id: Uuid, user_username: String, - user_email_id: Option, - user_email: Option, - user_email_created_at: Option>, - user_email_confirmed_at: Option>, + user_primary_user_email_id: Option, } #[tracing::instrument(skip_all, err)] @@ -66,18 +63,13 @@ pub async fn lookup_active_compat_access_token( cs.device_id AS "compat_session_device_id", u.user_id AS "user_id!", u.username AS "user_username!", - ue.user_email_id AS "user_email_id?", - ue.email AS "user_email?", - ue.created_at AS "user_email_created_at?", - ue.confirmed_at AS "user_email_confirmed_at?" + u.primary_user_email_id AS "user_primary_user_email_id" FROM compat_access_tokens ct INNER JOIN compat_sessions cs USING (compat_session_id) INNER JOIN users u USING (user_id) - LEFT JOIN user_emails ue - ON ue.user_email_id = u.primary_user_email_id WHERE ct.access_token = $1 AND (ct.expires_at < $2 OR ct.expires_at IS NULL) @@ -101,32 +93,11 @@ pub async fn lookup_active_compat_access_token( }; let user_id = Ulid::from(res.user_id); - let primary_email = match ( - res.user_email_id, - res.user_email, - res.user_email_created_at, - res.user_email_confirmed_at, - ) { - (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - id: id.into(), - email, - created_at, - confirmed_at, - }), - (None, None, None, None) => None, - _ => { - return Err(DatabaseInconsistencyError::on("compat_sessions") - .column("user_id") - .row(user_id) - .into()) - } - }; - let user = User { id: user_id, username: res.user_username, sub: user_id.to_string(), - primary_email, + primary_user_email_id: res.user_primary_user_email_id.map(Into::into), }; let id = res.compat_session_id.into(); @@ -162,10 +133,7 @@ pub struct CompatRefreshTokenLookup { compat_session_device_id: String, user_id: Uuid, user_username: String, - user_email_id: Option, - user_email: Option, - user_email_created_at: Option>, - user_email_confirmed_at: Option>, + user_primary_user_email_id: Option, } #[tracing::instrument(skip_all, err)] @@ -191,10 +159,7 @@ pub async fn lookup_active_compat_refresh_token( cs.device_id AS "compat_session_device_id", u.user_id, u.username AS "user_username!", - ue.user_email_id AS "user_email_id?", - ue.email AS "user_email?", - ue.created_at AS "user_email_created_at?", - ue.confirmed_at AS "user_email_confirmed_at?" + u.primary_user_email_id AS "user_primary_user_email_id" FROM compat_refresh_tokens cr INNER JOIN compat_sessions cs @@ -203,8 +168,6 @@ pub async fn lookup_active_compat_refresh_token( USING (compat_access_token_id) INNER JOIN users u USING (user_id) - LEFT JOIN user_emails ue - ON ue.user_email_id = u.primary_user_email_id WHERE cr.refresh_token = $1 AND cr.consumed_at IS NULL @@ -233,32 +196,11 @@ pub async fn lookup_active_compat_refresh_token( }; let user_id = Ulid::from(res.user_id); - let primary_email = match ( - res.user_email_id, - res.user_email, - res.user_email_created_at, - res.user_email_confirmed_at, - ) { - (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - id: id.into(), - email, - created_at, - confirmed_at, - }), - (None, None, None, None) => None, - _ => { - return Err(DatabaseInconsistencyError::on("users") - .column("primary_user_email_id") - .row(user_id) - .into()) - } - }; - let user = User { id: user_id, username: res.user_username, sub: user_id.to_string(), - primary_email, + primary_user_email_id: res.user_primary_user_email_id.map(Into::into), }; let session_id = res.compat_session_id.into(); @@ -528,10 +470,7 @@ struct CompatSsoLoginLookup { compat_session_device_id: Option, user_id: Option, user_username: Option, - user_email_id: Option, - user_email: Option, - user_email_created_at: Option>, - user_email_confirmed_at: Option>, + user_primary_user_email_id: Option, } impl TryFrom for CompatSsoLogin { @@ -546,32 +485,18 @@ impl TryFrom for CompatSsoLogin { .source(e) })?; - let primary_email = match ( - res.user_email_id, - res.user_email, - res.user_email_created_at, - res.user_email_confirmed_at, + let user = match ( + res.user_id, + res.user_username, + res.user_primary_user_email_id, ) { - (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - id: id.into(), - email, - created_at, - confirmed_at, - }), - (None, None, None, None) => None, - _ => { - return Err(DatabaseInconsistencyError::on("users").column("primary_user_email_id")) - } - }; - - let user = match (res.user_id, res.user_username, primary_email) { - (Some(id), Some(username), primary_email) => { + (Some(id), Some(username), primary_email_id) => { let id = Ulid::from(id); Some(User { id, username, sub: id.to_string(), - primary_email, + primary_user_email_id: primary_email_id.map(Into::into), }) } @@ -667,17 +592,12 @@ pub async fn get_compat_sso_login_by_id( cs.device_id AS "compat_session_device_id?", u.user_id AS "user_id?", u.username AS "user_username?", - ue.user_email_id AS "user_email_id?", - ue.email AS "user_email?", - ue.created_at AS "user_email_created_at?", - ue.confirmed_at AS "user_email_confirmed_at?" + u.primary_user_email_id AS "user_primary_user_email_id?" FROM compat_sso_logins cl LEFT JOIN compat_sessions cs USING (compat_session_id) LEFT JOIN users u USING (user_id) - LEFT JOIN user_emails ue - ON ue.user_email_id = u.primary_user_email_id WHERE cl.compat_sso_login_id = $1 "#, Uuid::from(id), @@ -725,17 +645,12 @@ pub async fn get_paginated_user_compat_sso_logins( cs.device_id AS "compat_session_device_id", u.user_id AS "user_id", u.username AS "user_username", - ue.user_email_id AS "user_email_id", - ue.email AS "user_email", - ue.created_at AS "user_email_created_at", - ue.confirmed_at AS "user_email_confirmed_at" + u.primary_user_email_id AS "user_primary_user_email_id?" FROM compat_sso_logins cl LEFT JOIN compat_sessions cs USING (compat_session_id) LEFT JOIN users u USING (user_id) - LEFT JOIN user_emails ue - ON ue.user_email_id = u.primary_user_email_id "#, ); @@ -781,17 +696,12 @@ pub async fn get_compat_sso_login_by_token( cs.device_id AS "compat_session_device_id?", u.user_id AS "user_id?", u.username AS "user_username?", - ue.user_email_id AS "user_email_id?", - ue.email AS "user_email?", - ue.created_at AS "user_email_created_at?", - ue.confirmed_at AS "user_email_confirmed_at?" + u.primary_user_email_id AS "user_primary_user_email_id?" FROM compat_sso_logins cl LEFT JOIN compat_sessions cs USING (compat_session_id) LEFT JOIN users u USING (user_id) - LEFT JOIN user_emails ue - ON ue.user_email_id = u.primary_user_email_id WHERE cl.login_token = $1 "#, token, diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 26865201..bda7ee2c 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -161,7 +161,7 @@ impl DatabaseInconsistencyError { } } -#[derive(Default, Debug, Clone, Copy)] +#[derive(Default, Debug, Clone)] pub struct Clock { _private: (), } diff --git a/crates/storage/src/oauth2/access_token.rs b/crates/storage/src/oauth2/access_token.rs index be983142..71e014e4 100644 --- a/crates/storage/src/oauth2/access_token.rs +++ b/crates/storage/src/oauth2/access_token.rs @@ -13,7 +13,7 @@ // limitations under the License. use chrono::{DateTime, Duration, Utc}; -use mas_data_model::{AccessToken, Authentication, BrowserSession, Session, User, UserEmail}; +use mas_data_model::{AccessToken, Authentication, BrowserSession, Session, User}; use rand::Rng; use sqlx::{PgConnection, PgExecutor}; use ulid::Ulid; @@ -84,12 +84,9 @@ pub struct OAuth2AccessTokenLookup { user_session_created_at: DateTime, user_id: Uuid, user_username: String, + user_primary_user_email_id: Option, user_session_last_authentication_id: Option, user_session_last_authentication_created_at: Option>, - user_email_id: Option, - user_email: Option, - user_email_created_at: Option>, - user_email_confirmed_at: Option>, } #[allow(clippy::too_many_lines)] @@ -100,24 +97,20 @@ pub async fn lookup_active_access_token( let res = sqlx::query_as!( OAuth2AccessTokenLookup, r#" - SELECT - at.oauth2_access_token_id, - at.access_token AS "oauth2_access_token", - at.created_at AS "oauth2_access_token_created_at", - at.expires_at AS "oauth2_access_token_expires_at", - os.oauth2_session_id AS "oauth2_session_id!", - os.oauth2_client_id AS "oauth2_client_id!", - os.scope AS "scope!", - us.user_session_id AS "user_session_id!", - us.created_at AS "user_session_created_at!", - u.user_id AS "user_id!", - u.username AS "user_username!", - usa.user_session_authentication_id AS "user_session_last_authentication_id?", - usa.created_at AS "user_session_last_authentication_created_at?", - ue.user_email_id AS "user_email_id?", - ue.email AS "user_email?", - ue.created_at AS "user_email_created_at?", - ue.confirmed_at AS "user_email_confirmed_at?" + SELECT at.oauth2_access_token_id + , at.access_token AS "oauth2_access_token" + , at.created_at AS "oauth2_access_token_created_at" + , at.expires_at AS "oauth2_access_token_expires_at" + , os.oauth2_session_id AS "oauth2_session_id!" + , os.oauth2_client_id AS "oauth2_client_id!" + , os.scope AS "scope!" + , us.user_session_id AS "user_session_id!" + , us.created_at AS "user_session_created_at!" + , u.user_id AS "user_id!" + , u.username AS "user_username!" + , u.primary_user_email_id AS "user_primary_user_email_id" + , usa.user_session_authentication_id AS "user_session_last_authentication_id?" + , usa.created_at AS "user_session_last_authentication_created_at?" FROM oauth2_access_tokens at INNER JOIN oauth2_sessions os @@ -128,8 +121,6 @@ pub async fn lookup_active_access_token( USING (user_id) LEFT JOIN user_session_authentications usa USING (user_session_id) - LEFT JOIN user_emails ue - ON ue.user_email_id = u.primary_user_email_id WHERE at.access_token = $1 AND at.revoked_at IS NULL @@ -162,32 +153,11 @@ pub async fn lookup_active_access_token( })?; let user_id = Ulid::from(res.user_id); - let primary_email = match ( - res.user_email_id, - res.user_email, - res.user_email_created_at, - res.user_email_confirmed_at, - ) { - (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - id: id.into(), - email, - created_at, - confirmed_at, - }), - (None, None, None, None) => None, - _ => { - return Err(DatabaseInconsistencyError::on("users") - .column("primary_user_email_id") - .row(user_id) - .into()) - } - }; - let user = User { id: user_id, username: res.user_username, sub: user_id.to_string(), - primary_email, + primary_user_email_id: res.user_primary_user_email_id.map(Into::into), }; let last_authentication = match ( diff --git a/crates/storage/src/oauth2/authorization_grant.rs b/crates/storage/src/oauth2/authorization_grant.rs index e00274cc..957400d9 100644 --- a/crates/storage/src/oauth2/authorization_grant.rs +++ b/crates/storage/src/oauth2/authorization_grant.rs @@ -17,7 +17,7 @@ use std::num::NonZeroU32; use chrono::{DateTime, Utc}; use mas_data_model::{ Authentication, AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, BrowserSession, - Client, Pkce, Session, User, UserEmail, + Client, Pkce, Session, User, }; use mas_iana::oauth::PkceCodeChallengeMethod; use oauth2_types::{requests::ResponseMode, scope::Scope}; @@ -154,12 +154,9 @@ struct GrantLookup { user_session_created_at: Option>, user_id: Option, user_username: Option, + user_primary_user_email_id: Option, user_session_last_authentication_id: Option, user_session_last_authentication_created_at: Option>, - user_email_id: Option, - user_email: Option, - user_email_created_at: Option>, - user_email_confirmed_at: Option>, } impl GrantLookup { @@ -197,34 +194,14 @@ impl GrantLookup { _ => return Err(DatabaseInconsistencyError::on("user_session_authentications").into()), }; - let primary_email = match ( - self.user_email_id, - self.user_email, - self.user_email_created_at, - self.user_email_confirmed_at, - ) { - (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - id: id.into(), - email, - created_at, - confirmed_at, - }), - (None, None, None, None) => None, - _ => { - return Err(DatabaseInconsistencyError::on("users") - .column("primary_user_email_id") - .into()) - } - }; - let session = match ( self.oauth2_session_id, self.user_session_id, self.user_session_created_at, self.user_id, self.user_username, + self.user_primary_user_email_id, last_authentication, - primary_email, ) { ( Some(session_id), @@ -232,15 +209,15 @@ impl GrantLookup { Some(user_session_created_at), Some(user_id), Some(user_username), + user_primary_user_email_id, last_authentication, - primary_email, ) => { let user_id = Ulid::from(user_id); let user = User { id: user_id, username: user_username, sub: user_id.to_string(), - primary_email, + primary_user_email_id: user_primary_user_email_id.map(Into::into), }; let browser_session = BrowserSession { @@ -439,12 +416,9 @@ pub async fn get_grant_by_id( us.created_at AS "user_session_created_at?", u.user_id AS "user_id?", u.username AS "user_username?", + u.primary_user_email_id AS "user_primary_user_email_id?", usa.user_session_authentication_id AS "user_session_last_authentication_id?", - usa.created_at AS "user_session_last_authentication_created_at?", - ue.user_email_id AS "user_email_id?", - ue.email AS "user_email?", - ue.created_at AS "user_email_created_at?", - ue.confirmed_at AS "user_email_confirmed_at?" + usa.created_at AS "user_session_last_authentication_created_at?" FROM oauth2_authorization_grants og LEFT JOIN oauth2_sessions os @@ -455,8 +429,6 @@ pub async fn get_grant_by_id( USING (user_id) LEFT JOIN user_session_authentications usa USING (user_session_id) - LEFT JOIN user_emails ue - ON ue.user_email_id = u.primary_user_email_id WHERE og.oauth2_authorization_grant_id = $1 @@ -508,12 +480,9 @@ pub async fn lookup_grant_by_code( us.created_at AS "user_session_created_at?", u.user_id AS "user_id?", u.username AS "user_username?", + u.primary_user_email_id AS "user_primary_user_email_id?", usa.user_session_authentication_id AS "user_session_last_authentication_id?", - usa.created_at AS "user_session_last_authentication_created_at?", - ue.user_email_id AS "user_email_id?", - ue.email AS "user_email?", - ue.created_at AS "user_email_created_at?", - ue.confirmed_at AS "user_email_confirmed_at?" + usa.created_at AS "user_session_last_authentication_created_at?" FROM oauth2_authorization_grants og LEFT JOIN oauth2_sessions os @@ -524,8 +493,6 @@ pub async fn lookup_grant_by_code( USING (user_id) LEFT JOIN user_session_authentications usa USING (user_session_id) - LEFT JOIN user_emails ue - ON ue.user_email_id = u.primary_user_email_id WHERE og.authorization_code = $1 diff --git a/crates/storage/src/oauth2/refresh_token.rs b/crates/storage/src/oauth2/refresh_token.rs index 74e111c9..79d90c01 100644 --- a/crates/storage/src/oauth2/refresh_token.rs +++ b/crates/storage/src/oauth2/refresh_token.rs @@ -13,9 +13,7 @@ // limitations under the License. use chrono::{DateTime, Utc}; -use mas_data_model::{ - AccessToken, Authentication, BrowserSession, RefreshToken, Session, User, UserEmail, -}; +use mas_data_model::{AccessToken, Authentication, BrowserSession, RefreshToken, Session, User}; use rand::Rng; use sqlx::{PgConnection, PgExecutor}; use ulid::Ulid; @@ -87,12 +85,9 @@ struct OAuth2RefreshTokenLookup { user_session_created_at: DateTime, user_id: Uuid, user_username: String, + user_primary_user_email_id: Option, user_session_last_authentication_id: Option, user_session_last_authentication_created_at: Option>, - user_email_id: Option, - user_email: Option, - user_email_created_at: Option>, - user_email_confirmed_at: Option>, } #[tracing::instrument(skip_all, err)] @@ -119,12 +114,9 @@ pub async fn lookup_active_refresh_token( us.created_at AS "user_session_created_at!", u.user_id AS "user_id!", u.username AS "user_username!", + u.primary_user_email_id AS "user_primary_user_email_id", usa.user_session_authentication_id AS "user_session_last_authentication_id?", - usa.created_at AS "user_session_last_authentication_created_at?", - ue.user_email_id AS "user_email_id?", - ue.email AS "user_email?", - ue.created_at AS "user_email_created_at?", - ue.confirmed_at AS "user_email_confirmed_at?" + usa.created_at AS "user_session_last_authentication_created_at?" FROM oauth2_refresh_tokens rt INNER JOIN oauth2_sessions os USING (oauth2_session_id) @@ -190,32 +182,11 @@ pub async fn lookup_active_refresh_token( })?; let user_id = Ulid::from(res.user_id); - let primary_email = match ( - res.user_email_id, - res.user_email, - res.user_email_created_at, - res.user_email_confirmed_at, - ) { - (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - id: id.into(), - email, - created_at, - confirmed_at, - }), - (None, None, None, None) => None, - _ => { - return Err(DatabaseInconsistencyError::on("users") - .column("primary_user_email_id") - .row(user_id) - .into()) - } - }; - let user = User { id: user_id, username: res.user_username, sub: user_id.to_string(), - primary_email, + primary_user_email_id: res.user_primary_user_email_id.map(Into::into), }; let last_authentication = match ( diff --git a/crates/storage/src/repository.rs b/crates/storage/src/repository.rs index 9e6ca807..f321e941 100644 --- a/crates/storage/src/repository.rs +++ b/crates/storage/src/repository.rs @@ -14,9 +14,12 @@ use sqlx::{PgConnection, Postgres, Transaction}; -use crate::upstream_oauth2::{ - PgUpstreamOAuthLinkRepository, PgUpstreamOAuthProviderRepository, - PgUpstreamOAuthSessionRepository, +use crate::{ + upstream_oauth2::{ + PgUpstreamOAuthLinkRepository, PgUpstreamOAuthProviderRepository, + PgUpstreamOAuthSessionRepository, + }, + user::{PgUserEmailRepository, PgUserRepository}, }; pub trait Repository { @@ -32,15 +35,27 @@ pub trait Repository { where Self: 'c; + type UserRepository<'c> + where + Self: 'c; + + type UserEmailRepository<'c> + where + Self: 'c; + fn upstream_oauth_link(&mut self) -> Self::UpstreamOAuthLinkRepository<'_>; fn upstream_oauth_provider(&mut self) -> Self::UpstreamOAuthProviderRepository<'_>; fn upstream_oauth_session(&mut self) -> Self::UpstreamOAuthSessionRepository<'_>; + fn user(&mut self) -> Self::UserRepository<'_>; + fn user_email(&mut self) -> Self::UserEmailRepository<'_>; } impl Repository for PgConnection { type UpstreamOAuthLinkRepository<'c> = PgUpstreamOAuthLinkRepository<'c> where Self: 'c; type UpstreamOAuthProviderRepository<'c> = PgUpstreamOAuthProviderRepository<'c> where Self: 'c; type UpstreamOAuthSessionRepository<'c> = PgUpstreamOAuthSessionRepository<'c> where Self: 'c; + type UserRepository<'c> = PgUserRepository<'c> where Self: 'c; + type UserEmailRepository<'c> = PgUserEmailRepository<'c> where Self: 'c; fn upstream_oauth_link(&mut self) -> Self::UpstreamOAuthLinkRepository<'_> { PgUpstreamOAuthLinkRepository::new(self) @@ -53,12 +68,22 @@ impl Repository for PgConnection { fn upstream_oauth_session(&mut self) -> Self::UpstreamOAuthSessionRepository<'_> { PgUpstreamOAuthSessionRepository::new(self) } + + fn user(&mut self) -> Self::UserRepository<'_> { + PgUserRepository::new(self) + } + + fn user_email(&mut self) -> Self::UserEmailRepository<'_> { + PgUserEmailRepository::new(self) + } } impl<'t> Repository for Transaction<'t, Postgres> { type UpstreamOAuthLinkRepository<'c> = PgUpstreamOAuthLinkRepository<'c> where Self: 'c; type UpstreamOAuthProviderRepository<'c> = PgUpstreamOAuthProviderRepository<'c> where Self: 'c; type UpstreamOAuthSessionRepository<'c> = PgUpstreamOAuthSessionRepository<'c> where Self: 'c; + type UserRepository<'c> = PgUserRepository<'c> where Self: 'c; + type UserEmailRepository<'c> = PgUserEmailRepository<'c> where Self: 'c; fn upstream_oauth_link(&mut self) -> Self::UpstreamOAuthLinkRepository<'_> { PgUpstreamOAuthLinkRepository::new(self) @@ -71,4 +96,12 @@ impl<'t> Repository for Transaction<'t, Postgres> { fn upstream_oauth_session(&mut self) -> Self::UpstreamOAuthSessionRepository<'_> { PgUpstreamOAuthSessionRepository::new(self) } + + fn user(&mut self) -> Self::UserRepository<'_> { + PgUserRepository::new(self) + } + + fn user_email(&mut self) -> Self::UserEmailRepository<'_> { + PgUserEmailRepository::new(self) + } } diff --git a/crates/storage/src/user/email.rs b/crates/storage/src/user/email.rs new file mode 100644 index 00000000..83784a56 --- /dev/null +++ b/crates/storage/src/user/email.rs @@ -0,0 +1,555 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use mas_data_model::{User, UserEmail, UserEmailVerification, UserEmailVerificationState}; +use rand::RngCore; +use sqlx::{PgConnection, QueryBuilder}; +use ulid::Ulid; +use uuid::Uuid; + +use crate::{ + pagination::{process_page, Page, QueryBuilderExt}, + Clock, DatabaseError, DatabaseInconsistencyError, LookupResultExt, +}; + +#[async_trait] +pub trait UserEmailRepository: Send + Sync { + type Error; + + async fn lookup(&mut self, id: Ulid) -> Result, Self::Error>; + async fn find(&mut self, user: &User, email: &str) -> Result, Self::Error>; + async fn get_primary(&mut self, user: &User) -> Result, Self::Error>; + + async fn all(&mut self, user: &User) -> Result, Self::Error>; + async fn list_paginated( + &mut self, + user: &User, + before: Option, + after: Option, + first: Option, + last: Option, + ) -> Result, Self::Error>; + async fn count(&mut self, user: &User) -> Result; + + async fn add( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &Clock, + user: &User, + email: String, + ) -> Result; + async fn remove(&mut self, user_email: UserEmail) -> Result<(), Self::Error>; + + async fn mark_as_verified( + &mut self, + clock: &Clock, + user_email: UserEmail, + ) -> Result; + + async fn set_as_primary(&mut self, user_email: &UserEmail) -> Result<(), Self::Error>; + + async fn add_verification_code( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &Clock, + user_email: &UserEmail, + max_age: chrono::Duration, + code: String, + ) -> Result; + + async fn find_verification_code( + &mut self, + clock: &Clock, + user_email: &UserEmail, + code: &str, + ) -> Result, Self::Error>; + + async fn consume_verification_code( + &mut self, + clock: &Clock, + verification: UserEmailVerification, + ) -> Result; +} + +pub struct PgUserEmailRepository<'c> { + conn: &'c mut PgConnection, +} + +impl<'c> PgUserEmailRepository<'c> { + pub fn new(conn: &'c mut PgConnection) -> Self { + Self { conn } + } +} + +#[derive(Debug, Clone, sqlx::FromRow)] +struct UserEmailLookup { + user_email_id: Uuid, + user_id: Uuid, + email: String, + created_at: DateTime, + confirmed_at: Option>, +} + +impl From for UserEmail { + fn from(e: UserEmailLookup) -> UserEmail { + UserEmail { + id: e.user_email_id.into(), + user_id: e.user_id.into(), + email: e.email, + created_at: e.created_at, + confirmed_at: e.confirmed_at, + } + } +} + +struct UserEmailConfirmationCodeLookup { + user_email_confirmation_code_id: Uuid, + user_email_id: Uuid, + code: String, + created_at: DateTime, + expires_at: DateTime, + consumed_at: Option>, +} + +impl UserEmailConfirmationCodeLookup { + fn into_verification(self, clock: &Clock) -> UserEmailVerification { + let now = clock.now(); + let state = if let Some(when) = self.consumed_at { + UserEmailVerificationState::AlreadyUsed { when } + } else if self.expires_at < now { + UserEmailVerificationState::Expired { + when: self.expires_at, + } + } else { + UserEmailVerificationState::Valid + }; + + UserEmailVerification { + id: self.user_email_confirmation_code_id.into(), + user_email_id: self.user_email_id.into(), + code: self.code, + state, + created_at: self.created_at, + } + } +} + +#[async_trait] +impl<'c> UserEmailRepository for PgUserEmailRepository<'c> { + type Error = DatabaseError; + + #[tracing::instrument( + skip_all, + fields(user_email.id = %id), + err, + )] + async fn lookup(&mut self, id: Ulid) -> Result, Self::Error> { + let res = sqlx::query_as!( + UserEmailLookup, + r#" + SELECT user_email_id + , user_id + , email + , created_at + , confirmed_at + FROM user_emails + + WHERE user_email_id = $1 + "#, + Uuid::from(id), + ) + .fetch_one(&mut *self.conn) + .await + .to_option()?; + + let Some(user_email) = res else { return Ok(None) }; + + Ok(Some(user_email.into())) + } + + #[tracing::instrument( + skip_all, + fields(%user.id, user_email.email = email), + err, + )] + async fn find(&mut self, user: &User, email: &str) -> Result, Self::Error> { + let res = sqlx::query_as!( + UserEmailLookup, + r#" + SELECT user_email_id + , user_id + , email + , created_at + , confirmed_at + FROM user_emails + + WHERE user_id = $1 AND email = $2 + "#, + Uuid::from(user.id), + email, + ) + .fetch_one(&mut *self.conn) + .await + .to_option()?; + + let Some(user_email) = res else { return Ok(None) }; + + Ok(Some(user_email.into())) + } + + #[tracing::instrument( + skip_all, + fields(%user.id), + err, + )] + async fn get_primary(&mut self, user: &User) -> Result, Self::Error> { + let Some(id) = user.primary_user_email_id else { return Ok(None) }; + + let user_email = self.lookup(id).await?.ok_or_else(|| { + DatabaseInconsistencyError::on("users") + .column("primary_user_email_id") + .row(user.id) + })?; + + Ok(Some(user_email)) + } + + #[tracing::instrument( + skip_all, + fields(%user.id), + err, + )] + async fn all(&mut self, user: &User) -> Result, Self::Error> { + let res = sqlx::query_as!( + UserEmailLookup, + r#" + SELECT user_email_id + , user_id + , email + , created_at + , confirmed_at + FROM user_emails + + WHERE user_id = $1 + + ORDER BY email ASC + "#, + Uuid::from(user.id), + ) + .fetch_all(&mut *self.conn) + .await?; + + Ok(res.into_iter().map(Into::into).collect()) + } + + #[tracing::instrument( + skip_all, + fields(%user.id), + err, + )] + async fn list_paginated( + &mut self, + user: &User, + before: Option, + after: Option, + first: Option, + last: Option, + ) -> Result, DatabaseError> { + let mut query = QueryBuilder::new( + r#" + SELECT user_email_id + , user_id + , email + , created_at + , confirmed_at + FROM user_emails + "#, + ); + + query + .push(" WHERE user_id = ") + .push_bind(Uuid::from(user.id)) + .generate_pagination("ue.user_email_id", before, after, first, last)?; + + let edges: Vec = query.build_query_as().fetch_all(&mut *self.conn).await?; + + let (has_previous_page, has_next_page, edges) = process_page(edges, first, last)?; + + let edges = edges.into_iter().map(Into::into).collect(); + + Ok(Page { + has_next_page, + has_previous_page, + edges, + }) + } + + #[tracing::instrument( + skip_all, + fields(%user.id), + err, + )] + async fn count(&mut self, user: &User) -> Result { + let res = sqlx::query_scalar!( + r#" + SELECT COUNT(*) + FROM user_emails + WHERE user_id = $1 + "#, + Uuid::from(user.id), + ) + .fetch_one(&mut *self.conn) + .await?; + + let res = res.unwrap_or_default(); + + Ok(res + .try_into() + .map_err(DatabaseError::to_invalid_operation)?) + } + + #[tracing::instrument( + skip_all, + fields( + %user.id, + user_email.id, + user_email.email = email, + ), + err, + )] + async fn add( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &Clock, + user: &User, + email: String, + ) -> Result { + let created_at = clock.now(); + let id = Ulid::from_datetime_with_source(created_at.into(), rng); + tracing::Span::current().record("user_email.id", tracing::field::display(id)); + + sqlx::query!( + r#" + INSERT INTO user_emails (user_email_id, user_id, email, created_at) + VALUES ($1, $2, $3, $4) + "#, + Uuid::from(id), + Uuid::from(user.id), + &email, + created_at, + ) + .execute(&mut *self.conn) + .await?; + + Ok(UserEmail { + id, + user_id: user.id, + email, + created_at, + confirmed_at: None, + }) + } + + #[tracing::instrument( + skip_all, + fields( + user.id = %user_email.user_id, + %user_email.id, + %user_email.email, + ), + err, + )] + async fn remove(&mut self, user_email: UserEmail) -> Result<(), Self::Error> { + sqlx::query!( + r#" + DELETE FROM user_emails + WHERE user_email_id = $1 + "#, + Uuid::from(user_email.id), + ) + .execute(&mut *self.conn) + .await?; + + Ok(()) + } + + async fn mark_as_verified( + &mut self, + clock: &Clock, + mut user_email: UserEmail, + ) -> Result { + let confirmed_at = clock.now(); + sqlx::query!( + r#" + UPDATE user_emails + SET confirmed_at = $2 + WHERE user_email_id = $1 + "#, + Uuid::from(user_email.id), + confirmed_at, + ) + .execute(&mut *self.conn) + .await?; + + user_email.confirmed_at = Some(confirmed_at); + Ok(user_email) + } + + async fn set_as_primary(&mut self, user_email: &UserEmail) -> Result<(), Self::Error> { + sqlx::query!( + r#" + UPDATE users + SET primary_user_email_id = user_emails.user_email_id + FROM user_emails + WHERE user_emails.user_email_id = $1 + AND users.user_id = user_emails.user_id + "#, + Uuid::from(user_email.id), + ) + .execute(&mut *self.conn) + .await?; + + Ok(()) + } + + #[tracing::instrument( + skip_all, + fields( + %user_email.id, + %user_email.email, + user_email_verification.id, + user_email_verification.code = code, + ), + err, + )] + async fn add_verification_code( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &Clock, + user_email: &UserEmail, + max_age: chrono::Duration, + code: String, + ) -> Result { + let created_at = clock.now(); + let id = Ulid::from_datetime_with_source(created_at.into(), rng); + tracing::Span::current().record("user_email_confirmation.id", tracing::field::display(id)); + let expires_at = created_at + max_age; + + sqlx::query!( + r#" + INSERT INTO user_email_confirmation_codes + (user_email_confirmation_code_id, user_email_id, code, created_at, expires_at) + VALUES ($1, $2, $3, $4, $5) + "#, + Uuid::from(id), + Uuid::from(user_email.id), + code, + created_at, + expires_at, + ) + .execute(&mut *self.conn) + .await?; + + let verification = UserEmailVerification { + id, + user_email_id: user_email.id, + code, + created_at, + state: UserEmailVerificationState::Valid, + }; + + Ok(verification) + } + + #[tracing::instrument( + skip_all, + fields( + %user_email.id, + user.id = %user_email.user_id, + ), + err, + )] + async fn find_verification_code( + &mut self, + clock: &Clock, + user_email: &UserEmail, + code: &str, + ) -> Result, Self::Error> { + let res = sqlx::query_as!( + UserEmailConfirmationCodeLookup, + r#" + SELECT user_email_confirmation_code_id + , user_email_id + , code + , created_at + , expires_at + , consumed_at + FROM user_email_confirmation_codes + WHERE code = $1 + AND user_email_id = $2 + "#, + code, + Uuid::from(user_email.id), + ) + .fetch_one(&mut *self.conn) + .await + .to_option()?; + + let Some(res) = res else { return Ok(None) }; + + Ok(Some(res.into_verification(clock))) + } + + #[tracing::instrument( + skip_all, + fields( + %user_email_verification.id, + user_email.id = %user_email_verification.user_email_id, + ), + err, + )] + async fn consume_verification_code( + &mut self, + clock: &Clock, + mut user_email_verification: UserEmailVerification, + ) -> Result { + if !matches!( + user_email_verification.state, + UserEmailVerificationState::Valid + ) { + return Err(DatabaseError::invalid_operation()); + } + + let consumed_at = clock.now(); + + sqlx::query!( + r#" + UPDATE user_email_confirmation_codes + SET consumed_at = $2 + WHERE user_email_confirmation_code_id = $1 + "#, + Uuid::from(user_email_verification.id), + consumed_at + ) + .execute(&mut *self.conn) + .await?; + + user_email_verification.state = + UserEmailVerificationState::AlreadyUsed { when: consumed_at }; + + Ok(user_email_verification) + } +} diff --git a/crates/storage/src/user/mod.rs b/crates/storage/src/user/mod.rs index 1b8c2c61..54b3689c 100644 --- a/crates/storage/src/user/mod.rs +++ b/crates/storage/src/user/mod.rs @@ -12,13 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +use async_trait::async_trait; use chrono::{DateTime, Utc}; -use mas_data_model::{ - Authentication, BrowserSession, User, UserEmail, UserEmailVerification, - UserEmailVerificationState, -}; -use rand::Rng; -use sqlx::{PgExecutor, QueryBuilder}; +use mas_data_model::{Authentication, BrowserSession, User}; +use rand::{Rng, RngCore}; +use sqlx::{PgConnection, PgExecutor, QueryBuilder}; use tracing::{info_span, Instrument}; use ulid::Ulid; use uuid::Uuid; @@ -29,35 +27,188 @@ use crate::{ }; mod authentication; +mod email; mod password; pub use self::{ authentication::{authenticate_session_with_password, authenticate_session_with_upstream}, + email::{PgUserEmailRepository, UserEmailRepository}, password::{add_user_password, lookup_user_password}, }; +#[async_trait] +pub trait UserRepository: Send + Sync { + type Error; + + async fn lookup(&mut self, id: Ulid) -> Result, Self::Error>; + async fn find_by_username(&mut self, username: &str) -> Result, Self::Error>; + async fn add( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &Clock, + username: String, + ) -> Result; + async fn exists(&mut self, username: &str) -> Result; +} + +pub struct PgUserRepository<'c> { + conn: &'c mut PgConnection, +} + +impl<'c> PgUserRepository<'c> { + pub fn new(conn: &'c mut PgConnection) -> Self { + Self { conn } + } +} + #[derive(Debug, Clone)] struct UserLookup { user_id: Uuid, - user_username: String, - user_email_id: Option, - user_email: Option, - user_email_created_at: Option>, - user_email_confirmed_at: Option>, + username: String, + primary_user_email_id: Option, + + #[allow(dead_code)] + created_at: DateTime, +} + +impl From for User { + fn from(value: UserLookup) -> Self { + let id = value.user_id.into(); + Self { + id, + username: value.username, + sub: id.to_string(), + primary_user_email_id: value.primary_user_email_id.map(Into::into), + } + } +} + +#[async_trait] +impl<'c> UserRepository for PgUserRepository<'c> { + type Error = DatabaseError; + + #[tracing::instrument( + skip_all, + fields(user.id = %id), + err, + )] + async fn lookup(&mut self, id: Ulid) -> Result, Self::Error> { + let res = sqlx::query_as!( + UserLookup, + r#" + SELECT user_id + , username + , primary_user_email_id + , created_at + FROM users + WHERE user_id = $1 + "#, + Uuid::from(id), + ) + .fetch_one(&mut *self.conn) + .await + .to_option()?; + + let Some(res) = res else { return Ok(None) }; + + Ok(Some(res.into())) + } + + #[tracing::instrument( + skip_all, + fields(user.username = username), + err, + )] + async fn find_by_username(&mut self, username: &str) -> Result, Self::Error> { + let res = sqlx::query_as!( + UserLookup, + r#" + SELECT user_id + , username + , primary_user_email_id + , created_at + FROM users + WHERE username = $1 + "#, + username, + ) + .fetch_one(&mut *self.conn) + .await + .to_option()?; + + let Some(res) = res else { return Ok(None) }; + + Ok(Some(res.into())) + } + + #[tracing::instrument( + skip_all, + fields( + user.username = username, + user.id, + ), + err, + )] + async fn add( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &Clock, + username: String, + ) -> Result { + let created_at = clock.now(); + let id = Ulid::from_datetime_with_source(created_at.into(), rng); + tracing::Span::current().record("user.id", tracing::field::display(id)); + + sqlx::query!( + r#" + INSERT INTO users (user_id, username, created_at) + VALUES ($1, $2, $3) + "#, + Uuid::from(id), + username, + created_at, + ) + .execute(&mut *self.conn) + .await?; + + Ok(User { + id, + username, + sub: id.to_string(), + primary_user_email_id: None, + }) + } + + #[tracing::instrument( + skip_all, + fields(user.username = username), + err, + )] + async fn exists(&mut self, username: &str) -> Result { + let exists = sqlx::query_scalar!( + r#" + SELECT EXISTS( + SELECT 1 FROM users WHERE username = $1 + ) AS "exists!" + "#, + username + ) + .fetch_one(&mut *self.conn) + .await?; + + Ok(exists) + } } #[derive(sqlx::FromRow)] struct SessionLookup { user_session_id: Uuid, + user_session_created_at: DateTime, user_id: Uuid, - username: String, - created_at: DateTime, + user_username: String, + user_primary_user_email_id: Option, last_authentication_id: Option, last_authd_at: Option>, - user_email_id: Option, - user_email: Option, - user_email_created_at: Option>, - user_email_confirmed_at: Option>, } impl TryInto for SessionLookup { @@ -65,31 +216,11 @@ impl TryInto for SessionLookup { fn try_into(self) -> Result { let id = Ulid::from(self.user_id); - let primary_email = match ( - self.user_email_id, - self.user_email, - self.user_email_created_at, - self.user_email_confirmed_at, - ) { - (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - id: id.into(), - email, - created_at, - confirmed_at, - }), - (None, None, None, None) => None, - _ => { - return Err(DatabaseInconsistencyError::on("users") - .column("primary_user_email_id") - .row(id)) - } - }; - let user = User { id, - username: self.username, + username: self.user_username, sub: id.to_string(), - primary_email, + primary_user_email_id: self.user_primary_user_email_id.map(Into::into), }; let last_authentication = match (self.last_authentication_id, self.last_authd_at) { @@ -108,7 +239,7 @@ impl TryInto for SessionLookup { Ok(BrowserSession { id: self.user_session_id.into(), user, - created_at: self.created_at, + created_at: self.user_session_created_at, last_authentication, }) } @@ -126,24 +257,18 @@ pub async fn lookup_active_session( let res = sqlx::query_as!( SessionLookup, r#" - SELECT - s.user_session_id, - u.user_id, - u.username, - s.created_at, - a.user_session_authentication_id AS "last_authentication_id?", - a.created_at AS "last_authd_at?", - ue.user_email_id AS "user_email_id?", - ue.email AS "user_email?", - ue.created_at AS "user_email_created_at?", - ue.confirmed_at AS "user_email_confirmed_at?" + SELECT s.user_session_id + , s.created_at AS "user_session_created_at" + , u.user_id + , u.username AS "user_username" + , u.primary_user_email_id AS "user_primary_user_email_id" + , a.user_session_authentication_id AS "last_authentication_id?" + , a.created_at AS "last_authd_at?" FROM user_sessions s INNER JOIN users u USING (user_id) LEFT JOIN user_session_authentications a USING (user_session_id) - LEFT JOIN user_emails ue - ON ue.user_email_id = u.primary_user_email_id WHERE s.user_session_id = $1 AND s.finished_at IS NULL ORDER BY a.created_at DESC LIMIT 1 @@ -279,44 +404,6 @@ pub async fn count_active_sessions( Ok(res) } -#[tracing::instrument( - skip_all, - fields( - user.username = username, - user.id, - ), - err, -)] -pub async fn add_user( - executor: impl PgExecutor<'_>, - mut rng: impl Rng + Send, - clock: &Clock, - username: &str, -) -> Result { - let created_at = clock.now(); - let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); - tracing::Span::current().record("user.id", tracing::field::display(id)); - - sqlx::query!( - r#" - INSERT INTO users (user_id, username, created_at) - VALUES ($1, $2, $3) - "#, - Uuid::from(id), - username, - created_at, - ) - .execute(executor) - .await?; - - Ok(User { - id, - username: username.to_owned(), - sub: id.to_string(), - primary_email: None, - }) -} - #[tracing::instrument( skip_all, fields(%user_session.id), @@ -343,662 +430,3 @@ pub async fn end_session( DatabaseError::ensure_affected_rows(&res, 1) } - -#[tracing::instrument( - skip_all, - fields(user.username = username), - err, -)] -pub async fn lookup_user_by_username( - executor: impl PgExecutor<'_>, - username: &str, -) -> Result, DatabaseError> { - let res = sqlx::query_as!( - UserLookup, - r#" - SELECT - u.user_id, - u.username AS user_username, - ue.user_email_id AS "user_email_id?", - ue.email AS "user_email?", - ue.created_at AS "user_email_created_at?", - ue.confirmed_at AS "user_email_confirmed_at?" - FROM users u - - LEFT JOIN user_emails ue - USING (user_id) - - WHERE u.username = $1 - "#, - username, - ) - .fetch_one(executor) - .instrument(info_span!("Fetch user")) - .await - .to_option()?; - - let Some(res) = res else { return Ok(None) }; - - let id = Ulid::from(res.user_id); - let primary_email = match ( - res.user_email_id, - res.user_email, - res.user_email_created_at, - res.user_email_confirmed_at, - ) { - (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - id: id.into(), - email, - created_at, - confirmed_at, - }), - (None, None, None, None) => None, - _ => { - return Err(DatabaseInconsistencyError::on("users") - .column("primary_user_email_id") - .row(id) - .into()) - } - }; - - Ok(Some(User { - id, - username: res.user_username, - sub: id.to_string(), - primary_email, - })) -} - -#[tracing::instrument( - skip_all, - fields(user.id = %id), - err, -)] -pub async fn lookup_user(executor: impl PgExecutor<'_>, id: Ulid) -> Result { - let res = sqlx::query_as!( - UserLookup, - r#" - SELECT - u.user_id, - u.username AS user_username, - ue.user_email_id AS "user_email_id?", - ue.email AS "user_email?", - ue.created_at AS "user_email_created_at?", - ue.confirmed_at AS "user_email_confirmed_at?" - FROM users u - - LEFT JOIN user_emails ue - USING (user_id) - - WHERE u.user_id = $1 - "#, - Uuid::from(id), - ) - .fetch_one(executor) - .instrument(info_span!("Fetch user")) - .await?; - - let id = Ulid::from(res.user_id); - let primary_email = match ( - res.user_email_id, - res.user_email, - res.user_email_created_at, - res.user_email_confirmed_at, - ) { - (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - id: id.into(), - email, - created_at, - confirmed_at, - }), - (None, None, None, None) => None, - _ => { - return Err(DatabaseInconsistencyError::on("users") - .column("primary_user_email_id") - .row(id) - .into()) - } - }; - - Ok(User { - id, - username: res.user_username, - sub: id.to_string(), - primary_email, - }) -} - -#[tracing::instrument( - skip_all, - fields(user.username = username), - err, -)] -pub async fn username_exists( - executor: impl PgExecutor<'_>, - username: &str, -) -> Result { - sqlx::query_scalar!( - r#" - SELECT EXISTS( - SELECT 1 FROM users WHERE username = $1 - ) AS "exists!" - "#, - username - ) - .fetch_one(executor) - .await -} - -#[derive(Debug, Clone, sqlx::FromRow)] -struct UserEmailLookup { - user_email_id: Uuid, - user_email: String, - user_email_created_at: DateTime, - user_email_confirmed_at: Option>, -} - -impl From for UserEmail { - fn from(e: UserEmailLookup) -> UserEmail { - UserEmail { - id: e.user_email_id.into(), - email: e.user_email, - created_at: e.user_email_created_at, - confirmed_at: e.user_email_confirmed_at, - } - } -} - -#[tracing::instrument( - skip_all, - fields(%user.id, %user.username), - err, -)] -pub async fn get_user_emails( - executor: impl PgExecutor<'_>, - user: &User, -) -> Result, sqlx::Error> { - let res = sqlx::query_as!( - UserEmailLookup, - r#" - SELECT - ue.user_email_id, - ue.email AS "user_email", - ue.created_at AS "user_email_created_at", - ue.confirmed_at AS "user_email_confirmed_at" - FROM user_emails ue - - WHERE ue.user_id = $1 - - ORDER BY ue.email ASC - "#, - Uuid::from(user.id), - ) - .fetch_all(executor) - .instrument(info_span!("Fetch user emails")) - .await?; - - Ok(res.into_iter().map(Into::into).collect()) -} - -#[tracing::instrument( - skip_all, - fields(%user.id, %user.username), - err, -)] -pub async fn count_user_emails( - executor: impl PgExecutor<'_>, - user: &User, -) -> Result { - let res = sqlx::query_scalar!( - r#" - SELECT COUNT(*) - FROM user_emails ue - WHERE ue.user_id = $1 - "#, - Uuid::from(user.id), - ) - .fetch_one(executor) - .instrument(info_span!("Count user emails")) - .await?; - - Ok(res.unwrap_or_default()) -} - -#[tracing::instrument( - skip_all, - fields(%user.id, %user.username), - err, -)] -pub async fn get_paginated_user_emails( - executor: impl PgExecutor<'_>, - user: &User, - before: Option, - after: Option, - first: Option, - last: Option, -) -> Result<(bool, bool, Vec), DatabaseError> { - let mut query = QueryBuilder::new( - r#" - SELECT - ue.user_email_id, - ue.email AS "user_email", - ue.created_at AS "user_email_created_at", - ue.confirmed_at AS "user_email_confirmed_at" - FROM user_emails ue - "#, - ); - - query - .push(" WHERE ue.user_id = ") - .push_bind(Uuid::from(user.id)) - .generate_pagination("ue.user_email_id", before, after, first, last)?; - - let span = info_span!("Fetch paginated user sessions", db.statement = query.sql()); - let page: Vec = query - .build_query_as() - .fetch_all(executor) - .instrument(span) - .await?; - - let (has_previous_page, has_next_page, page) = process_page(page, first, last)?; - - Ok(( - has_previous_page, - has_next_page, - page.into_iter().map(Into::into).collect(), - )) -} - -#[tracing::instrument( - skip_all, - fields( - %user.id, - %user.username, - user_email.id = %id, - ), - err, -)] -pub async fn get_user_email( - executor: impl PgExecutor<'_>, - user: &User, - id: Ulid, -) -> Result { - let res = sqlx::query_as!( - UserEmailLookup, - r#" - SELECT - ue.user_email_id, - ue.email AS "user_email", - ue.created_at AS "user_email_created_at", - ue.confirmed_at AS "user_email_confirmed_at" - FROM user_emails ue - - WHERE ue.user_id = $1 - AND ue.user_email_id = $2 - "#, - Uuid::from(user.id), - Uuid::from(id), - ) - .fetch_one(executor) - .instrument(info_span!("Fetch user emails")) - .await?; - - Ok(res.into()) -} - -#[tracing::instrument( - skip_all, - fields( - %user.id, - %user.username, - user_email.id, - user_email.email = %email, - ), - err, -)] -pub async fn add_user_email( - executor: impl PgExecutor<'_>, - mut rng: impl Rng + Send, - clock: &Clock, - user: &User, - email: String, -) -> Result { - let created_at = clock.now(); - let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); - tracing::Span::current().record("user_email.id", tracing::field::display(id)); - - sqlx::query!( - r#" - INSERT INTO user_emails (user_email_id, user_id, email, created_at) - VALUES ($1, $2, $3, $4) - "#, - Uuid::from(id), - Uuid::from(user.id), - &email, - created_at, - ) - .execute(executor) - .instrument(info_span!("Add user email")) - .await?; - - Ok(UserEmail { - id, - email, - created_at, - confirmed_at: None, - }) -} - -#[tracing::instrument( - skip_all, - fields( - %user_email.id, - %user_email.email, - ), - err, -)] -pub async fn set_user_email_as_primary( - executor: impl PgExecutor<'_>, - user_email: &UserEmail, -) -> Result<(), sqlx::Error> { - sqlx::query!( - r#" - UPDATE users - SET primary_user_email_id = user_emails.user_email_id - FROM user_emails - WHERE user_emails.user_email_id = $1 - AND users.user_id = user_emails.user_id - "#, - Uuid::from(user_email.id), - ) - .execute(executor) - .instrument(info_span!("Add user email")) - .await?; - - Ok(()) -} - -#[tracing::instrument( - skip_all, - fields( - %user_email.id, - %user_email.email, - ), - err, -)] -pub async fn remove_user_email( - executor: impl PgExecutor<'_>, - user_email: UserEmail, -) -> Result<(), sqlx::Error> { - sqlx::query!( - r#" - DELETE FROM user_emails - WHERE user_emails.user_email_id = $1 - "#, - Uuid::from(user_email.id), - ) - .execute(executor) - .instrument(info_span!("Remove user email")) - .await?; - - Ok(()) -} - -#[tracing::instrument( - skip_all, - fields( - %user.id, - user_email.email = email, - ), - err, -)] -pub async fn lookup_user_email( - executor: impl PgExecutor<'_>, - user: &User, - email: &str, -) -> Result, sqlx::Error> { - let res = sqlx::query_as!( - UserEmailLookup, - r#" - SELECT - ue.user_email_id, - ue.email AS "user_email", - ue.created_at AS "user_email_created_at", - ue.confirmed_at AS "user_email_confirmed_at" - FROM user_emails ue - - WHERE ue.user_id = $1 - AND ue.email = $2 - "#, - Uuid::from(user.id), - email, - ) - .fetch_one(executor) - .instrument(info_span!("Lookup user email")) - .await - .to_option()?; - - let Some(res) = res else { return Ok(None) }; - - Ok(Some(res.into())) -} - -#[tracing::instrument( - skip_all, - fields( - %user.id, - user_email.id = %id, - ), - err, -)] -pub async fn lookup_user_email_by_id( - executor: impl PgExecutor<'_>, - user: &User, - id: Ulid, -) -> Result, DatabaseError> { - let res = sqlx::query_as!( - UserEmailLookup, - r#" - SELECT - ue.user_email_id, - ue.email AS "user_email", - ue.created_at AS "user_email_created_at", - ue.confirmed_at AS "user_email_confirmed_at" - FROM user_emails ue - - WHERE ue.user_id = $1 - AND ue.user_email_id = $2 - "#, - Uuid::from(user.id), - Uuid::from(id), - ) - .fetch_one(executor) - .instrument(info_span!("Lookup user email")) - .await - .to_option()?; - - let Some(res) = res else { return Ok(None) }; - - Ok(Some(res.into())) -} - -#[tracing::instrument( - skip_all, - fields(%user_email.id), - err, -)] -pub async fn mark_user_email_as_verified( - executor: impl PgExecutor<'_>, - clock: &Clock, - mut user_email: UserEmail, -) -> Result { - let confirmed_at = clock.now(); - sqlx::query!( - r#" - UPDATE user_emails - SET confirmed_at = $2 - WHERE user_email_id = $1 - "#, - Uuid::from(user_email.id), - confirmed_at, - ) - .execute(executor) - .instrument(info_span!("Confirm user email")) - .await?; - - user_email.confirmed_at = Some(confirmed_at); - - Ok(user_email) -} - -struct UserEmailConfirmationCodeLookup { - user_email_confirmation_code_id: Uuid, - code: String, - created_at: DateTime, - expires_at: DateTime, - consumed_at: Option>, -} - -#[tracing::instrument( - skip_all, - fields(%user_email.id), - err, -)] -pub async fn lookup_user_email_verification_code( - executor: impl PgExecutor<'_>, - clock: &Clock, - user_email: UserEmail, - code: &str, -) -> Result, DatabaseError> { - let now = clock.now(); - - let res = sqlx::query_as!( - UserEmailConfirmationCodeLookup, - r#" - SELECT - ec.user_email_confirmation_code_id, - ec.code, - ec.created_at, - ec.expires_at, - ec.consumed_at - FROM user_email_confirmation_codes ec - WHERE ec.code = $1 - AND ec.user_email_id = $2 - "#, - code, - Uuid::from(user_email.id), - ) - .fetch_one(executor) - .instrument(info_span!("Lookup user email verification")) - .await - .to_option()?; - - let Some(res) = res else { return Ok(None) }; - - let state = if let Some(when) = res.consumed_at { - UserEmailVerificationState::AlreadyUsed { when } - } else if res.expires_at < now { - UserEmailVerificationState::Expired { - when: res.expires_at, - } - } else { - UserEmailVerificationState::Valid - }; - - Ok(Some(UserEmailVerification { - id: res.user_email_confirmation_code_id.into(), - code: res.code, - email: user_email, - state, - created_at: res.created_at, - })) -} - -#[tracing::instrument( - skip_all, - fields( - %user_email_verification.id, - ), - err, -)] -pub async fn consume_email_verification( - executor: impl PgExecutor<'_>, - clock: &Clock, - mut user_email_verification: UserEmailVerification, -) -> Result { - if !matches!( - user_email_verification.state, - UserEmailVerificationState::Valid - ) { - return Err(DatabaseError::invalid_operation()); - } - - let consumed_at = clock.now(); - - sqlx::query!( - r#" - UPDATE user_email_confirmation_codes - SET consumed_at = $2 - WHERE user_email_confirmation_code_id = $1 - "#, - Uuid::from(user_email_verification.id), - consumed_at - ) - .execute(executor) - .instrument(info_span!("Consume user email verification")) - .await?; - - user_email_verification.state = UserEmailVerificationState::AlreadyUsed { when: consumed_at }; - - Ok(user_email_verification) -} - -#[tracing::instrument( - skip_all, - fields( - %user_email.id, - %user_email.email, - user_email_confirmation.id, - user_email_confirmation.code = code, - ), - err, -)] -pub async fn add_user_email_verification_code( - executor: impl PgExecutor<'_>, - mut rng: impl Rng + Send, - clock: &Clock, - user_email: UserEmail, - max_age: chrono::Duration, - code: String, -) -> Result { - let created_at = clock.now(); - let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); - tracing::Span::current().record("user_email_confirmation.id", tracing::field::display(id)); - let expires_at = created_at + max_age; - - sqlx::query!( - r#" - INSERT INTO user_email_confirmation_codes - (user_email_confirmation_code_id, user_email_id, code, created_at, expires_at) - VALUES ($1, $2, $3, $4, $5) - "#, - Uuid::from(id), - Uuid::from(user_email.id), - code, - created_at, - expires_at, - ) - .execute(executor) - .instrument(info_span!("Add user email verification code")) - .await?; - - let verification = UserEmailVerification { - id, - email: user_email, - code, - created_at, - state: UserEmailVerificationState::Valid, - }; - - Ok(verification) -} diff --git a/crates/templates/src/context.rs b/crates/templates/src/context.rs index 642a760b..8e8e0c8a 100644 --- a/crates/templates/src/context.rs +++ b/crates/templates/src/context.rs @@ -618,6 +618,7 @@ impl TemplateContext for EmailVerificationContext { .map(|user| { let email = UserEmail { id: Ulid::from_datetime_with_source(now.into(), rng), + user_id: user.id, email: "foobar@example.com".to_owned(), created_at: now, confirmed_at: None, @@ -625,8 +626,8 @@ impl TemplateContext for EmailVerificationContext { let verification = UserEmailVerification { id: Ulid::from_datetime_with_source(now.into(), rng), + user_email_id: email.id, code: "123456".to_owned(), - email, created_at: now, state: mas_data_model::UserEmailVerificationState::Valid, }; @@ -684,6 +685,7 @@ impl TemplateContext for EmailVerificationPageContext { { let email = UserEmail { id: Ulid::from_datetime_with_source(now.into(), rng), + user_id: Ulid::from_datetime_with_source(now.into(), rng), email: "foobar@example.com".to_owned(), created_at: now, confirmed_at: None, diff --git a/docs/development/database.md b/docs/development/database.md index e9583124..513fe7ef 100644 --- a/docs/development/database.md +++ b/docs/development/database.md @@ -35,6 +35,8 @@ Note that migrations are embedded in the final binary and can be run from the se ## Writing database interactions +**TODO**: *This section is outdated.* + A typical interaction with the database look like this: ```rust