diff --git a/config/defaults.hjson b/config/defaults.hjson index 003adf11a..cf4bdf541 100644 --- a/config/defaults.hjson +++ b/config/defaults.hjson @@ -97,6 +97,9 @@ open_registration: true enable_nsfw: true community_creation_admin_only: true + require_email_verification: true + require_application: true + application_question: "string" } # the domain name of your instance (mandatory) hostname: "unset" diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 3b82e6aea..42df3df88 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 diff --git a/crates/api/src/local_user.rs b/crates/api/src/local_user.rs index 3217ea720..962124e2a 100644 --- a/crates/api/src/local_user.rs +++ b/crates/api/src/local_user.rs @@ -261,6 +261,7 @@ impl Perform for SaveUserSettings { 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| { diff --git a/crates/api/src/site.rs b/crates/api/src/site.rs index be9dc5da1..70d9310f4 100644 --- a/crates/api/src/site.rs +++ b/crates/api/src/site.rs @@ -19,9 +19,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, @@ -31,6 +37,10 @@ use lemmy_db_schema::{ use lemmy_db_views::{ comment_view::{CommentQueryBuilder, CommentView}, post_view::{PostQueryBuilder, PostView}, + registration_application_view::{ + RegistrationApplicationQueryBuilder, + RegistrationApplicationView, + }, site_view::SiteView, }; use lemmy_db_views_actor::{ @@ -550,3 +560,131 @@ 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??; + + // 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/src/person.rs b/crates/api_common/src/person.rs index 59df35d0a..2b36ae75b 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -28,6 +28,8 @@ pub struct Register { 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(Serialize, Deserialize)] diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 7a5e96d0e..4473db790 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::{ @@ -97,6 +98,9 @@ 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 auth: String, } @@ -112,6 +116,8 @@ pub struct EditSite { pub enable_nsfw: Option, pub community_creation_admin_only: Option, pub require_email_verification: Option, + pub require_application: Option, + pub application_question: Option, pub auth: String, } @@ -173,3 +179,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/src/site/read.rs b/crates/api_crud/src/site/read.rs index bf0996536..5b79a5d87 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -44,6 +44,7 @@ impl PerformCrud for GetSite { captcha_uuid: None, captcha_answer: None, honeypot: None, + answer: None, }; let admin_jwt = register .perform(context, websocket_id) @@ -62,6 +63,9 @@ impl PerformCrud for GetSite { open_registration: setup.open_registration, enable_nsfw: setup.enable_nsfw, community_creation_admin_only: setup.community_creation_admin_only, + require_email_verification: setup.require_email_verification, + require_application: setup.require_application, + application_question: setup.application_question.to_owned(), auth: admin_jwt, }; create_site.perform(context, websocket_id).await?; diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index 150b7fc76..962664391 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -40,6 +40,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)?; @@ -60,6 +61,8 @@ impl PerformCrud for EditSite { 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, }; let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form); diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index 77d81087d..c9ccf4ab5 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -21,6 +21,7 @@ use lemmy_db_schema::{ }, local_user::{LocalUser, LocalUserForm}, person::{Person, PersonForm}, + registration_application::{RegistrationApplication, RegistrationApplicationForm}, site::Site, }, traits::{Crud, Followable, Joinable}, @@ -47,8 +48,8 @@ impl PerformCrud for Register { ) -> Result { let data: &Register = self; - // no email verification if the site is not setup yet - let mut email_verification = false; + // 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? { @@ -56,6 +57,7 @@ impl PerformCrud for Register { return Err(ApiError::err_plain("registration_closed").into()); } email_verification = site.require_email_verification; + require_application = site.require_application; } password_length_check(&data.password)?; @@ -65,6 +67,10 @@ impl PerformCrud for Register { return Err(ApiError::err_plain("email_required").into()); } + if require_application && data.answer.is_none() { + return Err(ApiError::err_plain("registration_application_answer_required").into()); + } + // Make sure passwords match if data.password != data.password_verify { return Err(ApiError::err_plain("passwords_dont_match").into()); @@ -164,6 +170,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 @@ -243,6 +264,7 @@ impl PerformCrud for Register { &context.settings().hostname, )?) }; + // TODO this needs a "registration created" type response Ok(LoginResponse { jwt }) } } diff --git a/crates/db_schema/src/aggregates/site_aggregates.rs b/crates/db_schema/src/aggregates/site_aggregates.rs index aa5fe4efc..fb4e1f3cc 100644 --- a/crates/db_schema/src/aggregates/site_aggregates.rs +++ b/crates/db_schema/src/aggregates/site_aggregates.rs @@ -66,6 +66,8 @@ mod tests { updated: None, community_creation_admin_only: Some(false), require_email_verification: None, + require_application: None, + application_question: None, }; Site::create(&conn, &site_form).unwrap(); diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index 891370d01..c96e3a623 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -13,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/registration_application.rs b/crates/db_schema/src/impls/registration_application.rs new file mode 100644 index 000000000..91f43574b --- /dev/null +++ b/crates/db_schema/src/impls/registration_application.rs @@ -0,0 +1,30 @@ +use crate::{source::registration_application::*, traits::Crud}; +use diesel::{insert_into, result::Error, 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) + } +} diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index f3648b91c..e81dbd1c7 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -158,6 +158,7 @@ table! { show_read_posts -> Bool, show_new_post_notifs -> Bool, email_verified -> Bool, + accepted_application -> Bool, } } @@ -449,6 +450,8 @@ table! { description -> Nullable, community_creation_admin_only -> Bool, require_email_verification -> Bool, + require_application -> Bool, + application_question -> Nullable, } } @@ -569,6 +572,17 @@ table! { } } +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)); @@ -631,6 +645,8 @@ 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, @@ -674,5 +690,6 @@ allow_tables_to_appear_in_same_query!( comment_alias_1, person_alias_1, person_alias_2, - email_verification + email_verification, + registration_application ); diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index 5edb3bdfb..71d53dd89 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -24,6 +24,7 @@ pub struct LocalUser { 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 @@ -45,6 +46,7 @@ pub struct LocalUserForm { 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 diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index 891370d01..c96e3a623 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -13,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 e66bcaed7..2c8e16a89 100644 --- a/crates/db_schema/src/source/site.rs +++ b/crates/db_schema/src/source/site.rs @@ -21,6 +21,8 @@ pub struct Site { pub description: Option, pub community_creation_admin_only: bool, pub require_email_verification: bool, + pub require_application: bool, + pub application_question: Option, } #[derive(Insertable, AsChangeset, Default)] @@ -39,4 +41,6 @@ pub struct SiteForm { pub description: Option>, pub community_creation_admin_only: Option, pub require_email_verification: Option, + pub require_application: Option, + pub application_question: Option>, } 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..73be84cb8 --- /dev/null +++ b/crates/db_views/src/registration_application_view.rs @@ -0,0 +1,378 @@ +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, + person::{Person, PersonAlias1, PersonSafe, PersonSafeAlias1}, + registration_application::RegistrationApplication, + }, + traits::{MaybeOptional, ToSafe, ViewToVec}, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +pub struct RegistrationApplicationView { + pub registration_application: RegistrationApplication, + pub creator_local_user: LocalUser, + pub creator: PersonSafe, + pub admin: Option, +} + +type RegistrationApplicationViewTuple = ( + RegistrationApplication, + LocalUser, + 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, + local_user::all_columns, + 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, + local_user::all_columns, + 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}, + 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: inserted_sara_local_user.to_owned(), + 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/utils/src/settings/structs.rs b/crates/utils/src/settings/structs.rs index 1b8ac812e..99f208414 100644 --- a/crates/utils/src/settings/structs.rs +++ b/crates/utils/src/settings/structs.rs @@ -187,4 +187,10 @@ 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, } diff --git a/crates/websocket/src/lib.rs b/crates/websocket/src/lib.rs index 57fbe16f2..e8dc02645 100644 --- a/crates/websocket/src/lib.rs +++ b/crates/websocket/src/lib.rs @@ -126,6 +126,9 @@ pub enum UserOperation { BanFromCommunity, AddModToCommunity, AddAdmin, + GetUnreadRegistrationApplicationCount, + ListRegistrationApplications, + ApproveRegistrationApplication, BanPerson, Search, ResolveObject, 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 index 23531ad9c..8d04f921f 100644 --- a/migrations/2021-11-23-153753_add_invite_only_columns/down.sql +++ b/migrations/2021-11-23-153753_add_invite_only_columns/down.sql @@ -1,11 +1,8 @@ --- This file should undo anything in `up.sql` -- Add columns to site table alter table site drop column require_application; -alter table site drop column require_email; alter table site drop column application_question; -- Add pending to local_user alter table local_user drop column accepted_application; -alter table local_user drop column verified_email; 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 index 25272221f..a7143929f 100644 --- a/migrations/2021-11-23-153753_add_invite_only_columns/up.sql +++ b/migrations/2021-11-23-153753_add_invite_only_columns/up.sql @@ -1,19 +1,18 @@ -- Add columns to site table alter table site add column require_application boolean not null default false; -alter table site add column require_email boolean not null default false; alter table site add column application_question text; -- Add pending to local_user alter table local_user add column accepted_application boolean not null default false; -alter table local_user add column verified_email 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, - acceptor_id int references person on update cascade on delete cascade, - accepted boolean not null default false, + 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/src/api_routes.rs b/src/api_routes.rs index c7b59cf64..6e3b368f7 100644 --- a/src/api_routes.rs +++ b/src/api_routes.rs @@ -211,9 +211,21 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { ) // 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::), + ), ), ); }