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