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