]> Untitled Git - lemmy.git/blob - crates/api/src/lib.rs
User token revocation upon password change
[lemmy.git] / crates / api / src / lib.rs
1 use actix_web::{web, web::Data};
2 use lemmy_db_queries::{
3   source::{
4     community::{CommunityModerator_, Community_},
5     site::Site_,
6     user::UserSafeSettings_,
7   },
8   Crud,
9   DbPool,
10 };
11 use lemmy_db_schema::source::{
12   community::{Community, CommunityModerator},
13   post::Post,
14   site::Site,
15   user::{UserSafeSettings, User_},
16 };
17 use lemmy_db_views_actor::{
18   community_user_ban_view::CommunityUserBanView,
19   community_view::CommunityView,
20 };
21 use lemmy_structs::{blocking, comment::*, community::*, post::*, site::*, user::*, websocket::*};
22 use lemmy_utils::{claims::Claims, settings::Settings, ApiError, ConnectionId, LemmyError};
23 use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation};
24 use serde::Deserialize;
25 use std::{env, process::Command};
26 use url::Url;
27
28 pub mod comment;
29 pub mod community;
30 pub mod post;
31 pub mod routes;
32 pub mod site;
33 pub mod user;
34 pub mod websocket;
35
36 #[async_trait::async_trait(?Send)]
37 pub trait Perform {
38   type Response: serde::ser::Serialize + Send;
39
40   async fn perform(
41     &self,
42     context: &Data<LemmyContext>,
43     websocket_id: Option<ConnectionId>,
44   ) -> Result<Self::Response, LemmyError>;
45 }
46
47 pub(crate) async fn is_mod_or_admin(
48   pool: &DbPool,
49   user_id: i32,
50   community_id: i32,
51 ) -> Result<(), LemmyError> {
52   let is_mod_or_admin = blocking(pool, move |conn| {
53     CommunityView::is_mod_or_admin(conn, user_id, community_id)
54   })
55   .await?;
56   if !is_mod_or_admin {
57     return Err(ApiError::err("not_a_mod_or_admin").into());
58   }
59   Ok(())
60 }
61 pub async fn is_admin(pool: &DbPool, user_id: i32) -> Result<(), LemmyError> {
62   let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
63   if !user.admin {
64     return Err(ApiError::err("not_an_admin").into());
65   }
66   Ok(())
67 }
68
69 pub(crate) async fn get_post(post_id: i32, pool: &DbPool) -> Result<Post, LemmyError> {
70   match blocking(pool, move |conn| Post::read(conn, post_id)).await? {
71     Ok(post) => Ok(post),
72     Err(_e) => Err(ApiError::err("couldnt_find_post").into()),
73   }
74 }
75
76 pub(crate) async fn get_user_from_jwt(jwt: &str, pool: &DbPool) -> Result<User_, LemmyError> {
77   let claims = match Claims::decode(&jwt) {
78     Ok(claims) => claims.claims,
79     Err(_e) => return Err(ApiError::err("not_logged_in").into()),
80   };
81   let user_id = claims.id;
82   let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
83   // Check for a site ban
84   if user.banned {
85     return Err(ApiError::err("site_ban").into());
86   }
87   // if user's token was issued before user's password reset.
88   let user_validation_time = user.validator_time.timestamp_millis() / 1000;
89   if user_validation_time > claims.iat {
90     return Err(ApiError::err("not_logged_in").into());
91   }
92   Ok(user)
93 }
94
95 pub(crate) async fn get_user_from_jwt_opt(
96   jwt: &Option<String>,
97   pool: &DbPool,
98 ) -> Result<Option<User_>, LemmyError> {
99   match jwt {
100     Some(jwt) => Ok(Some(get_user_from_jwt(jwt, pool).await?)),
101     None => Ok(None),
102   }
103 }
104
105 pub(crate) async fn get_user_safe_settings_from_jwt(
106   jwt: &str,
107   pool: &DbPool,
108 ) -> Result<UserSafeSettings, LemmyError> {
109   let claims = match Claims::decode(&jwt) {
110     Ok(claims) => claims.claims,
111     Err(_e) => return Err(ApiError::err("not_logged_in").into()),
112   };
113   let user_id = claims.id;
114   let user = blocking(pool, move |conn| UserSafeSettings::read(conn, user_id)).await??;
115   // Check for a site ban
116   if user.banned {
117     return Err(ApiError::err("site_ban").into());
118   }
119   // if user's token was issued before user's password reset.
120   let user_validation_time = user.validator_time.timestamp_millis() / 1000;
121   if user_validation_time >= claims.iat {
122     return Err(ApiError::err("not_logged_in").into());
123   }
124   Ok(user)
125 }
126
127 pub(crate) async fn get_user_safe_settings_from_jwt_opt(
128   jwt: &Option<String>,
129   pool: &DbPool,
130 ) -> Result<Option<UserSafeSettings>, LemmyError> {
131   match jwt {
132     Some(jwt) => Ok(Some(get_user_safe_settings_from_jwt(jwt, pool).await?)),
133     None => Ok(None),
134   }
135 }
136
137 pub(crate) async fn check_community_ban(
138   user_id: i32,
139   community_id: i32,
140   pool: &DbPool,
141 ) -> Result<(), LemmyError> {
142   let is_banned = move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
143   if blocking(pool, is_banned).await? {
144     Err(ApiError::err("community_ban").into())
145   } else {
146     Ok(())
147   }
148 }
149
150 pub(crate) async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> {
151   if score == -1 {
152     let site = blocking(pool, move |conn| Site::read_simple(conn)).await??;
153     if !site.enable_downvotes {
154       return Err(ApiError::err("downvotes_disabled").into());
155     }
156   }
157   Ok(())
158 }
159
160 /// Returns a list of communities that the user moderates
161 /// or if a community_id is supplied validates the user is a moderator
162 /// of that community and returns the community id in a vec
163 ///
164 /// * `user_id` - the user id of the moderator
165 /// * `community_id` - optional community id to check for moderator privileges
166 /// * `pool` - the diesel db pool
167 pub(crate) async fn collect_moderated_communities(
168   user_id: i32,
169   community_id: Option<i32>,
170   pool: &DbPool,
171 ) -> Result<Vec<i32>, LemmyError> {
172   if let Some(community_id) = community_id {
173     // if the user provides a community_id, just check for mod/admin privileges
174     is_mod_or_admin(pool, user_id, community_id).await?;
175     Ok(vec![community_id])
176   } else {
177     let ids = blocking(pool, move |conn: &'_ _| {
178       CommunityModerator::get_user_moderated_communities(conn, user_id)
179     })
180     .await??;
181     Ok(ids)
182   }
183 }
184
185 pub(crate) fn check_optional_url(item: &Option<Option<String>>) -> Result<(), LemmyError> {
186   if let Some(Some(item)) = &item {
187     if Url::parse(item).is_err() {
188       return Err(ApiError::err("invalid_url").into());
189     }
190   }
191   Ok(())
192 }
193
194 pub(crate) async fn build_federated_instances(
195   pool: &DbPool,
196 ) -> Result<Option<FederatedInstances>, LemmyError> {
197   if Settings::get().federation.enabled {
198     let distinct_communities = blocking(pool, move |conn| {
199       Community::distinct_federated_communities(conn)
200     })
201     .await??;
202
203     let allowed = Settings::get().get_allowed_instances();
204     let blocked = Settings::get().get_blocked_instances();
205
206     let mut linked = distinct_communities
207       .iter()
208       .map(|actor_id| Ok(Url::parse(actor_id)?.host_str().unwrap_or("").to_string()))
209       .collect::<Result<Vec<String>, LemmyError>>()?;
210
211     linked.extend_from_slice(&allowed);
212     linked.retain(|a| !blocked.contains(a) && !a.eq("") && !a.eq(&Settings::get().hostname));
213
214     // Sort and remove dupes
215     linked.sort_unstable();
216     linked.dedup();
217
218     Ok(Some(FederatedInstances {
219       linked,
220       allowed,
221       blocked,
222     }))
223   } else {
224     Ok(None)
225   }
226 }
227
228 pub async fn match_websocket_operation(
229   context: LemmyContext,
230   id: ConnectionId,
231   op: UserOperation,
232   data: &str,
233 ) -> Result<String, LemmyError> {
234   match op {
235     // User ops
236     UserOperation::Login => do_websocket_operation::<Login>(context, id, op, data).await,
237     UserOperation::Register => do_websocket_operation::<Register>(context, id, op, data).await,
238     UserOperation::GetCaptcha => do_websocket_operation::<GetCaptcha>(context, id, op, data).await,
239     UserOperation::GetUserDetails => {
240       do_websocket_operation::<GetUserDetails>(context, id, op, data).await
241     }
242     UserOperation::GetReplies => do_websocket_operation::<GetReplies>(context, id, op, data).await,
243     UserOperation::AddAdmin => do_websocket_operation::<AddAdmin>(context, id, op, data).await,
244     UserOperation::BanUser => do_websocket_operation::<BanUser>(context, id, op, data).await,
245     UserOperation::GetUserMentions => {
246       do_websocket_operation::<GetUserMentions>(context, id, op, data).await
247     }
248     UserOperation::MarkUserMentionAsRead => {
249       do_websocket_operation::<MarkUserMentionAsRead>(context, id, op, data).await
250     }
251     UserOperation::MarkAllAsRead => {
252       do_websocket_operation::<MarkAllAsRead>(context, id, op, data).await
253     }
254     UserOperation::DeleteAccount => {
255       do_websocket_operation::<DeleteAccount>(context, id, op, data).await
256     }
257     UserOperation::PasswordReset => {
258       do_websocket_operation::<PasswordReset>(context, id, op, data).await
259     }
260     UserOperation::PasswordChange => {
261       do_websocket_operation::<PasswordChange>(context, id, op, data).await
262     }
263     UserOperation::UserJoin => do_websocket_operation::<UserJoin>(context, id, op, data).await,
264     UserOperation::PostJoin => do_websocket_operation::<PostJoin>(context, id, op, data).await,
265     UserOperation::CommunityJoin => {
266       do_websocket_operation::<CommunityJoin>(context, id, op, data).await
267     }
268     UserOperation::ModJoin => do_websocket_operation::<ModJoin>(context, id, op, data).await,
269     UserOperation::SaveUserSettings => {
270       do_websocket_operation::<SaveUserSettings>(context, id, op, data).await
271     }
272     UserOperation::GetReportCount => {
273       do_websocket_operation::<GetReportCount>(context, id, op, data).await
274     }
275
276     // Private Message ops
277     UserOperation::CreatePrivateMessage => {
278       do_websocket_operation::<CreatePrivateMessage>(context, id, op, data).await
279     }
280     UserOperation::EditPrivateMessage => {
281       do_websocket_operation::<EditPrivateMessage>(context, id, op, data).await
282     }
283     UserOperation::DeletePrivateMessage => {
284       do_websocket_operation::<DeletePrivateMessage>(context, id, op, data).await
285     }
286     UserOperation::MarkPrivateMessageAsRead => {
287       do_websocket_operation::<MarkPrivateMessageAsRead>(context, id, op, data).await
288     }
289     UserOperation::GetPrivateMessages => {
290       do_websocket_operation::<GetPrivateMessages>(context, id, op, data).await
291     }
292
293     // Site ops
294     UserOperation::GetModlog => do_websocket_operation::<GetModlog>(context, id, op, data).await,
295     UserOperation::CreateSite => do_websocket_operation::<CreateSite>(context, id, op, data).await,
296     UserOperation::EditSite => do_websocket_operation::<EditSite>(context, id, op, data).await,
297     UserOperation::GetSite => do_websocket_operation::<GetSite>(context, id, op, data).await,
298     UserOperation::GetSiteConfig => {
299       do_websocket_operation::<GetSiteConfig>(context, id, op, data).await
300     }
301     UserOperation::SaveSiteConfig => {
302       do_websocket_operation::<SaveSiteConfig>(context, id, op, data).await
303     }
304     UserOperation::Search => do_websocket_operation::<Search>(context, id, op, data).await,
305     UserOperation::TransferCommunity => {
306       do_websocket_operation::<TransferCommunity>(context, id, op, data).await
307     }
308     UserOperation::TransferSite => {
309       do_websocket_operation::<TransferSite>(context, id, op, data).await
310     }
311
312     // Community ops
313     UserOperation::GetCommunity => {
314       do_websocket_operation::<GetCommunity>(context, id, op, data).await
315     }
316     UserOperation::ListCommunities => {
317       do_websocket_operation::<ListCommunities>(context, id, op, data).await
318     }
319     UserOperation::CreateCommunity => {
320       do_websocket_operation::<CreateCommunity>(context, id, op, data).await
321     }
322     UserOperation::EditCommunity => {
323       do_websocket_operation::<EditCommunity>(context, id, op, data).await
324     }
325     UserOperation::DeleteCommunity => {
326       do_websocket_operation::<DeleteCommunity>(context, id, op, data).await
327     }
328     UserOperation::RemoveCommunity => {
329       do_websocket_operation::<RemoveCommunity>(context, id, op, data).await
330     }
331     UserOperation::FollowCommunity => {
332       do_websocket_operation::<FollowCommunity>(context, id, op, data).await
333     }
334     UserOperation::GetFollowedCommunities => {
335       do_websocket_operation::<GetFollowedCommunities>(context, id, op, data).await
336     }
337     UserOperation::BanFromCommunity => {
338       do_websocket_operation::<BanFromCommunity>(context, id, op, data).await
339     }
340     UserOperation::AddModToCommunity => {
341       do_websocket_operation::<AddModToCommunity>(context, id, op, data).await
342     }
343
344     // Post ops
345     UserOperation::CreatePost => do_websocket_operation::<CreatePost>(context, id, op, data).await,
346     UserOperation::GetPost => do_websocket_operation::<GetPost>(context, id, op, data).await,
347     UserOperation::GetPosts => do_websocket_operation::<GetPosts>(context, id, op, data).await,
348     UserOperation::EditPost => do_websocket_operation::<EditPost>(context, id, op, data).await,
349     UserOperation::DeletePost => do_websocket_operation::<DeletePost>(context, id, op, data).await,
350     UserOperation::RemovePost => do_websocket_operation::<RemovePost>(context, id, op, data).await,
351     UserOperation::LockPost => do_websocket_operation::<LockPost>(context, id, op, data).await,
352     UserOperation::StickyPost => do_websocket_operation::<StickyPost>(context, id, op, data).await,
353     UserOperation::CreatePostLike => {
354       do_websocket_operation::<CreatePostLike>(context, id, op, data).await
355     }
356     UserOperation::SavePost => do_websocket_operation::<SavePost>(context, id, op, data).await,
357     UserOperation::CreatePostReport => {
358       do_websocket_operation::<CreatePostReport>(context, id, op, data).await
359     }
360     UserOperation::ListPostReports => {
361       do_websocket_operation::<ListPostReports>(context, id, op, data).await
362     }
363     UserOperation::ResolvePostReport => {
364       do_websocket_operation::<ResolvePostReport>(context, id, op, data).await
365     }
366
367     // Comment ops
368     UserOperation::CreateComment => {
369       do_websocket_operation::<CreateComment>(context, id, op, data).await
370     }
371     UserOperation::EditComment => {
372       do_websocket_operation::<EditComment>(context, id, op, data).await
373     }
374     UserOperation::DeleteComment => {
375       do_websocket_operation::<DeleteComment>(context, id, op, data).await
376     }
377     UserOperation::RemoveComment => {
378       do_websocket_operation::<RemoveComment>(context, id, op, data).await
379     }
380     UserOperation::MarkCommentAsRead => {
381       do_websocket_operation::<MarkCommentAsRead>(context, id, op, data).await
382     }
383     UserOperation::SaveComment => {
384       do_websocket_operation::<SaveComment>(context, id, op, data).await
385     }
386     UserOperation::GetComments => {
387       do_websocket_operation::<GetComments>(context, id, op, data).await
388     }
389     UserOperation::CreateCommentLike => {
390       do_websocket_operation::<CreateCommentLike>(context, id, op, data).await
391     }
392     UserOperation::CreateCommentReport => {
393       do_websocket_operation::<CreateCommentReport>(context, id, op, data).await
394     }
395     UserOperation::ListCommentReports => {
396       do_websocket_operation::<ListCommentReports>(context, id, op, data).await
397     }
398     UserOperation::ResolveCommentReport => {
399       do_websocket_operation::<ResolveCommentReport>(context, id, op, data).await
400     }
401   }
402 }
403
404 async fn do_websocket_operation<'a, 'b, Data>(
405   context: LemmyContext,
406   id: ConnectionId,
407   op: UserOperation,
408   data: &str,
409 ) -> Result<String, LemmyError>
410 where
411   for<'de> Data: Deserialize<'de> + 'a,
412   Data: Perform,
413 {
414   let parsed_data: Data = serde_json::from_str(&data)?;
415   let res = parsed_data
416     .perform(&web::Data::new(context), Some(id))
417     .await?;
418   serialize_websocket_message(&op, &res)
419 }
420
421 pub(crate) fn captcha_espeak_wav_base64(captcha: &str) -> Result<String, LemmyError> {
422   let mut built_text = String::new();
423
424   // Building proper speech text for espeak
425   for mut c in captcha.chars() {
426     let new_str = if c.is_alphabetic() {
427       if c.is_lowercase() {
428         c.make_ascii_uppercase();
429         format!("lower case {} ... ", c)
430       } else {
431         c.make_ascii_uppercase();
432         format!("capital {} ... ", c)
433       }
434     } else {
435       format!("{} ...", c)
436     };
437
438     built_text.push_str(&new_str);
439   }
440
441   espeak_wav_base64(&built_text)
442 }
443
444 pub(crate) fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
445   // Make a temp file path
446   let uuid = uuid::Uuid::new_v4().to_string();
447   let file_path = format!(
448     "{}/lemmy_espeak_{}.wav",
449     env::temp_dir().to_string_lossy(),
450     &uuid
451   );
452
453   // Write the wav file
454   Command::new("espeak")
455     .arg("-w")
456     .arg(&file_path)
457     .arg(text)
458     .status()?;
459
460   // Read the wav file bytes
461   let bytes = std::fs::read(&file_path)?;
462
463   // Delete the file
464   std::fs::remove_file(file_path)?;
465
466   // Convert to base64
467   let base64 = base64::encode(bytes);
468
469   Ok(base64)
470 }
471
472 #[cfg(test)]
473 mod tests {
474   use crate::{captcha_espeak_wav_base64, get_user_from_jwt};
475   use lemmy_db_queries::{
476     establish_pooled_connection,
477     source::user::User,
478     Crud,
479     ListingType,
480     SortType,
481   };
482   use lemmy_db_schema::source::user::{UserForm, User_};
483   use lemmy_utils::claims::Claims;
484   use std::{
485     env::{current_dir, set_current_dir},
486     path::PathBuf,
487   };
488
489   #[actix_rt::test]
490   async fn test_should_not_validate_user_token_after_password_change() {
491     struct CwdGuard(PathBuf);
492     impl Drop for CwdGuard {
493       fn drop(&mut self) {
494         let _ = set_current_dir(&self.0);
495       }
496     }
497
498     let _dir_bkp = CwdGuard(current_dir().unwrap());
499
500     // so configs could be read
501     let _ = set_current_dir("../..");
502
503     let conn = establish_pooled_connection();
504
505     let new_user = UserForm {
506       name: "user_df342sgf".into(),
507       preferred_username: None,
508       password_encrypted: "nope".into(),
509       email: None,
510       matrix_user_id: None,
511       avatar: None,
512       banner: None,
513       admin: false,
514       banned: Some(false),
515       published: None,
516       updated: None,
517       show_nsfw: false,
518       theme: "browser".into(),
519       default_sort_type: SortType::Hot as i16,
520       default_listing_type: ListingType::Subscribed as i16,
521       lang: "browser".into(),
522       show_avatars: true,
523       send_notifications_to_email: false,
524       actor_id: None,
525       bio: None,
526       local: true,
527       private_key: None,
528       public_key: None,
529       last_refreshed_at: None,
530       inbox_url: None,
531       shared_inbox_url: None,
532     };
533
534     let inserted_user: User_ = User_::create(&conn.get().unwrap(), &new_user).unwrap();
535
536     let jwt_token = Claims::jwt(inserted_user.id, String::from("my-host.com")).unwrap();
537
538     get_user_from_jwt(&jwt_token, &conn)
539       .await
540       .expect("User should be decoded");
541
542     std::thread::sleep(std::time::Duration::from_secs(1));
543
544     User_::update_password(&conn.get().unwrap(), inserted_user.id, &"password111").unwrap();
545
546     let jwt_decode_res = get_user_from_jwt(&jwt_token, &conn).await;
547
548     jwt_decode_res.expect_err("JWT decode should fail after password change");
549   }
550
551   #[test]
552   fn test_espeak() {
553     assert!(captcha_espeak_wav_base64("WxRt2l").is_ok())
554   }
555 }