Skip to main content

max / makenotwork

3.9 KB · 132 lines History Blame Raw
1 //! user upsert, suspension and signature writes
2
3 use super::{PgPool, Uuid};
4
5 /// Upsert a user from MNW account data. Creates the user if they don't exist,
6 /// updates username/display_name if they do.
7 #[tracing::instrument(skip_all)]
8 pub async fn upsert_user(
9 pool: &PgPool,
10 mnw_account_id: Uuid,
11 username: &str,
12 display_name: Option<&str>,
13 ) -> Result<(), sqlx::Error> {
14 let mut tx = pool.begin().await?;
15 vacate_stale_username(&mut tx, mnw_account_id, username).await?;
16 sqlx::query!(
17 "INSERT INTO users (mnw_account_id, username, display_name)
18 VALUES ($1, $2, $3)
19 ON CONFLICT (mnw_account_id) DO UPDATE
20 SET username = $2, display_name = $3, updated_at = now()",
21 mnw_account_id,
22 username,
23 display_name,
24 )
25 .execute(&mut *tx)
26 .await?;
27 tx.commit().await?;
28 Ok(())
29 }
30
31 /// Free `username` from any *other* user row that still holds it, so the caller's
32 /// upsert can claim it without tripping the `users_username_key` unique index.
33 ///
34 /// MNW is the source of truth for usernames and they are reusable there; a local
35 /// mirror row holding a name its MNW account no longer owns is stale and must
36 /// yield. Run this on the same transaction as the upsert (a single statement
37 /// can't, because a non-deferrable unique index is checked per-row and the
38 /// rename wouldn't yet be visible to the insert). The vacated row keeps its data
39 /// under a collision-proof `mnw_<account-id>` placeholder until that user's own
40 /// next login resets it.
41 async fn vacate_stale_username(
42 conn: &mut sqlx::PgConnection,
43 mnw_account_id: Uuid,
44 username: &str,
45 ) -> Result<(), sqlx::Error> {
46 sqlx::query!(
47 "UPDATE users
48 SET username = 'mnw_' || mnw_account_id::text, updated_at = now()
49 WHERE username = $1 AND mnw_account_id <> $2",
50 username,
51 mnw_account_id,
52 )
53 .execute(&mut *conn)
54 .await?;
55 Ok(())
56 }
57
58 /// Public wrapper over the private `vacate_stale_username` for the OAuth login upsert in the
59 /// web crate, which builds its INSERT with a runtime query.
60 pub async fn vacate_username_for_login(
61 conn: &mut sqlx::PgConnection,
62 mnw_account_id: Uuid,
63 username: &str,
64 ) -> Result<(), sqlx::Error> {
65 vacate_stale_username(conn, mnw_account_id, username).await
66 }
67
68 /// Suspend a user.
69 #[tracing::instrument(skip_all)]
70 pub async fn suspend_user<'e, E: sqlx::PgExecutor<'e>>(
71 executor: E,
72 user_id: Uuid,
73 reason: Option<&str>,
74 ) -> Result<(), sqlx::Error> {
75 sqlx::query!(
76 "UPDATE users SET suspended_at = now(), suspension_reason = $2 WHERE mnw_account_id = $1",
77 user_id,
78 reason,
79 )
80 .execute(executor)
81 .await?;
82 Ok(())
83 }
84
85 /// Unsuspend a user.
86 #[tracing::instrument(skip_all)]
87 pub async fn unsuspend_user<'e, E: sqlx::PgExecutor<'e>>(
88 executor: E,
89 user_id: Uuid,
90 ) -> Result<(), sqlx::Error> {
91 sqlx::query!(
92 "UPDATE users SET suspended_at = NULL, suspension_reason = NULL WHERE mnw_account_id = $1",
93 user_id,
94 )
95 .execute(executor)
96 .await?;
97 Ok(())
98 }
99
100 /// Clear a user's saved signature (markdown + rendered html).
101 #[tracing::instrument(skip_all)]
102 pub async fn clear_user_signature(pool: &PgPool, user_id: Uuid) -> Result<(), sqlx::Error> {
103 sqlx::query!(
104 "UPDATE users SET signature_markdown = NULL, signature_html = NULL \
105 WHERE mnw_account_id = $1",
106 user_id,
107 )
108 .execute(pool)
109 .await?;
110 Ok(())
111 }
112
113 /// Save a user's signature markdown and its pre-rendered html.
114 #[tracing::instrument(skip_all)]
115 pub async fn set_user_signature(
116 pool: &PgPool,
117 user_id: Uuid,
118 markdown: &str,
119 html: &str,
120 ) -> Result<(), sqlx::Error> {
121 sqlx::query!(
122 "UPDATE users SET signature_markdown = $2, signature_html = $3 \
123 WHERE mnw_account_id = $1",
124 user_id,
125 markdown,
126 html,
127 )
128 .execute(pool)
129 .await?;
130 Ok(())
131 }
132