From c199215355b940b63d3afd20ec1cfebcb495600c Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Wed, 24 Nov 2021 22:20:00 +0100 Subject: [PATCH] Implement email verification (fixes #219) --- crates/api/src/lib.rs | 4 +- crates/api/src/local_user.rs | 80 ++++++++++++++++--- crates/api_common/src/person.rs | 10 ++- crates/api_common/src/site.rs | 1 + crates/api_crud/src/site/create.rs | 2 +- crates/api_crud/src/site/read.rs | 8 +- crates/api_crud/src/site/update.rs | 1 + crates/api_crud/src/user/create.rs | 50 +++++++----- crates/api_crud/src/user/delete.rs | 4 +- .../src/aggregates/site_aggregates.rs | 1 + .../db_schema/src/impls/email_verification.rs | 39 +++++++++ crates/db_schema/src/impls/local_user.rs | 6 +- crates/db_schema/src/impls/mod.rs | 1 + .../src/impls/password_reset_request.rs | 4 +- crates/db_schema/src/schema.rs | 13 +++ .../src/source/email_verification.rs | 18 +++++ crates/db_schema/src/source/local_user.rs | 6 +- crates/db_schema/src/source/mod.rs | 1 + crates/db_schema/src/source/site.rs | 4 +- crates/websocket/src/email.rs | 46 +++++++++++ crates/websocket/src/lib.rs | 1 + .../down.sql | 8 ++ .../up.sql | 15 ++++ src/api_routes.rs | 4 +- 24 files changed, 278 insertions(+), 49 deletions(-) create mode 100644 crates/db_schema/src/impls/email_verification.rs create mode 100644 crates/db_schema/src/source/email_verification.rs create mode 100644 crates/websocket/src/email.rs create mode 100644 migrations/2021-11-23-132840_email_verification/down.sql create mode 100644 migrations/2021-11-23-132840_email_verification/up.sql diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index d535c4678..3b82e6aea 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -219,8 +219,8 @@ mod tests { 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() }; diff --git a/crates/api/src/local_user.rs b/crates/api/src/local_user.rs index 6f63d14b1..3217ea720 100644 --- a/crates/api/src/local_user.rs +++ b/crates/api/src/local_user.rs @@ -19,6 +19,7 @@ use lemmy_db_schema::{ source::{ comment::Comment, community::Community, + email_verification::EmailVerification, local_user::{LocalUser, LocalUserForm}, moderator::*, password_reset_request::*, @@ -54,6 +55,7 @@ use lemmy_utils::{ LemmyError, }; use lemmy_websocket::{ + email::send_verification_email, messages::{CaptchaItem, SendAllMessage}, LemmyContext, UserOperation, @@ -88,13 +90,18 @@ impl Perform for Login { 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 Ok(LoginResponse { - jwt: Claims::jwt( + jwt: Some(Claims::jwt( local_user_view.local_user.id.0, &context.secret().jwt_secret, &context.settings().hostname, - )?, + )?), }) } } @@ -159,12 +166,29 @@ impl Perform for SaveUserSettings { 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); 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 = 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 bio.chars().count() > 300 { 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))?; 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, @@ -236,6 +260,7 @@ impl Perform for SaveUserSettings { 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, }; let local_user_res = blocking(context.pool(), move |conn| { @@ -259,11 +284,11 @@ impl Perform for SaveUserSettings { // Return the jwt Ok(LoginResponse { - jwt: Claims::jwt( + jwt: Some(Claims::jwt( updated_local_user.id.0, &context.secret().jwt_secret, &context.settings().hostname, - )?, + )?), }) } } @@ -307,11 +332,11 @@ impl Perform for ChangePassword { // Return the jwt Ok(LoginResponse { - jwt: Claims::jwt( + jwt: Some(Claims::jwt( updated_local_user.id.0, &context.secret().jwt_secret, &context.settings().hostname, - )?, + )?), }) } } @@ -775,11 +800,11 @@ impl Perform for PasswordChange { // Return the jwt Ok(LoginResponse { - jwt: Claims::jwt( + jwt: Some(Claims::jwt( updated_local_user.id.0, &context.secret().jwt_secret, &context.settings().hostname, - )?, + )?), }) } } @@ -860,3 +885,36 @@ impl Perform for GetUnreadCount { Ok(res) } } + +#[async_trait::async_trait(?Send)] +impl Perform for VerifyEmail { + type Response = (); + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + 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(()) + } +} diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index b93d47eae..59df35d0a 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -23,6 +23,7 @@ pub struct Register { pub password: String, pub password_verify: String, pub show_nsfw: bool, + /// email is mandatory if email verification is enabled on the server pub email: Option, pub captcha_uuid: Option, pub captcha_answer: Option, @@ -77,7 +78,9 @@ pub struct ChangePassword { #[derive(Serialize, Deserialize)] 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, } #[derive(Serialize, Deserialize)] @@ -278,3 +281,8 @@ pub struct GetUnreadCountResponse { pub mentions: i64, pub private_messages: i64, } + +#[derive(Serialize, Deserialize)] +pub struct VerifyEmail { + pub token: String, +} diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index db81fce46..7a5e96d0e 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -111,6 +111,7 @@ pub struct EditSite { pub open_registration: Option, pub enable_nsfw: Option, pub community_creation_admin_only: Option, + pub require_email_verification: Option, pub auth: String, } diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 865ade77f..fae1d96eb 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -66,8 +66,8 @@ impl PerformCrud for CreateSite { 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); diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index 837acc83c..bf0996536 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -45,7 +45,11 @@ impl PerformCrud for GetSite { captcha_answer: 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); let create_site = CreateSite { @@ -58,7 +62,7 @@ impl PerformCrud for GetSite { open_registration: setup.open_registration, enable_nsfw: setup.enable_nsfw, community_creation_admin_only: setup.community_creation_admin_only, - auth: login_response.jwt, + auth: admin_jwt, }; create_site.perform(context, websocket_id).await?; info!("Site {} created", setup.site_name); diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index b6f06e3ad..150b7fc76 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -59,6 +59,7 @@ impl PerformCrud for EditSite { 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, }; let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form); diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index 2ce2fa366..8fec810a8 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -24,8 +24,6 @@ use lemmy_db_schema::{ site::Site, }, traits::{Crud, Followable, Joinable}, - ListingType, - SortType, }; use lemmy_db_views_actor::person_view::PersonViewSafe; use lemmy_utils::{ @@ -36,7 +34,7 @@ use lemmy_utils::{ ConnectionId, LemmyError, }; -use lemmy_websocket::{messages::CheckCaptcha, LemmyContext}; +use lemmy_websocket::{email::send_verification_email, messages::CheckCaptcha, LemmyContext}; #[async_trait::async_trait(?Send)] impl PerformCrud for Register { @@ -49,16 +47,24 @@ impl PerformCrud for Register { ) -> Result { 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 if let Ok(site) = blocking(context.pool(), Site::read_simple).await? { if !site.open_registration { return Err(ApiError::err_plain("registration_closed").into()); } + email_verification = site.require_email_verification; } password_length_check(&data.password)?; honeypot_check(&data.honeypot)?; + if email_verification && data.email.is_none() { + return Err(ApiError::err_plain("email_required").into()); + } + // Make sure passwords match if data.password != data.password_verify { 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))?; // 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.to_owned()), - password_encrypted: data.password.to_owned(), + password_encrypted: Some(data.password.to_owned()), 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(!email_verification), + ..LocalUserForm::default() }; 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))?; } - // Return the jwt - Ok(LoginResponse { - jwt: Claims::jwt( + // Log the user in directly if email verification is not required + let jwt = 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.unwrap(), + &inserted_person.name, + context, + ) + .await?; + None + } else { + Some(Claims::jwt( inserted_local_user.id.0, &context.secret().jwt_secret, &context.settings().hostname, - )?, - }) + )?) + }; + Ok(LoginResponse { jwt }) } } diff --git a/crates/api_crud/src/user/delete.rs b/crates/api_crud/src/user/delete.rs index 28f42831e..0add7e958 100644 --- a/crates/api_crud/src/user/delete.rs +++ b/crates/api_crud/src/user/delete.rs @@ -47,8 +47,6 @@ impl PerformCrud for DeleteAccount { }) .await??; - Ok(LoginResponse { - jwt: data.auth.to_owned(), - }) + Ok(LoginResponse { jwt: None }) } } diff --git a/crates/db_schema/src/aggregates/site_aggregates.rs b/crates/db_schema/src/aggregates/site_aggregates.rs index 4b4b6c432..aa5fe4efc 100644 --- a/crates/db_schema/src/aggregates/site_aggregates.rs +++ b/crates/db_schema/src/aggregates/site_aggregates.rs @@ -65,6 +65,7 @@ mod tests { enable_nsfw: None, updated: None, community_creation_admin_only: Some(false), + require_email_verification: None, }; Site::create(&conn, &site_form).unwrap(); diff --git a/crates/db_schema/src/impls/email_verification.rs b/crates/db_schema/src/impls/email_verification.rs new file mode 100644 index 000000000..69f2136fe --- /dev/null +++ b/crates/db_schema/src/impls/email_verification.rs @@ -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 { + use crate::schema::email_verification::dsl::*; + insert_into(email_verification) + .values(form) + .get_result::(conn) + } + + fn read(conn: &PgConnection, id_: i32) -> Result { + use crate::schema::email_verification::dsl::*; + email_verification.find(id_).first::(conn) + } + + fn update(conn: &PgConnection, id_: i32, form: &EmailVerificationForm) -> Result { + use crate::schema::email_verification::dsl::*; + diesel::update(email_verification.find(id_)) + .set(form) + .get_result::(conn) + } + + fn delete(conn: &PgConnection, id_: i32) -> Result { + 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 { + use crate::schema::email_verification::dsl::*; + email_verification + .filter(verification_token.eq(token)) + .first::(conn) + } +} diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index 3a2d57695..a01aee06e 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -62,8 +62,10 @@ mod safe_settings_type { impl LocalUser { pub fn register(conn: &PgConnection, form: &LocalUserForm) -> Result { 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) diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index a1e45efa5..891370d01 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -3,6 +3,7 @@ pub mod comment; 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; diff --git a/crates/db_schema/src/impls/password_reset_request.rs b/crates/db_schema/src/impls/password_reset_request.rs index c5debd926..808f0ac01 100644 --- a/crates/db_schema/src/impls/password_reset_request.rs +++ b/crates/db_schema/src/impls/password_reset_request.rs @@ -93,8 +93,8 @@ mod tests { 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() }; diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 034edbe9d..f3648b91c 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -157,6 +157,7 @@ table! { show_scores -> Bool, show_read_posts -> Bool, show_new_post_notifs -> Bool, + email_verified -> Bool, } } @@ -447,6 +448,7 @@ table! { banner -> Nullable, description -> Nullable, 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 -> comment_alias_1 (parent_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!(site -> person (creator_id)); joinable!(site_aggregates -> site (site_id)); +joinable!(email_verification -> local_user (local_user_id)); allow_tables_to_appear_in_same_query!( activity, @@ -662,4 +674,5 @@ allow_tables_to_appear_in_same_query!( comment_alias_1, person_alias_1, person_alias_2, + email_verification ); diff --git a/crates/db_schema/src/source/email_verification.rs b/crates/db_schema/src/source/email_verification.rs new file mode 100644 index 000000000..6f51713e7 --- /dev/null +++ b/crates/db_schema/src/source/email_verification.rs @@ -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, +} diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index 34dd26f76..5edb3bdfb 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -23,14 +23,15 @@ pub struct LocalUser { pub show_scores: bool, pub show_read_posts: bool, pub show_new_post_notifs: bool, + pub email_verified: 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, + pub password_encrypted: Option, pub email: Option>, pub show_nsfw: Option, pub theme: Option, @@ -43,6 +44,7 @@ pub struct LocalUserForm { pub show_scores: Option, pub show_read_posts: Option, pub show_new_post_notifs: Option, + pub email_verified: Option, } /// A local user view that removes password encrypted diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index a1e45efa5..891370d01 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -3,6 +3,7 @@ pub mod comment; 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; diff --git a/crates/db_schema/src/source/site.rs b/crates/db_schema/src/source/site.rs index dd273f9da..e66bcaed7 100644 --- a/crates/db_schema/src/source/site.rs +++ b/crates/db_schema/src/source/site.rs @@ -20,9 +20,10 @@ pub struct Site { pub banner: Option, pub description: Option, pub community_creation_admin_only: bool, + pub require_email_verification: bool, } -#[derive(Insertable, AsChangeset)] +#[derive(Insertable, AsChangeset, Default)] #[table_name = "site"] pub struct SiteForm { pub name: String, @@ -37,4 +38,5 @@ pub struct SiteForm { pub banner: Option>, pub description: Option>, pub community_creation_admin_only: Option, + pub require_email_verification: Option, } diff --git a/crates/websocket/src/email.rs b/crates/websocket/src/email.rs new file mode 100644 index 000000000..97f5f8559 --- /dev/null +++ b/crates/websocket/src/email.rs @@ -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", + "" + ), + username, settings.hostname, verify_link + ); + send_email(&subject, new_email, username, &body, &context.settings()) + .map_err(|e| ApiError::err("email_send_failed", e))?; + + Ok(()) +} diff --git a/crates/websocket/src/lib.rs b/crates/websocket/src/lib.rs index f422ac51e..57fbe16f2 100644 --- a/crates/websocket/src/lib.rs +++ b/crates/websocket/src/lib.rs @@ -10,6 +10,7 @@ use reqwest::Client; use serde::Serialize; pub mod chat_server; +pub mod email; pub mod handlers; pub mod messages; pub mod routes; diff --git a/migrations/2021-11-23-132840_email_verification/down.sql b/migrations/2021-11-23-132840_email_verification/down.sql new file mode 100644 index 000000000..cd1eb7c29 --- /dev/null +++ b/migrations/2021-11-23-132840_email_verification/down.sql @@ -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; diff --git a/migrations/2021-11-23-132840_email_verification/up.sql b/migrations/2021-11-23-132840_email_verification/up.sql new file mode 100644 index 000000000..b6606a42e --- /dev/null +++ b/migrations/2021-11-23-132840_email_verification/up.sql @@ -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 + +); \ No newline at end of file diff --git a/src/api_routes.rs b/src/api_routes.rs index 9f06c5bef..c7b59cf64 100644 --- a/src/api_routes.rs +++ b/src/api_routes.rs @@ -205,7 +205,9 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { web::put().to(route_post::), ) .route("/report_count", web::get().to(route_get::)) - .route("/unread_count", web::get().to(route_get::)), + .route("/unread_count", web::get().to(route_get::)) + // TODO: currently GET for easier testing, but should probably be POST + .route("/verify_email", web::get().to(route_get::)), ) // Admin Actions .service(