users.rs

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