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