use super::{PgPool, Uuid}; /// Upsert a user from MNW account data. Creates the user if they don't exist, /// updates username/display_name if they do. #[tracing::instrument(skip_all)] pub async fn upsert_user( pool: &PgPool, mnw_account_id: Uuid, username: &str, display_name: Option<&str>, ) -> Result<(), sqlx::Error> { let mut tx = pool.begin().await?; vacate_stale_username(&mut tx, mnw_account_id, username).await?; sqlx::query!( "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, $2, $3) ON CONFLICT (mnw_account_id) DO UPDATE SET username = $2, display_name = $3, updated_at = now()", mnw_account_id, username, display_name, ) .execute(&mut *tx) .await?; tx.commit().await?; Ok(()) } /// Free `username` from any *other* user row that still holds it, so the caller's /// upsert can claim it without tripping the `users_username_key` unique index. /// /// MNW is the source of truth for usernames and they are reusable there; a local /// mirror row holding a name its MNW account no longer owns is stale and must /// yield. Run this on the same transaction as the upsert (a single statement /// can't, because a non-deferrable unique index is checked per-row and the /// rename wouldn't yet be visible to the insert). The vacated row keeps its data /// under a collision-proof `mnw_` placeholder until that user's own /// next login resets it. Closes the username-collision login lockout (S2). async fn vacate_stale_username( conn: &mut sqlx::PgConnection, mnw_account_id: Uuid, username: &str, ) -> Result<(), sqlx::Error> { sqlx::query!( "UPDATE users SET username = 'mnw_' || mnw_account_id::text, updated_at = now() WHERE username = $1 AND mnw_account_id <> $2", username, mnw_account_id, ) .execute(&mut *conn) .await?; Ok(()) } /// Public wrapper over [`vacate_stale_username`] for the OAuth login upsert in the /// web crate, which builds its INSERT with a runtime query. pub async fn vacate_username_for_login( conn: &mut sqlx::PgConnection, mnw_account_id: Uuid, username: &str, ) -> Result<(), sqlx::Error> { vacate_stale_username(conn, mnw_account_id, username).await } /// Suspend a user. #[tracing::instrument(skip_all)] pub async fn suspend_user<'e, E: sqlx::PgExecutor<'e>>( executor: E, user_id: Uuid, reason: Option<&str>, ) -> Result<(), sqlx::Error> { sqlx::query!( "UPDATE users SET suspended_at = now(), suspension_reason = $2 WHERE mnw_account_id = $1", user_id, reason, ) .execute(executor) .await?; Ok(()) } /// Unsuspend a user. #[tracing::instrument(skip_all)] pub async fn unsuspend_user<'e, E: sqlx::PgExecutor<'e>>( executor: E, user_id: Uuid, ) -> Result<(), sqlx::Error> { sqlx::query!( "UPDATE users SET suspended_at = NULL, suspension_reason = NULL WHERE mnw_account_id = $1", user_id, ) .execute(executor) .await?; Ok(()) } /// Clear a user's saved signature (markdown + rendered html). #[tracing::instrument(skip_all)] pub async fn clear_user_signature(pool: &PgPool, user_id: Uuid) -> Result<(), sqlx::Error> { sqlx::query!( "UPDATE users SET signature_markdown = NULL, signature_html = NULL \ WHERE mnw_account_id = $1", user_id, ) .execute(pool) .await?; Ok(()) } /// Save a user's signature markdown and its pre-rendered html. #[tracing::instrument(skip_all)] pub async fn set_user_signature( pool: &PgPool, user_id: Uuid, markdown: &str, html: &str, ) -> Result<(), sqlx::Error> { sqlx::query!( "UPDATE users SET signature_markdown = $2, signature_html = $3 \ WHERE mnw_account_id = $1", user_id, markdown, html, ) .execute(pool) .await?; Ok(()) }