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