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}