Adding views and functionality to registration application. #209

invite_instances
Dessalines 2021-12-03 21:15:20 -05:00
parent 9889d158f1
commit e28977c987
24 changed files with 716 additions and 13 deletions

View File

@ -97,6 +97,9 @@
open_registration: true open_registration: true
enable_nsfw: true enable_nsfw: true
community_creation_admin_only: true community_creation_admin_only: true
require_email_verification: true
require_application: true
application_question: "string"
} }
# the domain name of your instance (mandatory) # the domain name of your instance (mandatory)
hostname: "unset" hostname: "unset"

View File

@ -38,6 +38,15 @@ pub async fn match_websocket_operation(
UserOperation::GetCaptcha => do_websocket_operation::<GetCaptcha>(context, id, op, data).await, UserOperation::GetCaptcha => do_websocket_operation::<GetCaptcha>(context, id, op, data).await,
UserOperation::GetReplies => do_websocket_operation::<GetReplies>(context, id, op, data).await, UserOperation::GetReplies => do_websocket_operation::<GetReplies>(context, id, op, data).await,
UserOperation::AddAdmin => do_websocket_operation::<AddAdmin>(context, id, op, data).await, UserOperation::AddAdmin => do_websocket_operation::<AddAdmin>(context, id, op, data).await,
UserOperation::GetUnreadRegistrationApplicationCount => {
do_websocket_operation::<GetUnreadRegistrationApplicationCount>(context, id, op, data).await
}
UserOperation::ListRegistrationApplications => {
do_websocket_operation::<ListRegistrationApplications>(context, id, op, data).await
}
UserOperation::ApproveRegistrationApplication => {
do_websocket_operation::<ApproveRegistrationApplication>(context, id, op, data).await
}
UserOperation::BanPerson => do_websocket_operation::<BanPerson>(context, id, op, data).await, UserOperation::BanPerson => do_websocket_operation::<BanPerson>(context, id, op, data).await,
UserOperation::BlockPerson => { UserOperation::BlockPerson => {
do_websocket_operation::<BlockPerson>(context, id, op, data).await do_websocket_operation::<BlockPerson>(context, id, op, data).await

View File

@ -261,6 +261,7 @@ impl Perform for SaveUserSettings {
show_new_post_notifs: data.show_new_post_notifs, show_new_post_notifs: data.show_new_post_notifs,
send_notifications_to_email: data.send_notifications_to_email, send_notifications_to_email: data.send_notifications_to_email,
email_verified: None, email_verified: None,
accepted_application: None,
}; };
let local_user_res = blocking(context.pool(), move |conn| { let local_user_res = blocking(context.pool(), move |conn| {

View File

@ -19,9 +19,15 @@ use lemmy_apub::{
EndpointType, EndpointType,
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
diesel_option_overwrite,
from_opt_str_to_opt_enum, from_opt_str_to_opt_enum,
newtypes::PersonId, newtypes::PersonId,
source::{moderator::*, site::Site}, source::{
local_user::{LocalUser, LocalUserForm},
moderator::*,
registration_application::{RegistrationApplication, RegistrationApplicationForm},
site::Site,
},
traits::{Crud, DeleteableOrRemoveable}, traits::{Crud, DeleteableOrRemoveable},
DbPool, DbPool,
ListingType, ListingType,
@ -31,6 +37,10 @@ use lemmy_db_schema::{
use lemmy_db_views::{ use lemmy_db_views::{
comment_view::{CommentQueryBuilder, CommentView}, comment_view::{CommentQueryBuilder, CommentView},
post_view::{PostQueryBuilder, PostView}, post_view::{PostQueryBuilder, PostView},
registration_application_view::{
RegistrationApplicationQueryBuilder,
RegistrationApplicationView,
},
site_view::SiteView, site_view::SiteView,
}; };
use lemmy_db_views_actor::{ use lemmy_db_views_actor::{
@ -550,3 +560,131 @@ impl Perform for SaveSiteConfig {
Ok(GetSiteConfigResponse { config_hjson }) 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<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<Self::Response, LemmyError> {
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<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<Self::Response, LemmyError> {
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<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<Self::Response, LemmyError> {
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,
})
}
}

View File

@ -28,6 +28,8 @@ pub struct Register {
pub captcha_uuid: Option<String>, pub captcha_uuid: Option<String>,
pub captcha_answer: Option<String>, pub captcha_answer: Option<String>,
pub honeypot: Option<String>, pub honeypot: Option<String>,
/// An answer is mandatory if require application is enabled on the server
pub answer: Option<String>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]

View File

@ -3,6 +3,7 @@ use lemmy_db_views::{
comment_view::CommentView, comment_view::CommentView,
local_user_view::LocalUserSettingsView, local_user_view::LocalUserSettingsView,
post_view::PostView, post_view::PostView,
registration_application_view::RegistrationApplicationView,
site_view::SiteView, site_view::SiteView,
}; };
use lemmy_db_views_actor::{ use lemmy_db_views_actor::{
@ -97,6 +98,9 @@ pub struct CreateSite {
pub open_registration: Option<bool>, pub open_registration: Option<bool>,
pub enable_nsfw: Option<bool>, pub enable_nsfw: Option<bool>,
pub community_creation_admin_only: Option<bool>, pub community_creation_admin_only: Option<bool>,
pub require_email_verification: Option<bool>,
pub require_application: Option<bool>,
pub application_question: Option<String>,
pub auth: String, pub auth: String,
} }
@ -112,6 +116,8 @@ pub struct EditSite {
pub enable_nsfw: Option<bool>, pub enable_nsfw: Option<bool>,
pub community_creation_admin_only: Option<bool>, pub community_creation_admin_only: Option<bool>,
pub require_email_verification: Option<bool>, pub require_email_verification: Option<bool>,
pub require_application: Option<bool>,
pub application_question: Option<String>,
pub auth: String, pub auth: String,
} }
@ -173,3 +179,40 @@ pub struct FederatedInstances {
pub allowed: Option<Vec<String>>, pub allowed: Option<Vec<String>>,
pub blocked: Option<Vec<String>>, pub blocked: Option<Vec<String>>,
} }
#[derive(Serialize, Deserialize)]
pub struct ListRegistrationApplications {
/// Only shows the unread applications (IE those without an admin actor)
pub unread_only: Option<bool>,
pub page: Option<i64>,
pub limit: Option<i64>,
pub auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct ListRegistrationApplicationsResponse {
pub registration_applications: Vec<RegistrationApplicationView>,
}
#[derive(Serialize, Deserialize)]
pub struct ApproveRegistrationApplication {
pub id: i32,
pub approve: bool,
pub deny_reason: Option<String>,
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,
}

View File

@ -44,6 +44,7 @@ impl PerformCrud for GetSite {
captcha_uuid: None, captcha_uuid: None,
captcha_answer: None, captcha_answer: None,
honeypot: None, honeypot: None,
answer: None,
}; };
let admin_jwt = register let admin_jwt = register
.perform(context, websocket_id) .perform(context, websocket_id)
@ -62,6 +63,9 @@ impl PerformCrud for GetSite {
open_registration: setup.open_registration, open_registration: setup.open_registration,
enable_nsfw: setup.enable_nsfw, enable_nsfw: setup.enable_nsfw,
community_creation_admin_only: setup.community_creation_admin_only, 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, auth: admin_jwt,
}; };
create_site.perform(context, websocket_id).await?; create_site.perform(context, websocket_id).await?;

View File

@ -40,6 +40,7 @@ impl PerformCrud for EditSite {
let sidebar = diesel_option_overwrite(&data.sidebar); let sidebar = diesel_option_overwrite(&data.sidebar);
let description = diesel_option_overwrite(&data.description); 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 icon = diesel_option_overwrite_to_url(&data.icon)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?; let banner = diesel_option_overwrite_to_url(&data.banner)?;
@ -60,6 +61,8 @@ impl PerformCrud for EditSite {
enable_nsfw: data.enable_nsfw, enable_nsfw: data.enable_nsfw,
community_creation_admin_only: data.community_creation_admin_only, community_creation_admin_only: data.community_creation_admin_only,
require_email_verification: data.require_email_verification, 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); let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);

View File

@ -21,6 +21,7 @@ use lemmy_db_schema::{
}, },
local_user::{LocalUser, LocalUserForm}, local_user::{LocalUser, LocalUserForm},
person::{Person, PersonForm}, person::{Person, PersonForm},
registration_application::{RegistrationApplication, RegistrationApplicationForm},
site::Site, site::Site,
}, },
traits::{Crud, Followable, Joinable}, traits::{Crud, Followable, Joinable},
@ -47,8 +48,8 @@ impl PerformCrud for Register {
) -> Result<LoginResponse, LemmyError> { ) -> Result<LoginResponse, LemmyError> {
let data: &Register = self; let data: &Register = self;
// no email verification if the site is not setup yet // no email verification, or applications if the site is not setup yet
let mut email_verification = false; let (mut email_verification, mut require_application) = (false, false);
// Make sure site has open registration // Make sure site has open registration
if let Ok(site) = blocking(context.pool(), Site::read_simple).await? { 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()); return Err(ApiError::err_plain("registration_closed").into());
} }
email_verification = site.require_email_verification; email_verification = site.require_email_verification;
require_application = site.require_application;
} }
password_length_check(&data.password)?; password_length_check(&data.password)?;
@ -65,6 +67,10 @@ impl PerformCrud for Register {
return Err(ApiError::err_plain("email_required").into()); 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 // Make sure passwords match
if data.password != data.password_verify { if data.password != data.password_verify {
return Err(ApiError::err_plain("passwords_dont_match").into()); 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()?; let main_community_keypair = generate_actor_keypair()?;
// Create the main community if it doesn't exist // Create the main community if it doesn't exist
@ -243,6 +264,7 @@ impl PerformCrud for Register {
&context.settings().hostname, &context.settings().hostname,
)?) )?)
}; };
// TODO this needs a "registration created" type response
Ok(LoginResponse { jwt }) Ok(LoginResponse { jwt })
} }
} }

