From c883a49a4096b171f324d44c9fcacda55ed361ee Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 15 Dec 2021 14:49:59 -0500 Subject: [PATCH] First pass at invite-only migration. (#1949) * First pass at invite-only migration. * Implement email verification (fixes #219) * remove unwrap * Adding views and functionality to registration application. #209 * Add private instance site column, and back end checks. * Adding some message fields to LoginResponse * Adding private instance to site setup. * A few additions: - Add a DeleteAccount response. - RegistrationApplicationView now has the safe LocalUserSettings. - Adding VerifyEmail to websocket API, added a proper response type. * Adding and reorganizing some email helpers. * A few fixes for private sites: - Added a check_registration_application function. - Only send a verification email if its been changed. - VerifyEmail now returns LoginResponse. - Deleting the old tokens after a successful email verify. - If port is missing on email config, display a better error message. * Version 0.15.0-rc.3 * Adding published to email_verification table. * Adding fixes from comments. * Version 0.15.0-rc.4 * Adding modlog private site check. * Version 0.15.0-rc.6 Co-authored-by: Felix Ableitner --- Cargo.lock | 28 +- Cargo.toml | 26 +- config/defaults.hjson | 4 + crates/api/Cargo.toml | 20 +- crates/api/src/lib.rs | 16 +- crates/api/src/local_user.rs | 186 +++++--- crates/api/src/site.rs | 164 +++++++- crates/api_common/Cargo.toml | 12 +- crates/api_common/src/lib.rs | 185 +++++++- crates/api_common/src/person.rs | 19 +- crates/api_common/src/site.rs | 47 +++ crates/api_crud/Cargo.toml | 20 +- crates/api_crud/src/comment/read.rs | 11 +- crates/api_crud/src/community/read.rs | 12 +- crates/api_crud/src/post/read.rs | 12 +- crates/api_crud/src/private_message/create.rs | 7 +- crates/api_crud/src/site/create.rs | 2 +- crates/api_crud/src/site/read.rs | 13 +- crates/api_crud/src/site/update.rs | 40 +- crates/api_crud/src/user/create.rs | 103 +++-- crates/api_crud/src/user/delete.rs | 10 +- crates/api_crud/src/user/read.rs | 9 +- crates/apub/Cargo.toml | 16 +- crates/apub_lib/Cargo.toml | 6 +- crates/apub_lib_derive/Cargo.toml | 2 +- crates/db_schema/Cargo.toml | 6 +- .../src/aggregates/site_aggregates.rs | 4 + .../db_schema/src/impls/email_verification.rs | 55 +++ crates/db_schema/src/impls/local_user.rs | 24 +- crates/db_schema/src/impls/mod.rs | 2 + .../src/impls/password_reset_request.rs | 4 +- .../src/impls/registration_application.rs | 42 ++ crates/db_schema/src/newtypes.rs | 4 +- crates/db_schema/src/schema.rs | 32 ++ .../src/source/email_verification.rs | 19 + crates/db_schema/src/source/local_user.rs | 10 +- crates/db_schema/src/source/mod.rs | 2 + .../src/source/registration_application.rs | 25 ++ crates/db_schema/src/source/site.rs | 10 +- crates/db_views/Cargo.toml | 4 +- crates/db_views/src/lib.rs | 1 + .../src/registration_application_view.rs | 396 ++++++++++++++++++ crates/db_views_actor/Cargo.toml | 4 +- crates/db_views_moderator/Cargo.toml | 4 +- crates/routes/Cargo.toml | 16 +- crates/utils/Cargo.toml | 2 +- crates/utils/src/email.rs | 17 +- crates/utils/src/settings/structs.rs | 8 + crates/websocket/Cargo.toml | 12 +- crates/websocket/src/lib.rs | 4 + crates/websocket/src/send.rs | 46 +- .../down.sql | 8 + .../up.sql | 14 + .../down.sql | 9 + .../up.sql | 19 + .../down.sql | 1 + .../up.sql | 1 + src/api_routes.rs | 19 +- src/main.rs | 4 +- 59 files changed, 1540 insertions(+), 258 deletions(-) create mode 100644 crates/db_schema/src/impls/email_verification.rs create mode 100644 crates/db_schema/src/impls/registration_application.rs create mode 100644 crates/db_schema/src/source/email_verification.rs create mode 100644 crates/db_schema/src/source/registration_application.rs create mode 100644 crates/db_views/src/registration_application_view.rs create mode 100644 migrations/2021-11-23-132840_email_verification/down.sql create mode 100644 migrations/2021-11-23-132840_email_verification/up.sql create mode 100644 migrations/2021-11-23-153753_add_invite_only_columns/down.sql create mode 100644 migrations/2021-11-23-153753_add_invite_only_columns/up.sql create mode 100644 migrations/2021-12-09-225529_add_published_to_email_verification/down.sql create mode 100644 migrations/2021-12-09-225529_add_published_to_email_verification/up.sql diff --git a/Cargo.lock b/Cargo.lock index 9e1bab42f..07ad6d2e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1686,7 +1686,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lemmy_api" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" dependencies = [ "actix", "actix-rt", @@ -1729,7 +1729,7 @@ dependencies = [ [[package]] name = "lemmy_api_common" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" dependencies = [ "actix-web", "chrono", @@ -1747,7 +1747,7 @@ dependencies = [ [[package]] name = "lemmy_api_crud" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" dependencies = [ "actix", "actix-rt", @@ -1790,7 +1790,7 @@ dependencies = [ [[package]] name = "lemmy_apub" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" dependencies = [ "activitystreams-kinds", "actix", @@ -1836,7 +1836,7 @@ dependencies = [ [[package]] name = "lemmy_apub_lib" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" dependencies = [ "activitystreams", "actix-web", @@ -1863,7 +1863,7 @@ dependencies = [ [[package]] name = "lemmy_apub_lib_derive" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" dependencies = [ "proc-macro2 1.0.33", "quote 1.0.10", @@ -1873,7 +1873,7 @@ dependencies = [ [[package]] name = "lemmy_db_schema" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" dependencies = [ "bcrypt", "chrono", @@ -1895,7 +1895,7 @@ dependencies = [ [[package]] name = "lemmy_db_views" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" dependencies = [ "diesel", "lemmy_db_schema", @@ -1907,7 +1907,7 @@ dependencies = [ [[package]] name = "lemmy_db_views_actor" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" dependencies = [ "diesel", "lemmy_db_schema", @@ -1916,7 +1916,7 @@ dependencies = [ [[package]] name = "lemmy_db_views_moderator" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" dependencies = [ "diesel", "lemmy_db_schema", @@ -1925,7 +1925,7 @@ dependencies = [ [[package]] name = "lemmy_routes" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" dependencies = [ "actix", "actix-http", @@ -1956,7 +1956,7 @@ dependencies = [ [[package]] name = "lemmy_server" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" dependencies = [ "activitystreams", "actix", @@ -2000,7 +2000,7 @@ dependencies = [ [[package]] name = "lemmy_utils" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" dependencies = [ "actix-rt", "actix-web", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "lemmy_websocket" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" dependencies = [ "actix", "actix-web", diff --git a/Cargo.toml b/Cargo.toml index aa4fa7759..817fb7f6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lemmy_server" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" edition = "2018" description = "A link aggregator for the fediverse" license = "AGPL-3.0" @@ -31,18 +31,18 @@ members = [ ] [dependencies] -lemmy_api = { version = "=0.14.4-rc.4", path = "./crates/api" } -lemmy_api_crud = { version = "=0.14.4-rc.4", path = "./crates/api_crud" } -lemmy_apub = { version = "=0.14.4-rc.4", path = "./crates/apub" } -lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "./crates/apub_lib" } -lemmy_utils = { version = "=0.14.4-rc.4", path = "./crates/utils" } -lemmy_db_schema = { version = "=0.14.4-rc.4", path = "./crates/db_schema" } -lemmy_db_views = { version = "=0.14.4-rc.4", path = "./crates/db_views" } -lemmy_db_views_moderator = { version = "=0.14.4-rc.4", path = "./crates/db_views_moderator" } -lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "./crates/db_views_actor" } -lemmy_api_common = { version = "=0.14.4-rc.4", path = "crates/api_common" } -lemmy_websocket = { version = "=0.14.4-rc.4", path = "./crates/websocket" } -lemmy_routes = { version = "=0.14.4-rc.4", path = "./crates/routes" } +lemmy_api = { version = "=0.15.0-rc.6", path = "./crates/api" } +lemmy_api_crud = { version = "=0.15.0-rc.6", path = "./crates/api_crud" } +lemmy_apub = { version = "=0.15.0-rc.6", path = "./crates/apub" } +lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "./crates/apub_lib" } +lemmy_utils = { version = "=0.15.0-rc.6", path = "./crates/utils" } +lemmy_db_schema = { version = "=0.15.0-rc.6", path = "./crates/db_schema" } +lemmy_db_views = { version = "=0.15.0-rc.6", path = "./crates/db_views" } +lemmy_db_views_moderator = { version = "=0.15.0-rc.6", path = "./crates/db_views_moderator" } +lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "./crates/db_views_actor" } +lemmy_api_common = { version = "=0.15.0-rc.6", path = "crates/api_common" } +lemmy_websocket = { version = "=0.15.0-rc.6", path = "./crates/websocket" } +lemmy_routes = { version = "=0.15.0-rc.6", path = "./crates/routes" } diesel = "1.4.8" diesel_migrations = "1.4.0" chrono = { version = "0.4.19", features = ["serde"] } diff --git a/config/defaults.hjson b/config/defaults.hjson index 1126fef3f..9edf9fc01 100644 --- a/config/defaults.hjson +++ b/config/defaults.hjson @@ -97,6 +97,10 @@ open_registration: true enable_nsfw: true community_creation_admin_only: true + require_email_verification: true + require_application: true + application_question: "string" + private_instance: true } # the domain name of your instance (mandatory) hostname: "unset" diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index 907dec10c..d7258e5cf 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lemmy_api" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" edition = "2018" description = "A link aggregator for the fediverse" license = "AGPL-3.0" @@ -13,15 +13,15 @@ path = "src/lib.rs" doctest = false [dependencies] -lemmy_apub = { version = "=0.14.4-rc.4", path = "../apub" } -lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "../apub_lib" } -lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" } -lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" } -lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" } -lemmy_db_views_moderator = { version = "=0.14.4-rc.4", path = "../db_views_moderator" } -lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" } -lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" } -lemmy_websocket = { version = "=0.14.4-rc.4", path = "../websocket" } +lemmy_apub = { version = "=0.15.0-rc.6", path = "../apub" } +lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "../apub_lib" } +lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" } +lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" } +lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" } +lemmy_db_views_moderator = { version = "=0.15.0-rc.6", path = "../db_views_moderator" } +lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" } +lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" } +lemmy_websocket = { version = "=0.15.0-rc.6", path = "../websocket" } diesel = "1.4.8" bcrypt = "0.10.1" chrono = { version = "0.4.19", features = ["serde"] } diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index d535c4678..26a41d3d2 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -38,6 +38,15 @@ pub async fn match_websocket_operation( UserOperation::GetCaptcha => do_websocket_operation::(context, id, op, data).await, UserOperation::GetReplies => do_websocket_operation::(context, id, op, data).await, UserOperation::AddAdmin => do_websocket_operation::(context, id, op, data).await, + UserOperation::GetUnreadRegistrationApplicationCount => { + do_websocket_operation::(context, id, op, data).await + } + UserOperation::ListRegistrationApplications => { + do_websocket_operation::(context, id, op, data).await + } + UserOperation::ApproveRegistrationApplication => { + do_websocket_operation::(context, id, op, data).await + } UserOperation::BanPerson => do_websocket_operation::(context, id, op, data).await, UserOperation::BlockPerson => { do_websocket_operation::(context, id, op, data).await @@ -75,6 +84,9 @@ pub async fn match_websocket_operation( UserOperation::GetUnreadCount => { do_websocket_operation::(context, id, op, data).await } + UserOperation::VerifyEmail => { + do_websocket_operation::(context, id, op, data).await + } // Private Message ops UserOperation::MarkPrivateMessageAsRead => { @@ -219,8 +231,8 @@ mod tests { let inserted_person = Person::create(&conn, &new_person).unwrap(); let local_user_form = LocalUserForm { - person_id: inserted_person.id, - password_encrypted: "123456".to_string(), + person_id: Some(inserted_person.id), + password_encrypted: Some("123456".to_string()), ..LocalUserForm::default() }; diff --git a/crates/api/src/local_user.rs b/crates/api/src/local_user.rs index a0711d690..781a581a7 100644 --- a/crates/api/src/local_user.rs +++ b/crates/api/src/local_user.rs @@ -6,10 +6,14 @@ use captcha::{gen, Difficulty}; use chrono::Duration; use lemmy_api_common::{ blocking, + check_registration_application, get_local_user_view_from_jwt, is_admin, password_length_check, person::*, + send_email_verification_success, + send_password_reset_email, + send_verification_email, }; use lemmy_db_schema::{ diesel_option_overwrite, @@ -19,6 +23,7 @@ use lemmy_db_schema::{ source::{ comment::Comment, community::Community, + email_verification::EmailVerification, local_user::{LocalUser, LocalUserForm}, moderator::*, password_reset_request::*, @@ -46,12 +51,10 @@ use lemmy_db_views_actor::{ }; use lemmy_utils::{ claims::Claims, - email::send_email, location_info, - utils::{generate_random_string, is_valid_display_name, is_valid_matrix_id, naive_from_unix}, + utils::{is_valid_display_name, is_valid_matrix_id, naive_from_unix}, ConnectionId, LemmyError, - Sensitive, }; use lemmy_websocket::{ messages::{CaptchaItem, SendAllMessage}, @@ -90,14 +93,25 @@ impl Perform for Login { return Err(LemmyError::from_message("password_incorrect")); } + let site = blocking(context.pool(), Site::read_simple).await??; + if site.require_email_verification && !local_user_view.local_user.email_verified { + return Err(LemmyError::from_message("email_not_verified")); + } + + check_registration_application(&site, &local_user_view, context.pool()).await?; + // Return the jwt Ok(LoginResponse { - jwt: Claims::jwt( - local_user_view.local_user.id.0, - &context.secret().jwt_secret, - &context.settings().hostname, - )? - .into(), + jwt: Some( + Claims::jwt( + local_user_view.local_user.id.0, + &context.secret().jwt_secret, + &context.settings().hostname, + )? + .into(), + ), + verify_email_sent: false, + registration_created: false, }) } } @@ -164,11 +178,35 @@ impl Perform for SaveUserSettings { let avatar = diesel_option_overwrite_to_url(&data.avatar)?; let banner = diesel_option_overwrite_to_url(&data.banner)?; - let email = diesel_option_overwrite(&data.email.clone().map(Sensitive::into_inner)); let bio = diesel_option_overwrite(&data.bio); let display_name = diesel_option_overwrite(&data.display_name); let matrix_user_id = diesel_option_overwrite(&data.matrix_user_id); let bot_account = data.bot_account; + let email_deref = data.email.as_deref().map(|e| e.to_owned()); + let email = diesel_option_overwrite(&email_deref); + + if let Some(Some(email)) = &email { + let previous_email = local_user_view.local_user.email.unwrap_or_default(); + // Only send the verification email if there was an email change + if previous_email.ne(email) { + send_verification_email( + local_user_view.local_user.id, + email, + &local_user_view.person.name, + context.pool(), + &context.settings(), + ) + .await?; + } + } + + // When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value + if let Some(email) = &email { + let site_fut = blocking(context.pool(), Site::read_simple); + if email.is_none() && site_fut.await??.require_email_verification { + return Err(LemmyError::from_message("email_required")); + } + } if let Some(Some(bio)) = &bio { if bio.chars().count() > 300 { @@ -228,9 +266,9 @@ impl Perform for SaveUserSettings { .map_err(|e| e.with_message("user_already_exists"))?; let local_user_form = LocalUserForm { - person_id, + person_id: Some(person_id), email, - password_encrypted, + password_encrypted: Some(password_encrypted), show_nsfw: data.show_nsfw, show_bot_accounts: data.show_bot_accounts, show_scores: data.show_scores, @@ -242,6 +280,8 @@ impl Perform for SaveUserSettings { show_read_posts: data.show_read_posts, show_new_post_notifs: data.show_new_post_notifs, send_notifications_to_email: data.send_notifications_to_email, + email_verified: None, + accepted_application: None, }; let local_user_res = blocking(context.pool(), move |conn| { @@ -265,12 +305,16 @@ impl Perform for SaveUserSettings { // Return the jwt Ok(LoginResponse { - jwt: Claims::jwt( - updated_local_user.id.0, - &context.secret().jwt_secret, - &context.settings().hostname, - )? - .into(), + jwt: Some( + Claims::jwt( + updated_local_user.id.0, + &context.secret().jwt_secret, + &context.settings().hostname, + )? + .into(), + ), + verify_email_sent: false, + registration_created: false, }) } } @@ -315,12 +359,16 @@ impl Perform for ChangePassword { // Return the jwt Ok(LoginResponse { - jwt: Claims::jwt( - updated_local_user.id.0, - &context.secret().jwt_secret, - &context.settings().hostname, - )? - .into(), + jwt: Some( + Claims::jwt( + updated_local_user.id.0, + &context.secret().jwt_secret, + &context.settings().hostname, + )? + .into(), + ), + verify_email_sent: false, + registration_created: false, }) } } @@ -736,34 +784,8 @@ impl Perform for PasswordReset { .map_err(LemmyError::from) .map_err(|e| e.with_message("couldnt_find_that_username_or_email"))?; - // Generate a random token - let token = generate_random_string(); - - // Insert the row - let token2 = token.clone(); - let local_user_id = local_user_view.local_user.id; - blocking(context.pool(), move |conn| { - PasswordResetRequest::create_token(conn, local_user_id, &token2) - }) - .await??; - // Email the pure token to the user. - // TODO no i18n support here. - let email = &local_user_view.local_user.email.expect("email"); - let subject = &format!("Password reset for {}", local_user_view.person.name); - let protocol_and_hostname = &context.settings().get_protocol_and_hostname(); - let html = &format!("

Password Reset Request for {}


Click here to reset your password", local_user_view.person.name, protocol_and_hostname, &token); - send_email( - subject, - email, - &local_user_view.person.name, - html, - &context.settings(), - ) - .map_err(|e| anyhow::anyhow!("{}", e)) - .map_err(LemmyError::from) - .map_err(|e| e.with_message("email_send_failed"))?; - + send_password_reset_email(&local_user_view, context.pool(), &context.settings()).await?; Ok(PasswordResetResponse {}) } } @@ -805,12 +827,16 @@ impl Perform for PasswordChange { // Return the jwt Ok(LoginResponse { - jwt: Claims::jwt( - updated_local_user.id.0, - &context.secret().jwt_secret, - &context.settings().hostname, - )? - .into(), + jwt: Some( + Claims::jwt( + updated_local_user.id.0, + &context.secret().jwt_secret, + &context.settings().hostname, + )? + .into(), + ), + verify_email_sent: false, + registration_created: false, }) } } @@ -893,3 +919,49 @@ impl Perform for GetUnreadCount { Ok(res) } } + +#[async_trait::async_trait(?Send)] +impl Perform for VerifyEmail { + type Response = VerifyEmailResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let token = self.token.clone(); + let verification = blocking(context.pool(), move |conn| { + EmailVerification::read_for_token(conn, &token) + }) + .await? + .map_err(LemmyError::from) + .map_err(|e| e.with_message("token_not_found"))?; + + let form = LocalUserForm { + // necessary in case this is a new signup + email_verified: Some(true), + // necessary in case email of an existing user was changed + email: Some(Some(verification.email)), + ..LocalUserForm::default() + }; + let local_user_id = verification.local_user_id; + blocking(context.pool(), move |conn| { + LocalUser::update(conn, local_user_id, &form) + }) + .await??; + + let local_user_view = blocking(context.pool(), move |conn| { + LocalUserView::read(conn, local_user_id) + }) + .await??; + + send_email_verification_success(&local_user_view, &context.settings())?; + + blocking(context.pool(), move |conn| { + EmailVerification::delete_old_tokens_for_local_user(conn, local_user_id) + }) + .await??; + + Ok(VerifyEmailResponse {}) + } +} diff --git a/crates/api/src/site.rs b/crates/api/src/site.rs index 99906bf69..fdcd91cfe 100644 --- a/crates/api/src/site.rs +++ b/crates/api/src/site.rs @@ -5,9 +5,11 @@ use diesel::NotFound; use lemmy_api_common::{ blocking, build_federated_instances, + check_private_instance, get_local_user_view_from_jwt, get_local_user_view_from_jwt_opt, is_admin, + send_application_approved_email, site::*, }; use lemmy_apub::{ @@ -19,9 +21,15 @@ use lemmy_apub::{ EndpointType, }; use lemmy_db_schema::{ + diesel_option_overwrite, from_opt_str_to_opt_enum, newtypes::PersonId, - source::{moderator::*, site::Site}, + source::{ + local_user::{LocalUser, LocalUserForm}, + moderator::*, + registration_application::{RegistrationApplication, RegistrationApplicationForm}, + site::Site, + }, traits::{Crud, DeleteableOrRemoveable}, DbPool, ListingType, @@ -30,7 +38,12 @@ use lemmy_db_schema::{ }; use lemmy_db_views::{ comment_view::{CommentQueryBuilder, CommentView}, + local_user_view::LocalUserView, post_view::{PostQueryBuilder, PostView}, + registration_application_view::{ + RegistrationApplicationQueryBuilder, + RegistrationApplicationView, + }, site_view::SiteView, }; use lemmy_db_views_actor::{ @@ -64,6 +77,12 @@ impl Perform for GetModlog { ) -> Result { let data: &GetModlog = self; + let local_user_view = + get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) + .await?; + + check_private_instance(&local_user_view, context.pool()).await?; + let community_id = data.community_id; let mod_person_id = data.mod_person_id; let page = data.page; @@ -149,6 +168,8 @@ impl Perform for Search { get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) .await?; + check_private_instance(&local_user_view, context.pool()).await?; + let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw); let show_bot_accounts = local_user_view .as_ref() @@ -388,6 +409,8 @@ impl Perform for ResolveObject { let local_user_view = get_local_user_view_from_jwt_opt(self.auth.as_ref(), context.pool(), context.secret()) .await?; + check_private_instance(&local_user_view, context.pool()).await?; + let res = search_by_apub_id(&self.q, context) .await .map_err(LemmyError::from) @@ -555,3 +578,142 @@ impl Perform for SaveSiteConfig { Ok(GetSiteConfigResponse { config_hjson }) } } + +/// Lists registration applications, filterable by undenied only. +#[async_trait::async_trait(?Send)] +impl Perform for ListRegistrationApplications { + type Response = ListRegistrationApplicationsResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + // Make sure user is an admin + is_admin(&local_user_view)?; + + let unread_only = data.unread_only; + let verified_email_only = blocking(context.pool(), Site::read_simple) + .await?? + .require_email_verification; + + let page = data.page; + let limit = data.limit; + let registration_applications = blocking(context.pool(), move |conn| { + RegistrationApplicationQueryBuilder::create(conn) + .unread_only(unread_only) + .verified_email_only(verified_email_only) + .page(page) + .limit(limit) + .list() + }) + .await??; + + let res = Self::Response { + registration_applications, + }; + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for ApproveRegistrationApplication { + type Response = RegistrationApplicationResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let app_id = data.id; + + // Only let admins do this + is_admin(&local_user_view)?; + + // Update the registration with reason, admin_id + let deny_reason = diesel_option_overwrite(&data.deny_reason); + let app_form = RegistrationApplicationForm { + admin_id: Some(local_user_view.person.id), + deny_reason, + ..RegistrationApplicationForm::default() + }; + + let registration_application = blocking(context.pool(), move |conn| { + RegistrationApplication::update(conn, app_id, &app_form) + }) + .await??; + + // Update the local_user row + let local_user_form = LocalUserForm { + accepted_application: Some(data.approve), + ..LocalUserForm::default() + }; + + let approved_user_id = registration_application.local_user_id; + blocking(context.pool(), move |conn| { + LocalUser::update(conn, approved_user_id, &local_user_form) + }) + .await??; + + if data.approve { + let approved_local_user_view = blocking(context.pool(), move |conn| { + LocalUserView::read(conn, approved_user_id) + }) + .await??; + + if approved_local_user_view.local_user.email.is_some() { + send_application_approved_email(&approved_local_user_view, &context.settings())?; + } + } + + // Read the view + let registration_application = blocking(context.pool(), move |conn| { + RegistrationApplicationView::read(conn, app_id) + }) + .await??; + + Ok(Self::Response { + registration_application, + }) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for GetUnreadRegistrationApplicationCount { + type Response = GetUnreadRegistrationApplicationCountResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + // Only let admins do this + is_admin(&local_user_view)?; + + let verified_email_only = blocking(context.pool(), Site::read_simple) + .await?? + .require_email_verification; + + let registration_applications = blocking(context.pool(), move |conn| { + RegistrationApplicationView::get_unread_count(conn, verified_email_only) + }) + .await??; + + Ok(Self::Response { + registration_applications, + }) + } +} diff --git a/crates/api_common/Cargo.toml b/crates/api_common/Cargo.toml index 1bac4e5cd..602d10cb7 100644 --- a/crates/api_common/Cargo.toml +++ b/crates/api_common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lemmy_api_common" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" edition = "2018" description = "A link aggregator for the fediverse" license = "AGPL-3.0" @@ -13,11 +13,11 @@ path = "src/lib.rs" doctest = false [dependencies] -lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" } -lemmy_db_views_moderator = { version = "=0.14.4-rc.4", path = "../db_views_moderator" } -lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" } -lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" } -lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" } +lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" } +lemmy_db_views_moderator = { version = "=0.15.0-rc.6", path = "../db_views_moderator" } +lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" } +lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" } +lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" } serde = { version = "1.0.131", features = ["derive"] } diesel = "1.4.8" actix-web = { version = "4.0.0-beta.14", default-features = false, features = ["cookies"] } diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index 29e26e1e6..cd854a6d9 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -10,8 +10,11 @@ use lemmy_db_schema::{ newtypes::{CommunityId, LocalUserId, PersonId, PostId}, source::{ community::Community, + email_verification::{EmailVerification, EmailVerificationForm}, + password_reset_request::PasswordResetRequest, person_block::PersonBlock, post::{Post, PostRead, PostReadForm}, + registration_application::RegistrationApplication, secret::Secret, site::Site, }, @@ -23,7 +26,14 @@ use lemmy_db_views_actor::{ community_person_ban_view::CommunityPersonBanView, community_view::CommunityView, }; -use lemmy_utils::{claims::Claims, settings::structs::FederationConfig, LemmyError, Sensitive}; +use lemmy_utils::{ + claims::Claims, + email::send_email, + settings::structs::{FederationConfig, Settings}, + utils::generate_random_string, + LemmyError, + Sensitive, +}; use url::Url; pub async fn blocking(pool: &DbPool, f: F) -> Result @@ -252,6 +262,19 @@ pub async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), Le Ok(()) } +pub async fn check_private_instance( + local_user_view: &Option, + pool: &DbPool, +) -> Result<(), LemmyError> { + if local_user_view.is_none() { + let site = blocking(pool, Site::read_simple).await??; + if site.private_instance { + return Err(LemmyError::from_message("instance_is_private")); + } + } + Ok(()) +} + pub async fn build_federated_instances( pool: &DbPool, federation_config: &FederationConfig, @@ -320,3 +343,163 @@ pub fn honeypot_check(honeypot: &Option) -> Result<(), LemmyError> { Ok(()) } } + +pub fn send_email_to_user( + local_user_view: &LocalUserView, + subject_text: &str, + body_text: &str, + comment_content: &str, + settings: &Settings, +) { + if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email { + return; + } + + if let Some(user_email) = &local_user_view.local_user.email { + let subject = &format!( + "{} - {} {}", + subject_text, settings.hostname, local_user_view.person.name, + ); + let html = &format!( + "

