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