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