users.rs

  1use anyhow::Context as _;
  2use chrono::NaiveDateTime;
  3
  4use super::*;
  5
  6impl Database {
  7    /// Creates a new user.
  8    pub async fn create_user(
  9        &self,
 10        email_address: &str,
 11        name: Option<&str>,
 12        admin: bool,
 13        params: NewUserParams,
 14    ) -> Result<NewUserResult> {
 15        self.transaction(|tx| async {
 16            let tx = tx;
 17            let user = user::Entity::insert(user::ActiveModel {
 18                email_address: ActiveValue::set(Some(email_address.into())),
 19                name: ActiveValue::set(name.map(|s| s.into())),
 20                github_login: ActiveValue::set(params.github_login.clone()),
 21                github_user_id: ActiveValue::set(params.github_user_id),
 22                admin: ActiveValue::set(admin),
 23                metrics_id: ActiveValue::set(Uuid::new_v4()),
 24                ..Default::default()
 25            })
 26            .on_conflict(
 27                OnConflict::column(user::Column::GithubUserId)
 28                    .update_columns([
 29                        user::Column::Admin,
 30                        user::Column::EmailAddress,
 31                        user::Column::GithubLogin,
 32                    ])
 33                    .to_owned(),
 34            )
 35            .exec_with_returning(&*tx)
 36            .await?;
 37
 38            Ok(NewUserResult {
 39                user_id: user.id,
 40                metrics_id: user.metrics_id.to_string(),
 41                signup_device_id: None,
 42                inviting_user_id: None,
 43            })
 44        })
 45        .await
 46    }
 47
 48    /// Returns a user by ID. There are no access checks here, so this should only be used internally.
 49    pub async fn get_user_by_id(&self, id: UserId) -> Result<Option<user::Model>> {
 50        self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) })
 51            .await
 52    }
 53
 54    /// Returns all users by ID. There are no access checks here, so this should only be used internally.
 55    pub async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<user::Model>> {
 56        if ids.len() >= 10000_usize {
 57            return Err(anyhow!("too many users"))?;
 58        }
 59        self.transaction(|tx| async {
 60            let tx = tx;
 61            Ok(user::Entity::find()
 62                .filter(user::Column::Id.is_in(ids.iter().copied()))
 63                .all(&*tx)
 64                .await?)
 65        })
 66        .await
 67    }
 68
 69    /// Returns all users flagged as staff.
 70    pub async fn get_staff_users(&self) -> Result<Vec<user::Model>> {
 71        self.transaction(|tx| async {
 72            let tx = tx;
 73            Ok(user::Entity::find()
 74                .filter(user::Column::Admin.eq(true))
 75                .all(&*tx)
 76                .await?)
 77        })
 78        .await
 79    }
 80
 81    /// Returns a user by email address. There are no access checks here, so this should only be used internally.
 82    pub async fn get_user_by_email(&self, email: &str) -> Result<Option<User>> {
 83        self.transaction(|tx| async move {
 84            Ok(user::Entity::find()
 85                .filter(user::Column::EmailAddress.eq(email))
 86                .one(&*tx)
 87                .await?)
 88        })
 89        .await
 90    }
 91
 92    /// Returns a user by GitHub user ID. There are no access checks here, so this should only be used internally.
 93    pub async fn get_user_by_github_user_id(&self, github_user_id: i32) -> Result<Option<User>> {
 94        self.transaction(|tx| async move {
 95            Ok(user::Entity::find()
 96                .filter(user::Column::GithubUserId.eq(github_user_id))
 97                .one(&*tx)
 98                .await?)
 99        })
100        .await
101    }
102
103    /// Returns a user by GitHub login. There are no access checks here, so this should only be used internally.
104    pub async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
105        self.transaction(|tx| async move {
106            Ok(user::Entity::find()
107                .filter(user::Column::GithubLogin.eq(github_login))
108                .one(&*tx)
109                .await?)
110        })
111        .await
112    }
113
114    pub async fn update_or_create_user_by_github_account(
115        &self,
116        github_login: &str,
117        github_user_id: i32,
118        github_email: Option<&str>,
119        github_name: Option<&str>,
120        github_user_created_at: DateTimeUtc,
121        initial_channel_id: Option<ChannelId>,
122    ) -> Result<User> {
123        self.transaction(|tx| async move {
124            self.update_or_create_user_by_github_account_tx(
125                github_login,
126                github_user_id,
127                github_email,
128                github_name,
129                github_user_created_at.naive_utc(),
130                initial_channel_id,
131                &tx,
132            )
133            .await
134        })
135        .await
136    }
137
138    pub async fn update_or_create_user_by_github_account_tx(
139        &self,
140        github_login: &str,
141        github_user_id: i32,
142        github_email: Option<&str>,
143        github_name: Option<&str>,
144        github_user_created_at: NaiveDateTime,
145        initial_channel_id: Option<ChannelId>,
146        tx: &DatabaseTransaction,
147    ) -> Result<User> {
148        if let Some(existing_user) = self
149            .get_user_by_github_user_id_or_github_login(github_user_id, github_login, tx)
150            .await?
151        {
152            let mut existing_user = existing_user.into_active_model();
153            existing_user.github_login = ActiveValue::set(github_login.into());
154            existing_user.github_user_created_at = ActiveValue::set(Some(github_user_created_at));
155
156            if let Some(github_email) = github_email {
157                existing_user.email_address = ActiveValue::set(Some(github_email.into()));
158            }
159
160            if let Some(github_name) = github_name {
161                existing_user.name = ActiveValue::set(Some(github_name.into()));
162            }
163
164            Ok(existing_user.update(tx).await?)
165        } else {
166            let user = user::Entity::insert(user::ActiveModel {
167                email_address: ActiveValue::set(github_email.map(|email| email.into())),
168                name: ActiveValue::set(github_name.map(|name| name.into())),
169                github_login: ActiveValue::set(github_login.into()),
170                github_user_id: ActiveValue::set(github_user_id),
171                github_user_created_at: ActiveValue::set(Some(github_user_created_at)),
172                admin: ActiveValue::set(false),
173                invite_count: ActiveValue::set(0),
174                invite_code: ActiveValue::set(None),
175                metrics_id: ActiveValue::set(Uuid::new_v4()),
176                ..Default::default()
177            })
178            .exec_with_returning(tx)
179            .await?;
180            if let Some(channel_id) = initial_channel_id {
181                channel_member::Entity::insert(channel_member::ActiveModel {
182                    id: ActiveValue::NotSet,
183                    channel_id: ActiveValue::Set(channel_id),
184                    user_id: ActiveValue::Set(user.id),
185                    accepted: ActiveValue::Set(true),
186                    role: ActiveValue::Set(ChannelRole::Guest),
187                })
188                .exec(tx)
189                .await?;
190            }
191            Ok(user)
192        }
193    }
194
195    /// Tries to retrieve a user, first by their GitHub user ID, and then by their GitHub login.
196    ///
197    /// Returns `None` if a user is not found with this GitHub user ID or GitHub login.
198    pub async fn get_user_by_github_user_id_or_github_login(
199        &self,
200        github_user_id: i32,
201        github_login: &str,
202        tx: &DatabaseTransaction,
203    ) -> Result<Option<User>> {
204        if let Some(user_by_github_user_id) = user::Entity::find()
205            .filter(user::Column::GithubUserId.eq(github_user_id))
206            .one(tx)
207            .await?
208        {
209            return Ok(Some(user_by_github_user_id));
210        }
211
212        if let Some(user_by_github_login) = user::Entity::find()
213            .filter(user::Column::GithubLogin.eq(github_login))
214            .one(tx)
215            .await?
216        {
217            return Ok(Some(user_by_github_login));
218        }
219
220        Ok(None)
221    }
222
223    /// get_all_users returns the next page of users. To get more call again with
224    /// the same limit and the page incremented by 1.
225    pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>> {
226        self.transaction(|tx| async move {
227            Ok(user::Entity::find()
228                .order_by_asc(user::Column::GithubLogin)
229                .limit(limit as u64)
230                .offset(page as u64 * limit as u64)
231                .all(&*tx)
232                .await?)
233        })
234        .await
235    }
236
237    /// Returns the metrics id for the user.
238    pub async fn get_user_metrics_id(&self, id: UserId) -> Result<String> {
239        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
240        enum QueryAs {
241            MetricsId,
242        }
243
244        self.transaction(|tx| async move {
245            let metrics_id: Uuid = user::Entity::find_by_id(id)
246                .select_only()
247                .column(user::Column::MetricsId)
248                .into_values::<_, QueryAs>()
249                .one(&*tx)
250                .await?
251                .context("could not find user")?;
252            Ok(metrics_id.to_string())
253        })
254        .await
255    }
256
257    /// Sets "connected_once" on the user for analytics.
258    pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
259        self.transaction(|tx| async move {
260            user::Entity::update_many()
261                .filter(user::Column::Id.eq(id))
262                .set(user::ActiveModel {
263                    connected_once: ActiveValue::set(connected_once),
264                    ..Default::default()
265                })
266                .exec(&*tx)
267                .await?;
268            Ok(())
269        })
270        .await
271    }
272
273    /// Sets "accepted_tos_at" on the user to the given timestamp.
274    pub async fn set_user_accepted_tos_at(
275        &self,
276        id: UserId,
277        accepted_tos_at: Option<DateTime>,
278    ) -> Result<()> {
279        self.transaction(|tx| async move {
280            user::Entity::update_many()
281                .filter(user::Column::Id.eq(id))
282                .set(user::ActiveModel {
283                    accepted_tos_at: ActiveValue::set(accepted_tos_at),
284                    ..Default::default()
285                })
286                .exec(&*tx)
287                .await?;
288            Ok(())
289        })
290        .await
291    }
292
293    /// hard delete the user.
294    pub async fn destroy_user(&self, id: UserId) -> Result<()> {
295        self.transaction(|tx| async move {
296            access_token::Entity::delete_many()
297                .filter(access_token::Column::UserId.eq(id))
298                .exec(&*tx)
299                .await?;
300            user::Entity::delete_by_id(id).exec(&*tx).await?;
301            Ok(())
302        })
303        .await
304    }
305
306    /// Find users where github_login ILIKE name_query.
307    pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result<Vec<User>> {
308        self.transaction(|tx| async {
309            let tx = tx;
310            let like_string = Self::fuzzy_like_string(name_query);
311            let query = "
312                SELECT users.*
313                FROM users
314                WHERE github_login ILIKE $1
315                ORDER BY github_login <-> $2
316                LIMIT $3
317            ";
318
319            Ok(user::Entity::find()
320                .from_raw_sql(Statement::from_sql_and_values(
321                    self.pool.get_database_backend(),
322                    query,
323                    vec![like_string.into(), name_query.into(), limit.into()],
324                ))
325                .all(&*tx)
326                .await?)
327        })
328        .await
329    }
330
331    /// fuzzy_like_string creates a string for matching in-order using fuzzy_search_users.
332    /// e.g. "cir" would become "%c%i%r%"
333    pub fn fuzzy_like_string(string: &str) -> String {
334        let mut result = String::with_capacity(string.len() * 2 + 1);
335        for c in string.chars() {
336            if c.is_alphanumeric() {
337                result.push('%');
338                result.push(c);
339            }
340        }
341        result.push('%');
342        result
343    }
344
345    /// Returns all feature flags.
346    pub async fn list_feature_flags(&self) -> Result<Vec<feature_flag::Model>> {
347        self.transaction(|tx| async move { Ok(feature_flag::Entity::find().all(&*tx).await?) })
348            .await
349    }
350
351    /// Creates a new feature flag.
352    pub async fn create_user_flag(&self, flag: &str, enabled_for_all: bool) -> Result<FlagId> {
353        self.transaction(|tx| async move {
354            let flag = feature_flag::Entity::insert(feature_flag::ActiveModel {
355                flag: ActiveValue::set(flag.to_string()),
356                enabled_for_all: ActiveValue::set(enabled_for_all),
357                ..Default::default()
358            })
359            .exec(&*tx)
360            .await?
361            .last_insert_id;
362
363            Ok(flag)
364        })
365        .await
366    }
367
368    /// Add the given user to the feature flag
369    pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> {
370        self.transaction(|tx| async move {
371            user_feature::Entity::insert(user_feature::ActiveModel {
372                user_id: ActiveValue::set(user),
373                feature_id: ActiveValue::set(flag),
374            })
375            .exec(&*tx)
376            .await?;
377
378            Ok(())
379        })
380        .await
381    }
382
383    /// Returns the active flags for the user.
384    pub async fn get_user_flags(&self, user: UserId) -> Result<Vec<String>> {
385        self.transaction(|tx| async move {
386            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
387            enum QueryAs {
388                Flag,
389            }
390
391            let flags_enabled_for_all = feature_flag::Entity::find()
392                .filter(feature_flag::Column::EnabledForAll.eq(true))
393                .select_only()
394                .column(feature_flag::Column::Flag)
395                .into_values::<_, QueryAs>()
396                .all(&*tx)
397                .await?;
398
399            let flags_enabled_for_user = user::Model {
400                id: user,
401                ..Default::default()
402            }
403            .find_linked(user::UserFlags)
404            .select_only()
405            .column(feature_flag::Column::Flag)
406            .into_values::<_, QueryAs>()
407            .all(&*tx)
408            .await?;
409
410            let mut all_flags = HashSet::from_iter(flags_enabled_for_all);
411            all_flags.extend(flags_enabled_for_user);
412
413            Ok(all_flags.into_iter().collect())
414        })
415        .await
416    }
417
418    pub async fn get_users_missing_github_user_created_at(&self) -> Result<Vec<user::Model>> {
419        self.transaction(|tx| async move {
420            Ok(user::Entity::find()
421                .filter(user::Column::GithubUserCreatedAt.is_null())
422                .all(&*tx)
423                .await?)
424        })
425        .await
426    }
427}