View File

@ -66,6 +66,8 @@ mod tests {
updated: None, updated: None,
community_creation_admin_only: Some(false), community_creation_admin_only: Some(false),
require_email_verification: None, require_email_verification: None,
require_application: None,
application_question: None,
}; };
Site::create(&conn, &site_form).unwrap(); Site::create(&conn, &site_form).unwrap();

View File

@ -13,5 +13,6 @@ pub mod person_mention;
pub mod post; pub mod post;
pub mod post_report; pub mod post_report;
pub mod private_message; pub mod private_message;
pub mod registration_application;
pub mod secret; pub mod secret;
pub mod site; pub mod site;

View File

@ -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<Self, Error> {
use crate::schema::registration_application::dsl::*;
insert_into(registration_application)
.values(form)
.get_result::<Self>(conn)
}
fn read(conn: &PgConnection, id_: Self::IdType) -> Result<Self, Error> {
use crate::schema::registration_application::dsl::*;
registration_application.find(id_).first::<Self>(conn)
}
fn update(conn: &PgConnection, id_: Self::IdType, form: &Self::Form) -> Result<Self, Error> {
use crate::schema::registration_application::dsl::*;
diesel::update(registration_application.find(id_))
.set(form)
.get_result::<Self>(conn)
}
fn delete(conn: &PgConnection, id_: Self::IdType) -> Result<usize, Error> {
use crate::schema::registration_application::dsl::*;
diesel::delete(registration_application.find(id_)).execute(conn)
}
}

