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