signups.rs

  1use super::*;
  2use hyper::StatusCode;
  3
  4impl Database {
  5    pub async fn create_invite_from_code(
  6        &self,
  7        code: &str,
  8        email_address: &str,
  9        device_id: Option<&str>,
 10        added_to_mailing_list: bool,
 11    ) -> Result<Invite> {
 12        self.transaction(|tx| async move {
 13            let existing_user = user::Entity::find()
 14                .filter(user::Column::EmailAddress.eq(email_address))
 15                .one(&*tx)
 16                .await?;
 17
 18            if existing_user.is_some() {
 19                Err(anyhow!("email address is already in use"))?;
 20            }
 21
 22            let inviting_user_with_invites = match user::Entity::find()
 23                .filter(
 24                    user::Column::InviteCode
 25                        .eq(code)
 26                        .and(user::Column::InviteCount.gt(0)),
 27                )
 28                .one(&*tx)
 29                .await?
 30            {
 31                Some(inviting_user) => inviting_user,
 32                None => {
 33                    return Err(Error::Http(
 34                        StatusCode::UNAUTHORIZED,
 35                        "unable to find an invite code with invites remaining".to_string(),
 36                    ))?
 37                }
 38            };
 39            user::Entity::update_many()
 40                .filter(
 41                    user::Column::Id
 42                        .eq(inviting_user_with_invites.id)
 43                        .and(user::Column::InviteCount.gt(0)),
 44                )
 45                .col_expr(
 46                    user::Column::InviteCount,
 47                    Expr::col(user::Column::InviteCount).sub(1),
 48                )
 49                .exec(&*tx)
 50                .await?;
 51
 52            let signup = signup::Entity::insert(signup::ActiveModel {
 53                email_address: ActiveValue::set(email_address.into()),
 54                email_confirmation_code: ActiveValue::set(random_email_confirmation_code()),
 55                email_confirmation_sent: ActiveValue::set(false),
 56                inviting_user_id: ActiveValue::set(Some(inviting_user_with_invites.id)),
 57                platform_linux: ActiveValue::set(false),
 58                platform_mac: ActiveValue::set(false),
 59                platform_windows: ActiveValue::set(false),
 60                platform_unknown: ActiveValue::set(true),
 61                device_id: ActiveValue::set(device_id.map(|device_id| device_id.into())),
 62                added_to_mailing_list: ActiveValue::set(added_to_mailing_list),
 63                ..Default::default()
 64            })
 65            .on_conflict(
 66                OnConflict::column(signup::Column::EmailAddress)
 67                    .update_column(signup::Column::InvitingUserId)
 68                    .to_owned(),
 69            )
 70            .exec_with_returning(&*tx)
 71            .await?;
 72
 73            Ok(Invite {
 74                email_address: signup.email_address,
 75                email_confirmation_code: signup.email_confirmation_code,
 76            })
 77        })
 78        .await
 79    }
 80
 81    pub async fn create_user_from_invite(
 82        &self,
 83        invite: &Invite,
 84        user: NewUserParams,
 85    ) -> Result<Option<NewUserResult>> {
 86        self.transaction(|tx| async {
 87            let tx = tx;
 88            let signup = signup::Entity::find()
 89                .filter(
 90                    signup::Column::EmailAddress
 91                        .eq(invite.email_address.as_str())
 92                        .and(
 93                            signup::Column::EmailConfirmationCode
 94                                .eq(invite.email_confirmation_code.as_str()),
 95                        ),
 96                )
 97                .one(&*tx)
 98                .await?
 99                .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "no such invite".to_string()))?;
100
101            if signup.user_id.is_some() {
102                return Ok(None);
103            }
104
105            let user = user::Entity::insert(user::ActiveModel {
106                email_address: ActiveValue::set(Some(invite.email_address.clone())),
107                github_login: ActiveValue::set(user.github_login.clone()),
108                github_user_id: ActiveValue::set(Some(user.github_user_id)),
109                admin: ActiveValue::set(false),
110                invite_count: ActiveValue::set(user.invite_count),
111                invite_code: ActiveValue::set(Some(random_invite_code())),
112                metrics_id: ActiveValue::set(Uuid::new_v4()),
113                ..Default::default()
114            })
115            .on_conflict(
116                OnConflict::column(user::Column::GithubLogin)
117                    .update_columns([
118                        user::Column::EmailAddress,
119                        user::Column::GithubUserId,
120                        user::Column::Admin,
121                    ])
122                    .to_owned(),
123            )
124            .exec_with_returning(&*tx)
125            .await?;
126
127            let mut signup = signup.into_active_model();
128            signup.user_id = ActiveValue::set(Some(user.id));
129            let signup = signup.update(&*tx).await?;
130
131            if let Some(inviting_user_id) = signup.inviting_user_id {
132                let (user_id_a, user_id_b, a_to_b) = if inviting_user_id < user.id {
133                    (inviting_user_id, user.id, true)
134                } else {
135                    (user.id, inviting_user_id, false)
136                };
137
138                contact::Entity::insert(contact::ActiveModel {
139                    user_id_a: ActiveValue::set(user_id_a),
140                    user_id_b: ActiveValue::set(user_id_b),
141                    a_to_b: ActiveValue::set(a_to_b),
142                    should_notify: ActiveValue::set(true),
143                    accepted: ActiveValue::set(true),
144                    ..Default::default()
145                })
146                .on_conflict(OnConflict::new().do_nothing().to_owned())
147                .exec_without_returning(&*tx)
148                .await?;
149            }
150
151            Ok(Some(NewUserResult {
152                user_id: user.id,
153                metrics_id: user.metrics_id.to_string(),
154                inviting_user_id: signup.inviting_user_id,
155                signup_device_id: signup.device_id,
156            }))
157        })
158        .await
159    }
160
161    pub async fn set_invite_count_for_user(&self, id: UserId, count: i32) -> Result<()> {
162        self.transaction(|tx| async move {
163            if count > 0 {
164                user::Entity::update_many()
165                    .filter(
166                        user::Column::Id
167                            .eq(id)
168                            .and(user::Column::InviteCode.is_null()),
169                    )
170                    .set(user::ActiveModel {
171                        invite_code: ActiveValue::set(Some(random_invite_code())),
172                        ..Default::default()
173                    })
174                    .exec(&*tx)
175                    .await?;
176            }
177
178            user::Entity::update_many()
179                .filter(user::Column::Id.eq(id))
180                .set(user::ActiveModel {
181                    invite_count: ActiveValue::set(count),
182                    ..Default::default()
183                })
184                .exec(&*tx)
185                .await?;
186            Ok(())
187        })
188        .await
189    }
190
191    pub async fn get_invite_code_for_user(&self, id: UserId) -> Result<Option<(String, i32)>> {
192        self.transaction(|tx| async move {
193            match user::Entity::find_by_id(id).one(&*tx).await? {
194                Some(user) if user.invite_code.is_some() => {
195                    Ok(Some((user.invite_code.unwrap(), user.invite_count)))
196                }
197                _ => Ok(None),
198            }
199        })
200        .await
201    }
202
203    pub async fn get_user_for_invite_code(&self, code: &str) -> Result<User> {
204        self.transaction(|tx| async move {
205            user::Entity::find()
206                .filter(user::Column::InviteCode.eq(code))
207                .one(&*tx)
208                .await?
209                .ok_or_else(|| {
210                    Error::Http(
211                        StatusCode::NOT_FOUND,
212                        "that invite code does not exist".to_string(),
213                    )
214                })
215        })
216        .await
217    }
218
219    pub async fn create_signup(&self, signup: &NewSignup) -> Result<()> {
220        self.transaction(|tx| async move {
221            signup::Entity::insert(signup::ActiveModel {
222                email_address: ActiveValue::set(signup.email_address.clone()),
223                email_confirmation_code: ActiveValue::set(random_email_confirmation_code()),
224                email_confirmation_sent: ActiveValue::set(false),
225                platform_mac: ActiveValue::set(signup.platform_mac),
226                platform_windows: ActiveValue::set(signup.platform_windows),
227                platform_linux: ActiveValue::set(signup.platform_linux),
228                platform_unknown: ActiveValue::set(false),
229                editor_features: ActiveValue::set(Some(signup.editor_features.clone())),
230                programming_languages: ActiveValue::set(Some(signup.programming_languages.clone())),
231                device_id: ActiveValue::set(signup.device_id.clone()),
232                added_to_mailing_list: ActiveValue::set(signup.added_to_mailing_list),
233                ..Default::default()
234            })
235            .on_conflict(
236                OnConflict::column(signup::Column::EmailAddress)
237                    .update_columns([
238                        signup::Column::PlatformMac,
239                        signup::Column::PlatformWindows,
240                        signup::Column::PlatformLinux,
241                        signup::Column::EditorFeatures,
242                        signup::Column::ProgrammingLanguages,
243                        signup::Column::DeviceId,
244                        signup::Column::AddedToMailingList,
245                    ])
246                    .to_owned(),
247            )
248            .exec(&*tx)
249            .await?;
250            Ok(())
251        })
252        .await
253    }
254
255    pub async fn get_signup(&self, email_address: &str) -> Result<signup::Model> {
256        self.transaction(|tx| async move {
257            let signup = signup::Entity::find()
258                .filter(signup::Column::EmailAddress.eq(email_address))
259                .one(&*tx)
260                .await?
261                .ok_or_else(|| {
262                    anyhow!("signup with email address {} doesn't exist", email_address)
263                })?;
264
265            Ok(signup)
266        })
267        .await
268    }
269
270    pub async fn get_waitlist_summary(&self) -> Result<WaitlistSummary> {
271        self.transaction(|tx| async move {
272            let query = "
273                SELECT
274                    COUNT(*) as count,
275                    COALESCE(SUM(CASE WHEN platform_linux THEN 1 ELSE 0 END), 0) as linux_count,
276                    COALESCE(SUM(CASE WHEN platform_mac THEN 1 ELSE 0 END), 0) as mac_count,
277                    COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count,
278                    COALESCE(SUM(CASE WHEN platform_unknown THEN 1 ELSE 0 END), 0) as unknown_count
279                FROM (
280                    SELECT *
281                    FROM signups
282                    WHERE
283                        NOT email_confirmation_sent
284                ) AS unsent
285            ";
286            Ok(
287                WaitlistSummary::find_by_statement(Statement::from_sql_and_values(
288                    self.pool.get_database_backend(),
289                    query.into(),
290                    vec![],
291                ))
292                .one(&*tx)
293                .await?
294                .ok_or_else(|| anyhow!("invalid result"))?,
295            )
296        })
297        .await
298    }
299
300    pub async fn record_sent_invites(&self, invites: &[Invite]) -> Result<()> {
301        let emails = invites
302            .iter()
303            .map(|s| s.email_address.as_str())
304            .collect::<Vec<_>>();
305        self.transaction(|tx| async {
306            let tx = tx;
307            signup::Entity::update_many()
308                .filter(signup::Column::EmailAddress.is_in(emails.iter().copied()))
309                .set(signup::ActiveModel {
310                    email_confirmation_sent: ActiveValue::set(true),
311                    ..Default::default()
312                })
313                .exec(&*tx)
314                .await?;
315            Ok(())
316        })
317        .await
318    }
319
320    pub async fn get_unsent_invites(&self, count: usize) -> Result<Vec<Invite>> {
321        self.transaction(|tx| async move {
322            Ok(signup::Entity::find()
323                .select_only()
324                .column(signup::Column::EmailAddress)
325                .column(signup::Column::EmailConfirmationCode)
326                .filter(
327                    signup::Column::EmailConfirmationSent.eq(false).and(
328                        signup::Column::PlatformMac
329                            .eq(true)
330                            .or(signup::Column::PlatformUnknown.eq(true)),
331                    ),
332                )
333                .order_by_asc(signup::Column::CreatedAt)
334                .limit(count as u64)
335                .into_model()
336                .all(&*tx)
337                .await?)
338        })
339        .await
340    }
341}
342
343fn random_invite_code() -> String {
344    nanoid::nanoid!(16)
345}
346
347fn random_email_confirmation_code() -> String {
348    nanoid::nanoid!(64)
349}