View File

@ -158,6 +158,7 @@ table! {
show_read_posts -> Bool, show_read_posts -> Bool,
show_new_post_notifs -> Bool, show_new_post_notifs -> Bool,
email_verified -> Bool, email_verified -> Bool,
accepted_application -> Bool,
} }
} }
@ -449,6 +450,8 @@ table! {
description -> Nullable<Text>, description -> Nullable<Text>,
community_creation_admin_only -> Bool, community_creation_admin_only -> Bool,
require_email_verification -> Bool, require_email_verification -> Bool,
require_application -> Bool,
application_question -> Nullable<Text>,
} }
} }
@ -569,6 +572,17 @@ table! {
} }
} }
table! {
registration_application (id) {
id -> Int4,
local_user_id -> Int4,
answer -> Text,
admin_id -> Nullable<Int4>,
deny_reason -> Nullable<Text>,
published -> Timestamp,
}
}
joinable!(comment_alias_1 -> person_alias_1 (creator_id)); joinable!(comment_alias_1 -> person_alias_1 (creator_id));
joinable!(comment -> comment_alias_1 (parent_id)); joinable!(comment -> comment_alias_1 (parent_id));
joinable!(person_mention -> person_alias_1 (recipient_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 -> person (creator_id));
joinable!(site_aggregates -> site (site_id)); joinable!(site_aggregates -> site (site_id));
joinable!(email_verification -> local_user (local_user_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!( allow_tables_to_appear_in_same_query!(
activity, activity,
@ -674,5 +690,6 @@ allow_tables_to_appear_in_same_query!(
comment_alias_1, comment_alias_1,
person_alias_1, person_alias_1,
person_alias_2, person_alias_2,
email_verification email_verification,
registration_application
); );

View File

@ -24,6 +24,7 @@ pub struct LocalUser {
pub show_read_posts: bool, pub show_read_posts: bool,
pub show_new_post_notifs: bool, pub show_new_post_notifs: bool,
pub email_verified: bool, pub email_verified: bool,
pub accepted_application: bool,
} }
// TODO redo these, check table defaults // TODO redo these, check table defaults
@ -45,6 +46,7 @@ pub struct LocalUserForm {
pub show_read_posts: Option<bool>, pub show_read_posts: Option<bool>,
pub show_new_post_notifs: Option<bool>, pub show_new_post_notifs: Option<bool>,
pub email_verified: Option<bool>, pub email_verified: Option<bool>,
pub accepted_application: Option<bool>,
} }
/// A local user view that removes password encrypted /// A local user view that removes password encrypted

View File

@ -13,5 +13,6 @@ pub mod person_mention;
pub mod post; pub mod post;
pub mod post_report; pub mod post_report;
pub mod private_message; pub mod private_message;
pub mod registration_application;
pub mod secret; pub mod secret;
pub mod site; pub mod site;

View File

@ -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<PersonId>,
pub deny_reason: Option<String>,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Default)]
#[table_name = "registration_application"]
pub struct RegistrationApplicationForm {
pub local_user_id: Option<LocalUserId>,
pub answer: Option<String>,
pub admin_id: Option<PersonId>,
pub deny_reason: Option<Option<String>>,
}

