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