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