View File

@ -21,6 +21,8 @@ pub struct Site {
pub description: Option<String>, pub description: Option<String>,
pub community_creation_admin_only: bool, pub community_creation_admin_only: bool,
pub require_email_verification: bool, pub require_email_verification: bool,
pub require_application: bool,
pub application_question: Option<String>,
} }
#[derive(Insertable, AsChangeset, Default)] #[derive(Insertable, AsChangeset, Default)]
@ -39,4 +41,6 @@ pub struct SiteForm {
pub description: Option<Option<String>>, pub description: Option<Option<String>>,
pub community_creation_admin_only: Option<bool>, pub community_creation_admin_only: Option<bool>,
pub require_email_verification: Option<bool>, pub require_email_verification: Option<bool>,
pub require_application: Option<bool>,
pub application_question: Option<Option<String>>,
} }

View File

@ -7,4 +7,5 @@ pub mod local_user_view;
pub mod post_report_view; pub mod post_report_view;
pub mod post_view; pub mod post_view;
pub mod private_message_view; pub mod private_message_view;
pub mod registration_application_view;
pub mod site_view; pub mod site_view;

View File

@ -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<PersonSafeAlias1>,
}
type RegistrationApplicationViewTuple = (
RegistrationApplication,
LocalUser,
PersonSafe,
Option<PersonSafeAlias1>,
);
impl RegistrationApplicationView {
pub fn read(conn: &PgConnection, registration_application_id: i32) -> Result<Self, Error> {
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::<RegistrationApplicationViewTuple>(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<i64, 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())),
)
.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::<i64>(conn)
}
}
pub struct RegistrationApplicationQueryBuilder<'a> {
conn: &'a PgConnection,
unread_only: Option<bool>,
verified_email_only: Option<bool>,
page: Option<i64>,
limit: Option<i64>,
}
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<T: MaybeOptional<bool>>(mut self, unread_only: T) -> Self {
self.unread_only = unread_only.get_optional();
self
}
pub fn verified_email_only<T: MaybeOptional<bool>>(mut self, verified_email_only: T) -> Self {
self.verified_email_only = verified_email_only.get_optional();
self
}
pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
self.page = page.get_optional();
self
}
pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
self.limit = limit.get_optional();
self
}
pub fn list(self) -> Result<Vec<RegistrationApplicationView>, 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::<RegistrationApplicationViewTuple>(self.conn)?;
Ok(RegistrationApplicationView::from_tuple_to_vec(res))
}
}
impl ViewToVec for RegistrationApplicationView {
type DbTuple = RegistrationApplicationViewTuple;
fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
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::<Vec<Self>>()
}
}
#[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();
}
}

