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_column(user::Column::GithubLogin)
 24                    .to_owned(),
 25            )
 26            .exec_with_returning(&*tx)
 27            .await?;
 28
 29            Ok(NewUserResult {
 30                user_id: user.id,
 31                metrics_id: user.metrics_id.to_string(),
 32                signup_device_id: None,
 33                inviting_user_id: None,
 34            })
 35        })
 36        .await
 37    }
 38
 39    /// Returns a user by ID. There are no access checks here, so this should only be used internally.
 40    pub async fn get_user_by_id(&self, id: UserId) -> Result<Option<user::Model>> {
 41        self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) })
 42            .await
 43    }
 44
 45    /// Returns all users by ID. There are no access checks here, so this should only be used internally.
 46    pub async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<user::Model>> {
 47        self.transaction(|tx| async {
 48            let tx = tx;
 49            Ok(user::Entity::find()
 50                .filter(user::Column::Id.is_in(ids.iter().copied()))
 51                .all(&*tx)
 52                .await?)
 53        })
 54        .await
 55    }
 56
 57    /// Returns a user by GitHub login. There are no access checks here, so this should only be used internally.
 58    pub async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
 59        self.transaction(|tx| async move {
 60            Ok(user::Entity::find()
 61                .filter(user::Column::GithubLogin.eq(github_login))
 62                .one(&*tx)
 63                .await?)
 64        })
 65        .await
 66    }
 67
 68    pub async fn get_or_create_user_by_github_account(
 69        &self,
 70        github_login: &str,
 71        github_user_id: Option<i32>,
 72        github_email: Option<&str>,
 73    ) -> Result<Option<User>> {
 74        self.transaction(|tx| async move {
 75            let tx = &*tx;
 76            if let Some(github_user_id) = github_user_id {
 77                if let Some(user_by_github_user_id) = user::Entity::find()
 78                    .filter(user::Column::GithubUserId.eq(github_user_id))
 79                    .one(tx)
 80                    .await?
 81                {
 82                    let mut user_by_github_user_id = user_by_github_user_id.into_active_model();
 83                    user_by_github_user_id.github_login = ActiveValue::set(github_login.into());
 84                    Ok(Some(user_by_github_user_id.update(tx).await?))
 85                } else if let Some(user_by_github_login) = user::Entity::find()
 86                    .filter(user::Column::GithubLogin.eq(github_login))
 87                    .one(tx)
 88                    .await?
 89                {
 90                    let mut user_by_github_login = user_by_github_login.into_active_model();
 91                    user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id));
 92                    Ok(Some(user_by_github_login.update(tx).await?))
 93                } else {
 94                    let user = user::Entity::insert(user::ActiveModel {
 95                        email_address: ActiveValue::set(github_email.map(|email| email.into())),
 96                        github_login: ActiveValue::set(github_login.into()),
 97                        github_user_id: ActiveValue::set(Some(github_user_id)),
 98                        admin: ActiveValue::set(false),
 99                        invite_count: ActiveValue::set(0),
100                        invite_code: ActiveValue::set(None),
101                        metrics_id: ActiveValue::set(Uuid::new_v4()),
102                        ..Default::default()
103                    })
104                    .exec_with_returning(&*tx)
105                    .await?;
106                    Ok(Some(user))
107                }
108            } else {
109                Ok(user::Entity::find()
110                    .filter(user::Column::GithubLogin.eq(github_login))
111                    .one(tx)
112                    .await?)
113            }
114        })
115        .await
116    }
117
118    /// get_all_users returns the next page of users. To get more call again with
119    /// the same limit and the page incremented by 1.
120    pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>> {
121        self.transaction(|tx| async move {
122            Ok(user::Entity::find()
123                .order_by_asc(user::Column::GithubLogin)
124                .limit(limit as u64)
125                .offset(page as u64 * limit as u64)
126                .all(&*tx)
127                .await?)
128        })
129        .await
130    }
131
132    /// Returns the metrics id for the user.
133    pub async fn get_user_metrics_id(&self, id: UserId) -> Result<String> {
134        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
135        enum QueryAs {
136            MetricsId,
137        }
138
139        self.transaction(|tx| async move {
140            let metrics_id: Uuid = user::Entity::find_by_id(id)
141                .select_only()
142                .column(user::Column::MetricsId)
143                .into_values::<_, QueryAs>()
144                .one(&*tx)
145                .await?
146                .ok_or_else(|| anyhow!("could not find user"))?;
147            Ok(metrics_id.to_string())
148        })
149        .await
150    }
151
152    /// Set "connected_once" on the user for analytics.
153    pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
154        self.transaction(|tx| async move {
155            user::Entity::update_many()
156                .filter(user::Column::Id.eq(id))
157                .set(user::ActiveModel {
158                    connected_once: ActiveValue::set(connected_once),
159                    ..Default::default()
160                })
161                .exec(&*tx)
162                .await?;
163            Ok(())
164        })
165        .await
166    }
167
168    /// hard delete the user.
169    pub async fn destroy_user(&self, id: UserId) -> Result<()> {
170        self.transaction(|tx| async move {
171            access_token::Entity::delete_many()
172                .filter(access_token::Column::UserId.eq(id))
173                .exec(&*tx)
174                .await?;
175            user::Entity::delete_by_id(id).exec(&*tx).await?;
176            Ok(())
177        })
178        .await
179    }
180
181    /// Find users where github_login ILIKE name_query.
182    pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result<Vec<User>> {
183        self.transaction(|tx| async {
184            let tx = tx;
185            let like_string = Self::fuzzy_like_string(name_query);
186            let query = "
187                SELECT users.*
188                FROM users
189                WHERE github_login ILIKE $1
190                ORDER BY github_login <-> $2
191                LIMIT $3
192            ";
193
194            Ok(user::Entity::find()
195                .from_raw_sql(Statement::from_sql_and_values(
196                    self.pool.get_database_backend(),
197                    query,
198                    vec![like_string.into(), name_query.into(), limit.into()],
199                ))
200                .all(&*tx)
201                .await?)
202        })
203        .await
204    }
205
206    /// fuzzy_like_string creates a string for matching in-order using fuzzy_search_users.
207    /// e.g. "cir" would become "%c%i%r%"
208    pub fn fuzzy_like_string(string: &str) -> String {
209        let mut result = String::with_capacity(string.len() * 2 + 1);
210        for c in string.chars() {
211            if c.is_alphanumeric() {
212                result.push('%');
213                result.push(c);
214            }
215        }
216        result.push('%');
217        result
218    }
219
220    /// Creates a new feature flag.
221    pub async fn create_user_flag(&self, flag: &str) -> Result<FlagId> {
222        self.transaction(|tx| async move {
223            let flag = feature_flag::Entity::insert(feature_flag::ActiveModel {
224                flag: ActiveValue::set(flag.to_string()),
225                ..Default::default()
226            })
227            .exec(&*tx)
228            .await?
229            .last_insert_id;
230
231            Ok(flag)
232        })
233        .await
234    }
235
236    /// Add the given user to the feature flag
237    pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> {
238        self.transaction(|tx| async move {
239            user_feature::Entity::insert(user_feature::ActiveModel {
240                user_id: ActiveValue::set(user),
241                feature_id: ActiveValue::set(flag),
242            })
243            .exec(&*tx)
244            .await?;
245
246            Ok(())
247        })
248        .await
249    }
250
251    /// Return the active flags for the user.
252    pub async fn get_user_flags(&self, user: UserId) -> Result<Vec<String>> {
253        self.transaction(|tx| async move {
254            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
255            enum QueryAs {
256                Flag,
257            }
258
259            let flags = user::Model {
260                id: user,
261                ..Default::default()
262            }
263            .find_linked(user::UserFlags)
264            .select_only()
265            .column(feature_flag::Column::Flag)
266            .into_values::<_, QueryAs>()
267            .all(&*tx)
268            .await?;
269
270            Ok(flags)
271        })
272        .await
273    }
274}