mirror of https://github.com/LemmyNet/lemmy.git
Implement email verification (fixes #219)
parent
97b8b9c255
commit
c199215355
|
@ -219,8 +219,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()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,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::*,
|
||||||
|
@ -54,6 +55,7 @@ use lemmy_utils::{
|
||||||
LemmyError,
|
LemmyError,
|
||||||
};
|
};
|
||||||
use lemmy_websocket::{
|
use lemmy_websocket::{
|
||||||
|
email::send_verification_email,
|
||||||
messages::{CaptchaItem, SendAllMessage},
|
messages::{CaptchaItem, SendAllMessage},
|
||||||
LemmyContext,
|
LemmyContext,
|
||||||
UserOperation,
|
UserOperation,
|
||||||
|
@ -88,13 +90,18 @@ impl Perform for Login {
|
||||||
return Err(ApiError::err_plain("password_incorrect").into());
|
return Err(ApiError::err_plain("password_incorrect").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let site = blocking(context.pool(), Site::read_simple).await??;
|
||||||
|
if site.require_email_verification && !local_user_view.local_user.email_verified {
|
||||||
|
return Err(ApiError::err_plain("email_not_verified").into());
|
||||||
|
}
|
||||||
|
|
||||||
// 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,
|
||||||
)?,
|
)?),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,12 +166,29 @@ 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);
|
|
||||||
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 = if let Some(email) = &data.email {
|
||||||
|
let site = blocking(context.pool(), Site::read_simple).await??;
|
||||||
|
if site.require_email_verification {
|
||||||
|
send_verification_email(
|
||||||
|
local_user_view.local_user.id,
|
||||||
|
email,
|
||||||
|
&local_user_view.person.name,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
diesel_option_overwrite(&data.email)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(Some(bio)) = &bio {
|
if let Some(Some(bio)) = &bio {
|
||||||
if bio.chars().count() > 300 {
|
if bio.chars().count() > 300 {
|
||||||
return Err(ApiError::err_plain("bio_length_overflow").into());
|
return Err(ApiError::err_plain("bio_length_overflow").into());
|
||||||
|
@ -222,9 +246,9 @@ impl Perform for SaveUserSettings {
|
||||||
.map_err(|e| ApiError::err("user_already_exists", e))?;
|
.map_err(|e| ApiError::err("user_already_exists", e))?;
|
||||||
|
|
||||||
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,
|
||||||
|
@ -236,6 +260,7 @@ 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
let local_user_res = blocking(context.pool(), move |conn| {
|
let local_user_res = blocking(context.pool(), move |conn| {
|
||||||
|
@ -259,11 +284,11 @@ 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,
|
||||||
)?,
|
)?),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -307,11 +332,11 @@ 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,
|
||||||
)?,
|
)?),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -775,11 +800,11 @@ 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,
|
||||||
)?,
|
)?),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -860,3 +885,36 @@ impl Perform for GetUnreadCount {
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl Perform for VerifyEmail {
|
||||||
|
type Response = ();
|
||||||
|
|
||||||
|
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(|e| ApiError::err("token_not_found", e))?;
|
||||||
|
|
||||||
|
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??;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ pub struct Register {
|
||||||
pub password: String,
|
pub password: String,
|
||||||
pub password_verify: String,
|
pub password_verify: String,
|
||||||
pub show_nsfw: bool,
|
pub show_nsfw: bool,
|
||||||
|
/// email is mandatory if email verification is enabled on the server
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub captcha_uuid: Option<String>,
|
pub captcha_uuid: Option<String>,
|
||||||
pub captcha_answer: Option<String>,
|
pub captcha_answer: Option<String>,
|
||||||
|
@ -77,7 +78,9 @@ pub struct ChangePassword {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct LoginResponse {
|
pub struct LoginResponse {
|
||||||
pub jwt: String,
|
/// This is None in response to `Register` if email verification is enabled, and in response to
|
||||||
|
/// `DeleteAccount`.
|
||||||
|
pub jwt: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -278,3 +281,8 @@ 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,
|
||||||
|
}
|
||||||
|
|
|
@ -111,6 +111,7 @@ 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 auth: String,
|
pub auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -45,7 +45,11 @@ impl PerformCrud for GetSite {
|
||||||
captcha_answer: None,
|
captcha_answer: None,
|
||||||
honeypot: None,
|
honeypot: 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 {
|
||||||
|
@ -58,7 +62,7 @@ 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,
|
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);
|
||||||
|
|
|
@ -59,6 +59,7 @@ 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
|
let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
|
||||||
|
|
|
@ -24,8 +24,6 @@ use lemmy_db_schema::{
|
||||||
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::{
|
||||||
|
@ -36,7 +34,7 @@ use lemmy_utils::{
|
||||||
ConnectionId,
|
ConnectionId,
|
||||||
LemmyError,
|
LemmyError,
|
||||||
};
|
};
|
||||||
use lemmy_websocket::{messages::CheckCaptcha, LemmyContext};
|
use lemmy_websocket::{email::send_verification_email, messages::CheckCaptcha, LemmyContext};
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl PerformCrud for Register {
|
impl PerformCrud for Register {
|
||||||
|
@ -49,16 +47,24 @@ impl PerformCrud for Register {
|
||||||
) -> Result<LoginResponse, LemmyError> {
|
) -> Result<LoginResponse, LemmyError> {
|
||||||
let data: &Register = self;
|
let data: &Register = self;
|
||||||
|
|
||||||
|
// no email verification if the site is not setup yet
|
||||||
|
let mut email_verification = 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(ApiError::err_plain("registration_closed").into());
|
return Err(ApiError::err_plain("registration_closed").into());
|
||||||
}
|
}
|
||||||
|
email_verification = site.require_email_verification;
|
||||||
}
|
}
|
||||||
|
|
||||||
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(ApiError::err_plain("email_required").into());
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure passwords match
|
// Make sure passwords match
|
||||||
if data.password != data.password_verify {
|
if data.password != data.password_verify {
|
||||||
return Err(ApiError::err_plain("passwords_dont_match").into());
|
return Err(ApiError::err_plain("passwords_dont_match").into());
|
||||||
|
@ -124,22 +130,13 @@ impl PerformCrud for Register {
|
||||||
.map_err(|e| ApiError::err("user_already_exists", e))?;
|
.map_err(|e| ApiError::err("user_already_exists", e))?;
|
||||||
|
|
||||||
// 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.to_owned()),
|
email: Some(data.email.to_owned()),
|
||||||
password_encrypted: data.password.to_owned(),
|
password_encrypted: Some(data.password.to_owned()),
|
||||||
show_nsfw: Some(data.show_nsfw),
|
show_nsfw: Some(data.show_nsfw),
|
||||||
show_bot_accounts: Some(true),
|
email_verified: Some(!email_verification),
|
||||||
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| {
|
||||||
|
@ -228,13 +225,24 @@ impl PerformCrud for Register {
|
||||||
.map_err(|e| ApiError::err("community_moderator_already_exists", e))?;
|
.map_err(|e| ApiError::err("community_moderator_already_exists", e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the jwt
|
// Log the user in directly if email verification is not required
|
||||||
Ok(LoginResponse {
|
let jwt = if email_verification {
|
||||||
jwt: Claims::jwt(
|
send_verification_email(
|
||||||
|
inserted_local_user.id,
|
||||||
|
// we check at the beginning of this method that email is set
|
||||||
|
&inserted_local_user.email.unwrap(),
|
||||||
|
&inserted_person.name,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
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,
|
||||||
)?,
|
)?)
|
||||||
})
|
};
|
||||||
|
Ok(LoginResponse { jwt })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,8 +47,6 @@ impl PerformCrud for DeleteAccount {
|
||||||
})
|
})
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
Ok(LoginResponse {
|
Ok(LoginResponse { jwt: None })
|
||||||
jwt: data.auth.to_owned(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,7 @@ 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
Site::create(&conn, &site_form).unwrap();
|
Site::create(&conn, &site_form).unwrap();
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
use crate::{source::email_verification::*, traits::Crud};
|
||||||
|
use diesel::{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))
|
||||||
|
.first::<Self>(conn)
|
||||||
|
}
|
||||||
|
}
|
|
@ -62,8 +62,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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -157,6 +157,7 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,6 +448,7 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -558,6 +560,15 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
email_verification (id) {
|
||||||
|
id -> Int4,
|
||||||
|
local_user_id -> Int4,
|
||||||
|
email -> Text,
|
||||||
|
verification_token -> Varchar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 +630,7 @@ 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));
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(
|
allow_tables_to_appear_in_same_query!(
|
||||||
activity,
|
activity,
|
||||||
|
@ -662,4 +674,5 @@ 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
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, AsChangeset)]
|
||||||
|
#[table_name = "email_verification"]
|
||||||
|
pub struct EmailVerificationForm {
|
||||||
|
pub local_user_id: LocalUserId,
|
||||||
|
pub email: String,
|
||||||
|
pub verification_token: String,
|
||||||
|
}
|
|
@ -23,14 +23,15 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 +44,7 @@ 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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A local user view that removes password encrypted
|
/// A local user view that removes password encrypted
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -20,9 +20,10 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 +38,5 @@ 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>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
use crate::LemmyContext;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_db_schema::{
|
||||||
|
newtypes::LocalUserId,
|
||||||
|
source::email_verification::{EmailVerification, EmailVerificationForm},
|
||||||
|
traits::Crud,
|
||||||
|
};
|
||||||
|
use lemmy_utils::{email::send_email, utils::generate_random_string, ApiError, LemmyError};
|
||||||
|
|
||||||
|
pub async fn send_verification_email(
|
||||||
|
local_user_id: LocalUserId,
|
||||||
|
new_email: &str,
|
||||||
|
username: &str,
|
||||||
|
context: &LemmyContext,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let settings = context.settings();
|
||||||
|
let form = EmailVerificationForm {
|
||||||
|
local_user_id,
|
||||||
|
email: new_email.to_string(),
|
||||||
|
verification_token: generate_random_string(),
|
||||||
|
};
|
||||||
|
// TODO: link should be replaced with a frontend route once that exists
|
||||||
|
let verify_link = format!(
|
||||||
|
"{}/api/v3/user/verify_email?token={}",
|
||||||
|
settings.get_protocol_and_hostname(),
|
||||||
|
&form.verification_token
|
||||||
|
);
|
||||||
|
blocking(context.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.\n\n",
|
||||||
|
"<a href=\"{}\"></a>"
|
||||||
|
),
|
||||||
|
username, settings.hostname, verify_link
|
||||||
|
);
|
||||||
|
send_email(&subject, new_email, username, &body, &context.settings())
|
||||||
|
.map_err(|e| ApiError::err("email_send_failed", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ use reqwest::Client;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
pub mod chat_server;
|
pub mod chat_server;
|
||||||
|
pub mod email;
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
|
|
|
@ -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;
|
|
@ -0,0 +1,15 @@
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
);
|
|
@ -205,7 +205,9 @@ 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>))
|
||||||
|
// TODO: currently GET for easier testing, but should probably be POST
|
||||||
|
.route("/verify_email", web::get().to(route_get::<VerifyEmail>)),
|
||||||
)
|
)
|
||||||
// Admin Actions
|
// Admin Actions
|
||||||
.service(
|
.service(
|
||||||
|
|
Loading…
Reference in New Issue