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 /// hard delete the user.
229 pub async fn destroy_user(&self, id: UserId) -> Result<()> {
230 self.transaction(|tx| async move {
231 access_token::Entity::delete_many()
232 .filter(access_token::Column::UserId.eq(id))
233 .exec(&*tx)
234 .await?;
235 user::Entity::delete_by_id(id).exec(&*tx).await?;
236 Ok(())
237 })
238 .await
239 }
240
241 /// Find users where github_login ILIKE name_query.
242 pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result<Vec<User>> {
243 self.transaction(|tx| async {
244 let tx = tx;
245 let like_string = Self::fuzzy_like_string(name_query);
246 let query = "
247 SELECT users.*
248 FROM users
249 WHERE github_login ILIKE $1
250 ORDER BY github_login <-> $2
251 LIMIT $3
252 ";
253
254 Ok(user::Entity::find()
255 .from_raw_sql(Statement::from_sql_and_values(
256 self.pool.get_database_backend(),
257 query,
258 vec![like_string.into(), name_query.into(), limit.into()],
259 ))
260 .all(&*tx)
261 .await?)
262 })
263 .await
264 }
265
266 /// fuzzy_like_string creates a string for matching in-order using fuzzy_search_users.
267 /// e.g. "cir" would become "%c%i%r%"
268 pub fn fuzzy_like_string(string: &str) -> String {
269 let mut result = String::with_capacity(string.len() * 2 + 1);
270 for c in string.chars() {
271 if c.is_alphanumeric() {
272 result.push('%');
273 result.push(c);
274 }
275 }
276 result.push('%');
277 result
278 }
279
280 /// Creates a new feature flag.
281 pub async fn create_user_flag(&self, flag: &str) -> Result<FlagId> {
282 self.transaction(|tx| async move {
283 let flag = feature_flag::Entity::insert(feature_flag::ActiveModel {
284 flag: ActiveValue::set(flag.to_string()),
285 ..Default::default()
286 })
287 .exec(&*tx)
288 .await?
289 .last_insert_id;
290
291 Ok(flag)
292 })
293 .await
294 }
295
296 /// Add the given user to the feature flag
297 pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> {
298 self.transaction(|tx| async move {
299 user_feature::Entity::insert(user_feature::ActiveModel {
300 user_id: ActiveValue::set(user),
301 feature_id: ActiveValue::set(flag),
302 })
303 .exec(&*tx)
304 .await?;
305
306 Ok(())
307 })
308 .await
309 }
310
311 /// Returns the active flags for the user.
312 pub async fn get_user_flags(&self, user: UserId) -> Result<Vec<String>> {
313 self.transaction(|tx| async move {
314 #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
315 enum QueryAs {
316 Flag,
317 }
318
319 let flags = user::Model {
320 id: user,
321 ..Default::default()
322 }
323 .find_linked(user::UserFlags)
324 .select_only()
325 .column(feature_flag::Column::Flag)
326 .into_values::<_, QueryAs>()
327 .all(&*tx)
328 .await?;
329
330 Ok(flags)
331 })
332 .await
333 }
334}