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