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 GitHub login. There are no access checks here, so this should only be used internally.
65 pub async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
66 self.transaction(|tx| async move {
67 Ok(user::Entity::find()
68 .filter(user::Column::GithubLogin.eq(github_login))
69 .one(&*tx)
70 .await?)
71 })
72 .await
73 }
74
75 pub async fn get_or_create_user_by_github_account(
76 &self,
77 github_login: &str,
78 github_user_id: Option<i32>,
79 github_email: Option<&str>,
80 initial_channel_id: Option<ChannelId>,
81 ) -> Result<User> {
82 self.transaction(|tx| async move {
83 self.get_or_create_user_by_github_account_tx(
84 github_login,
85 github_user_id,
86 github_email,
87 initial_channel_id,
88 &tx,
89 )
90 .await
91 })
92 .await
93 }
94
95 pub async fn get_or_create_user_by_github_account_tx(
96 &self,
97 github_login: &str,
98 github_user_id: Option<i32>,
99 github_email: Option<&str>,
100 initial_channel_id: Option<ChannelId>,
101 tx: &DatabaseTransaction,
102 ) -> Result<User> {
103 if let Some(github_user_id) = github_user_id {
104 if let Some(user_by_github_user_id) = user::Entity::find()
105 .filter(user::Column::GithubUserId.eq(github_user_id))
106 .one(tx)
107 .await?
108 {
109 let mut user_by_github_user_id = user_by_github_user_id.into_active_model();
110 user_by_github_user_id.github_login = ActiveValue::set(github_login.into());
111 Ok(user_by_github_user_id.update(tx).await?)
112 } else if let Some(user_by_github_login) = user::Entity::find()
113 .filter(user::Column::GithubLogin.eq(github_login))
114 .one(tx)
115 .await?
116 {
117 let mut user_by_github_login = user_by_github_login.into_active_model();
118 user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id));
119 Ok(user_by_github_login.update(tx).await?)
120 } else {
121 let user = user::Entity::insert(user::ActiveModel {
122 email_address: ActiveValue::set(github_email.map(|email| email.into())),
123 github_login: ActiveValue::set(github_login.into()),
124 github_user_id: ActiveValue::set(Some(github_user_id)),
125 admin: ActiveValue::set(false),
126 invite_count: ActiveValue::set(0),
127 invite_code: ActiveValue::set(None),
128 metrics_id: ActiveValue::set(Uuid::new_v4()),
129 ..Default::default()
130 })
131 .exec_with_returning(tx)
132 .await?;
133 if let Some(channel_id) = initial_channel_id {
134 channel_member::Entity::insert(channel_member::ActiveModel {
135 id: ActiveValue::NotSet,
136 channel_id: ActiveValue::Set(channel_id),
137 user_id: ActiveValue::Set(user.id),
138 accepted: ActiveValue::Set(true),
139 role: ActiveValue::Set(ChannelRole::Guest),
140 })
141 .exec(tx)
142 .await?;
143 }
144 Ok(user)
145 }
146 } else {
147 let user = user::Entity::find()
148 .filter(user::Column::GithubLogin.eq(github_login))
149 .one(tx)
150 .await?
151 .ok_or_else(|| anyhow!("no such user {}", github_login))?;
152 Ok(user)
153 }
154 }
155
156 /// get_all_users returns the next page of users. To get more call again with
157 /// the same limit and the page incremented by 1.
158 pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>> {
159 self.transaction(|tx| async move {
160 Ok(user::Entity::find()
161 .order_by_asc(user::Column::GithubLogin)
162 .limit(limit as u64)
163 .offset(page as u64 * limit as u64)
164 .all(&*tx)
165 .await?)
166 })
167 .await
168 }
169
170 /// Returns the metrics id for the user.
171 pub async fn get_user_metrics_id(&self, id: UserId) -> Result<String> {
172 #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
173 enum QueryAs {
174 MetricsId,
175 }
176
177 self.transaction(|tx| async move {
178 let metrics_id: Uuid = user::Entity::find_by_id(id)
179 .select_only()
180 .column(user::Column::MetricsId)
181 .into_values::<_, QueryAs>()
182 .one(&*tx)
183 .await?
184 .ok_or_else(|| anyhow!("could not find user"))?;
185 Ok(metrics_id.to_string())
186 })
187 .await
188 }
189
190 /// Sets "connected_once" on the user for analytics.
191 pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
192 self.transaction(|tx| async move {
193 user::Entity::update_many()
194 .filter(user::Column::Id.eq(id))
195 .set(user::ActiveModel {
196 connected_once: ActiveValue::set(connected_once),
197 ..Default::default()
198 })
199 .exec(&*tx)
200 .await?;
201 Ok(())
202 })
203 .await
204 }
205
206 /// hard delete the user.
207 pub async fn destroy_user(&self, id: UserId) -> Result<()> {
208 self.transaction(|tx| async move {
209 access_token::Entity::delete_many()
210 .filter(access_token::Column::UserId.eq(id))
211 .exec(&*tx)
212 .await?;
213 user::Entity::delete_by_id(id).exec(&*tx).await?;
214 Ok(())
215 })
216 .await
217 }
218
219 /// Find users where github_login ILIKE name_query.
220 pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result<Vec<User>> {
221 self.transaction(|tx| async {
222 let tx = tx;
223 let like_string = Self::fuzzy_like_string(name_query);
224 let query = "
225 SELECT users.*
226 FROM users
227 WHERE github_login ILIKE $1
228 ORDER BY github_login <-> $2
229 LIMIT $3
230 ";
231
232 Ok(user::Entity::find()
233 .from_raw_sql(Statement::from_sql_and_values(
234 self.pool.get_database_backend(),
235 query,
236 vec![like_string.into(), name_query.into(), limit.into()],
237 ))
238 .all(&*tx)
239 .await?)
240 })
241 .await
242 }
243
244 /// fuzzy_like_string creates a string for matching in-order using fuzzy_search_users.
245 /// e.g. "cir" would become "%c%i%r%"
246 pub fn fuzzy_like_string(string: &str) -> String {
247 let mut result = String::with_capacity(string.len() * 2 + 1);
248 for c in string.chars() {
249 if c.is_alphanumeric() {
250 result.push('%');
251 result.push(c);
252 }
253 }
254 result.push('%');
255 result
256 }
257
258 /// Creates a new feature flag.
259 pub async fn create_user_flag(&self, flag: &str) -> Result<FlagId> {
260 self.transaction(|tx| async move {
261 let flag = feature_flag::Entity::insert(feature_flag::ActiveModel {
262 flag: ActiveValue::set(flag.to_string()),
263 ..Default::default()
264 })
265 .exec(&*tx)
266 .await?
267 .last_insert_id;
268
269 Ok(flag)
270 })
271 .await
272 }
273
274 /// Add the given user to the feature flag
275 pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> {
276 self.transaction(|tx| async move {
277 user_feature::Entity::insert(user_feature::ActiveModel {
278 user_id: ActiveValue::set(user),
279 feature_id: ActiveValue::set(flag),
280 })
281 .exec(&*tx)
282 .await?;
283
284 Ok(())
285 })
286 .await
287 }
288
289 /// Returns the active flags for the user.
290 pub async fn get_user_flags(&self, user: UserId) -> Result<Vec<String>> {
291 self.transaction(|tx| async move {
292 #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
293 enum QueryAs {
294 Flag,
295 }
296
297 let flags = user::Model {
298 id: user,
299 ..Default::default()
300 }
301 .find_linked(user::UserFlags)
302 .select_only()
303 .column(feature_flag::Column::Flag)
304 .into_values::<_, QueryAs>()
305 .all(&*tx)
306 .await?;
307
308 Ok(flags)
309 })
310 .await
311 }
312}