diff --git a/crates/cli/src/manage.rs b/crates/cli/src/manage.rs index 12ed6cc4..190cded7 100644 --- a/crates/cli/src/manage.rs +++ b/crates/cli/src/manage.rs @@ -15,7 +15,9 @@ use argon2::Argon2; use clap::Parser; use mas_config::DatabaseConfig; -use mas_storage::user::register_user; +use mas_storage::user::{ + lookup_user_by_username, lookup_user_email, mark_user_email_as_verified, register_user, +}; use tracing::{info, warn}; use super::RootCommand; @@ -33,6 +35,9 @@ enum ManageSubcommand { /// List active users Users, + + /// Mark email address as verified + VerifyEmail { username: String, email: String }, } impl ManageCommand { @@ -54,6 +59,20 @@ impl ManageCommand { SC::Users => { warn!("Not implemented yet"); + Ok(()) + } + SC::VerifyEmail { username, email } => { + let config: DatabaseConfig = root.load_config()?; + let pool = config.connect().await?; + let mut txn = pool.begin().await?; + + let user = lookup_user_by_username(&mut txn, username).await?; + let email = lookup_user_email(&mut txn, &user, email).await?; + let email = mark_user_email_as_verified(&mut txn, email).await?; + + txn.commit().await?; + info!(?email, "Email marked as verified"); + Ok(()) } } diff --git a/crates/storage/sqlx-data.json b/crates/storage/sqlx-data.json index d1747e68..29d6dc94 100644 --- a/crates/storage/sqlx-data.json +++ b/crates/storage/sqlx-data.json @@ -675,6 +675,26 @@ ] } }, + "7de9cfa6e90ba20f5b298ea387cf13a7e40d0f5b3eb903a80d06fbe33074d596": { + "query": "\n UPDATE user_emails\n SET confirmed_at = NOW()\n WHERE id = $1\n RETURNING confirmed_at\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "confirmed_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + true + ] + } + }, "88ac8783bd5881c42eafd9cf87a16fe6031f3153fd6a8618e689694584aeb2de": { "query": "\n DELETE FROM oauth2_access_tokens\n WHERE id = $1\n ", "describe": { @@ -1209,6 +1229,45 @@ ] } }, + "db34b3d7fa5d824e63f388d660615d748e11c1406e8166da907e0a54a665e37a": { + "query": "\n SELECT \n ue.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_emails ue\n\n WHERE ue.user_id = $1\n AND ue.email = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "user_email_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "user_email", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "user_email_created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "user_email_confirmed_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + true + ] + } + }, "dda03ba41249bff965cb8f129acc15f4e40807adb9b75dee0ac43edd7809de84": { "query": "\n INSERT INTO users (username)\n VALUES ($1)\n RETURNING id\n ", "describe": { diff --git a/crates/storage/src/user.rs b/crates/storage/src/user.rs index 752458ba..da553c87 100644 --- a/crates/storage/src/user.rs +++ b/crates/storage/src/user.rs @@ -631,3 +631,57 @@ pub async fn remove_user_email( Ok(()) } + +#[tracing::instrument(skip(executor))] +pub async fn lookup_user_email( + executor: impl PgExecutor<'_>, + user: &User, + email: &str, +) -> anyhow::Result> { + let res = sqlx::query_as!( + UserEmailLookup, + r#" + SELECT + ue.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 user_emails ue + + WHERE ue.user_id = $1 + AND ue.email = $2 + "#, + user.data, + email, + ) + .fetch_one(executor) + .instrument(info_span!("Lookup user email")) + .await + .context("could not lookup user email")?; + + Ok(res.into()) +} + +#[tracing::instrument(skip(executor))] +pub async fn mark_user_email_as_verified( + executor: impl PgExecutor<'_>, + mut email: UserEmail, +) -> anyhow::Result> { + let confirmed_at = sqlx::query_scalar!( + r#" + UPDATE user_emails + SET confirmed_at = NOW() + WHERE id = $1 + RETURNING confirmed_at + "#, + email.data, + ) + .fetch_one(executor) + .instrument(info_span!("Confirm user email")) + .await + .context("could not update user email")?; + + email.confirmed_at = confirmed_at; + + Ok(email) +}