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