diff --git a/crates/chattyness-db/src/queries/memberships.rs b/crates/chattyness-db/src/queries/memberships.rs index 212fe1e..cff70c6 100644 --- a/crates/chattyness-db/src/queries/memberships.rs +++ b/crates/chattyness-db/src/queries/memberships.rs @@ -35,12 +35,30 @@ pub async fn get_user_membership( } /// Create a new membership (join a realm). +/// This function is idempotent - if the membership already exists, it returns the existing id. pub async fn create_membership( pool: &PgPool, user_id: Uuid, realm_id: Uuid, role: RealmRole, ) -> Result { + // Check if membership already exists + let existing: Option<(Uuid,)> = sqlx::query_as( + r#" + SELECT id FROM realm.memberships + WHERE user_id = $1 AND realm_id = $2 + "#, + ) + .bind(user_id) + .bind(realm_id) + .fetch_optional(pool) + .await?; + + if let Some((id,)) = existing { + return Ok(id); + } + + // Create new membership let (membership_id,): (Uuid,) = sqlx::query_as( r#" INSERT INTO realm.memberships (realm_id, user_id, role) @@ -54,7 +72,7 @@ pub async fn create_membership( .fetch_one(pool) .await?; - // Update member count on the realm + // Only increment count for new memberships sqlx::query("UPDATE realm.realms SET member_count = member_count + 1 WHERE id = $1") .bind(realm_id) .execute(pool) @@ -64,12 +82,30 @@ pub async fn create_membership( } /// Create a new membership using a connection (for RLS support). +/// This function is idempotent - if the membership already exists, it returns the existing id. pub async fn create_membership_conn( conn: &mut sqlx::PgConnection, user_id: Uuid, realm_id: Uuid, role: RealmRole, ) -> Result { + // Check if membership already exists + let existing: Option<(Uuid,)> = sqlx::query_as( + r#" + SELECT id FROM realm.memberships + WHERE user_id = $1 AND realm_id = $2 + "#, + ) + .bind(user_id) + .bind(realm_id) + .fetch_optional(&mut *conn) + .await?; + + if let Some((id,)) = existing { + return Ok(id); + } + + // Create new membership let (membership_id,): (Uuid,) = sqlx::query_as( r#" INSERT INTO realm.memberships (realm_id, user_id, role) @@ -83,7 +119,7 @@ pub async fn create_membership_conn( .fetch_one(&mut *conn) .await?; - // Update member count on the realm + // Only increment count for new memberships sqlx::query("UPDATE realm.realms SET member_count = member_count + 1 WHERE id = $1") .bind(realm_id) .execute(&mut *conn) diff --git a/crates/chattyness-user-ui/src/api/auth.rs b/crates/chattyness-user-ui/src/api/auth.rs index beb3757..e776ce0 100644 --- a/crates/chattyness-user-ui/src/api/auth.rs +++ b/crates/chattyness-user-ui/src/api/auth.rs @@ -368,6 +368,7 @@ pub async fn signup( /// Creates a real user account with the 'guest' tag. Guests are regular users /// with limited capabilities (no prop pickup, etc.) that can be reaped after 24 hours. pub async fn guest_login( + rls_conn: crate::auth::RlsConn, State(pool): State, session: Session, Json(req): Json, @@ -393,6 +394,17 @@ pub async fn guest_login( // Create guest user (no password) - trigger creates avatar automatically let user_id = users::create_guest_user(&pool, &guest_name).await?; + // Set RLS context to the new guest user for membership creation + rls_conn + .set_user_id(user_id) + .await + .map_err(|e| AppError::Internal(format!("Failed to set RLS context: {}", e)))?; + + // Create membership for the guest so their position can be persisted + let mut conn = rls_conn.acquire().await; + memberships::create_membership_conn(&mut *conn, user_id, realm.id, RealmRole::Member).await?; + drop(conn); + // Set up tower session (same as regular user login) session .insert(SESSION_USER_ID_KEY, user_id)