{}


{} - {}

inbox", + body_text, + local_user_view.person.name, + comment_content, + settings.get_protocol_and_hostname() + ); + match send_email( + subject, + user_email, + &local_user_view.person.name, + html, + settings, + ) { + Ok(_o) => _o, + Err(e) => tracing::error!("{}", e), + }; + } +} + +pub async fn send_password_reset_email( + local_user_view: &LocalUserView, + pool: &DbPool, + settings: &Settings, +) -> Result<(), LemmyError> { + // Generate a random token + let token = generate_random_string(); + + // Insert the row + let token2 = token.clone(); + let local_user_id = local_user_view.local_user.id; + blocking(pool, move |conn| { + PasswordResetRequest::create_token(conn, local_user_id, &token2) + }) + .await??; + + let email = &local_user_view.local_user.email.to_owned().expect("email"); + let subject = &format!("Password reset for {}", local_user_view.person.name); + let protocol_and_hostname = settings.get_protocol_and_hostname(); + let html = &format!("

Password Reset Request for {}


Click here to reset your password", local_user_view.person.name, protocol_and_hostname, &token); + send_email(subject, email, &local_user_view.person.name, html, settings) +} + +/// Send a verification email +pub async fn send_verification_email( + local_user_id: LocalUserId, + new_email: &str, + username: &str, + pool: &DbPool, + settings: &Settings, +) -> Result<(), LemmyError> { + let form = EmailVerificationForm { + local_user_id, + email: new_email.to_string(), + verification_token: generate_random_string(), + }; + let verify_link = format!( + "{}/verify_email/{}", + settings.get_protocol_and_hostname(), + &form.verification_token + ); + blocking(pool, move |conn| EmailVerification::create(conn, &form)).await??; + + let subject = format!("Verify your email address for {}", settings.hostname); + let body = format!( + concat!( + "Please click the link below to verify your email address ", + "for the account @{}@{}. Ignore this email if the account isn't yours.