View File

@ -187,4 +187,10 @@ pub struct SetupConfig {
pub enable_nsfw: Option<bool>, pub enable_nsfw: Option<bool>,
#[default(None)] #[default(None)]
pub community_creation_admin_only: Option<bool>, pub community_creation_admin_only: Option<bool>,
#[default(None)]
pub require_email_verification: Option<bool>,
#[default(None)]
pub require_application: Option<bool>,
#[default(None)]
pub application_question: Option<String>,
} }

View File

@ -126,6 +126,9 @@ pub enum UserOperation {
BanFromCommunity, BanFromCommunity,
AddModToCommunity, AddModToCommunity,
AddAdmin, AddAdmin,
GetUnreadRegistrationApplicationCount,
ListRegistrationApplications,
ApproveRegistrationApplication,
BanPerson, BanPerson,
Search, Search,
ResolveObject, ResolveObject,

View File

@ -1,11 +1,8 @@
-- This file should undo anything in `up.sql`
-- Add columns to site table -- Add columns to site table
alter table site drop column require_application; alter table site drop column require_application;
alter table site drop column require_email;
alter table site drop column application_question; alter table site drop column application_question;
-- Add pending to local_user -- Add pending to local_user
alter table local_user drop column accepted_application; alter table local_user drop column accepted_application;
alter table local_user drop column verified_email;
drop table registration_application; drop table registration_application;

View File

@ -1,19 +1,18 @@
-- Add columns to site table -- Add columns to site table
alter table site add column require_application boolean not null default false; 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; alter table site add column application_question text;
-- Add pending to local_user -- Add pending to local_user
alter table local_user add column accepted_application boolean not null default false; 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 ( create table registration_application (
id serial primary key, id serial primary key,
local_user_id int references local_user on update cascade on delete cascade not null, local_user_id int references local_user on update cascade on delete cascade not null,
answer text not null, answer text not null,
acceptor_id int references person on update cascade on delete cascade, admin_id int references person on update cascade on delete cascade,
accepted boolean not null default false,
deny_reason text, deny_reason text,
published timestamp not null default now(), published timestamp not null default now(),
unique(local_user_id) unique(local_user_id)
); );
create index idx_registration_application_published on registration_application (published desc);

View File

@ -211,9 +211,21 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
) )
// Admin Actions // Admin Actions
.service( .service(
web::resource("/admin/add") web::scope("/admin")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route(web::post().to(route_post::<AddAdmin>)), .route("/add", web::post().to(route_post::<AddAdmin>))
.route(
"/registration_application/count",
web::get().to(route_get::<GetUnreadRegistrationApplicationCount>),
)
.route(
"/registration_application/list",
web::get().to(route_get::<ListRegistrationApplications>),
)
.route(
"/registration_application/approve",
web::put().to(route_post::<ApproveRegistrationApplication>),
),
), ),
); );
} }