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