users.rs

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