1use chrono::NaiveDateTime;
2
3use super::*;
4
5impl Database {
6 /// Creates a new user.
7 pub async fn create_user(
8 &self,
9 email_address: &str,
10 name: Option<&str>,
11 admin: bool,
12 params: NewUserParams,
13 ) -> Result<NewUserResult> {
14 self.transaction(|tx| async {
15 let tx = tx;
16 let user = user::Entity::insert(user::ActiveModel {
17 email_address: ActiveValue::set(Some(email_address.into())),
18 name: ActiveValue::set(name.map(|s| s.into())),
19 github_login: ActiveValue::set(params.github_login.clone()),
20 github_user_id: ActiveValue::set(params.github_user_id),
21 admin: ActiveValue::set(admin),
22 metrics_id: ActiveValue::set(Uuid::new_v4()),
23 ..Default::default()
24 })
25 .on_conflict(
26 OnConflict::column(user::Column::GithubUserId)
27 .update_columns([
28 user::Column::Admin,
29 user::Column::EmailAddress,
30 user::Column::GithubLogin,
31 ])
32 .to_owned(),
33 )
34 .exec_with_returning(&*tx)
35 .await?;
36
37 Ok(NewUserResult {
38 user_id: user.id,
39 metrics_id: user.metrics_id.to_string(),
40 signup_device_id: None,
41 inviting_user_id: None,
42 })
43 })
44 .await
45 }
46
47 /// Returns a user by ID. There are no access checks here, so this should only be used internally.
48 pub async fn get_user_by_id(&self, id: UserId) -> Result<Option<user::Model>> {
49 self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) })
50 .await
51 }
52
53 /// Returns all users by ID. There are no access checks here, so this should only be used internally.
54 pub async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<user::Model>> {
55 if ids.len() >= 10000_usize {
56 return Err(anyhow!("too many users"))?;
57 }
58 self.transaction(|tx| async {
59 let tx = tx;
60 Ok(user::Entity::find()
61 .filter(user::Column::Id.is_in(ids.iter().copied()))
62 .all(&*tx)
63 .await?)
64 })
65 .await
66 }
67
68 /// Returns a user by email address. There are no access checks here, so this should only be used internally.
69 pub async fn get_user_by_email(&self, email: &str) -> Result<Option<User>> {
70 self.transaction(|tx| async move {
71 Ok(user::Entity::find()
72 .filter(user::Column::EmailAddress.eq(email))
73 .one(&*tx)
74 .await?)
75 })
76 .await
77 }
78
79 /// Returns a user by GitHub user ID. There are no access checks here, so this should only be used internally.
80 pub async fn get_user_by_github_user_id(&self, github_user_id: i32) -> Result<Option<User>> {
81 self.transaction(|tx| async move {
82 Ok(user::Entity::find()
83 .filter(user::Column::GithubUserId.eq(github_user_id))
84 .one(&*tx)
85 .await?)
86 })
87 .await
88 }
89
90 /// Returns a user by GitHub login. There are no access checks here, so this should only be used internally.
91 pub async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
92 self.transaction(|tx| async move {
93 Ok(user::Entity::find()
94 .filter(user::Column::GithubLogin.eq(github_login))
95 .one(&*tx)
96 .await?)
97 })
98 .await
99 }
100
101 pub async fn get_or_create_user_by_github_account(
102 &self,
103 github_login: &str,
104 github_user_id: i32,
105 github_email: Option<&str>,
106 github_name: Option<&str>,
107 github_user_created_at: DateTimeUtc,
108 initial_channel_id: Option<ChannelId>,
109 ) -> Result<User> {
110 self.transaction(|tx| async move {
111 self.get_or_create_user_by_github_account_tx(
112 github_login,
113 github_user_id,
114 github_email,
115 github_name,
116 github_user_created_at.naive_utc(),
117 initial_channel_id,
118 &tx,
119 )
120 .await
121 })
122 .await
123 }
124
125 pub async fn get_or_create_user_by_github_account_tx(
126 &self,
127 github_login: &str,
128 github_user_id: i32,
129 github_email: Option<&str>,
130 github_name: Option<&str>,
131 github_user_created_at: NaiveDateTime,
132 initial_channel_id: Option<ChannelId>,
133 tx: &DatabaseTransaction,
134 ) -> Result<User> {
135 if let Some(existing_user) = self
136 .get_user_by_github_user_id_or_github_login(github_user_id, github_login, tx)
137 .await?
138 {
139 let mut existing_user = existing_user.into_active_model();
140 existing_user.github_login = ActiveValue::set(github_login.into());
141 existing_user.github_user_created_at = ActiveValue::set(Some(github_user_created_at));
142
143 if let Some(github_email) = github_email {
144 existing_user.email_address = ActiveValue::set(Some(github_email.into()));
145 }
146
147 if let Some(github_name) = github_name {
148 existing_user.name = ActiveValue::set(Some(github_name.into()));
149 }
150
151 Ok(existing_user.update(tx).await?)
152 } else {
153 let user = user::Entity::insert(user::ActiveModel {
154 email_address: ActiveValue::set(github_email.map(|email| email.into())),
155 name: ActiveValue::set(github_name.map(|name| name.into())),
156 github_login: ActiveValue::set(github_login.into()),
157 github_user_id: ActiveValue::set(github_user_id),
158 github_user_created_at: ActiveValue::set(Some(github_user_created_at)),
159 admin: ActiveValue::set(false),
160 invite_count: ActiveValue::set(0),
161 invite_code: ActiveValue::set(None),
162 metrics_id: ActiveValue::set(Uuid::new_v4()),
163 ..Default::default()
164 })
165 .exec_with_returning(tx)
166 .await?;
167 if let Some(channel_id) = initial_channel_id {
168 channel_member::Entity::insert(channel_member::ActiveModel {
169 id: ActiveValue::NotSet,
170 channel_id: ActiveValue::Set(channel_id),
171 user_id: ActiveValue::Set(user.id),
172 accepted: ActiveValue::Set(true),
173 role: ActiveValue::Set(ChannelRole::Guest),
174 })
175 .exec(tx)
176 .await?;
177 }
178 Ok(user)
179 }
180 }
181
182 /// Tries to retrieve a user, first by their GitHub user ID, and then by their GitHub login.
183 ///
184 /// Returns `None` if a user is not found with this GitHub user ID or GitHub login.
185 pub async fn get_user_by_github_user_id_or_github_login(
186 &self,
187 github_user_id: i32,
188 github_login: &str,
189 tx: &DatabaseTransaction,
190 ) -> Result<Option<User>> {
191 if let Some(user_by_github_user_id) = user::Entity::find()
192 .filter(user::Column::GithubUserId.eq(github_user_id))
193 .one(tx)
194 .await?
195 {
196 return Ok(Some(user_by_github_user_id));
197 }
198
199 if let Some(user_by_github_login) = user::Entity::find()
200 .filter(user::Column::GithubLogin.eq(github_login))
201 .one(tx)
202 .await?
203 {
204 return Ok(Some(user_by_github_login));
205 }
206
207 Ok(None)
208 }
209
210 /// get_all_users returns the next page of users. To get more call again with
211 /// the same limit and the page incremented by 1.
212 pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>> {
213 self.transaction(|tx| async move {
214 Ok(user::Entity::find()
215 .order_by_asc(user::Column::GithubLogin)
216 .limit(limit as u64)
217 .offset(page as u64 * limit as u64)
218 .all(&*tx)
219 .await?)
220 })
221 .await
222 }
223
224 /// Returns the metrics id for the user.
225 pub async fn get_user_metrics_id(&self, id: UserId) -> Result<String> {
226 #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
227 enum QueryAs {
228 MetricsId,
229 }
230
231 self.transaction(|tx| async move {
232 let metrics_id: Uuid = user::Entity::find_by_id(id)
233 .select_only()
234 .column(user::Column::MetricsId)
235 .into_values::<_, QueryAs>()
236 .one(&*tx)
237 .await?
238 .ok_or_else(|| anyhow!("could not find user"))?;
239 Ok(metrics_id.to_string())
240 })
241 .await
242 }
243
244 /// Sets "connected_once" on the user for analytics.
245 pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
246 self.transaction(|tx| async move {
247 user::Entity::update_many()
248 .filter(user::Column::Id.eq(id))
249 .set(user::ActiveModel {
250 connected_once: ActiveValue::set(connected_once),
251 ..Default::default()
252 })
253 .exec(&*tx)
254 .await?;
255 Ok(())
256 })
257 .await
258 }
259
260 /// Sets "accepted_tos_at" on the user to the given timestamp.
261 pub async fn set_user_accepted_tos_at(
262 &self,
263 id: UserId,
264 accepted_tos_at: Option<DateTime>,
265 ) -> Result<()> {
266 self.transaction(|tx| async move {
267 user::Entity::update_many()
268 .filter(user::Column::Id.eq(id))
269 .set(user::ActiveModel {
270 accepted_tos_at: ActiveValue::set(accepted_tos_at),
271 ..Default::default()
272 })
273 .exec(&*tx)
274 .await?;
275 Ok(())
276 })
277 .await
278 }
279
280 /// hard delete the user.
281 pub async fn destroy_user(&self, id: UserId) -> Result<()> {
282 self.transaction(|tx| async move {
283 access_token::Entity::delete_many()
284 .filter(access_token::Column::UserId.eq(id))
285 .exec(&*tx)
286 .await?;
287 user::Entity::delete_by_id(id).exec(&*tx).await?;
288 Ok(())
289 })
290 .await
291 }
292
293 /// Find users where github_login ILIKE name_query.
294 pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result<Vec<User>> {
295 self.transaction(|tx| async {
296 let tx = tx;
297 let like_string = Self::fuzzy_like_string(name_query);
298 let query = "
299 SELECT users.*
300 FROM users
301 WHERE github_login ILIKE $1
302 ORDER BY github_login <-> $2
303 LIMIT $3
304 ";
305
306 Ok(user::Entity::find()
307 .from_raw_sql(Statement::from_sql_and_values(
308 self.pool.get_database_backend(),
309 query,
310 vec![like_string.into(), name_query.into(), limit.into()],
311 ))
312 .all(&*tx)
313 .await?)
314 })
315 .await
316 }
317
318 /// fuzzy_like_string creates a string for matching in-order using fuzzy_search_users.
319 /// e.g. "cir" would become "%c%i%r%"
320 pub fn fuzzy_like_string(string: &str) -> String {
321 let mut result = String::with_capacity(string.len() * 2 + 1);
322 for c in string.chars() {
323 if c.is_alphanumeric() {
324 result.push('%');
325 result.push(c);
326 }
327 }
328 result.push('%');
329 result
330 }
331
332 /// Returns all feature flags.
333 pub async fn list_feature_flags(&self) -> Result<Vec<feature_flag::Model>> {
334 self.transaction(|tx| async move { Ok(feature_flag::Entity::find().all(&*tx).await?) })
335 .await
336 }
337
338 /// Creates a new feature flag.
339 pub async fn create_user_flag(&self, flag: &str, enabled_for_all: bool) -> Result<FlagId> {
340 self.transaction(|tx| async move {
341 let flag = feature_flag::Entity::insert(feature_flag::ActiveModel {
342 flag: ActiveValue::set(flag.to_string()),
343 enabled_for_all: ActiveValue::set(enabled_for_all),
344 ..Default::default()
345 })
346 .exec(&*tx)
347 .await?
348 .last_insert_id;
349
350 Ok(flag)
351 })
352 .await
353 }
354
355 /// Add the given user to the feature flag
356 pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> {
357 self.transaction(|tx| async move {
358 user_feature::Entity::insert(user_feature::ActiveModel {
359 user_id: ActiveValue::set(user),
360 feature_id: ActiveValue::set(flag),
361 })
362 .exec(&*tx)
363 .await?;
364
365 Ok(())
366 })
367 .await
368 }
369
370 /// Returns the active flags for the user.
371 pub async fn get_user_flags(&self, user: UserId) -> Result<Vec<String>> {
372 self.transaction(|tx| async move {
373 #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
374 enum QueryAs {
375 Flag,
376 }
377
378 let flags_enabled_for_all = feature_flag::Entity::find()
379 .filter(feature_flag::Column::EnabledForAll.eq(true))
380 .select_only()
381 .column(feature_flag::Column::Flag)
382 .into_values::<_, QueryAs>()
383 .all(&*tx)
384 .await?;
385
386 let flags_enabled_for_user = user::Model {
387 id: user,
388 ..Default::default()
389 }
390 .find_linked(user::UserFlags)
391 .select_only()
392 .column(feature_flag::Column::Flag)
393 .into_values::<_, QueryAs>()
394 .all(&*tx)
395 .await?;
396
397 let mut all_flags = HashSet::from_iter(flags_enabled_for_all);
398 all_flags.extend(flags_enabled_for_user);
399
400 Ok(all_flags.into_iter().collect())
401 })
402 .await
403 }
404
405 pub async fn get_users_missing_github_user_created_at(&self) -> Result<Vec<user::Model>> {
406 self.transaction(|tx| async move {
407 Ok(user::Entity::find()
408 .filter(user::Column::GithubUserCreatedAt.is_null())
409 .all(&*tx)
410 .await?)
411 })
412 .await
413 }
414}