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