* 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>
[[package]]
name = "lemmy_api"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
dependencies = [
"actix",
"actix-rt",
[[package]]
name = "lemmy_api_common"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
dependencies = [
"actix-web",
"chrono",
[[package]]
name = "lemmy_api_crud"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
dependencies = [
"actix",
"actix-rt",
[[package]]
name = "lemmy_apub"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
dependencies = [
"activitystreams-kinds",
"actix",
[[package]]
name = "lemmy_apub_lib"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
dependencies = [
"activitystreams",
"actix-web",
[[package]]
name = "lemmy_apub_lib_derive"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
dependencies = [
"proc-macro2 1.0.33",
"quote 1.0.10",
[[package]]
name = "lemmy_db_schema"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
dependencies = [
"bcrypt",
"chrono",
[[package]]
name = "lemmy_db_views"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
dependencies = [
"diesel",
"lemmy_db_schema",
[[package]]
name = "lemmy_db_views_actor"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
dependencies = [
"diesel",
"lemmy_db_schema",
[[package]]
name = "lemmy_db_views_moderator"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
dependencies = [
"diesel",
"lemmy_db_schema",
[[package]]
name = "lemmy_routes"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
dependencies = [
"actix",
"actix-http",
[[package]]
name = "lemmy_server"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
dependencies = [
"activitystreams",
"actix",
[[package]]
name = "lemmy_utils"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
dependencies = [
"actix-rt",
"actix-web",
[[package]]
name = "lemmy_websocket"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
dependencies = [
"actix",
"actix-web",
[package]
name = "lemmy_server"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
edition = "2018"
description = "A link aggregator for the fediverse"
license = "AGPL-3.0"
]
[dependencies]
-lemmy_api = { version = "=0.14.4-rc.4", path = "./crates/api" }
-lemmy_api_crud = { version = "=0.14.4-rc.4", path = "./crates/api_crud" }
-lemmy_apub = { version = "=0.14.4-rc.4", path = "./crates/apub" }
-lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "./crates/apub_lib" }
-lemmy_utils = { version = "=0.14.4-rc.4", path = "./crates/utils" }
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "./crates/db_schema" }
-lemmy_db_views = { version = "=0.14.4-rc.4", path = "./crates/db_views" }
-lemmy_db_views_moderator = { version = "=0.14.4-rc.4", path = "./crates/db_views_moderator" }
-lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "./crates/db_views_actor" }
-lemmy_api_common = { version = "=0.14.4-rc.4", path = "crates/api_common" }
-lemmy_websocket = { version = "=0.14.4-rc.4", path = "./crates/websocket" }
-lemmy_routes = { version = "=0.14.4-rc.4", path = "./crates/routes" }
+lemmy_api = { version = "=0.15.0-rc.6", path = "./crates/api" }
+lemmy_api_crud = { version = "=0.15.0-rc.6", path = "./crates/api_crud" }
+lemmy_apub = { version = "=0.15.0-rc.6", path = "./crates/apub" }
+lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "./crates/apub_lib" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "./crates/utils" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "./crates/db_schema" }
+lemmy_db_views = { version = "=0.15.0-rc.6", path = "./crates/db_views" }
+lemmy_db_views_moderator = { version = "=0.15.0-rc.6", path = "./crates/db_views_moderator" }
+lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "./crates/db_views_actor" }
+lemmy_api_common = { version = "=0.15.0-rc.6", path = "crates/api_common" }
+lemmy_websocket = { version = "=0.15.0-rc.6", path = "./crates/websocket" }
+lemmy_routes = { version = "=0.15.0-rc.6", path = "./crates/routes" }
diesel = "1.4.8"
diesel_migrations = "1.4.0"
chrono = { version = "0.4.19", features = ["serde"] }
open_registration: true
enable_nsfw: true
community_creation_admin_only: true
+ require_email_verification: true
+ require_application: true
+ application_question: "string"
+ private_instance: true
}
# the domain name of your instance (mandatory)
hostname: "unset"
[package]
name = "lemmy_api"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
edition = "2018"
description = "A link aggregator for the fediverse"
license = "AGPL-3.0"
doctest = false
[dependencies]
-lemmy_apub = { version = "=0.14.4-rc.4", path = "../apub" }
-lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "../apub_lib" }
-lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" }
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
-lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" }
-lemmy_db_views_moderator = { version = "=0.14.4-rc.4", path = "../db_views_moderator" }
-lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" }
-lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" }
-lemmy_websocket = { version = "=0.14.4-rc.4", path = "../websocket" }
+lemmy_apub = { version = "=0.15.0-rc.6", path = "../apub" }
+lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "../apub_lib" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
+lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
+lemmy_db_views_moderator = { version = "=0.15.0-rc.6", path = "../db_views_moderator" }
+lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
+lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" }
+lemmy_websocket = { version = "=0.15.0-rc.6", path = "../websocket" }
diesel = "1.4.8"
bcrypt = "0.10.1"
chrono = { version = "0.4.19", features = ["serde"] }
UserOperation::GetCaptcha => do_websocket_operation::<GetCaptcha>(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::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::BlockPerson => {
do_websocket_operation::<BlockPerson>(context, id, op, data).await
UserOperation::GetUnreadCount => {
do_websocket_operation::<GetUnreadCount>(context, id, op, data).await
}
+ UserOperation::VerifyEmail => {
+ do_websocket_operation::<VerifyEmail>(context, id, op, data).await
+ }
// Private Message ops
UserOperation::MarkPrivateMessageAsRead => {
let inserted_person = Person::create(&conn, &new_person).unwrap();
let local_user_form = LocalUserForm {
- person_id: inserted_person.id,
- password_encrypted: "123456".to_string(),
+ person_id: Some(inserted_person.id),
+ password_encrypted: Some("123456".to_string()),
..LocalUserForm::default()
};
use chrono::Duration;
use lemmy_api_common::{
blocking,
+ check_registration_application,
get_local_user_view_from_jwt,
is_admin,
password_length_check,
person::*,
+ send_email_verification_success,
+ send_password_reset_email,
+ send_verification_email,
};
use lemmy_db_schema::{
diesel_option_overwrite,
source::{
comment::Comment,
community::Community,
+ email_verification::EmailVerification,
local_user::{LocalUser, LocalUserForm},
moderator::*,
password_reset_request::*,
};
use lemmy_utils::{
claims::Claims,
- email::send_email,
location_info,
- utils::{generate_random_string, is_valid_display_name, is_valid_matrix_id, naive_from_unix},
+ utils::{is_valid_display_name, is_valid_matrix_id, naive_from_unix},
ConnectionId,
LemmyError,
- Sensitive,
};
use lemmy_websocket::{
messages::{CaptchaItem, SendAllMessage},
return Err(LemmyError::from_message("password_incorrect"));
}
+ let site = blocking(context.pool(), Site::read_simple).await??;
+ if site.require_email_verification && !local_user_view.local_user.email_verified {
+ return Err(LemmyError::from_message("email_not_verified"));
+ }
+
+ check_registration_application(&site, &local_user_view, context.pool()).await?;
+
// Return the jwt
Ok(LoginResponse {
- jwt: Claims::jwt(
- local_user_view.local_user.id.0,
- &context.secret().jwt_secret,
- &context.settings().hostname,
- )?
- .into(),
+ jwt: Some(
+ Claims::jwt(
+ local_user_view.local_user.id.0,
+ &context.secret().jwt_secret,
+ &context.settings().hostname,
+ )?
+ .into(),
+ ),
+ verify_email_sent: false,
+ registration_created: false,
})
}
}
let avatar = diesel_option_overwrite_to_url(&data.avatar)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?;
- let email = diesel_option_overwrite(&data.email.clone().map(Sensitive::into_inner));
let bio = diesel_option_overwrite(&data.bio);
let display_name = diesel_option_overwrite(&data.display_name);
let matrix_user_id = diesel_option_overwrite(&data.matrix_user_id);
let bot_account = data.bot_account;
+ let email_deref = data.email.as_deref().map(|e| e.to_owned());
+ let email = diesel_option_overwrite(&email_deref);
+
+ if let Some(Some(email)) = &email {
+ let previous_email = local_user_view.local_user.email.unwrap_or_default();
+ // Only send the verification email if there was an email change
+ if previous_email.ne(email) {
+ send_verification_email(
+ local_user_view.local_user.id,
+ email,
+ &local_user_view.person.name,
+ context.pool(),
+ &context.settings(),
+ )
+ .await?;
+ }
+ }
+
+ // When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value
+ if let Some(email) = &email {
+ let site_fut = blocking(context.pool(), Site::read_simple);
+ if email.is_none() && site_fut.await??.require_email_verification {
+ return Err(LemmyError::from_message("email_required"));
+ }
+ }
if let Some(Some(bio)) = &bio {
if bio.chars().count() > 300 {
.map_err(|e| e.with_message("user_already_exists"))?;
let local_user_form = LocalUserForm {
- person_id,
+ person_id: Some(person_id),
email,
- password_encrypted,
+ password_encrypted: Some(password_encrypted),
show_nsfw: data.show_nsfw,
show_bot_accounts: data.show_bot_accounts,
show_scores: data.show_scores,
show_read_posts: data.show_read_posts,
show_new_post_notifs: data.show_new_post_notifs,
send_notifications_to_email: data.send_notifications_to_email,
+ email_verified: None,
+ accepted_application: None,
};
let local_user_res = blocking(context.pool(), move |conn| {
// Return the jwt
Ok(LoginResponse {
- jwt: Claims::jwt(
- updated_local_user.id.0,
- &context.secret().jwt_secret,
- &context.settings().hostname,
- )?
- .into(),
+ jwt: Some(
+ Claims::jwt(
+ updated_local_user.id.0,
+ &context.secret().jwt_secret,
+ &context.settings().hostname,
+ )?
+ .into(),
+ ),
+ verify_email_sent: false,
+ registration_created: false,
})
}
}
// Return the jwt
Ok(LoginResponse {
- jwt: Claims::jwt(
- updated_local_user.id.0,
- &context.secret().jwt_secret,
- &context.settings().hostname,
- )?
- .into(),
+ jwt: Some(
+ Claims::jwt(
+ updated_local_user.id.0,
+ &context.secret().jwt_secret,
+ &context.settings().hostname,
+ )?
+ .into(),
+ ),
+ verify_email_sent: false,
+ registration_created: false,
})
}
}
.map_err(LemmyError::from)
.map_err(|e| e.with_message("couldnt_find_that_username_or_email"))?;
- // Generate a random token
- let token = generate_random_string();
-
- // Insert the row
- let token2 = token.clone();
- let local_user_id = local_user_view.local_user.id;
- blocking(context.pool(), move |conn| {
- PasswordResetRequest::create_token(conn, local_user_id, &token2)
- })
- .await??;
-
// Email the pure token to the user.
- // TODO no i18n support here.
- let email = &local_user_view.local_user.email.expect("email");
- let subject = &format!("Password reset for {}", local_user_view.person.name);
- let protocol_and_hostname = &context.settings().get_protocol_and_hostname();
- let html = &format!("<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"))?;
-
+ send_password_reset_email(&local_user_view, context.pool(), &context.settings()).await?;
Ok(PasswordResetResponse {})
}
}
// Return the jwt
Ok(LoginResponse {
- jwt: Claims::jwt(
- updated_local_user.id.0,
- &context.secret().jwt_secret,
- &context.settings().hostname,
- )?
- .into(),
+ jwt: Some(
+ Claims::jwt(
+ updated_local_user.id.0,
+ &context.secret().jwt_secret,
+ &context.settings().hostname,
+ )?
+ .into(),
+ ),
+ verify_email_sent: false,
+ registration_created: false,
})
}
}
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 {})
+ }
+}
use lemmy_api_common::{
blocking,
build_federated_instances,
+ check_private_instance,
get_local_user_view_from_jwt,
get_local_user_view_from_jwt_opt,
is_admin,
+ send_application_approved_email,
site::*,
};
use lemmy_apub::{
EndpointType,
};
use lemmy_db_schema::{
+ diesel_option_overwrite,
from_opt_str_to_opt_enum,
newtypes::PersonId,
- source::{moderator::*, site::Site},
+ source::{
+ local_user::{LocalUser, LocalUserForm},
+ moderator::*,
+ registration_application::{RegistrationApplication, RegistrationApplicationForm},
+ site::Site,
+ },
traits::{Crud, DeleteableOrRemoveable},
DbPool,
ListingType,
};
use lemmy_db_views::{
comment_view::{CommentQueryBuilder, CommentView},
+ local_user_view::LocalUserView,
post_view::{PostQueryBuilder, PostView},
+ registration_application_view::{
+ RegistrationApplicationQueryBuilder,
+ RegistrationApplicationView,
+ },
site_view::SiteView,
};
use lemmy_db_views_actor::{
) -> Result<GetModlogResponse, LemmyError> {
let data: &GetModlog = self;
+ let local_user_view =
+ get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
+ .await?;
+
+ check_private_instance(&local_user_view, context.pool()).await?;
+
let community_id = data.community_id;
let mod_person_id = data.mod_person_id;
let page = data.page;
get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
.await?;
+ check_private_instance(&local_user_view, context.pool()).await?;
+
let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw);
let show_bot_accounts = local_user_view
.as_ref()
let local_user_view =
get_local_user_view_from_jwt_opt(self.auth.as_ref(), context.pool(), context.secret())
.await?;
+ check_private_instance(&local_user_view, context.pool()).await?;
+
let res = search_by_apub_id(&self.q, context)
.await
.map_err(LemmyError::from)
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,
+ })
+ }
+}
[package]
name = "lemmy_api_common"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
edition = "2018"
description = "A link aggregator for the fediverse"
license = "AGPL-3.0"
doctest = false
[dependencies]
-lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" }
-lemmy_db_views_moderator = { version = "=0.14.4-rc.4", path = "../db_views_moderator" }
-lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" }
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
-lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" }
+lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
+lemmy_db_views_moderator = { version = "=0.15.0-rc.6", path = "../db_views_moderator" }
+lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
serde = { version = "1.0.131", features = ["derive"] }
diesel = "1.4.8"
actix-web = { version = "4.0.0-beta.14", default-features = false, features = ["cookies"] }
newtypes::{CommunityId, LocalUserId, PersonId, PostId},
source::{
community::Community,
+ email_verification::{EmailVerification, EmailVerificationForm},
+ password_reset_request::PasswordResetRequest,
person_block::PersonBlock,
post::{Post, PostRead, PostReadForm},
+ registration_application::RegistrationApplication,
secret::Secret,
site::Site,
},
community_person_ban_view::CommunityPersonBanView,
community_view::CommunityView,
};
-use lemmy_utils::{claims::Claims, settings::structs::FederationConfig, LemmyError, Sensitive};
+use lemmy_utils::{
+ claims::Claims,
+ email::send_email,
+ settings::structs::{FederationConfig, Settings},
+ utils::generate_random_string,
+ LemmyError,
+ Sensitive,
+};
use url::Url;
pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
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(
pool: &DbPool,
federation_config: &FederationConfig,
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(())
+}
pub password: Sensitive<String>,
pub password_verify: Sensitive<String>,
pub show_nsfw: bool,
+ /// email is mandatory if email verification is enabled on the server
pub email: Option<Sensitive<String>>,
pub captcha_uuid: Option<String>,
pub captcha_answer: 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)]
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)]
pub auth: Sensitive<String>,
}
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct DeleteAccountResponse {}
+
#[derive(Debug, Serialize, Deserialize)]
pub struct PasswordReset {
pub email: Sensitive<String>,
pub mentions: i64,
pub private_messages: i64,
}
+
+#[derive(Serialize, Deserialize)]
+pub struct VerifyEmail {
+ pub token: String,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct VerifyEmailResponse {}
comment_view::CommentView,
local_user_view::LocalUserSettingsView,
post_view::PostView,
+ registration_application_view::RegistrationApplicationView,
site_view::SiteView,
};
use lemmy_db_views_actor::{
pub community_id: Option<CommunityId>,
pub page: Option<i64>,
pub limit: Option<i64>,
+ pub auth: Option<Sensitive<String>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub open_registration: Option<bool>,
pub enable_nsfw: 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 open_registration: Option<bool>,
pub enable_nsfw: 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 allowed: 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,
+}
[package]
name = "lemmy_api_crud"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
edition = "2018"
description = "A link aggregator for the fediverse"
license = "AGPL-3.0"
documentation = "https://join-lemmy.org/docs/en/index.html"
[dependencies]
-lemmy_apub = { version = "=0.14.4-rc.4", path = "../apub" }
-lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "../apub_lib" }
-lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" }
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
-lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" }
-lemmy_db_views_moderator = { version = "=0.14.4-rc.4", path = "../db_views_moderator" }
-lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" }
-lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" }
-lemmy_websocket = { version = "=0.14.4-rc.4", path = "../websocket" }
+lemmy_apub = { version = "=0.15.0-rc.6", path = "../apub" }
+lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "../apub_lib" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
+lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
+lemmy_db_views_moderator = { version = "=0.15.0-rc.6", path = "../db_views_moderator" }
+lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
+lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" }
+lemmy_websocket = { version = "=0.15.0-rc.6", path = "../websocket" }
diesel = "1.4.8"
bcrypt = "0.10.1"
chrono = { version = "0.4.19", features = ["serde"] }
use crate::PerformCrud;
use actix_web::web::Data;
-use lemmy_api_common::{blocking, comment::*, get_local_user_view_from_jwt_opt};
+use lemmy_api_common::{
+ blocking,
+ check_private_instance,
+ comment::*,
+ get_local_user_view_from_jwt_opt,
+};
use lemmy_apub::{
fetcher::webfinger::webfinger_resolve,
objects::community::ApubCommunity,
get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
.await?;
+ check_private_instance(&local_user_view, context.pool()).await?;
+
let person_id = local_user_view.map(|u| u.person.id);
let id = data.id;
let comment_view = blocking(context.pool(), move |conn| {
get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
.await?;
+ check_private_instance(&local_user_view, context.pool()).await?;
+
let show_bot_accounts = local_user_view
.as_ref()
.map(|t| t.local_user.show_bot_accounts);
use crate::PerformCrud;
use actix_web::web::Data;
-use lemmy_api_common::{blocking, community::*, get_local_user_view_from_jwt_opt};
+use lemmy_api_common::{
+ blocking,
+ check_private_instance,
+ community::*,
+ get_local_user_view_from_jwt_opt,
+};
use lemmy_apub::{
fetcher::webfinger::webfinger_resolve,
objects::community::ApubCommunity,
let local_user_view =
get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
.await?;
+
+ check_private_instance(&local_user_view, context.pool()).await?;
+
let person_id = local_user_view.map(|u| u.person.id);
let community_id = match data.id {
get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
.await?;
+ check_private_instance(&local_user_view, context.pool()).await?;
+
let person_id = local_user_view.to_owned().map(|l| l.person.id);
// Don't show NSFW by default
use crate::PerformCrud;
use actix_web::web::Data;
-use lemmy_api_common::{blocking, get_local_user_view_from_jwt_opt, mark_post_as_read, post::*};
+use lemmy_api_common::{
+ blocking,
+ check_private_instance,
+ get_local_user_view_from_jwt_opt,
+ mark_post_as_read,
+ post::*,
+};
use lemmy_apub::{
fetcher::webfinger::webfinger_resolve,
objects::community::ApubCommunity,
get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
.await?;
+ check_private_instance(&local_user_view, context.pool()).await?;
+
let show_bot_accounts = local_user_view
.as_ref()
.map(|t| t.local_user.show_bot_accounts);
get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
.await?;
+ check_private_instance(&local_user_view, context.pool()).await?;
+
let person_id = local_user_view.to_owned().map(|l| l.person.id);
let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw);
check_person_block,
get_local_user_view_from_jwt,
person::{CreatePrivateMessage, PrivateMessageResponse},
+ send_email_to_user,
};
use lemmy_apub::{
generate_local_apub_endpoint,
};
use lemmy_db_views::local_user_view::LocalUserView;
use lemmy_utils::{utils::remove_slurs, ConnectionId, LemmyError};
-use lemmy_websocket::{
- send::{send_email_to_user, send_pm_ws_message},
- LemmyContext,
- UserOperationCrud,
-};
+use lemmy_websocket::{send::send_pm_ws_message, LemmyContext, UserOperationCrud};
#[async_trait::async_trait(?Send)]
impl PerformCrud for CreatePrivateMessage {
enable_downvotes: data.enable_downvotes,
open_registration: data.open_registration,
enable_nsfw: data.enable_nsfw,
- updated: None,
community_creation_admin_only: data.community_creation_admin_only,
+ ..SiteForm::default()
};
let create_site = move |conn: &'_ _| Site::create(conn, &site_form);
captcha_uuid: None,
captcha_answer: None,
honeypot: None,
+ answer: None,
};
- let login_response = register.perform(context, websocket_id).await?;
+ let admin_jwt = register
+ .perform(context, websocket_id)
+ .await?
+ .jwt
+ .expect("jwt is returned from registration on newly created site");
info!("Admin {} created", setup.admin_username);
let create_site = CreateSite {
open_registration: setup.open_registration,
enable_nsfw: setup.enable_nsfw,
community_creation_admin_only: setup.community_creation_admin_only,
- auth: login_response.jwt,
+ require_email_verification: setup.require_email_verification,
+ require_application: setup.require_application,
+ application_question: setup.application_question.to_owned(),
+ private_instance: setup.private_instance,
+ auth: admin_jwt,
};
create_site.perform(context, websocket_id).await?;
info!("Site {} created", setup.site_name);
diesel_option_overwrite,
diesel_option_overwrite_to_url,
naive_now,
- source::site::{Site, SiteForm},
+ source::{
+ local_user::LocalUser,
+ site::{Site, SiteForm},
+ },
traits::Crud,
};
use lemmy_db_views::site_view::SiteView;
let sidebar = diesel_option_overwrite(&data.sidebar);
let description = diesel_option_overwrite(&data.description);
+ let application_question = diesel_option_overwrite(&data.application_question);
let icon = diesel_option_overwrite_to_url(&data.icon)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?;
open_registration: data.open_registration,
enable_nsfw: data.enable_nsfw,
community_creation_admin_only: data.community_creation_admin_only,
+ require_email_verification: data.require_email_verification,
+ require_application: data.require_application,
+ application_question,
+ private_instance: data.private_instance,
};
- let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
- blocking(context.pool(), update_site)
+ let update_site = blocking(context.pool(), move |conn| {
+ Site::update(conn, 1, &site_form)
+ })
+ .await?
+ .map_err(LemmyError::from)
+ .map_err(|e| e.with_message("couldnt_update_site"))?;
+
+ // TODO can't think of a better way to do this.
+ // If the server suddenly requires email verification, or required applications, no old users
+ // will be able to log in. It really only wants this to be a requirement for NEW signups.
+ // So if it was set from false, to true, you need to update all current users columns to be verified.
+
+ if !found_site.require_application && update_site.require_application {
+ blocking(context.pool(), move |conn| {
+ LocalUser::set_all_users_registration_applications_accepted(conn)
+ })
.await?
.map_err(LemmyError::from)
- .map_err(|e| e.with_message("couldnt_update_site"))?;
+ .map_err(|e| e.with_message("couldnt_set_all_registrations_accepted"))?;
+ }
+
+ if !found_site.require_email_verification && update_site.require_email_verification {
+ blocking(context.pool(), move |conn| {
+ LocalUser::set_all_users_email_verified(conn)
+ })
+ .await?
+ .map_err(LemmyError::from)
+ .map_err(|e| e.with_message("couldnt_set_all_email_verified"))?;
+ }
let site_view = blocking(context.pool(), SiteView::read).await??;
use crate::PerformCrud;
use actix_web::web::Data;
-use lemmy_api_common::{blocking, honeypot_check, password_length_check, person::*};
+use lemmy_api_common::{
+ blocking,
+ honeypot_check,
+ password_length_check,
+ person::*,
+ send_verification_email,
+};
use lemmy_apub::{
generate_followers_url,
generate_inbox_url,
},
local_user::{LocalUser, LocalUserForm},
person::{Person, PersonForm},
+ registration_application::{RegistrationApplication, RegistrationApplicationForm},
site::Site,
},
traits::{Crud, Followable, Joinable},
- ListingType,
- SortType,
};
use lemmy_db_views_actor::person_view::PersonViewSafe;
use lemmy_utils::{
) -> Result<LoginResponse, LemmyError> {
let data: &Register = self;
+ // no email verification, or applications if the site is not setup yet
+ let (mut email_verification, mut require_application) = (false, false);
+
// Make sure site has open registration
if let Ok(site) = blocking(context.pool(), Site::read_simple).await? {
if !site.open_registration {
return Err(LemmyError::from_message("registration_closed"));
}
+ email_verification = site.require_email_verification;
+ require_application = site.require_application;
}
password_length_check(&data.password)?;
honeypot_check(&data.honeypot)?;
+ if email_verification && data.email.is_none() {
+ return Err(LemmyError::from_message("email_required"));
+ }
+
+ if require_application && data.answer.is_none() {
+ return Err(LemmyError::from_message(
+ "registration_application_answer_required",
+ ));
+ }
+
// Make sure passwords match
if data.password != data.password_verify {
return Err(LemmyError::from_message("passwords_dont_match"));
.map_err(|e| e.with_message("user_already_exists"))?;
// Create the local user
- // TODO some of these could probably use the DB defaults
let local_user_form = LocalUserForm {
- person_id: inserted_person.id,
+ person_id: Some(inserted_person.id),
email: Some(data.email.as_deref().map(|s| s.to_owned())),
- password_encrypted: data.password.to_string(),
+ password_encrypted: Some(data.password.to_string()),
show_nsfw: Some(data.show_nsfw),
- show_bot_accounts: Some(true),
- theme: Some("browser".into()),
- default_sort_type: Some(SortType::Active as i16),
- default_listing_type: Some(ListingType::Subscribed as i16),
- lang: Some("browser".into()),
- show_avatars: Some(true),
- show_scores: Some(true),
- show_read_posts: Some(true),
- show_new_post_notifs: Some(false),
- send_notifications_to_email: Some(false),
+ email_verified: Some(false),
+ ..LocalUserForm::default()
};
let inserted_local_user = match blocking(context.pool(), move |conn| {
}
};
+ if require_application {
+ // Create the registration application
+ let form = RegistrationApplicationForm {
+ local_user_id: Some(inserted_local_user.id),
+ // We already made sure answer was not null above
+ answer: data.answer.to_owned(),
+ ..RegistrationApplicationForm::default()
+ };
+
+ blocking(context.pool(), move |conn| {
+ RegistrationApplication::create(conn, &form)
+ })
+ .await??;
+ }
+
let main_community_keypair = generate_actor_keypair()?;
// Create the main community if it doesn't exist
.map_err(|e| e.with_message("community_moderator_already_exists"))?;
}
- // Return the jwt
- Ok(LoginResponse {
- jwt: Claims::jwt(
- inserted_local_user.id.0,
- &context.secret().jwt_secret,
- &context.settings().hostname,
- )?
- .into(),
- })
+ let mut login_response = LoginResponse {
+ jwt: None,
+ registration_created: false,
+ verify_email_sent: false,
+ };
+
+ // Log the user in directly if email verification and application aren't required
+ if !require_application && !email_verification {
+ login_response.jwt = Some(
+ Claims::jwt(
+ inserted_local_user.id.0,
+ &context.secret().jwt_secret,
+ &context.settings().hostname,
+ )?
+ .into(),
+ );
+ } else {
+ if email_verification {
+ send_verification_email(
+ inserted_local_user.id,
+ // we check at the beginning of this method that email is set
+ &inserted_local_user.email.expect("email was provided"),
+ &inserted_person.name,
+ context.pool(),
+ &context.settings(),
+ )
+ .await?;
+ login_response.verify_email_sent = true;
+ }
+
+ if require_application {
+ login_response.registration_created = true;
+ }
+ }
+
+ Ok(login_response)
}
}
#[async_trait::async_trait(?Send)]
impl PerformCrud for DeleteAccount {
- type Response = LoginResponse;
+ type Response = DeleteAccountResponse;
#[tracing::instrument(skip(self, context, _websocket_id))]
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
- ) -> Result<LoginResponse, LemmyError> {
- let data: &DeleteAccount = self;
+ ) -> Result<Self::Response, LemmyError> {
+ let data = self;
let local_user_view =
get_local_user_view_from_jwt(data.auth.as_ref(), context.pool(), context.secret()).await?;
})
.await??;
- Ok(LoginResponse {
- jwt: data.auth.clone(),
- })
+ Ok(DeleteAccountResponse {})
}
}
use crate::PerformCrud;
use actix_web::web::Data;
-use lemmy_api_common::{blocking, get_local_user_view_from_jwt_opt, person::*};
+use lemmy_api_common::{
+ blocking,
+ check_private_instance,
+ get_local_user_view_from_jwt_opt,
+ person::*,
+};
use lemmy_apub::{
fetcher::webfinger::webfinger_resolve,
objects::person::ApubPerson,
get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
.await?;
+ check_private_instance(&local_user_view, context.pool()).await?;
+
let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw);
let show_bot_accounts = local_user_view
.as_ref()
[package]
name = "lemmy_apub"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
edition = "2018"
description = "A link aggregator for the fediverse"
license = "AGPL-3.0"
doctest = false
[dependencies]
-lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" }
-lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "../apub_lib" }
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
-lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" }
-lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" }
-lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" }
-lemmy_websocket = { version = "=0.14.4-rc.4", path = "../websocket" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
+lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "../apub_lib" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
+lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
+lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
+lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" }
+lemmy_websocket = { version = "=0.15.0-rc.6", path = "../websocket" }
diesel = "1.4.8"
activitystreams-kinds = "0.1.2"
bcrypt = "0.10.1"
[package]
name = "lemmy_apub_lib"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
edition = "2018"
description = "A link aggregator for the fediverse"
license = "AGPL-3.0"
documentation = "https://join-lemmy.org/docs/en/index.html"
[dependencies]
-lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" }
-lemmy_apub_lib_derive = { version = "=0.14.4-rc.4", path = "../apub_lib_derive" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
+lemmy_apub_lib_derive = { version = "=0.15.0-rc.6", path = "../apub_lib_derive" }
activitystreams = "0.7.0-alpha.14"
serde = { version = "1.0.131", features = ["derive"] }
async-trait = "0.1.52"
[package]
name = "lemmy_apub_lib_derive"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
edition = "2018"
description = "A link aggregator for the fediverse"
license = "AGPL-3.0"
[package]
name = "lemmy_db_schema"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
edition = "2018"
description = "A link aggregator for the fediverse"
license = "AGPL-3.0"
doctest = false
[dependencies]
-lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" }
-lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "../apub_lib" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
+lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "../apub_lib" }
diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"] }
diesel_migrations = "1.4.0"
chrono = { version = "0.4.19", features = ["serde"] }
enable_nsfw: None,
updated: None,
community_creation_admin_only: Some(false),
+ require_email_verification: None,
+ require_application: None,
+ application_question: None,
+ private_instance: None,
};
Site::create(&conn, &site_form).unwrap();
--- /dev/null
+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)
+ }
+}
show_scores,
show_read_posts,
show_new_post_notifs,
+ email_verified,
+ accepted_application,
);
impl ToSafeSettings for LocalUser {
show_scores,
show_read_posts,
show_new_post_notifs,
+ email_verified,
+ accepted_application,
)
}
}
impl LocalUser {
pub fn register(conn: &PgConnection, form: &LocalUserForm) -> Result<Self, Error> {
let mut edited_user = form.clone();
- let password_hash =
- hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
+ let password_hash = form
+ .password_encrypted
+ .as_ref()
+ .map(|p| hash(p, DEFAULT_COST).expect("Couldn't hash password"));
edited_user.password_encrypted = password_hash;
Self::create(conn, &edited_user)
))
.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 {
pub mod comment_report;
pub mod community;
pub mod community_block;
+pub mod email_verification;
pub mod local_user;
pub mod moderator;
pub mod password_reset_request;
pub mod post;
pub mod post_report;
pub mod private_message;
+pub mod registration_application;
pub mod secret;
pub mod site;
let inserted_person = Person::create(&conn, &new_person).unwrap();
let new_local_user = LocalUserForm {
- person_id: inserted_person.id,
- password_encrypted: "pass".to_string(),
+ person_id: Some(inserted_person.id),
+ password_encrypted: Some("pass".to_string()),
..LocalUserForm::default()
};
--- /dev/null
+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)
+ }
+}
)]
pub struct CommunityId(pub i32);
-#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)]
+#[derive(
+ Debug, Copy, Clone, Hash, Eq, PartialEq, Default, Serialize, Deserialize, DieselNewType,
+)]
pub struct LocalUserId(pub i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)]
show_scores -> Bool,
show_read_posts -> Bool,
show_new_post_notifs -> Bool,
+ email_verified -> Bool,
+ accepted_application -> Bool,
}
}
banner -> Nullable<Varchar>,
description -> Nullable<Text>,
community_creation_admin_only -> Bool,
+ require_email_verification -> Bool,
+ require_application -> Bool,
+ application_question -> Nullable<Text>,
+ private_instance -> Bool,
}
}
}
}
+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 -> comment_alias_1 (parent_id));
joinable!(person_mention -> person_alias_1 (recipient_id));
joinable!(post_saved -> post (post_id));
joinable!(site -> person (creator_id));
joinable!(site_aggregates -> site (site_id));
+joinable!(email_verification -> local_user (local_user_id));
+joinable!(registration_application -> local_user (local_user_id));
+joinable!(registration_application -> person (admin_id));
allow_tables_to_appear_in_same_query!(
activity,
comment_alias_1,
person_alias_1,
person_alias_2,
+ email_verification,
+ registration_application
);
--- /dev/null
+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,
+}
pub show_scores: bool,
pub show_read_posts: bool,
pub show_new_post_notifs: bool,
+ pub email_verified: bool,
+ pub accepted_application: bool,
}
// TODO redo these, check table defaults
#[derive(Insertable, AsChangeset, Clone, Default)]
#[table_name = "local_user"]
pub struct LocalUserForm {
- pub person_id: PersonId,
- pub password_encrypted: String,
+ pub person_id: Option<PersonId>,
+ pub password_encrypted: Option<String>,
pub email: Option<Option<String>>,
pub show_nsfw: Option<bool>,
pub theme: Option<String>,
pub show_scores: Option<bool>,
pub show_read_posts: 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
pub show_scores: bool,
pub show_read_posts: bool,
pub show_new_post_notifs: bool,
+ pub email_verified: bool,
+ pub accepted_application: bool,
}
pub mod comment_report;
pub mod community;
pub mod community_block;
+pub mod email_verification;
pub mod local_user;
pub mod moderator;
pub mod password_reset_request;
pub mod post;
pub mod post_report;
pub mod private_message;
+pub mod registration_application;
pub mod secret;
pub mod site;
--- /dev/null
+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>>,
+}
pub banner: Option<DbUrl>,
pub description: Option<String>,
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"]
pub struct SiteForm {
pub name: String,
pub banner: Option<Option<DbUrl>>,
pub description: Option<Option<String>>,
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>,
}
[package]
name = "lemmy_db_views"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
edition = "2018"
description = "A link aggregator for the fediverse"
license = "AGPL-3.0"
doctest = false
[dependencies]
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"] }
serde = { version = "1.0.131", features = ["derive"] }
tracing = "0.1.29"
pub mod post_report_view;
pub mod post_view;
pub mod private_message_view;
+pub mod registration_application_view;
pub mod site_view;
--- /dev/null
+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();
+ }
+}
[package]
name = "lemmy_db_views_actor"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
edition = "2018"
description = "A link aggregator for the fediverse"
license = "AGPL-3.0"
doctest = false
[dependencies]
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"] }
serde = { version = "1.0.131", features = ["derive"] }
[package]
name = "lemmy_db_views_moderator"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
edition = "2018"
description = "A link aggregator for the fediverse"
license = "AGPL-3.0"
doctest = false
[dependencies]
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"] }
serde = { version = "1.0.131", features = ["derive"] }
[package]
name = "lemmy_routes"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
edition = "2018"
description = "A link aggregator for the fediverse"
license = "AGPL-3.0"
doctest = false
[dependencies]
-lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" }
-lemmy_websocket = { version = "=0.14.4-rc.4", path = "../websocket" }
-lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" }
-lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" }
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
-lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" }
-lemmy_apub = { version = "=0.14.4-rc.4", path = "../apub" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
+lemmy_websocket = { version = "=0.15.0-rc.6", path = "../websocket" }
+lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
+lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
+lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" }
+lemmy_apub = { version = "=0.15.0-rc.6", path = "../apub" }
diesel = "1.4.8"
actix = "0.12.0"
actix-web = { version = "4.0.0-beta.14", default-features = false, features = ["rustls"] }
[package]
name = "lemmy_utils"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
edition = "2018"
description = "A link aggregator for the fediverse"
license = "AGPL-3.0"
-use crate::settings::structs::Settings;
+use crate::{settings::structs::Settings, LemmyError};
use lettre::{
message::{header, Mailbox, MultiPart, SinglePart},
transport::smtp::{
to_username: &str,
html: &str,
settings: &Settings,
-) -> Result<(), String> {
- let email_config = settings.email.to_owned().ok_or("no_email_setup")?;
+) -> Result<(), LemmyError> {
+ let email_config = settings
+ .email
+ .to_owned()
+ .ok_or_else(|| LemmyError::from_message("no_email_setup"))?;
let domain = settings.hostname.to_owned();
let (smtp_server, smtp_port) = {
let email_and_port = email_config.smtp_server.split(':').collect::<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[1]
match result {
Ok(_) => Ok(()),
- Err(e) => Err(e.to_string()),
+ Err(e) => Err(LemmyError::from(e).with_message("email_send_failed")),
}
}
pub enable_nsfw: Option<bool>,
#[default(None)]
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>,
}
[package]
name = "lemmy_websocket"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
edition = "2018"
description = "A link aggregator for the fediverse"
license = "AGPL-3.0"
doctest = false
[dependencies]
-lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" }
-lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" }
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
-lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" }
-lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
+lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
+lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
+lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
reqwest = { version = "0.11.7", features = ["json"] }
reqwest-middleware = "0.1.3"
tracing = "0.1.29"
ListPostReports,
GetReportCount,
GetUnreadCount,
+ VerifyEmail,
FollowCommunity,
GetReplies,
GetPersonMentions,
BanFromCommunity,
AddModToCommunity,
AddAdmin,
+ GetUnreadRegistrationApplicationCount,
+ ListRegistrationApplications,
+ ApproveRegistrationApplication,
BanPerson,
Search,
ResolveObject,
community::CommunityResponse,
person::PrivateMessageResponse,
post::PostResponse,
+ send_email_to_user,
};
use lemmy_db_schema::{
newtypes::{CommentId, CommunityId, LocalUserId, PersonId, PostId, PrivateMessageId},
private_message_view::PrivateMessageView,
};
use lemmy_db_views_actor::community_view::CommunityView;
-use lemmy_utils::{
- email::send_email,
- settings::structs::Settings,
- utils::MentionData,
- ConnectionId,
- LemmyError,
-};
-use tracing::error;
+use lemmy_utils::{utils::MentionData, ConnectionId, LemmyError};
pub async fn send_post_ws_message<OP: ToString + Send + OperationType + 'static>(
post_id: PostId,
};
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),
- };
- }
-}
--- /dev/null
+-- 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;
--- /dev/null
+-- 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
+);
--- /dev/null
+-- 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;
--- /dev/null
+-- 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);
--- /dev/null
+alter table email_verification drop column published;
--- /dev/null
+alter table email_verification add column published timestamp not null default now();
web::put().to(route_post::<ChangePassword>),
)
.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
.service(
- web::resource("/admin/add")
+ web::scope("/admin")
.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>),
+ ),
),
);
}
};
use doku::json::{AutoComments, Formatting};
use lemmy_api::match_websocket_operation;
-use lemmy_api_common::blocking;
+use lemmy_api_common::{blocking, check_private_instance_and_federation_enabled};
use lemmy_api_crud::match_websocket_operation_crud;
use lemmy_apub_lib::activity_queue::create_activity_queue;
use lemmy_db_schema::{get_database_url_from_env, source::secret::Secret};
let activity_queue = queue_manager.queue_handle().clone();
+ check_private_instance_and_federation_enabled(&pool, &settings).await?;
+
let chat_server = ChatServer::startup(
pool.clone(),
rate_limiter.clone(),