", + "Verify your email" + ), + username, settings.hostname, verify_link + ); + send_email(&subject, new_email, username, &body, settings)?; + + Ok(()) +} + +pub fn send_email_verification_success( + local_user_view: &LocalUserView, + settings: &Settings, +) -> Result<(), LemmyError> { + let email = &local_user_view.local_user.email.to_owned().expect("email"); + let subject = &format!("Email verified for {}", local_user_view.person.actor_id); + let html = "Your email has been verified."; + send_email(subject, email, &local_user_view.person.name, html, settings) +} + +pub fn send_application_approved_email( + local_user_view: &LocalUserView, + settings: &Settings, +) -> Result<(), LemmyError> { + let email = &local_user_view.local_user.email.to_owned().expect("email"); + let subject = &format!( + "Registration approved for {}", + local_user_view.person.actor_id + ); + let html = &format!( + "Your registration application has been approved. Welcome to {}!", + settings.hostname + ); + send_email(subject, email, &local_user_view.person.name, html, settings) +} + +pub async fn check_registration_application( + site: &Site, + local_user_view: &LocalUserView, + pool: &DbPool, +) -> Result<(), LemmyError> { + if site.require_application + && !local_user_view.local_user.accepted_application + && !local_user_view.person.admin + { + // Fetch the registration, see if its denied + let local_user_id = local_user_view.local_user.id; + let registration = blocking(pool, move |conn| { + RegistrationApplication::find_by_local_user_id(conn, local_user_id) + }) + .await??; + if registration.deny_reason.is_some() { + return Err(LemmyError::from_message("registration_denied")); + } else { + return Err(LemmyError::from_message("registration_application_pending")); + } + } + Ok(()) +} + +/// TODO this check should be removed after https://github.com/LemmyNet/lemmy/issues/868 is done. +pub async fn check_private_instance_and_federation_enabled( + pool: &DbPool, + settings: &Settings, +) -> Result<(), LemmyError> { + let site_opt = blocking(pool, Site::read_simple).await?; + + if let Ok(site) = site_opt { + if site.private_instance && settings.federation.enabled { + return Err(LemmyError::from_message( + "Cannot have both private instance and federation enabled.", + )); + } + } + Ok(()) +} diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index 5dddc5dcc..47c26591e 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -24,10 +24,13 @@ pub struct Register { pub password: Sensitive, pub password_verify: Sensitive, pub show_nsfw: bool, + /// email is mandatory if email verification is enabled on the server pub email: Option>, pub captcha_uuid: Option, pub captcha_answer: Option, pub honeypot: Option, + /// An answer is mandatory if require application is enabled on the server + pub answer: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -78,7 +81,10 @@ pub struct ChangePassword { #[derive(Debug, Serialize, Deserialize)] pub struct LoginResponse { - pub jwt: Sensitive, + /// This is None in response to `Register` if email verification is enabled, or the server requires registration applications. + pub jwt: Option>, + pub registration_created: bool, + pub verify_email_sent: bool, } #[derive(Debug, Serialize, Deserialize)] @@ -194,6 +200,9 @@ pub struct DeleteAccount { pub auth: Sensitive, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DeleteAccountResponse {} + #[derive(Debug, Serialize, Deserialize)] pub struct PasswordReset { pub email: Sensitive, @@ -279,3 +288,11 @@ pub struct GetUnreadCountResponse { pub mentions: i64, pub private_messages: i64, } + +#[derive(Serialize, Deserialize)] +pub struct VerifyEmail { + pub token: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct VerifyEmailResponse {} diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index ff53cb513..b53b99d4f 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -3,6 +3,7 @@ use lemmy_db_views::{ comment_view::CommentView, local_user_view::LocalUserSettingsView, post_view::PostView, + registration_application_view::RegistrationApplicationView, site_view::SiteView, }; use lemmy_db_views_actor::{ @@ -71,6 +72,7 @@ pub struct GetModlog { pub community_id: Option, pub page: Option, pub limit: Option, + pub auth: Option>, } #[derive(Debug, Serialize, Deserialize)] @@ -98,6 +100,10 @@ pub struct CreateSite { pub open_registration: Option, pub enable_nsfw: Option, pub community_creation_admin_only: Option, + pub require_email_verification: Option, + pub require_application: Option, + pub application_question: Option, + pub private_instance: Option, pub auth: Sensitive, } @@ -112,6 +118,10 @@ pub struct EditSite { pub open_registration: Option, pub enable_nsfw: Option, pub community_creation_admin_only: Option, + pub require_email_verification: Option, + pub require_application: Option, + pub application_question: Option, + pub private_instance: Option, pub auth: Sensitive, } @@ -173,3 +183,40 @@ pub struct FederatedInstances { pub allowed: Option>, pub blocked: Option>, } + +#[derive(Serialize, Deserialize)] +pub struct ListRegistrationApplications { + /// Only shows the unread applications (IE those without an admin actor) + pub unread_only: Option, + pub page: Option, + pub limit: Option, + pub auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct ListRegistrationApplicationsResponse { + pub registration_applications: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct ApproveRegistrationApplication { + pub id: i32, + pub approve: bool, + pub deny_reason: Option, + pub auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct RegistrationApplicationResponse { + pub registration_application: RegistrationApplicationView, +} + +#[derive(Serialize, Deserialize)] +pub struct GetUnreadRegistrationApplicationCount { + pub auth: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct GetUnreadRegistrationApplicationCountResponse { + pub registration_applications: i64, +} diff --git a/crates/api_crud/Cargo.toml b/crates/api_crud/Cargo.toml index cbbbe4b08..659385cd8 100644 --- a/crates/api_crud/Cargo.toml +++ b/crates/api_crud/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lemmy_api_crud" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" edition = "2018" description = "A link aggregator for the fediverse" license = "AGPL-3.0" @@ -8,15 +8,15 @@ homepage = "https://join-lemmy.org/" documentation = "https://join-lemmy.org/docs/en/index.html" [dependencies] -lemmy_apub = { version = "=0.14.4-rc.4", path = "../apub" } -lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "../apub_lib" } -lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" } -lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" } -lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" } -lemmy_db_views_moderator = { version = "=0.14.4-rc.4", path = "../db_views_moderator" } -lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" } -lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" } -lemmy_websocket = { version = "=0.14.4-rc.4", path = "../websocket" } +lemmy_apub = { version = "=0.15.0-rc.6", path = "../apub" } +lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "../apub_lib" } +lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" } +lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" } +lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" } +lemmy_db_views_moderator = { version = "=0.15.0-rc.6", path = "../db_views_moderator" } +lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" } +lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" } +lemmy_websocket = { version = "=0.15.0-rc.6", path = "../websocket" } diesel = "1.4.8" bcrypt = "0.10.1" chrono = { version = "0.4.19", features = ["serde"] } diff --git a/crates/api_crud/src/comment/read.rs b/crates/api_crud/src/comment/read.rs index bd1f475fd..459469917 100644 --- a/crates/api_crud/src/comment/read.rs +++ b/crates/api_crud/src/comment/read.rs @@ -1,6 +1,11 @@ use crate::PerformCrud; use actix_web::web::Data; -use lemmy_api_common::{blocking, comment::*, get_local_user_view_from_jwt_opt}; +use lemmy_api_common::{ + blocking, + check_private_instance, + comment::*, + get_local_user_view_from_jwt_opt, +}; use lemmy_apub::{ fetcher::webfinger::webfinger_resolve, objects::community::ApubCommunity, @@ -31,6 +36,8 @@ impl PerformCrud for GetComment { get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) .await?; + check_private_instance(&local_user_view, context.pool()).await?; + let person_id = local_user_view.map(|u| u.person.id); let id = data.id; let comment_view = blocking(context.pool(), move |conn| { @@ -63,6 +70,8 @@ impl PerformCrud for GetComments { get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) .await?; + check_private_instance(&local_user_view, context.pool()).await?; + let show_bot_accounts = local_user_view .as_ref() .map(|t| t.local_user.show_bot_accounts); diff --git a/crates/api_crud/src/community/read.rs b/crates/api_crud/src/community/read.rs index 32ccf73d0..2ec4054b2 100644 --- a/crates/api_crud/src/community/read.rs +++ b/crates/api_crud/src/community/read.rs @@ -1,6 +1,11 @@ use crate::PerformCrud; use actix_web::web::Data; -use lemmy_api_common::{blocking, community::*, get_local_user_view_from_jwt_opt}; +use lemmy_api_common::{ + blocking, + check_private_instance, + community::*, + get_local_user_view_from_jwt_opt, +}; use lemmy_apub::{ fetcher::webfinger::webfinger_resolve, objects::community::ApubCommunity, @@ -34,6 +39,9 @@ impl PerformCrud for GetCommunity { let local_user_view = get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) .await?; + + check_private_instance(&local_user_view, context.pool()).await?; + let person_id = local_user_view.map(|u| u.person.id); let community_id = match data.id { @@ -105,6 +113,8 @@ impl PerformCrud for ListCommunities { get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) .await?; + check_private_instance(&local_user_view, context.pool()).await?; + let person_id = local_user_view.to_owned().map(|l| l.person.id); // Don't show NSFW by default diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs index 42e84254e..10ecefc38 100644 --- a/crates/api_crud/src/post/read.rs +++ b/crates/api_crud/src/post/read.rs @@ -1,6 +1,12 @@ use crate::PerformCrud; use actix_web::web::Data; -use lemmy_api_common::{blocking, get_local_user_view_from_jwt_opt, mark_post_as_read, post::*}; +use lemmy_api_common::{ + blocking, + check_private_instance, + get_local_user_view_from_jwt_opt, + mark_post_as_read, + post::*, +}; use lemmy_apub::{ fetcher::webfinger::webfinger_resolve, objects::community::ApubCommunity, @@ -38,6 +44,8 @@ impl PerformCrud for GetPost { get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) .await?; + check_private_instance(&local_user_view, context.pool()).await?; + let show_bot_accounts = local_user_view .as_ref() .map(|t| t.local_user.show_bot_accounts); @@ -130,6 +138,8 @@ impl PerformCrud for GetPosts { get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) .await?; + check_private_instance(&local_user_view, context.pool()).await?; + let person_id = local_user_view.to_owned().map(|l| l.person.id); let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw); diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs index ae75d9eaf..54edc2408 100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@ -5,6 +5,7 @@ use lemmy_api_common::{ check_person_block, get_local_user_view_from_jwt, person::{CreatePrivateMessage, PrivateMessageResponse}, + send_email_to_user, }; use lemmy_apub::{ generate_local_apub_endpoint, @@ -20,11 +21,7 @@ use lemmy_db_schema::{ }; use lemmy_db_views::local_user_view::LocalUserView; use lemmy_utils::{utils::remove_slurs, ConnectionId, LemmyError}; -use lemmy_websocket::{ - send::{send_email_to_user, send_pm_ws_message}, - LemmyContext, - UserOperationCrud, -}; +use lemmy_websocket::{send::send_pm_ws_message, LemmyContext, UserOperationCrud}; #[async_trait::async_trait(?Send)] impl PerformCrud for CreatePrivateMessage { diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index f638eb7f5..043a31c2b 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -66,8 +66,8 @@ impl PerformCrud for CreateSite { enable_downvotes: data.enable_downvotes, open_registration: data.open_registration, enable_nsfw: data.enable_nsfw, - updated: None, community_creation_admin_only: data.community_creation_admin_only, + ..SiteForm::default() }; let create_site = move |conn: &'_ _| Site::create(conn, &site_form); diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index 4410d66ad..06146b96f 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -45,8 +45,13 @@ impl PerformCrud for GetSite { captcha_uuid: None, captcha_answer: None, honeypot: None, + answer: None, }; - let login_response = register.perform(context, websocket_id).await?; + let admin_jwt = register + .perform(context, websocket_id) + .await? + .jwt + .expect("jwt is returned from registration on newly created site"); info!("Admin {} created", setup.admin_username); let create_site = CreateSite { @@ -59,7 +64,11 @@ impl PerformCrud for GetSite { open_registration: setup.open_registration, enable_nsfw: setup.enable_nsfw, community_creation_admin_only: setup.community_creation_admin_only, - auth: login_response.jwt, + require_email_verification: setup.require_email_verification, + require_application: setup.require_application, + application_question: setup.application_question.to_owned(), + private_instance: setup.private_instance, + auth: admin_jwt, }; create_site.perform(context, websocket_id).await?; info!("Site {} created", setup.site_name); diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index 21dc50a25..bcd6a1a32 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -11,7 +11,10 @@ use lemmy_db_schema::{ diesel_option_overwrite, diesel_option_overwrite_to_url, naive_now, - source::site::{Site, SiteForm}, + source::{ + local_user::LocalUser, + site::{Site, SiteForm}, + }, traits::Crud, }; use lemmy_db_views::site_view::SiteView; @@ -42,6 +45,7 @@ impl PerformCrud for EditSite { let sidebar = diesel_option_overwrite(&data.sidebar); let description = diesel_option_overwrite(&data.description); + let application_question = diesel_option_overwrite(&data.application_question); let icon = diesel_option_overwrite_to_url(&data.icon)?; let banner = diesel_option_overwrite_to_url(&data.banner)?; @@ -61,13 +65,41 @@ impl PerformCrud for EditSite { open_registration: data.open_registration, enable_nsfw: data.enable_nsfw, community_creation_admin_only: data.community_creation_admin_only, + require_email_verification: data.require_email_verification, + require_application: data.require_application, + application_question, + private_instance: data.private_instance, }; - let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form); - blocking(context.pool(), update_site) + let update_site = blocking(context.pool(), move |conn| { + Site::update(conn, 1, &site_form) + }) + .await? + .map_err(LemmyError::from) + .map_err(|e| e.with_message("couldnt_update_site"))?; + + // TODO can't think of a better way to do this. + // If the server suddenly requires email verification, or required applications, no old users + // will be able to log in. It really only wants this to be a requirement for NEW signups. + // So if it was set from false, to true, you need to update all current users columns to be verified. + + if !found_site.require_application && update_site.require_application { + blocking(context.pool(), move |conn| { + LocalUser::set_all_users_registration_applications_accepted(conn) + }) .await? .map_err(LemmyError::from) - .map_err(|e| e.with_message("couldnt_update_site"))?; + .map_err(|e| e.with_message("couldnt_set_all_registrations_accepted"))?; + } + + if !found_site.require_email_verification && update_site.require_email_verification { + blocking(context.pool(), move |conn| { + LocalUser::set_all_users_email_verified(conn) + }) + .await? + .map_err(LemmyError::from) + .map_err(|e| e.with_message("couldnt_set_all_email_verified"))?; + } let site_view = blocking(context.pool(), SiteView::read).await??; diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index a9ea98619..6f8a5fa0e 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -1,6 +1,12 @@ use crate::PerformCrud; use actix_web::web::Data; -use lemmy_api_common::{blocking, honeypot_check, password_length_check, person::*}; +use lemmy_api_common::{ + blocking, + honeypot_check, + password_length_check, + person::*, + send_verification_email, +}; use lemmy_apub::{ generate_followers_url, generate_inbox_url, @@ -21,11 +27,10 @@ use lemmy_db_schema::{ }, local_user::{LocalUser, LocalUserForm}, person::{Person, PersonForm}, + registration_application::{RegistrationApplication, RegistrationApplicationForm}, site::Site, }, traits::{Crud, Followable, Joinable}, - ListingType, - SortType, }; use lemmy_db_views_actor::person_view::PersonViewSafe; use lemmy_utils::{ @@ -49,16 +54,31 @@ impl PerformCrud for Register { ) -> Result { let data: &Register = self; + // no email verification, or applications if the site is not setup yet + let (mut email_verification, mut require_application) = (false, false); + // Make sure site has open registration if let Ok(site) = blocking(context.pool(), Site::read_simple).await? { if !site.open_registration { return Err(LemmyError::from_message("registration_closed")); } + email_verification = site.require_email_verification; + require_application = site.require_application; } password_length_check(&data.password)?; honeypot_check(&data.honeypot)?; + if email_verification && data.email.is_none() { + return Err(LemmyError::from_message("email_required")); + } + + if require_application && data.answer.is_none() { + return Err(LemmyError::from_message( + "registration_application_answer_required", + )); + } + // Make sure passwords match if data.password != data.password_verify { return Err(LemmyError::from_message("passwords_dont_match")); @@ -125,22 +145,13 @@ impl PerformCrud for Register { .map_err(|e| e.with_message("user_already_exists"))?; // Create the local user - // TODO some of these could probably use the DB defaults let local_user_form = LocalUserForm { - person_id: inserted_person.id, + person_id: Some(inserted_person.id), email: Some(data.email.as_deref().map(|s| s.to_owned())), - password_encrypted: data.password.to_string(), + password_encrypted: Some(data.password.to_string()), show_nsfw: Some(data.show_nsfw), - show_bot_accounts: Some(true), - theme: Some("browser".into()), - default_sort_type: Some(SortType::Active as i16), - default_listing_type: Some(ListingType::Subscribed as i16), - lang: Some("browser".into()), - show_avatars: Some(true), - show_scores: Some(true), - show_read_posts: Some(true), - show_new_post_notifs: Some(false), - send_notifications_to_email: Some(false), + email_verified: Some(false), + ..LocalUserForm::default() }; let inserted_local_user = match blocking(context.pool(), move |conn| { @@ -168,6 +179,21 @@ impl PerformCrud for Register { } }; + if require_application { + // Create the registration application + let form = RegistrationApplicationForm { + local_user_id: Some(inserted_local_user.id), + // We already made sure answer was not null above + answer: data.answer.to_owned(), + ..RegistrationApplicationForm::default() + }; + + blocking(context.pool(), move |conn| { + RegistrationApplication::create(conn, &form) + }) + .await??; + } + let main_community_keypair = generate_actor_keypair()?; // Create the main community if it doesn't exist @@ -231,14 +257,41 @@ impl PerformCrud for Register { .map_err(|e| e.with_message("community_moderator_already_exists"))?; } - // Return the jwt - Ok(LoginResponse { - jwt: Claims::jwt( - inserted_local_user.id.0, - &context.secret().jwt_secret, - &context.settings().hostname, - )? - .into(), - }) + let mut login_response = LoginResponse { + jwt: None, + registration_created: false, + verify_email_sent: false, + }; + + // Log the user in directly if email verification and application aren't required + if !require_application && !email_verification { + login_response.jwt = Some( + Claims::jwt( + inserted_local_user.id.0, + &context.secret().jwt_secret, + &context.settings().hostname, + )? + .into(), + ); + } else { + if email_verification { + send_verification_email( + inserted_local_user.id, + // we check at the beginning of this method that email is set + &inserted_local_user.email.expect("email was provided"), + &inserted_person.name, + context.pool(), + &context.settings(), + ) + .await?; + login_response.verify_email_sent = true; + } + + if require_application { + login_response.registration_created = true; + } + } + + Ok(login_response) } } diff --git a/crates/api_crud/src/user/delete.rs b/crates/api_crud/src/user/delete.rs index 0400ffe06..c3977e726 100644 --- a/crates/api_crud/src/user/delete.rs +++ b/crates/api_crud/src/user/delete.rs @@ -8,15 +8,15 @@ use lemmy_websocket::LemmyContext; #[async_trait::async_trait(?Send)] impl PerformCrud for DeleteAccount { - type Response = LoginResponse; + type Response = DeleteAccountResponse; #[tracing::instrument(skip(self, context, _websocket_id))] async fn perform( &self, context: &Data, _websocket_id: Option, - ) -> Result { - let data: &DeleteAccount = self; + ) -> Result { + let data = self; let local_user_view = get_local_user_view_from_jwt(data.auth.as_ref(), context.pool(), context.secret()).await?; @@ -50,8 +50,6 @@ impl PerformCrud for DeleteAccount { }) .await??; - Ok(LoginResponse { - jwt: data.auth.clone(), - }) + Ok(DeleteAccountResponse {}) } } diff --git a/crates/api_crud/src/user/read.rs b/crates/api_crud/src/user/read.rs index 20864ab6c..efa058b18 100644 --- a/crates/api_crud/src/user/read.rs +++ b/crates/api_crud/src/user/read.rs @@ -1,6 +1,11 @@ use crate::PerformCrud; use actix_web::web::Data; -use lemmy_api_common::{blocking, get_local_user_view_from_jwt_opt, person::*}; +use lemmy_api_common::{ + blocking, + check_private_instance, + get_local_user_view_from_jwt_opt, + person::*, +}; use lemmy_apub::{ fetcher::webfinger::webfinger_resolve, objects::person::ApubPerson, @@ -31,6 +36,8 @@ impl PerformCrud for GetPersonDetails { get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) .await?; + check_private_instance(&local_user_view, context.pool()).await?; + let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw); let show_bot_accounts = local_user_view .as_ref() diff --git a/crates/apub/Cargo.toml b/crates/apub/Cargo.toml index ac1552446..d02a41344 100644 --- a/crates/apub/Cargo.toml +++ b/crates/apub/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lemmy_apub" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" edition = "2018" description = "A link aggregator for the fediverse" license = "AGPL-3.0" @@ -13,13 +13,13 @@ path = "src/lib.rs" doctest = false [dependencies] -lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" } -lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "../apub_lib" } -lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" } -lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" } -lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" } -lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" } -lemmy_websocket = { version = "=0.14.4-rc.4", path = "../websocket" } +lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" } +lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "../apub_lib" } +lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" } +lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" } +lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" } +lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" } +lemmy_websocket = { version = "=0.15.0-rc.6", path = "../websocket" } diesel = "1.4.8" activitystreams-kinds = "0.1.2" bcrypt = "0.10.1" diff --git a/crates/apub_lib/Cargo.toml b/crates/apub_lib/Cargo.toml index c65821a5b..a46d3c356 100644 --- a/crates/apub_lib/Cargo.toml +++ b/crates/apub_lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lemmy_apub_lib" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" edition = "2018" description = "A link aggregator for the fediverse" license = "AGPL-3.0" @@ -8,8 +8,8 @@ homepage = "https://join-lemmy.org/" documentation = "https://join-lemmy.org/docs/en/index.html" [dependencies] -lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" } -lemmy_apub_lib_derive = { version = "=0.14.4-rc.4", path = "../apub_lib_derive" } +lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" } +lemmy_apub_lib_derive = { version = "=0.15.0-rc.6", path = "../apub_lib_derive" } activitystreams = "0.7.0-alpha.14" serde = { version = "1.0.131", features = ["derive"] } async-trait = "0.1.52" diff --git a/crates/apub_lib_derive/Cargo.toml b/crates/apub_lib_derive/Cargo.toml index c0c47219f..2b2faf2c8 100644 --- a/crates/apub_lib_derive/Cargo.toml +++ b/crates/apub_lib_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lemmy_apub_lib_derive" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" edition = "2018" description = "A link aggregator for the fediverse" license = "AGPL-3.0" diff --git a/crates/db_schema/Cargo.toml b/crates/db_schema/Cargo.toml index 21e4a1b9b..e6b795e9c 100644 --- a/crates/db_schema/Cargo.toml +++ b/crates/db_schema/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lemmy_db_schema" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" edition = "2018" description = "A link aggregator for the fediverse" license = "AGPL-3.0" @@ -11,8 +11,8 @@ documentation = "https://join-lemmy.org/docs/en/index.html" doctest = false [dependencies] -lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" } -lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "../apub_lib" } +lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" } +lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "../apub_lib" } diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"] } diesel_migrations = "1.4.0" chrono = { version = "0.4.19", features = ["serde"] } diff --git a/crates/db_schema/src/aggregates/site_aggregates.rs b/crates/db_schema/src/aggregates/site_aggregates.rs index 4b4b6c432..08d4dd01e 100644 --- a/crates/db_schema/src/aggregates/site_aggregates.rs +++ b/crates/db_schema/src/aggregates/site_aggregates.rs @@ -65,6 +65,10 @@ mod tests { enable_nsfw: None, updated: None, community_creation_admin_only: Some(false), + require_email_verification: None, + require_application: None, + application_question: None, + private_instance: None, }; Site::create(&conn, &site_form).unwrap(); diff --git a/crates/db_schema/src/impls/email_verification.rs b/crates/db_schema/src/impls/email_verification.rs new file mode 100644 index 000000000..c27037391 --- /dev/null +++ b/crates/db_schema/src/impls/email_verification.rs @@ -0,0 +1,55 @@ +use crate::{newtypes::LocalUserId, source::email_verification::*, traits::Crud}; +use diesel::{ + dsl::*, + insert_into, + result::Error, + ExpressionMethods, + PgConnection, + QueryDsl, + RunQueryDsl, +}; + +impl Crud for EmailVerification { + type Form = EmailVerificationForm; + type IdType = i32; + fn create(conn: &PgConnection, form: &EmailVerificationForm) -> Result { + use crate::schema::email_verification::dsl::*; + insert_into(email_verification) + .values(form) + .get_result::(conn) + } + + fn read(conn: &PgConnection, id_: i32) -> Result { + use crate::schema::email_verification::dsl::*; + email_verification.find(id_).first::(conn) + } + + fn update(conn: &PgConnection, id_: i32, form: &EmailVerificationForm) -> Result { + use crate::schema::email_verification::dsl::*; + diesel::update(email_verification.find(id_)) + .set(form) + .get_result::(conn) + } + + fn delete(conn: &PgConnection, id_: i32) -> Result { + use crate::schema::email_verification::dsl::*; + diesel::delete(email_verification.find(id_)).execute(conn) + } +} + +impl EmailVerification { + pub fn read_for_token(conn: &PgConnection, token: &str) -> Result { + use crate::schema::email_verification::dsl::*; + email_verification + .filter(verification_token.eq(token)) + .filter(published.gt(now - 7.days())) + .first::(conn) + } + pub fn delete_old_tokens_for_local_user( + conn: &PgConnection, + local_user_id_: LocalUserId, + ) -> Result { + use crate::schema::email_verification::dsl::*; + diesel::delete(email_verification.filter(local_user_id.eq(local_user_id_))).execute(conn) + } +} diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index 3a2d57695..833d6bdb9 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -31,6 +31,8 @@ mod safe_settings_type { show_scores, show_read_posts, show_new_post_notifs, + email_verified, + accepted_application, ); impl ToSafeSettings for LocalUser { @@ -54,6 +56,8 @@ mod safe_settings_type { show_scores, show_read_posts, show_new_post_notifs, + email_verified, + accepted_application, ) } } @@ -62,8 +66,10 @@ mod safe_settings_type { impl LocalUser { pub fn register(conn: &PgConnection, form: &LocalUserForm) -> Result { let mut edited_user = form.clone(); - let password_hash = - hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password"); + let password_hash = form + .password_encrypted + .as_ref() + .map(|p| hash(p, DEFAULT_COST).expect("Couldn't hash password")); edited_user.password_encrypted = password_hash; Self::create(conn, &edited_user) @@ -83,6 +89,20 @@ impl LocalUser { )) .get_result::(conn) } + + pub fn set_all_users_email_verified(conn: &PgConnection) -> Result, Error> { + diesel::update(local_user) + .set(email_verified.eq(true)) + .get_results::(conn) + } + + pub fn set_all_users_registration_applications_accepted( + conn: &PgConnection, + ) -> Result, Error> { + diesel::update(local_user) + .set(accepted_application.eq(true)) + .get_results::(conn) + } } impl Crud for LocalUser { diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index a1e45efa5..c96e3a623 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -3,6 +3,7 @@ pub mod comment; pub mod comment_report; pub mod community; pub mod community_block; +pub mod email_verification; pub mod local_user; pub mod moderator; pub mod password_reset_request; @@ -12,5 +13,6 @@ pub mod person_mention; pub mod post; pub mod post_report; pub mod private_message; +pub mod registration_application; pub mod secret; pub mod site; diff --git a/crates/db_schema/src/impls/password_reset_request.rs b/crates/db_schema/src/impls/password_reset_request.rs index c5debd926..808f0ac01 100644 --- a/crates/db_schema/src/impls/password_reset_request.rs +++ b/crates/db_schema/src/impls/password_reset_request.rs @@ -93,8 +93,8 @@ mod tests { let inserted_person = Person::create(&conn, &new_person).unwrap(); let new_local_user = LocalUserForm { - person_id: inserted_person.id, - password_encrypted: "pass".to_string(), + person_id: Some(inserted_person.id), + password_encrypted: Some("pass".to_string()), ..LocalUserForm::default() }; diff --git a/crates/db_schema/src/impls/registration_application.rs b/crates/db_schema/src/impls/registration_application.rs new file mode 100644 index 000000000..5147dbb7a --- /dev/null +++ b/crates/db_schema/src/impls/registration_application.rs @@ -0,0 +1,42 @@ +use crate::{newtypes::LocalUserId, source::registration_application::*, traits::Crud}; +use diesel::{insert_into, result::Error, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl}; + +impl Crud for RegistrationApplication { + type Form = RegistrationApplicationForm; + type IdType = i32; + fn create(conn: &PgConnection, form: &Self::Form) -> Result { + use crate::schema::registration_application::dsl::*; + insert_into(registration_application) + .values(form) + .get_result::(conn) + } + + fn read(conn: &PgConnection, id_: Self::IdType) -> Result { + use crate::schema::registration_application::dsl::*; + registration_application.find(id_).first::(conn) + } + + fn update(conn: &PgConnection, id_: Self::IdType, form: &Self::Form) -> Result { + use crate::schema::registration_application::dsl::*; + diesel::update(registration_application.find(id_)) + .set(form) + .get_result::(conn) + } + + fn delete(conn: &PgConnection, id_: Self::IdType) -> Result { + use crate::schema::registration_application::dsl::*; + diesel::delete(registration_application.find(id_)).execute(conn) + } +} + +impl RegistrationApplication { + pub fn find_by_local_user_id( + conn: &PgConnection, + local_user_id_: LocalUserId, + ) -> Result { + use crate::schema::registration_application::dsl::*; + registration_application + .filter(local_user_id.eq(local_user_id_)) + .first::(conn) + } +} diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index 9219d77f9..b863a2500 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -43,7 +43,9 @@ impl fmt::Display for CommentId { )] pub struct CommunityId(pub i32); -#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)] +#[derive( + Debug, Copy, Clone, Hash, Eq, PartialEq, Default, Serialize, Deserialize, DieselNewType, +)] pub struct LocalUserId(pub i32); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)] diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 034edbe9d..89ba5a113 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -157,6 +157,8 @@ table! { show_scores -> Bool, show_read_posts -> Bool, show_new_post_notifs -> Bool, + email_verified -> Bool, + accepted_application -> Bool, } } @@ -447,6 +449,10 @@ table! { banner -> Nullable, description -> Nullable, community_creation_admin_only -> Bool, + require_email_verification -> Bool, + require_application -> Bool, + application_question -> Nullable, + private_instance -> Bool, } } @@ -558,6 +564,27 @@ table! { } } +table! { + email_verification (id) { + id -> Int4, + local_user_id -> Int4, + email -> Text, + verification_token -> Varchar, + published -> Timestamp, + } +} + +table! { + registration_application (id) { + id -> Int4, + local_user_id -> Int4, + answer -> Text, + admin_id -> Nullable, + deny_reason -> Nullable, + published -> Timestamp, + } +} + joinable!(comment_alias_1 -> person_alias_1 (creator_id)); joinable!(comment -> comment_alias_1 (parent_id)); joinable!(person_mention -> person_alias_1 (recipient_id)); @@ -619,6 +646,9 @@ joinable!(post_saved -> person (person_id)); joinable!(post_saved -> post (post_id)); joinable!(site -> person (creator_id)); joinable!(site_aggregates -> site (site_id)); +joinable!(email_verification -> local_user (local_user_id)); +joinable!(registration_application -> local_user (local_user_id)); +joinable!(registration_application -> person (admin_id)); allow_tables_to_appear_in_same_query!( activity, @@ -662,4 +692,6 @@ allow_tables_to_appear_in_same_query!( comment_alias_1, person_alias_1, person_alias_2, + email_verification, + registration_application ); diff --git a/crates/db_schema/src/source/email_verification.rs b/crates/db_schema/src/source/email_verification.rs new file mode 100644 index 000000000..e36f29012 --- /dev/null +++ b/crates/db_schema/src/source/email_verification.rs @@ -0,0 +1,19 @@ +use crate::{newtypes::LocalUserId, schema::email_verification}; + +#[derive(Queryable, Identifiable, Clone)] +#[table_name = "email_verification"] +pub struct EmailVerification { + pub id: i32, + pub local_user_id: LocalUserId, + pub email: String, + pub verification_code: String, + pub published: chrono::NaiveDateTime, +} + +#[derive(Insertable, AsChangeset)] +#[table_name = "email_verification"] +pub struct EmailVerificationForm { + pub local_user_id: LocalUserId, + pub email: String, + pub verification_token: String, +} diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index 34dd26f76..88defa6db 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -23,14 +23,16 @@ pub struct LocalUser { pub show_scores: bool, pub show_read_posts: bool, pub show_new_post_notifs: bool, + pub email_verified: bool, + pub accepted_application: bool, } // TODO redo these, check table defaults #[derive(Insertable, AsChangeset, Clone, Default)] #[table_name = "local_user"] pub struct LocalUserForm { - pub person_id: PersonId, - pub password_encrypted: String, + pub person_id: Option, + pub password_encrypted: Option, pub email: Option>, pub show_nsfw: Option, pub theme: Option, @@ -43,6 +45,8 @@ pub struct LocalUserForm { pub show_scores: Option, pub show_read_posts: Option, pub show_new_post_notifs: Option, + pub email_verified: Option, + pub accepted_application: Option, } /// A local user view that removes password encrypted @@ -64,4 +68,6 @@ pub struct LocalUserSettings { pub show_scores: bool, pub show_read_posts: bool, pub show_new_post_notifs: bool, + pub email_verified: bool, + pub accepted_application: bool, } diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index a1e45efa5..c96e3a623 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -3,6 +3,7 @@ pub mod comment; pub mod comment_report; pub mod community; pub mod community_block; +pub mod email_verification; pub mod local_user; pub mod moderator; pub mod password_reset_request; @@ -12,5 +13,6 @@ pub mod person_mention; pub mod post; pub mod post_report; pub mod private_message; +pub mod registration_application; pub mod secret; pub mod site; diff --git a/crates/db_schema/src/source/registration_application.rs b/crates/db_schema/src/source/registration_application.rs new file mode 100644 index 000000000..01f702d81 --- /dev/null +++ b/crates/db_schema/src/source/registration_application.rs @@ -0,0 +1,25 @@ +use crate::{ + newtypes::{LocalUserId, PersonId}, + schema::registration_application, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] +#[table_name = "registration_application"] +pub struct RegistrationApplication { + pub id: i32, + pub local_user_id: LocalUserId, + pub answer: String, + pub admin_id: Option, + pub deny_reason: Option, + pub published: chrono::NaiveDateTime, +} + +#[derive(Insertable, AsChangeset, Default)] +#[table_name = "registration_application"] +pub struct RegistrationApplicationForm { + pub local_user_id: Option, + pub answer: Option, + pub admin_id: Option, + pub deny_reason: Option>, +} diff --git a/crates/db_schema/src/source/site.rs b/crates/db_schema/src/source/site.rs index dd273f9da..f99ffd887 100644 --- a/crates/db_schema/src/source/site.rs +++ b/crates/db_schema/src/source/site.rs @@ -20,9 +20,13 @@ pub struct Site { pub banner: Option, pub description: Option, pub community_creation_admin_only: bool, + pub require_email_verification: bool, + pub require_application: bool, + pub application_question: Option, + pub private_instance: bool, } -#[derive(Insertable, AsChangeset)] +#[derive(Insertable, AsChangeset, Default)] #[table_name = "site"] pub struct SiteForm { pub name: String, @@ -37,4 +41,8 @@ pub struct SiteForm { pub banner: Option>, pub description: Option>, pub community_creation_admin_only: Option, + pub require_email_verification: Option, + pub require_application: Option, + pub application_question: Option>, + pub private_instance: Option, } diff --git a/crates/db_views/Cargo.toml b/crates/db_views/Cargo.toml index 53302daf0..ec2f7077d 100644 --- a/crates/db_views/Cargo.toml +++ b/crates/db_views/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lemmy_db_views" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" edition = "2018" description = "A link aggregator for the fediverse" license = "AGPL-3.0" @@ -11,7 +11,7 @@ documentation = "https://join-lemmy.org/docs/en/index.html" doctest = false [dependencies] -lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" } +lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" } diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"] } serde = { version = "1.0.131", features = ["derive"] } tracing = "0.1.29" diff --git a/crates/db_views/src/lib.rs b/crates/db_views/src/lib.rs index 54435c1e2..cb9fefcfd 100644 --- a/crates/db_views/src/lib.rs +++ b/crates/db_views/src/lib.rs @@ -7,4 +7,5 @@ pub mod local_user_view; pub mod post_report_view; pub mod post_view; pub mod private_message_view; +pub mod registration_application_view; pub mod site_view; diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs new file mode 100644 index 000000000..1a5fc9bbb --- /dev/null +++ b/crates/db_views/src/registration_application_view.rs @@ -0,0 +1,396 @@ +use diesel::{dsl::count, result::Error, *}; +use lemmy_db_schema::{ + limit_and_offset, + schema::{local_user, person, person_alias_1, registration_application}, + source::{ + local_user::{LocalUser, LocalUserSettings}, + person::{Person, PersonAlias1, PersonSafe, PersonSafeAlias1}, + registration_application::RegistrationApplication, + }, + traits::{MaybeOptional, ToSafe, ToSafeSettings, ViewToVec}, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +pub struct RegistrationApplicationView { + pub registration_application: RegistrationApplication, + pub creator_local_user: LocalUserSettings, + pub creator: PersonSafe, + pub admin: Option, +} + +type RegistrationApplicationViewTuple = ( + RegistrationApplication, + LocalUserSettings, + PersonSafe, + Option, +); + +impl RegistrationApplicationView { + pub fn read(conn: &PgConnection, registration_application_id: i32) -> Result { + let (registration_application, creator_local_user, creator, admin) = + registration_application::table + .find(registration_application_id) + .inner_join( + local_user::table.on(registration_application::local_user_id.eq(local_user::id)), + ) + .inner_join(person::table.on(local_user::person_id.eq(person::id))) + .left_join( + person_alias_1::table + .on(registration_application::admin_id.eq(person_alias_1::id.nullable())), + ) + .order_by(registration_application::published.desc()) + .select(( + registration_application::all_columns, + LocalUser::safe_settings_columns_tuple(), + Person::safe_columns_tuple(), + PersonAlias1::safe_columns_tuple().nullable(), + )) + .first::(conn)?; + + Ok(RegistrationApplicationView { + registration_application, + creator_local_user, + creator, + admin, + }) + } + + /// Returns the current unread registration_application count + pub fn get_unread_count(conn: &PgConnection, verified_email_only: bool) -> Result { + let mut query = registration_application::table + .inner_join(local_user::table.on(registration_application::local_user_id.eq(local_user::id))) + .inner_join(person::table.on(local_user::person_id.eq(person::id))) + .left_join( + person_alias_1::table + .on(registration_application::admin_id.eq(person_alias_1::id.nullable())), + ) + .filter(registration_application::admin_id.is_null()) + .into_boxed(); + + if verified_email_only { + query = query.filter(local_user::email_verified.eq(true)) + } + + query + .select(count(registration_application::id)) + .first::(conn) + } +} + +pub struct RegistrationApplicationQueryBuilder<'a> { + conn: &'a PgConnection, + unread_only: Option, + verified_email_only: Option, + page: Option, + limit: Option, +} + +impl<'a> RegistrationApplicationQueryBuilder<'a> { + pub fn create(conn: &'a PgConnection) -> Self { + RegistrationApplicationQueryBuilder { + conn, + unread_only: None, + verified_email_only: None, + page: None, + limit: None, + } + } + + pub fn unread_only>(mut self, unread_only: T) -> Self { + self.unread_only = unread_only.get_optional(); + self + } + + pub fn verified_email_only>(mut self, verified_email_only: T) -> Self { + self.verified_email_only = verified_email_only.get_optional(); + self + } + + pub fn page>(mut self, page: T) -> Self { + self.page = page.get_optional(); + self + } + + pub fn limit>(mut self, limit: T) -> Self { + self.limit = limit.get_optional(); + self + } + + pub fn list(self) -> Result, Error> { + let mut query = registration_application::table + .inner_join(local_user::table.on(registration_application::local_user_id.eq(local_user::id))) + .inner_join(person::table.on(local_user::person_id.eq(person::id))) + .left_join( + person_alias_1::table + .on(registration_application::admin_id.eq(person_alias_1::id.nullable())), + ) + .order_by(registration_application::published.desc()) + .select(( + registration_application::all_columns, + LocalUser::safe_settings_columns_tuple(), + Person::safe_columns_tuple(), + PersonAlias1::safe_columns_tuple().nullable(), + )) + .into_boxed(); + + if self.unread_only.unwrap_or(false) { + query = query.filter(registration_application::admin_id.is_null()) + } + + if self.verified_email_only.unwrap_or(false) { + query = query.filter(local_user::email_verified.eq(true)) + } + + let (limit, offset) = limit_and_offset(self.page, self.limit); + + query = query + .limit(limit) + .offset(offset) + .order_by(registration_application::published.desc()); + + let res = query.load::(self.conn)?; + + Ok(RegistrationApplicationView::from_tuple_to_vec(res)) + } +} + +impl ViewToVec for RegistrationApplicationView { + type DbTuple = RegistrationApplicationViewTuple; + fn from_tuple_to_vec(items: Vec) -> Vec { + items + .iter() + .map(|a| Self { + registration_application: a.0.to_owned(), + creator_local_user: a.1.to_owned(), + creator: a.2.to_owned(), + admin: a.3.to_owned(), + }) + .collect::>() + } +} + +#[cfg(test)] +mod tests { + use crate::registration_application_view::{ + RegistrationApplicationQueryBuilder, + RegistrationApplicationView, + }; + use lemmy_db_schema::{ + establish_unpooled_connection, + source::{ + local_user::{LocalUser, LocalUserForm, LocalUserSettings}, + person::*, + registration_application::{RegistrationApplication, RegistrationApplicationForm}, + }, + traits::Crud, + }; + use serial_test::serial; + + #[test] + #[serial] + fn test_crud() { + let conn = establish_unpooled_connection(); + + let timmy_person_form = PersonForm { + name: "timmy_rav".into(), + admin: Some(true), + ..PersonForm::default() + }; + + let inserted_timmy_person = Person::create(&conn, &timmy_person_form).unwrap(); + + let timmy_local_user_form = LocalUserForm { + person_id: Some(inserted_timmy_person.id), + password_encrypted: Some("nada".to_string()), + ..LocalUserForm::default() + }; + + let _inserted_timmy_local_user = LocalUser::create(&conn, &timmy_local_user_form).unwrap(); + + let sara_person_form = PersonForm { + name: "sara_rav".into(), + ..PersonForm::default() + }; + + let inserted_sara_person = Person::create(&conn, &sara_person_form).unwrap(); + + let sara_local_user_form = LocalUserForm { + person_id: Some(inserted_sara_person.id), + password_encrypted: Some("nada".to_string()), + ..LocalUserForm::default() + }; + + let inserted_sara_local_user = LocalUser::create(&conn, &sara_local_user_form).unwrap(); + + // Sara creates an application + let sara_app_form = RegistrationApplicationForm { + local_user_id: Some(inserted_sara_local_user.id), + answer: Some("LET ME IIIIINN".to_string()), + ..RegistrationApplicationForm::default() + }; + + let sara_app = RegistrationApplication::create(&conn, &sara_app_form).unwrap(); + + let read_sara_app_view = RegistrationApplicationView::read(&conn, sara_app.id).unwrap(); + + let jess_person_form = PersonForm { + name: "jess_rav".into(), + ..PersonForm::default() + }; + + let inserted_jess_person = Person::create(&conn, &jess_person_form).unwrap(); + + let jess_local_user_form = LocalUserForm { + person_id: Some(inserted_jess_person.id), + password_encrypted: Some("nada".to_string()), + ..LocalUserForm::default() + }; + + let inserted_jess_local_user = LocalUser::create(&conn, &jess_local_user_form).unwrap(); + + // Sara creates an application + let jess_app_form = RegistrationApplicationForm { + local_user_id: Some(inserted_jess_local_user.id), + answer: Some("LET ME IIIIINN".to_string()), + ..RegistrationApplicationForm::default() + }; + + let jess_app = RegistrationApplication::create(&conn, &jess_app_form).unwrap(); + + let read_jess_app_view = RegistrationApplicationView::read(&conn, jess_app.id).unwrap(); + + let mut expected_sara_app_view = RegistrationApplicationView { + registration_application: sara_app.to_owned(), + creator_local_user: LocalUserSettings { + id: inserted_sara_local_user.id, + person_id: inserted_sara_local_user.person_id, + email: inserted_sara_local_user.email, + show_nsfw: inserted_sara_local_user.show_nsfw, + theme: inserted_sara_local_user.theme, + default_sort_type: inserted_sara_local_user.default_sort_type, + default_listing_type: inserted_sara_local_user.default_listing_type, + lang: inserted_sara_local_user.lang, + show_avatars: inserted_sara_local_user.show_avatars, + send_notifications_to_email: inserted_sara_local_user.send_notifications_to_email, + validator_time: inserted_sara_local_user.validator_time, + show_bot_accounts: inserted_sara_local_user.show_bot_accounts, + show_scores: inserted_sara_local_user.show_scores, + show_read_posts: inserted_sara_local_user.show_read_posts, + show_new_post_notifs: inserted_sara_local_user.show_new_post_notifs, + email_verified: inserted_sara_local_user.email_verified, + accepted_application: inserted_sara_local_user.accepted_application, + }, + creator: PersonSafe { + id: inserted_sara_person.id, + name: inserted_sara_person.name.to_owned(), + display_name: None, + published: inserted_sara_person.published, + avatar: None, + actor_id: inserted_sara_person.actor_id.to_owned(), + local: true, + banned: false, + deleted: false, + admin: false, + bot_account: false, + bio: None, + banner: None, + updated: None, + inbox_url: inserted_sara_person.inbox_url.to_owned(), + shared_inbox_url: None, + matrix_user_id: None, + }, + admin: None, + }; + + assert_eq!(read_sara_app_view, expected_sara_app_view); + + // Do a batch read of the applications + let apps = RegistrationApplicationQueryBuilder::create(&conn) + .unread_only(true) + .list() + .unwrap(); + + assert_eq!( + apps, + [ + read_jess_app_view.to_owned(), + expected_sara_app_view.to_owned() + ] + ); + + // Make sure the counts are correct + let unread_count = RegistrationApplicationView::get_unread_count(&conn, false).unwrap(); + assert_eq!(unread_count, 2); + + // Approve the application + let approve_form = RegistrationApplicationForm { + admin_id: Some(inserted_timmy_person.id), + deny_reason: None, + ..RegistrationApplicationForm::default() + }; + + RegistrationApplication::update(&conn, sara_app.id, &approve_form).unwrap(); + + // Update the local_user row + let approve_local_user_form = LocalUserForm { + accepted_application: Some(true), + ..LocalUserForm::default() + }; + + LocalUser::update(&conn, inserted_sara_local_user.id, &approve_local_user_form).unwrap(); + + let read_sara_app_view_after_approve = + RegistrationApplicationView::read(&conn, sara_app.id).unwrap(); + + // Make sure the columns changed + expected_sara_app_view + .creator_local_user + .accepted_application = true; + expected_sara_app_view.registration_application.admin_id = Some(inserted_timmy_person.id); + + expected_sara_app_view.admin = Some(PersonSafeAlias1 { + id: inserted_timmy_person.id, + name: inserted_timmy_person.name.to_owned(), + display_name: None, + published: inserted_timmy_person.published, + avatar: None, + actor_id: inserted_timmy_person.actor_id.to_owned(), + local: true, + banned: false, + deleted: false, + admin: true, + bot_account: false, + bio: None, + banner: None, + updated: None, + inbox_url: inserted_timmy_person.inbox_url.to_owned(), + shared_inbox_url: None, + matrix_user_id: None, + }); + assert_eq!(read_sara_app_view_after_approve, expected_sara_app_view); + + // Do a batch read of apps again + // It should show only jessicas which is unresolved + let apps_after_resolve = RegistrationApplicationQueryBuilder::create(&conn) + .unread_only(true) + .list() + .unwrap(); + assert_eq!(apps_after_resolve, vec![read_jess_app_view]); + + // Make sure the counts are correct + let unread_count_after_approve = + RegistrationApplicationView::get_unread_count(&conn, false).unwrap(); + assert_eq!(unread_count_after_approve, 1); + + // Make sure the not undenied_only has all the apps + let all_apps = RegistrationApplicationQueryBuilder::create(&conn) + .list() + .unwrap(); + assert_eq!(all_apps.len(), 2); + + Person::delete(&conn, inserted_timmy_person.id).unwrap(); + Person::delete(&conn, inserted_sara_person.id).unwrap(); + Person::delete(&conn, inserted_jess_person.id).unwrap(); + } +} diff --git a/crates/db_views_actor/Cargo.toml b/crates/db_views_actor/Cargo.toml index 488e1f623..f94324afc 100644 --- a/crates/db_views_actor/Cargo.toml +++ b/crates/db_views_actor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lemmy_db_views_actor" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" edition = "2018" description = "A link aggregator for the fediverse" license = "AGPL-3.0" @@ -11,6 +11,6 @@ documentation = "https://join-lemmy.org/docs/en/index.html" doctest = false [dependencies] -lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" } +lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" } diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"] } serde = { version = "1.0.131", features = ["derive"] } diff --git a/crates/db_views_moderator/Cargo.toml b/crates/db_views_moderator/Cargo.toml index adebabc57..1a2348075 100644 --- a/crates/db_views_moderator/Cargo.toml +++ b/crates/db_views_moderator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lemmy_db_views_moderator" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" edition = "2018" description = "A link aggregator for the fediverse" license = "AGPL-3.0" @@ -11,6 +11,6 @@ documentation = "https://join-lemmy.org/docs/en/index.html" doctest = false [dependencies] -lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" } +lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" } diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"] } serde = { version = "1.0.131", features = ["derive"] } diff --git a/crates/routes/Cargo.toml b/crates/routes/Cargo.toml index 33597e62d..a832c17a4 100644 --- a/crates/routes/Cargo.toml +++ b/crates/routes/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lemmy_routes" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" edition = "2018" description = "A link aggregator for the fediverse" license = "AGPL-3.0" @@ -11,13 +11,13 @@ documentation = "https://join-lemmy.org/docs/en/index.html" doctest = false [dependencies] -lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" } -lemmy_websocket = { version = "=0.14.4-rc.4", path = "../websocket" } -lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" } -lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" } -lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" } -lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" } -lemmy_apub = { version = "=0.14.4-rc.4", path = "../apub" } +lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" } +lemmy_websocket = { version = "=0.15.0-rc.6", path = "../websocket" } +lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" } +lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" } +lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" } +lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" } +lemmy_apub = { version = "=0.15.0-rc.6", path = "../apub" } diesel = "1.4.8" actix = "0.12.0" actix-web = { version = "4.0.0-beta.14", default-features = false, features = ["rustls"] } diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index b0da4cb7b..20fcef5b3 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lemmy_utils" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" edition = "2018" description = "A link aggregator for the fediverse" license = "AGPL-3.0" diff --git a/crates/utils/src/email.rs b/crates/utils/src/email.rs index eb5e0d1c5..d9ac1710d 100644 --- a/crates/utils/src/email.rs +++ b/crates/utils/src/email.rs @@ -1,4 +1,4 @@ -use crate::settings::structs::Settings; +use crate::{settings::structs::Settings, LemmyError}; use lettre::{ message::{header, Mailbox, MultiPart, SinglePart}, transport::smtp::{ @@ -20,12 +20,21 @@ pub fn send_email( to_username: &str, html: &str, settings: &Settings, -) -> Result<(), String> { - let email_config = settings.email.to_owned().ok_or("no_email_setup")?; +) -> Result<(), LemmyError> { + let email_config = settings + .email + .to_owned() + .ok_or_else(|| LemmyError::from_message("no_email_setup"))?; let domain = settings.hostname.to_owned(); let (smtp_server, smtp_port) = { let email_and_port = email_config.smtp_server.split(':').collect::>(); + if email_and_port.len() == 1 { + return Err(LemmyError::from_message( + "email.smtp_server needs a port, IE smtp.xxx.com:465", + )); + } + ( email_and_port[0], email_and_port[1] @@ -87,6 +96,6 @@ pub fn send_email( match result { Ok(_) => Ok(()), - Err(e) => Err(e.to_string()), + Err(e) => Err(LemmyError::from(e).with_message("email_send_failed")), } } diff --git a/crates/utils/src/settings/structs.rs b/crates/utils/src/settings/structs.rs index ae7bd544a..2de87c5be 100644 --- a/crates/utils/src/settings/structs.rs +++ b/crates/utils/src/settings/structs.rs @@ -190,4 +190,12 @@ pub struct SetupConfig { pub enable_nsfw: Option, #[default(None)] pub community_creation_admin_only: Option, + #[default(None)] + pub require_email_verification: Option, + #[default(None)] + pub require_application: Option, + #[default(None)] + pub application_question: Option, + #[default(None)] + pub private_instance: Option, } diff --git a/crates/websocket/Cargo.toml b/crates/websocket/Cargo.toml index 2099fc950..f11671783 100644 --- a/crates/websocket/Cargo.toml +++ b/crates/websocket/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lemmy_websocket" -version = "0.14.4-rc.4" +version = "0.15.0-rc.6" edition = "2018" description = "A link aggregator for the fediverse" license = "AGPL-3.0" @@ -13,11 +13,11 @@ path = "src/lib.rs" doctest = false [dependencies] -lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" } -lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" } -lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" } -lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" } -lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" } +lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" } +lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" } +lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" } +lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" } +lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" } reqwest = { version = "0.11.7", features = ["json"] } reqwest-middleware = "0.1.3" tracing = "0.1.29" diff --git a/crates/websocket/src/lib.rs b/crates/websocket/src/lib.rs index 554717d41..5a132a18b 100644 --- a/crates/websocket/src/lib.rs +++ b/crates/websocket/src/lib.rs @@ -117,6 +117,7 @@ pub enum UserOperation { ListPostReports, GetReportCount, GetUnreadCount, + VerifyEmail, FollowCommunity, GetReplies, GetPersonMentions, @@ -125,6 +126,9 @@ pub enum UserOperation { BanFromCommunity, AddModToCommunity, AddAdmin, + GetUnreadRegistrationApplicationCount, + ListRegistrationApplications, + ApproveRegistrationApplication, BanPerson, Search, ResolveObject, diff --git a/crates/websocket/src/send.rs b/crates/websocket/src/send.rs index ac0752c64..e7f265b50 100644 --- a/crates/websocket/src/send.rs +++ b/crates/websocket/src/send.rs @@ -10,6 +10,7 @@ use lemmy_api_common::{ community::CommunityResponse, person::PrivateMessageResponse, post::PostResponse, + send_email_to_user, }; use lemmy_db_schema::{ newtypes::{CommentId, CommunityId, LocalUserId, PersonId, PostId, PrivateMessageId}, @@ -28,14 +29,7 @@ use lemmy_db_views::{ private_message_view::PrivateMessageView, }; use lemmy_db_views_actor::community_view::CommunityView; -use lemmy_utils::{ - email::send_email, - settings::structs::Settings, - utils::MentionData, - ConnectionId, - LemmyError, -}; -use tracing::error; +use lemmy_utils::{utils::MentionData, ConnectionId, LemmyError}; pub async fn send_post_ws_message( post_id: PostId, @@ -296,39 +290,3 @@ pub async fn send_local_notifs( }; Ok(recipient_ids) } - -pub fn send_email_to_user( - local_user_view: &LocalUserView, - subject_text: &str, - body_text: &str, - comment_content: &str, - settings: &Settings, -) { - if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email { - return; - } - - if let Some(user_email) = &local_user_view.local_user.email { - let subject = &format!( - "{} - {} {}", - subject_text, settings.hostname, local_user_view.person.name, - ); - let html = &format!( - "

