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 <me@nutomic.com>
This commit is contained in:
Dessalines 2021-12-15 14:49:59 -05:00 committed by GitHub
parent 1410c5659c
commit c883a49a40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 1540 additions and 258 deletions

28
Cargo.lock generated
View file

@ -1686,7 +1686,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "lemmy_api" name = "lemmy_api"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
dependencies = [ dependencies = [
"actix", "actix",
"actix-rt", "actix-rt",
@ -1729,7 +1729,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_api_common" name = "lemmy_api_common"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"chrono", "chrono",
@ -1747,7 +1747,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_api_crud" name = "lemmy_api_crud"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
dependencies = [ dependencies = [
"actix", "actix",
"actix-rt", "actix-rt",
@ -1790,7 +1790,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_apub" name = "lemmy_apub"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
dependencies = [ dependencies = [
"activitystreams-kinds", "activitystreams-kinds",
"actix", "actix",
@ -1836,7 +1836,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_apub_lib" name = "lemmy_apub_lib"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
dependencies = [ dependencies = [
"activitystreams", "activitystreams",
"actix-web", "actix-web",
@ -1863,7 +1863,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_apub_lib_derive" name = "lemmy_apub_lib_derive"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
dependencies = [ dependencies = [
"proc-macro2 1.0.33", "proc-macro2 1.0.33",
"quote 1.0.10", "quote 1.0.10",
@ -1873,7 +1873,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_db_schema" name = "lemmy_db_schema"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
dependencies = [ dependencies = [
"bcrypt", "bcrypt",
"chrono", "chrono",
@ -1895,7 +1895,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_db_views" name = "lemmy_db_views"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
dependencies = [ dependencies = [
"diesel", "diesel",
"lemmy_db_schema", "lemmy_db_schema",
@ -1907,7 +1907,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_db_views_actor" name = "lemmy_db_views_actor"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
dependencies = [ dependencies = [
"diesel", "diesel",
"lemmy_db_schema", "lemmy_db_schema",
@ -1916,7 +1916,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_db_views_moderator" name = "lemmy_db_views_moderator"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
dependencies = [ dependencies = [
"diesel", "diesel",
"lemmy_db_schema", "lemmy_db_schema",
@ -1925,7 +1925,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_routes" name = "lemmy_routes"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
dependencies = [ dependencies = [
"actix", "actix",
"actix-http", "actix-http",
@ -1956,7 +1956,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_server" name = "lemmy_server"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
dependencies = [ dependencies = [
"activitystreams", "activitystreams",
"actix", "actix",
@ -2000,7 +2000,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_utils" name = "lemmy_utils"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
dependencies = [ dependencies = [
"actix-rt", "actix-rt",
"actix-web", "actix-web",
@ -2038,7 +2038,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_websocket" name = "lemmy_websocket"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
dependencies = [ dependencies = [
"actix", "actix",
"actix-web", "actix-web",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lemmy_server" name = "lemmy_server"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
edition = "2018" edition = "2018"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -31,18 +31,18 @@ members = [
] ]
[dependencies] [dependencies]
lemmy_api = { version = "=0.14.4-rc.4", path = "./crates/api" } lemmy_api = { version = "=0.15.0-rc.6", path = "./crates/api" }
lemmy_api_crud = { version = "=0.14.4-rc.4", path = "./crates/api_crud" } lemmy_api_crud = { version = "=0.15.0-rc.6", path = "./crates/api_crud" }
lemmy_apub = { version = "=0.14.4-rc.4", path = "./crates/apub" } lemmy_apub = { version = "=0.15.0-rc.6", path = "./crates/apub" }
lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "./crates/apub_lib" } lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "./crates/apub_lib" }
lemmy_utils = { version = "=0.14.4-rc.4", path = "./crates/utils" } lemmy_utils = { version = "=0.15.0-rc.6", path = "./crates/utils" }
lemmy_db_schema = { version = "=0.14.4-rc.4", path = "./crates/db_schema" } lemmy_db_schema = { version = "=0.15.0-rc.6", path = "./crates/db_schema" }
lemmy_db_views = { version = "=0.14.4-rc.4", path = "./crates/db_views" } lemmy_db_views = { version = "=0.15.0-rc.6", path = "./crates/db_views" }
lemmy_db_views_moderator = { version = "=0.14.4-rc.4", path = "./crates/db_views_moderator" } lemmy_db_views_moderator = { version = "=0.15.0-rc.6", path = "./crates/db_views_moderator" }
lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "./crates/db_views_actor" } lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "./crates/db_views_actor" }
lemmy_api_common = { version = "=0.14.4-rc.4", path = "crates/api_common" } lemmy_api_common = { version = "=0.15.0-rc.6", path = "crates/api_common" }
lemmy_websocket = { version = "=0.14.4-rc.4", path = "./crates/websocket" } lemmy_websocket = { version = "=0.15.0-rc.6", path = "./crates/websocket" }
lemmy_routes = { version = "=0.14.4-rc.4", path = "./crates/routes" } lemmy_routes = { version = "=0.15.0-rc.6", path = "./crates/routes" }
diesel = "1.4.8" diesel = "1.4.8"
diesel_migrations = "1.4.0" diesel_migrations = "1.4.0"
chrono = { version = "0.4.19", features = ["serde"] } chrono = { version = "0.4.19", features = ["serde"] }

View file

@ -97,6 +97,10 @@
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"
private_instance: true
} }
# the domain name of your instance (mandatory) # the domain name of your instance (mandatory)
hostname: "unset" hostname: "unset"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lemmy_api" name = "lemmy_api"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
edition = "2018" edition = "2018"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -13,15 +13,15 @@ path = "src/lib.rs"
doctest = false doctest = false
[dependencies] [dependencies]
lemmy_apub = { version = "=0.14.4-rc.4", path = "../apub" } lemmy_apub = { version = "=0.15.0-rc.6", path = "../apub" }
lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "../apub_lib" } lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "../apub_lib" }
lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" } lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" } lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" } lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
lemmy_db_views_moderator = { version = "=0.14.4-rc.4", path = "../db_views_moderator" } lemmy_db_views_moderator = { version = "=0.15.0-rc.6", path = "../db_views_moderator" }
lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" } lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" } lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" }
lemmy_websocket = { version = "=0.14.4-rc.4", path = "../websocket" } lemmy_websocket = { version = "=0.15.0-rc.6", path = "../websocket" }
diesel = "1.4.8" diesel = "1.4.8"
bcrypt = "0.10.1" bcrypt = "0.10.1"
chrono = { version = "0.4.19", features = ["serde"] } chrono = { version = "0.4.19", features = ["serde"] }

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
@ -75,6 +84,9 @@ pub async fn match_websocket_operation(
UserOperation::GetUnreadCount => { UserOperation::GetUnreadCount => {
do_websocket_operation::<GetUnreadCount>(context, id, op, data).await do_websocket_operation::<GetUnreadCount>(context, id, op, data).await
} }
UserOperation::VerifyEmail => {
do_websocket_operation::<VerifyEmail>(context, id, op, data).await
}
// Private Message ops // Private Message ops
UserOperation::MarkPrivateMessageAsRead => { UserOperation::MarkPrivateMessageAsRead => {
@ -219,8 +231,8 @@ mod tests {
let inserted_person = Person::create(&conn, &new_person).unwrap(); let inserted_person = Person::create(&conn, &new_person).unwrap();
let local_user_form = LocalUserForm { let local_user_form = LocalUserForm {
person_id: inserted_person.id, person_id: Some(inserted_person.id),
password_encrypted: "123456".to_string(), password_encrypted: Some("123456".to_string()),
..LocalUserForm::default() ..LocalUserForm::default()
}; };

View file

@ -6,10 +6,14 @@ use captcha::{gen, Difficulty};
use chrono::Duration; use chrono::Duration;
use lemmy_api_common::{ use lemmy_api_common::{
blocking, blocking,
check_registration_application,
get_local_user_view_from_jwt, get_local_user_view_from_jwt,
is_admin, is_admin,
password_length_check, password_length_check,
person::*, person::*,
send_email_verification_success,
send_password_reset_email,
send_verification_email,
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
diesel_option_overwrite, diesel_option_overwrite,
@ -19,6 +23,7 @@ use lemmy_db_schema::{
source::{ source::{
comment::Comment, comment::Comment,
community::Community, community::Community,
email_verification::EmailVerification,
local_user::{LocalUser, LocalUserForm}, local_user::{LocalUser, LocalUserForm},
moderator::*, moderator::*,
password_reset_request::*, password_reset_request::*,
@ -46,12 +51,10 @@ use lemmy_db_views_actor::{
}; };
use lemmy_utils::{ use lemmy_utils::{
claims::Claims, claims::Claims,
email::send_email,
location_info, 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, ConnectionId,
LemmyError, LemmyError,
Sensitive,
}; };
use lemmy_websocket::{ use lemmy_websocket::{
messages::{CaptchaItem, SendAllMessage}, messages::{CaptchaItem, SendAllMessage},
@ -90,14 +93,25 @@ impl Perform for Login {
return Err(LemmyError::from_message("password_incorrect")); 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 // Return the jwt
Ok(LoginResponse { Ok(LoginResponse {
jwt: Claims::jwt( jwt: Some(
Claims::jwt(
local_user_view.local_user.id.0, local_user_view.local_user.id.0,
&context.secret().jwt_secret, &context.secret().jwt_secret,
&context.settings().hostname, &context.settings().hostname,
)? )?
.into(), .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 avatar = diesel_option_overwrite_to_url(&data.avatar)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?; 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 bio = diesel_option_overwrite(&data.bio);
let display_name = diesel_option_overwrite(&data.display_name); let display_name = diesel_option_overwrite(&data.display_name);
let matrix_user_id = diesel_option_overwrite(&data.matrix_user_id); let matrix_user_id = diesel_option_overwrite(&data.matrix_user_id);
let bot_account = data.bot_account; 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 let Some(Some(bio)) = &bio {
if bio.chars().count() > 300 { if bio.chars().count() > 300 {
@ -228,9 +266,9 @@ impl Perform for SaveUserSettings {
.map_err(|e| e.with_message("user_already_exists"))?; .map_err(|e| e.with_message("user_already_exists"))?;
let local_user_form = LocalUserForm { let local_user_form = LocalUserForm {
person_id, person_id: Some(person_id),
email, email,
password_encrypted, password_encrypted: Some(password_encrypted),
show_nsfw: data.show_nsfw, show_nsfw: data.show_nsfw,
show_bot_accounts: data.show_bot_accounts, show_bot_accounts: data.show_bot_accounts,
show_scores: data.show_scores, show_scores: data.show_scores,
@ -242,6 +280,8 @@ impl Perform for SaveUserSettings {
show_read_posts: data.show_read_posts, show_read_posts: data.show_read_posts,
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,
accepted_application: None,
}; };
let local_user_res = blocking(context.pool(), move |conn| { let local_user_res = blocking(context.pool(), move |conn| {
@ -265,12 +305,16 @@ impl Perform for SaveUserSettings {
// Return the jwt // Return the jwt
Ok(LoginResponse { Ok(LoginResponse {
jwt: Claims::jwt( jwt: Some(
Claims::jwt(
updated_local_user.id.0, updated_local_user.id.0,
&context.secret().jwt_secret, &context.secret().jwt_secret,
&context.settings().hostname, &context.settings().hostname,
)? )?
.into(), .into(),
),
verify_email_sent: false,
registration_created: false,
}) })
} }
} }
@ -315,12 +359,16 @@ impl Perform for ChangePassword {
// Return the jwt // Return the jwt
Ok(LoginResponse { Ok(LoginResponse {
jwt: Claims::jwt( jwt: Some(
Claims::jwt(
updated_local_user.id.0, updated_local_user.id.0,
&context.secret().jwt_secret, &context.secret().jwt_secret,
&context.settings().hostname, &context.settings().hostname,
)? )?
.into(), .into(),
),
verify_email_sent: false,
registration_created: false,
}) })
} }
} }
@ -736,34 +784,8 @@ impl Perform for PasswordReset {
.map_err(LemmyError::from) .map_err(LemmyError::from)
.map_err(|e| e.with_message("couldnt_find_that_username_or_email"))?; .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. // Email the pure token to the user.
// TODO no i18n support here. send_password_reset_email(&local_user_view, context.pool(), &context.settings()).await?;
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!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", 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"))?;
Ok(PasswordResetResponse {}) Ok(PasswordResetResponse {})
} }
} }
@ -805,12 +827,16 @@ impl Perform for PasswordChange {
// Return the jwt // Return the jwt
Ok(LoginResponse { Ok(LoginResponse {
jwt: Claims::jwt( jwt: Some(
Claims::jwt(
updated_local_user.id.0, updated_local_user.id.0,
&context.secret().jwt_secret, &context.secret().jwt_secret,
&context.settings().hostname, &context.settings().hostname,
)? )?
.into(), .into(),
),
verify_email_sent: false,
registration_created: false,
}) })
} }
} }
@ -893,3 +919,49 @@ impl Perform for GetUnreadCount {
Ok(res) Ok(res)
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for VerifyEmail {
type Response = VerifyEmailResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<usize>,
) -> Result<Self::Response, LemmyError> {
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 {})
}
}

View file

@ -5,9 +5,11 @@ use diesel::NotFound;
use lemmy_api_common::{ use lemmy_api_common::{
blocking, blocking,
build_federated_instances, build_federated_instances,
check_private_instance,
get_local_user_view_from_jwt, get_local_user_view_from_jwt,
get_local_user_view_from_jwt_opt, get_local_user_view_from_jwt_opt,
is_admin, is_admin,
send_application_approved_email,
site::*, site::*,
}; };
use lemmy_apub::{ use lemmy_apub::{
@ -19,9 +21,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,
@ -30,7 +38,12 @@ use lemmy_db_schema::{
}; };
use lemmy_db_views::{ use lemmy_db_views::{
comment_view::{CommentQueryBuilder, CommentView}, comment_view::{CommentQueryBuilder, CommentView},
local_user_view::LocalUserView,
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::{
@ -64,6 +77,12 @@ impl Perform for GetModlog {
) -> Result<GetModlogResponse, LemmyError> { ) -> Result<GetModlogResponse, LemmyError> {
let data: &GetModlog = self; 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 community_id = data.community_id;
let mod_person_id = data.mod_person_id; let mod_person_id = data.mod_person_id;
let page = data.page; 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()) get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
.await?; .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_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw);
let show_bot_accounts = local_user_view let show_bot_accounts = local_user_view
.as_ref() .as_ref()
@ -388,6 +409,8 @@ impl Perform for ResolveObject {
let local_user_view = let local_user_view =
get_local_user_view_from_jwt_opt(self.auth.as_ref(), context.pool(), context.secret()) get_local_user_view_from_jwt_opt(self.auth.as_ref(), context.pool(), context.secret())
.await?; .await?;
check_private_instance(&local_user_view, context.pool()).await?;
let res = search_by_apub_id(&self.q, context) let res = search_by_apub_id(&self.q, context)
.await .await
.map_err(LemmyError::from) .map_err(LemmyError::from)
@ -555,3 +578,142 @@ 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??;
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<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

@ -1,6 +1,6 @@
[package] [package]
name = "lemmy_api_common" name = "lemmy_api_common"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
edition = "2018" edition = "2018"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -13,11 +13,11 @@ path = "src/lib.rs"
doctest = false doctest = false
[dependencies] [dependencies]
lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" } lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
lemmy_db_views_moderator = { version = "=0.14.4-rc.4", path = "../db_views_moderator" } lemmy_db_views_moderator = { version = "=0.15.0-rc.6", path = "../db_views_moderator" }
lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" } lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" } lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" } lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
serde = { version = "1.0.131", features = ["derive"] } serde = { version = "1.0.131", features = ["derive"] }
diesel = "1.4.8" diesel = "1.4.8"
actix-web = { version = "4.0.0-beta.14", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-beta.14", default-features = false, features = ["cookies"] }

View file

@ -10,8 +10,11 @@ use lemmy_db_schema::{
newtypes::{CommunityId, LocalUserId, PersonId, PostId}, newtypes::{CommunityId, LocalUserId, PersonId, PostId},
source::{ source::{
community::Community, community::Community,
email_verification::{EmailVerification, EmailVerificationForm},
password_reset_request::PasswordResetRequest,
person_block::PersonBlock, person_block::PersonBlock,
post::{Post, PostRead, PostReadForm}, post::{Post, PostRead, PostReadForm},
registration_application::RegistrationApplication,
secret::Secret, secret::Secret,
site::Site, site::Site,
}, },
@ -23,7 +26,14 @@ use lemmy_db_views_actor::{
community_person_ban_view::CommunityPersonBanView, community_person_ban_view::CommunityPersonBanView,
community_view::CommunityView, 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; use url::Url;
pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError> pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
@ -252,6 +262,19 @@ pub async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), Le
Ok(()) Ok(())
} }
pub async fn check_private_instance(
local_user_view: &Option<LocalUserView>,
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( pub async fn build_federated_instances(
pool: &DbPool, pool: &DbPool,
federation_config: &FederationConfig, federation_config: &FederationConfig,
@ -320,3 +343,163 @@ pub fn honeypot_check(honeypot: &Option<String>) -> Result<(), LemmyError> {
Ok(()) 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!(
"<h1>{}</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
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!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", 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.<br><br>",
"<a href=\"{}\">Verify your email</a>"
),
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(())
}

View file

@ -24,10 +24,13 @@ pub struct Register {
pub password: Sensitive<String>, pub password: Sensitive<String>,
pub password_verify: Sensitive<String>, pub password_verify: Sensitive<String>,
pub show_nsfw: bool, pub show_nsfw: bool,
/// email is mandatory if email verification is enabled on the server
pub email: Option<Sensitive<String>>, pub email: Option<Sensitive<String>>,
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(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -78,7 +81,10 @@ pub struct ChangePassword {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct LoginResponse { pub struct LoginResponse {
pub jwt: Sensitive<String>, /// This is None in response to `Register` if email verification is enabled, or the server requires registration applications.
pub jwt: Option<Sensitive<String>>,
pub registration_created: bool,
pub verify_email_sent: bool,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -194,6 +200,9 @@ pub struct DeleteAccount {
pub auth: Sensitive<String>, pub auth: Sensitive<String>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DeleteAccountResponse {}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct PasswordReset { pub struct PasswordReset {
pub email: Sensitive<String>, pub email: Sensitive<String>,
@ -279,3 +288,11 @@ pub struct GetUnreadCountResponse {
pub mentions: i64, pub mentions: i64,
pub private_messages: i64, pub private_messages: i64,
} }
#[derive(Serialize, Deserialize)]
pub struct VerifyEmail {
pub token: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct VerifyEmailResponse {}

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::{
@ -71,6 +72,7 @@ pub struct GetModlog {
pub community_id: Option<CommunityId>, pub community_id: Option<CommunityId>,
pub page: Option<i64>, pub page: Option<i64>,
pub limit: Option<i64>, pub limit: Option<i64>,
pub auth: Option<Sensitive<String>>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -98,6 +100,10 @@ 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 private_instance: Option<bool>,
pub auth: Sensitive<String>, pub auth: Sensitive<String>,
} }
@ -112,6 +118,10 @@ pub struct EditSite {
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 private_instance: Option<bool>,
pub auth: Sensitive<String>, pub auth: Sensitive<String>,
} }
@ -173,3 +183,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

@ -1,6 +1,6 @@
[package] [package]
name = "lemmy_api_crud" name = "lemmy_api_crud"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
edition = "2018" edition = "2018"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -8,15 +8,15 @@ homepage = "https://join-lemmy.org/"
documentation = "https://join-lemmy.org/docs/en/index.html" documentation = "https://join-lemmy.org/docs/en/index.html"
[dependencies] [dependencies]
lemmy_apub = { version = "=0.14.4-rc.4", path = "../apub" } lemmy_apub = { version = "=0.15.0-rc.6", path = "../apub" }
lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "../apub_lib" } lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "../apub_lib" }
lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" } lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" } lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" } lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
lemmy_db_views_moderator = { version = "=0.14.4-rc.4", path = "../db_views_moderator" } lemmy_db_views_moderator = { version = "=0.15.0-rc.6", path = "../db_views_moderator" }
lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" } lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" } lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" }
lemmy_websocket = { version = "=0.14.4-rc.4", path = "../websocket" } lemmy_websocket = { version = "=0.15.0-rc.6", path = "../websocket" }
diesel = "1.4.8" diesel = "1.4.8"
bcrypt = "0.10.1" bcrypt = "0.10.1"
chrono = { version = "0.4.19", features = ["serde"] } chrono = { version = "0.4.19", features = ["serde"] }

View file

@ -1,6 +1,11 @@
use crate::PerformCrud; use crate::PerformCrud;
use actix_web::web::Data; 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::{ use lemmy_apub::{
fetcher::webfinger::webfinger_resolve, fetcher::webfinger::webfinger_resolve,
objects::community::ApubCommunity, 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()) get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
.await?; .await?;
check_private_instance(&local_user_view, context.pool()).await?;
let person_id = local_user_view.map(|u| u.person.id); let person_id = local_user_view.map(|u| u.person.id);
let id = data.id; let id = data.id;
let comment_view = blocking(context.pool(), move |conn| { 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()) get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
.await?; .await?;
check_private_instance(&local_user_view, context.pool()).await?;
let show_bot_accounts = local_user_view let show_bot_accounts = local_user_view
.as_ref() .as_ref()
.map(|t| t.local_user.show_bot_accounts); .map(|t| t.local_user.show_bot_accounts);

View file

@ -1,6 +1,11 @@
use crate::PerformCrud; use crate::PerformCrud;
use actix_web::web::Data; 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::{ use lemmy_apub::{
fetcher::webfinger::webfinger_resolve, fetcher::webfinger::webfinger_resolve,
objects::community::ApubCommunity, objects::community::ApubCommunity,
@ -34,6 +39,9 @@ impl PerformCrud for GetCommunity {
let local_user_view = let local_user_view =
get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
.await?; .await?;
check_private_instance(&local_user_view, context.pool()).await?;
let person_id = local_user_view.map(|u| u.person.id); let person_id = local_user_view.map(|u| u.person.id);
let community_id = match data.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()) get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
.await?; .await?;
check_private_instance(&local_user_view, context.pool()).await?;
let person_id = local_user_view.to_owned().map(|l| l.person.id); let person_id = local_user_view.to_owned().map(|l| l.person.id);
// Don't show NSFW by default // Don't show NSFW by default

View file

@ -1,6 +1,12 @@
use crate::PerformCrud; use crate::PerformCrud;
use actix_web::web::Data; 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::{ use lemmy_apub::{
fetcher::webfinger::webfinger_resolve, fetcher::webfinger::webfinger_resolve,
objects::community::ApubCommunity, 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()) get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
.await?; .await?;
check_private_instance(&local_user_view, context.pool()).await?;
let show_bot_accounts = local_user_view let show_bot_accounts = local_user_view
.as_ref() .as_ref()
.map(|t| t.local_user.show_bot_accounts); .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()) get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
.await?; .await?;
check_private_instance(&local_user_view, context.pool()).await?;
let person_id = local_user_view.to_owned().map(|l| l.person.id); 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); let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw);

View file

@ -5,6 +5,7 @@ use lemmy_api_common::{
check_person_block, check_person_block,
get_local_user_view_from_jwt, get_local_user_view_from_jwt,
person::{CreatePrivateMessage, PrivateMessageResponse}, person::{CreatePrivateMessage, PrivateMessageResponse},
send_email_to_user,
}; };
use lemmy_apub::{ use lemmy_apub::{
generate_local_apub_endpoint, generate_local_apub_endpoint,
@ -20,11 +21,7 @@ use lemmy_db_schema::{
}; };
use lemmy_db_views::local_user_view::LocalUserView; use lemmy_db_views::local_user_view::LocalUserView;
use lemmy_utils::{utils::remove_slurs, ConnectionId, LemmyError}; use lemmy_utils::{utils::remove_slurs, ConnectionId, LemmyError};
use lemmy_websocket::{ use lemmy_websocket::{send::send_pm_ws_message, LemmyContext, UserOperationCrud};
send::{send_email_to_user, send_pm_ws_message},
LemmyContext,
UserOperationCrud,
};
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl PerformCrud for CreatePrivateMessage { impl PerformCrud for CreatePrivateMessage {

View file

@ -66,8 +66,8 @@ impl PerformCrud for CreateSite {
enable_downvotes: data.enable_downvotes, enable_downvotes: data.enable_downvotes,
open_registration: data.open_registration, open_registration: data.open_registration,
enable_nsfw: data.enable_nsfw, enable_nsfw: data.enable_nsfw,
updated: None,
community_creation_admin_only: data.community_creation_admin_only, community_creation_admin_only: data.community_creation_admin_only,
..SiteForm::default()
}; };
let create_site = move |conn: &'_ _| Site::create(conn, &site_form); let create_site = move |conn: &'_ _| Site::create(conn, &site_form);

View file

@ -45,8 +45,13 @@ impl PerformCrud for GetSite {
captcha_uuid: None, captcha_uuid: None,
captcha_answer: None, captcha_answer: None,
honeypot: 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); info!("Admin {} created", setup.admin_username);
let create_site = CreateSite { let create_site = CreateSite {
@ -59,7 +64,11 @@ 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,
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?; create_site.perform(context, websocket_id).await?;
info!("Site {} created", setup.site_name); info!("Site {} created", setup.site_name);

View file

@ -11,7 +11,10 @@ use lemmy_db_schema::{
diesel_option_overwrite, diesel_option_overwrite,
diesel_option_overwrite_to_url, diesel_option_overwrite_to_url,
naive_now, naive_now,
source::site::{Site, SiteForm}, source::{
local_user::LocalUser,
site::{Site, SiteForm},
},
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::site_view::SiteView; use lemmy_db_views::site_view::SiteView;
@ -42,6 +45,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)?;
@ -61,14 +65,42 @@ impl PerformCrud for EditSite {
open_registration: data.open_registration, open_registration: data.open_registration,
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_application: data.require_application,
application_question,
private_instance: data.private_instance,
}; };
let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form); let update_site = blocking(context.pool(), move |conn| {
blocking(context.pool(), update_site) Site::update(conn, 1, &site_form)
})
.await? .await?
.map_err(LemmyError::from) .map_err(LemmyError::from)
.map_err(|e| e.with_message("couldnt_update_site"))?; .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_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??; let site_view = blocking(context.pool(), SiteView::read).await??;
let res = SiteResponse { site_view }; let res = SiteResponse { site_view };

View file

@ -1,6 +1,12 @@
use crate::PerformCrud; use crate::PerformCrud;
use actix_web::web::Data; 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::{ use lemmy_apub::{
generate_followers_url, generate_followers_url,
generate_inbox_url, generate_inbox_url,
@ -21,11 +27,10 @@ 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},
ListingType,
SortType,
}; };
use lemmy_db_views_actor::person_view::PersonViewSafe; use lemmy_db_views_actor::person_view::PersonViewSafe;
use lemmy_utils::{ use lemmy_utils::{
@ -49,16 +54,31 @@ impl PerformCrud for Register {
) -> Result<LoginResponse, LemmyError> { ) -> Result<LoginResponse, LemmyError> {
let data: &Register = self; 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 // 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? {
if !site.open_registration { if !site.open_registration {
return Err(LemmyError::from_message("registration_closed")); return Err(LemmyError::from_message("registration_closed"));
} }
email_verification = site.require_email_verification;
require_application = site.require_application;
} }
password_length_check(&data.password)?; password_length_check(&data.password)?;
honeypot_check(&data.honeypot)?; 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 // Make sure passwords match
if data.password != data.password_verify { if data.password != data.password_verify {
return Err(LemmyError::from_message("passwords_dont_match")); 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"))?; .map_err(|e| e.with_message("user_already_exists"))?;
// Create the local user // Create the local user
// TODO some of these could probably use the DB defaults
let local_user_form = LocalUserForm { 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())), 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_nsfw: Some(data.show_nsfw),
show_bot_accounts: Some(true), email_verified: Some(false),
theme: Some("browser".into()), ..LocalUserForm::default()
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),
}; };
let inserted_local_user = match blocking(context.pool(), move |conn| { 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()?; 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
@ -231,14 +257,41 @@ impl PerformCrud for Register {
.map_err(|e| e.with_message("community_moderator_already_exists"))?; .map_err(|e| e.with_message("community_moderator_already_exists"))?;
} }
// Return the jwt let mut login_response = LoginResponse {
Ok(LoginResponse { jwt: None,
jwt: Claims::jwt( 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, inserted_local_user.id.0,
&context.secret().jwt_secret, &context.secret().jwt_secret,
&context.settings().hostname, &context.settings().hostname,
)? )?
.into(), .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)
} }
} }

View file

@ -8,15 +8,15 @@ use lemmy_websocket::LemmyContext;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl PerformCrud for DeleteAccount { impl PerformCrud for DeleteAccount {
type Response = LoginResponse; type Response = DeleteAccountResponse;
#[tracing::instrument(skip(self, context, _websocket_id))] #[tracing::instrument(skip(self, context, _websocket_id))]
async fn perform( async fn perform(
&self, &self,
context: &Data<LemmyContext>, context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>, _websocket_id: Option<ConnectionId>,
) -> Result<LoginResponse, LemmyError> { ) -> Result<Self::Response, LemmyError> {
let data: &DeleteAccount = self; let data = self;
let local_user_view = let local_user_view =
get_local_user_view_from_jwt(data.auth.as_ref(), context.pool(), context.secret()).await?; get_local_user_view_from_jwt(data.auth.as_ref(), context.pool(), context.secret()).await?;
@ -50,8 +50,6 @@ impl PerformCrud for DeleteAccount {
}) })
.await??; .await??;
Ok(LoginResponse { Ok(DeleteAccountResponse {})
jwt: data.auth.clone(),
})
} }
} }

View file

@ -1,6 +1,11 @@
use crate::PerformCrud; use crate::PerformCrud;
use actix_web::web::Data; 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::{ use lemmy_apub::{
fetcher::webfinger::webfinger_resolve, fetcher::webfinger::webfinger_resolve,
objects::person::ApubPerson, 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()) get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
.await?; .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_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw);
let show_bot_accounts = local_user_view let show_bot_accounts = local_user_view
.as_ref() .as_ref()

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lemmy_apub" name = "lemmy_apub"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
edition = "2018" edition = "2018"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -13,13 +13,13 @@ path = "src/lib.rs"
doctest = false doctest = false
[dependencies] [dependencies]
lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" } lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "../apub_lib" } lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "../apub_lib" }
lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" } lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" } lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" } lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" } lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" }
lemmy_websocket = { version = "=0.14.4-rc.4", path = "../websocket" } lemmy_websocket = { version = "=0.15.0-rc.6", path = "../websocket" }
diesel = "1.4.8" diesel = "1.4.8"
activitystreams-kinds = "0.1.2" activitystreams-kinds = "0.1.2"
bcrypt = "0.10.1" bcrypt = "0.10.1"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lemmy_apub_lib" name = "lemmy_apub_lib"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
edition = "2018" edition = "2018"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -8,8 +8,8 @@ homepage = "https://join-lemmy.org/"
documentation = "https://join-lemmy.org/docs/en/index.html" documentation = "https://join-lemmy.org/docs/en/index.html"
[dependencies] [dependencies]
lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" } lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
lemmy_apub_lib_derive = { version = "=0.14.4-rc.4", path = "../apub_lib_derive" } lemmy_apub_lib_derive = { version = "=0.15.0-rc.6", path = "../apub_lib_derive" }
activitystreams = "0.7.0-alpha.14" activitystreams = "0.7.0-alpha.14"
serde = { version = "1.0.131", features = ["derive"] } serde = { version = "1.0.131", features = ["derive"] }
async-trait = "0.1.52" async-trait = "0.1.52"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lemmy_apub_lib_derive" name = "lemmy_apub_lib_derive"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
edition = "2018" edition = "2018"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lemmy_db_schema" name = "lemmy_db_schema"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
edition = "2018" edition = "2018"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -11,8 +11,8 @@ documentation = "https://join-lemmy.org/docs/en/index.html"
doctest = false doctest = false
[dependencies] [dependencies]
lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" } lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "../apub_lib" } lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "../apub_lib" }
diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"] } diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"] }
diesel_migrations = "1.4.0" diesel_migrations = "1.4.0"
chrono = { version = "0.4.19", features = ["serde"] } chrono = { version = "0.4.19", features = ["serde"] }

View file

@ -65,6 +65,10 @@ mod tests {
enable_nsfw: None, enable_nsfw: None,
updated: None, updated: None,
community_creation_admin_only: Some(false), 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(); Site::create(&conn, &site_form).unwrap();

View file

@ -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<Self, Error> {
use crate::schema::email_verification::dsl::*;
insert_into(email_verification)
.values(form)
.get_result::<Self>(conn)
}
fn read(conn: &PgConnection, id_: i32) -> Result<Self, Error> {
use crate::schema::email_verification::dsl::*;
email_verification.find(id_).first::<Self>(conn)
}
fn update(conn: &PgConnection, id_: i32, form: &EmailVerificationForm) -> Result<Self, Error> {
use crate::schema::email_verification::dsl::*;
diesel::update(email_verification.find(id_))
.set(form)
.get_result::<Self>(conn)
}
fn delete(conn: &PgConnection, id_: i32) -> Result<usize, Error> {
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<Self, Error> {
use crate::schema::email_verification::dsl::*;
email_verification
.filter(verification_token.eq(token))
.filter(published.gt(now - 7.days()))
.first::<Self>(conn)
}
pub fn delete_old_tokens_for_local_user(
conn: &PgConnection,
local_user_id_: LocalUserId,
) -> Result<usize, Error> {
use crate::schema::email_verification::dsl::*;
diesel::delete(email_verification.filter(local_user_id.eq(local_user_id_))).execute(conn)
}
}

View file

@ -31,6 +31,8 @@ mod safe_settings_type {
show_scores, show_scores,
show_read_posts, show_read_posts,
show_new_post_notifs, show_new_post_notifs,
email_verified,
accepted_application,
); );
impl ToSafeSettings for LocalUser { impl ToSafeSettings for LocalUser {
@ -54,6 +56,8 @@ mod safe_settings_type {
show_scores, show_scores,
show_read_posts, show_read_posts,
show_new_post_notifs, show_new_post_notifs,
email_verified,
accepted_application,
) )
} }
} }
@ -62,8 +66,10 @@ mod safe_settings_type {
impl LocalUser { impl LocalUser {
pub fn register(conn: &PgConnection, form: &LocalUserForm) -> Result<Self, Error> { pub fn register(conn: &PgConnection, form: &LocalUserForm) -> Result<Self, Error> {
let mut edited_user = form.clone(); let mut edited_user = form.clone();
let password_hash = let password_hash = form
hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password"); .password_encrypted
.as_ref()
.map(|p| hash(p, DEFAULT_COST).expect("Couldn't hash password"));
edited_user.password_encrypted = password_hash; edited_user.password_encrypted = password_hash;
Self::create(conn, &edited_user) Self::create(conn, &edited_user)
@ -83,6 +89,20 @@ impl LocalUser {
)) ))
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
pub fn set_all_users_email_verified(conn: &PgConnection) -> Result<Vec<Self>, Error> {
diesel::update(local_user)
.set(email_verified.eq(true))
.get_results::<Self>(conn)
}
pub fn set_all_users_registration_applications_accepted(
conn: &PgConnection,
) -> Result<Vec<Self>, Error> {
diesel::update(local_user)
.set(accepted_application.eq(true))
.get_results::<Self>(conn)
}
} }
impl Crud for LocalUser { impl Crud for LocalUser {

View file

@ -3,6 +3,7 @@ pub mod comment;
pub mod comment_report; pub mod comment_report;
pub mod community; pub mod community;
pub mod community_block; pub mod community_block;
pub mod email_verification;
pub mod local_user; pub mod local_user;
pub mod moderator; pub mod moderator;
pub mod password_reset_request; pub mod password_reset_request;
@ -12,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

@ -93,8 +93,8 @@ mod tests {
let inserted_person = Person::create(&conn, &new_person).unwrap(); let inserted_person = Person::create(&conn, &new_person).unwrap();
let new_local_user = LocalUserForm { let new_local_user = LocalUserForm {
person_id: inserted_person.id, person_id: Some(inserted_person.id),
password_encrypted: "pass".to_string(), password_encrypted: Some("pass".to_string()),
..LocalUserForm::default() ..LocalUserForm::default()
}; };

View file

@ -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<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)
}
}
impl RegistrationApplication {
pub fn find_by_local_user_id(
conn: &PgConnection,
local_user_id_: LocalUserId,
) -> Result<Self, Error> {
use crate::schema::registration_application::dsl::*;
registration_application
.filter(local_user_id.eq(local_user_id_))
.first::<Self>(conn)
}
}

View file

@ -43,7 +43,9 @@ impl fmt::Display for CommentId {
)] )]
pub struct CommunityId(pub i32); 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); pub struct LocalUserId(pub i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)] #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)]

View file

@ -157,6 +157,8 @@ table! {
show_scores -> Bool, show_scores -> Bool,
show_read_posts -> Bool, show_read_posts -> Bool,
show_new_post_notifs -> Bool, show_new_post_notifs -> Bool,
email_verified -> Bool,
accepted_application -> Bool,
} }
} }
@ -447,6 +449,10 @@ table! {
banner -> Nullable<Varchar>, banner -> Nullable<Varchar>,
description -> Nullable<Text>, description -> Nullable<Text>,
community_creation_admin_only -> Bool, community_creation_admin_only -> Bool,
require_email_verification -> Bool,
require_application -> Bool,
application_question -> Nullable<Text>,
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<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));
@ -619,6 +646,9 @@ joinable!(post_saved -> person (person_id));
joinable!(post_saved -> post (post_id)); 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!(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,
@ -662,4 +692,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,
registration_application
); );

View file

@ -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,
}

View file

@ -23,14 +23,16 @@ pub struct LocalUser {
pub show_scores: bool, pub show_scores: bool,
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 accepted_application: bool,
} }
// TODO redo these, check table defaults // TODO redo these, check table defaults
#[derive(Insertable, AsChangeset, Clone, Default)] #[derive(Insertable, AsChangeset, Clone, Default)]
#[table_name = "local_user"] #[table_name = "local_user"]
pub struct LocalUserForm { pub struct LocalUserForm {
pub person_id: PersonId, pub person_id: Option<PersonId>,
pub password_encrypted: String, pub password_encrypted: Option<String>,
pub email: Option<Option<String>>, pub email: Option<Option<String>>,
pub show_nsfw: Option<bool>, pub show_nsfw: Option<bool>,
pub theme: Option<String>, pub theme: Option<String>,
@ -43,6 +45,8 @@ pub struct LocalUserForm {
pub show_scores: Option<bool>, pub show_scores: Option<bool>,
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 accepted_application: Option<bool>,
} }
/// A local user view that removes password encrypted /// A local user view that removes password encrypted
@ -64,4 +68,6 @@ pub struct LocalUserSettings {
pub show_scores: bool, pub show_scores: bool,
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 accepted_application: bool,
} }

View file

@ -3,6 +3,7 @@ pub mod comment;
pub mod comment_report; pub mod comment_report;
pub mod community; pub mod community;
pub mod community_block; pub mod community_block;
pub mod email_verification;
pub mod local_user; pub mod local_user;
pub mod moderator; pub mod moderator;
pub mod password_reset_request; pub mod password_reset_request;
@ -12,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

@ -20,9 +20,13 @@ pub struct Site {
pub banner: Option<DbUrl>, pub banner: Option<DbUrl>,
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_application: bool,
pub application_question: Option<String>,
pub private_instance: bool,
} }
#[derive(Insertable, AsChangeset)] #[derive(Insertable, AsChangeset, Default)]
#[table_name = "site"] #[table_name = "site"]
pub struct SiteForm { pub struct SiteForm {
pub name: String, pub name: String,
@ -37,4 +41,8 @@ pub struct SiteForm {
pub banner: Option<Option<DbUrl>>, pub banner: Option<Option<DbUrl>>,
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_application: Option<bool>,
pub application_question: Option<Option<String>>,
pub private_instance: Option<bool>,
} }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lemmy_db_views" name = "lemmy_db_views"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
edition = "2018" edition = "2018"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -11,7 +11,7 @@ documentation = "https://join-lemmy.org/docs/en/index.html"
doctest = false doctest = false
[dependencies] [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"] } diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"] }
serde = { version = "1.0.131", features = ["derive"] } serde = { version = "1.0.131", features = ["derive"] }
tracing = "0.1.29" tracing = "0.1.29"

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,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<PersonSafeAlias1>,
}
type RegistrationApplicationViewTuple = (
RegistrationApplication,
LocalUserSettings,
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,
LocalUser::safe_settings_columns_tuple(),
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,
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::<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, 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();
}
}

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lemmy_db_views_actor" name = "lemmy_db_views_actor"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
edition = "2018" edition = "2018"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -11,6 +11,6 @@ documentation = "https://join-lemmy.org/docs/en/index.html"
doctest = false doctest = false
[dependencies] [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"] } diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"] }
serde = { version = "1.0.131", features = ["derive"] } serde = { version = "1.0.131", features = ["derive"] }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lemmy_db_views_moderator" name = "lemmy_db_views_moderator"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
edition = "2018" edition = "2018"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -11,6 +11,6 @@ documentation = "https://join-lemmy.org/docs/en/index.html"
doctest = false doctest = false
[dependencies] [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"] } diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"] }
serde = { version = "1.0.131", features = ["derive"] } serde = { version = "1.0.131", features = ["derive"] }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lemmy_routes" name = "lemmy_routes"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
edition = "2018" edition = "2018"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -11,13 +11,13 @@ documentation = "https://join-lemmy.org/docs/en/index.html"
doctest = false doctest = false
[dependencies] [dependencies]
lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" } lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
lemmy_websocket = { version = "=0.14.4-rc.4", path = "../websocket" } lemmy_websocket = { version = "=0.15.0-rc.6", path = "../websocket" }
lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" } lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" } lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" } lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" } lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" }
lemmy_apub = { version = "=0.14.4-rc.4", path = "../apub" } lemmy_apub = { version = "=0.15.0-rc.6", path = "../apub" }
diesel = "1.4.8" diesel = "1.4.8"
actix = "0.12.0" actix = "0.12.0"
actix-web = { version = "4.0.0-beta.14", default-features = false, features = ["rustls"] } actix-web = { version = "4.0.0-beta.14", default-features = false, features = ["rustls"] }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lemmy_utils" name = "lemmy_utils"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
edition = "2018" edition = "2018"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"

View file

@ -1,4 +1,4 @@
use crate::settings::structs::Settings; use crate::{settings::structs::Settings, LemmyError};
use lettre::{ use lettre::{
message::{header, Mailbox, MultiPart, SinglePart}, message::{header, Mailbox, MultiPart, SinglePart},
transport::smtp::{ transport::smtp::{
@ -20,12 +20,21 @@ pub fn send_email(
to_username: &str, to_username: &str,
html: &str, html: &str,
settings: &Settings, settings: &Settings,
) -> Result<(), String> { ) -> Result<(), LemmyError> {
let email_config = settings.email.to_owned().ok_or("no_email_setup")?; let email_config = settings
.email
.to_owned()
.ok_or_else(|| LemmyError::from_message("no_email_setup"))?;
let domain = settings.hostname.to_owned(); let domain = settings.hostname.to_owned();
let (smtp_server, smtp_port) = { let (smtp_server, smtp_port) = {
let email_and_port = email_config.smtp_server.split(':').collect::<Vec<&str>>(); let email_and_port = email_config.smtp_server.split(':').collect::<Vec<&str>>();
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[0],
email_and_port[1] email_and_port[1]
@ -87,6 +96,6 @@ pub fn send_email(
match result { match result {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(e) => Err(e.to_string()), Err(e) => Err(LemmyError::from(e).with_message("email_send_failed")),
} }
} }

View file

@ -190,4 +190,12 @@ 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>,
#[default(None)]
pub private_instance: Option<bool>,
} }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lemmy_websocket" name = "lemmy_websocket"
version = "0.14.4-rc.4" version = "0.15.0-rc.6"
edition = "2018" edition = "2018"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -13,11 +13,11 @@ path = "src/lib.rs"
doctest = false doctest = false
[dependencies] [dependencies]
lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" } lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" } lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" }
lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" } lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" } lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" } lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
reqwest = { version = "0.11.7", features = ["json"] } reqwest = { version = "0.11.7", features = ["json"] }
reqwest-middleware = "0.1.3" reqwest-middleware = "0.1.3"
tracing = "0.1.29" tracing = "0.1.29"

View file

@ -117,6 +117,7 @@ pub enum UserOperation {
ListPostReports, ListPostReports,
GetReportCount, GetReportCount,
GetUnreadCount, GetUnreadCount,
VerifyEmail,
FollowCommunity, FollowCommunity,
GetReplies, GetReplies,
GetPersonMentions, GetPersonMentions,
@ -125,6 +126,9 @@ pub enum UserOperation {
BanFromCommunity, BanFromCommunity,
AddModToCommunity, AddModToCommunity,
AddAdmin, AddAdmin,
GetUnreadRegistrationApplicationCount,
ListRegistrationApplications,
ApproveRegistrationApplication,
BanPerson, BanPerson,
Search, Search,
ResolveObject, ResolveObject,

View file

@ -10,6 +10,7 @@ use lemmy_api_common::{
community::CommunityResponse, community::CommunityResponse,
person::PrivateMessageResponse, person::PrivateMessageResponse,
post::PostResponse, post::PostResponse,
send_email_to_user,
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::{CommentId, CommunityId, LocalUserId, PersonId, PostId, PrivateMessageId}, newtypes::{CommentId, CommunityId, LocalUserId, PersonId, PostId, PrivateMessageId},
@ -28,14 +29,7 @@ use lemmy_db_views::{
private_message_view::PrivateMessageView, private_message_view::PrivateMessageView,
}; };
use lemmy_db_views_actor::community_view::CommunityView; use lemmy_db_views_actor::community_view::CommunityView;
use lemmy_utils::{ use lemmy_utils::{utils::MentionData, ConnectionId, LemmyError};
email::send_email,
settings::structs::Settings,
utils::MentionData,
ConnectionId,
LemmyError,
};
use tracing::error;
pub async fn send_post_ws_message<OP: ToString + Send + OperationType + 'static>( pub async fn send_post_ws_message<OP: ToString + Send + OperationType + 'static>(
post_id: PostId, post_id: PostId,
@ -296,39 +290,3 @@ pub async fn send_local_notifs(
}; };
Ok(recipient_ids) 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!(
"<h1>{}</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
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),
};
}
}

View file

@ -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;

View file

@ -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
);

View file

@ -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;

View file

@ -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);

View file

@ -0,0 +1 @@
alter table email_verification drop column published;

View file

@ -0,0 +1 @@
alter table email_verification add column published timestamp not null default now();

View file

@ -210,13 +210,26 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
web::put().to(route_post::<ChangePassword>), web::put().to(route_post::<ChangePassword>),
) )
.route("/report_count", web::get().to(route_get::<GetReportCount>)) .route("/report_count", web::get().to(route_get::<GetReportCount>))
.route("/unread_count", web::get().to(route_get::<GetUnreadCount>)), .route("/unread_count", web::get().to(route_get::<GetUnreadCount>))
.route("/verify_email", web::post().to(route_post::<VerifyEmail>)),
) )
// 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>),
),
), ),
); );
} }

View file

@ -9,7 +9,7 @@ use diesel::{
}; };
use doku::json::{AutoComments, Formatting}; use doku::json::{AutoComments, Formatting};
use lemmy_api::match_websocket_operation; 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_api_crud::match_websocket_operation_crud;
use lemmy_apub_lib::activity_queue::create_activity_queue; use lemmy_apub_lib::activity_queue::create_activity_queue;
use lemmy_db_schema::{get_database_url_from_env, source::secret::Secret}; 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(); let activity_queue = queue_manager.queue_handle().clone();
check_private_instance_and_federation_enabled(&pool, &settings).await?;
let chat_server = ChatServer::startup( let chat_server = ChatServer::startup(
pool.clone(), pool.clone(),
rate_limiter.clone(), rate_limiter.clone(),