Skip to main content

max / makenotwork

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