{}


{} - {}

inbox", - body_text, - local_user_view.person.name, - comment_content, - settings.get_protocol_and_hostname() - ); - match send_email( - subject, - user_email, - &local_user_view.person.name, - html, - settings, - ) { - Ok(_o) => _o, - Err(e) => error!("{}", e), - }; - } -} diff --git a/migrations/2021-11-23-132840_email_verification/down.sql b/migrations/2021-11-23-132840_email_verification/down.sql new file mode 100644 index 000000000..cd1eb7c29 --- /dev/null +++ b/migrations/2021-11-23-132840_email_verification/down.sql @@ -0,0 +1,8 @@ +-- revert defaults from db for local user init +alter table local_user alter column theme set default 'darkly'; +alter table local_user alter column default_listing_type set default 1; + +-- remove tables and columns for optional email verification +alter table site drop column require_email_verification; +alter table local_user drop column email_verified; +drop table email_verification; diff --git a/migrations/2021-11-23-132840_email_verification/up.sql b/migrations/2021-11-23-132840_email_verification/up.sql new file mode 100644 index 000000000..29a20e00c --- /dev/null +++ b/migrations/2021-11-23-132840_email_verification/up.sql @@ -0,0 +1,14 @@ +-- use defaults from db for local user init +alter table local_user alter column theme set default 'browser'; +alter table local_user alter column default_listing_type set default 2; + +-- add tables and columns for optional email verification +alter table site add column require_email_verification boolean not null default false; +alter table local_user add column email_verified boolean not null default false; + +create table email_verification ( + id serial primary key, + local_user_id int references local_user(id) on update cascade on delete cascade not null, + email text not null, + verification_token text not null +); diff --git a/migrations/2021-11-23-153753_add_invite_only_columns/down.sql b/migrations/2021-11-23-153753_add_invite_only_columns/down.sql new file mode 100644 index 000000000..52a1a2808 --- /dev/null +++ b/migrations/2021-11-23-153753_add_invite_only_columns/down.sql @@ -0,0 +1,9 @@ +-- Add columns to site table +alter table site drop column require_application; +alter table site drop column application_question; +alter table site drop column private_instance; + +-- Add pending to local_user +alter table local_user drop column accepted_application; + +drop table registration_application; diff --git a/migrations/2021-11-23-153753_add_invite_only_columns/up.sql b/migrations/2021-11-23-153753_add_invite_only_columns/up.sql new file mode 100644 index 000000000..b3f8a18dc --- /dev/null +++ b/migrations/2021-11-23-153753_add_invite_only_columns/up.sql @@ -0,0 +1,19 @@ +-- Add columns to site table +alter table site add column require_application boolean not null default false; +alter table site add column application_question text; +alter table site add column private_instance boolean not null default false; + +-- Add pending to local_user +alter table local_user add column accepted_application boolean not null default false; + +create table registration_application ( + id serial primary key, + local_user_id int references local_user on update cascade on delete cascade not null, + answer text not null, + admin_id int references person on update cascade on delete cascade, + deny_reason text, + published timestamp not null default now(), + unique(local_user_id) +); + +create index idx_registration_application_published on registration_application (published desc); diff --git a/migrations/2021-12-09-225529_add_published_to_email_verification/down.sql b/migrations/2021-12-09-225529_add_published_to_email_verification/down.sql new file mode 100644 index 000000000..21405db11 --- /dev/null +++ b/migrations/2021-12-09-225529_add_published_to_email_verification/down.sql @@ -0,0 +1 @@ +alter table email_verification drop column published; diff --git a/migrations/2021-12-09-225529_add_published_to_email_verification/up.sql b/migrations/2021-12-09-225529_add_published_to_email_verification/up.sql new file mode 100644 index 000000000..79dd32bfb --- /dev/null +++ b/migrations/2021-12-09-225529_add_published_to_email_verification/up.sql @@ -0,0 +1 @@ +alter table email_verification add column published timestamp not null default now(); diff --git a/src/api_routes.rs b/src/api_routes.rs index 3fbb7f053..88466485b 100644 --- a/src/api_routes.rs +++ b/src/api_routes.rs @@ -210,13 +210,26 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { web::put().to(route_post::), ) .route("/report_count", web::get().to(route_get::)) - .route("/unread_count", web::get().to(route_get::)), + .route("/unread_count", web::get().to(route_get::)) + .route("/verify_email", web::post().to(route_post::)), ) // Admin Actions .service( - web::resource("/admin/add") + web::scope("/admin") .wrap(rate_limit.message()) - .route(web::post().to(route_post::)), + .route("/add", web::post().to(route_post::)) + .route( + "/registration_application/count", + web::get().to(route_get::), + ) + .route( + "/registration_application/list", + web::get().to(route_get::), + ) + .route( + "/registration_application/approve", + web::put().to(route_post::), + ), ), ); } diff --git a/src/main.rs b/src/main.rs index cf29e7790..252d37a38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use diesel::{ }; use doku::json::{AutoComments, Formatting}; use lemmy_api::match_websocket_operation; -use lemmy_api_common::blocking; +use lemmy_api_common::{blocking, check_private_instance_and_federation_enabled}; use lemmy_api_crud::match_websocket_operation_crud; use lemmy_apub_lib::activity_queue::create_activity_queue; use lemmy_db_schema::{get_database_url_from_env, source::secret::Secret}; @@ -103,6 +103,8 @@ async fn main() -> Result<(), LemmyError> { let activity_queue = queue_manager.queue_handle().clone(); + check_private_instance_and_federation_enabled(&pool, &settings).await?; + let chat_server = ChatServer::startup( pool.clone(), rate_limiter.clone(),