~vpzom/lotide

e1683a89fd09948fa3931449e75af15a4398eb32 — Colin Reeder 29 days ago af39475
Initial work on Forgot Password
M Cargo.lock => Cargo.lock +7 -0
@@ 144,6 144,12 @@ dependencies = [
]

[[package]]
name = "bs58"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "476e9cd489f9e121e02ffa6014a8ef220ecb15c05ed23fc34cca13925dc283fb"

[[package]]
name = "bumpalo"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 939,6 945,7 @@ dependencies = [
 "activitystreams-ext",
 "async-trait",
 "bcrypt",
 "bs58",
 "bytes",
 "chrono",
 "deadpool-postgres",

M Cargo.toml => Cargo.toml +2 -0
@@ 43,6 43,8 @@ activitystreams = "0.7.0-alpha.3"
activitystreams-ext = "0.1.0-alpha.2"
fast_chemail = "0.9.6"
lettre = { version = "0.10.0-alpha.2", features = ["tokio02", "tokio02-native-tls"] }
rand = "0.7.3"
bs58 = "0.3.1"

[dev-dependencies]
rand = "0.7.3"

A migrations/20200926033710_email-lower-idx/down.sql => migrations/20200926033710_email-lower-idx/down.sql +4 -0
@@ 0,0 1,4 @@
BEGIN;
	DROP INDEX person_lower_email_address_idx;
	ALTER TABLE person ADD CONSTRAINT person_email_address_key UNIQUE (email_address);
COMMIT;

A migrations/20200926033710_email-lower-idx/up.sql => migrations/20200926033710_email-lower-idx/up.sql +4 -0
@@ 0,0 1,4 @@
BEGIN;
	CREATE UNIQUE INDEX person_lower_email_address_idx ON person (LOWER(email_address)) WHERE local;
	ALTER TABLE person DROP CONSTRAINT person_email_address_key;
COMMIT;

A migrations/20200926034626_forgot-password-key/down.sql => migrations/20200926034626_forgot-password-key/down.sql +3 -0
@@ 0,0 1,3 @@
BEGIN;
	DROP TABLE forgot_password_key;
COMMIT;

A migrations/20200926034626_forgot-password-key/up.sql => migrations/20200926034626_forgot-password-key/up.sql +7 -0
@@ 0,0 1,7 @@
BEGIN;
	CREATE TABLE forgot_password_key (
		key INTEGER PRIMARY KEY,
		person BIGINT NOT NULL REFERENCES person,
		created TIMESTAMPTZ NOT NULL
	);
COMMIT;

M res/lang/en.ftl => res/lang/en.ftl +3 -0
@@ 2,12 2,15 @@ comment_content_conflict = Exactly one of content_markdown and content_text must
comment_not_yours = That's not your comment
community_edit_denied = You are not authorized to modify this community
community_name_disallowed_chars = Community name contains disallowed characters
email_content_forgot_password = Hi { $username }, if you requested a password reset from lotide, use this code: { $key }
email_not_configured = Email is not configured on this server
moderators_only_local = Only local users can be community moderators
must_be_moderator = You must be a community moderator to perform this action
name_in_use = That name is already in use
no_password = No password set for this user
no_such_comment = No such comment
no_such_community = No such community
no_such_local_user_by_email = No local user found by that email address
no_such_local_user_by_name = No local user found by that name
no_such_post = No such post
no_such_user = No such user

M src/routes/api/mod.rs => src/routes/api/mod.rs +105 -1
@@ 1,5 1,7 @@
use crate::routes::well_known::{FingerRequestQuery, FingerResponse};
use crate::{CommentLocalID, CommunityLocalID, PostLocalID, UserLocalID};
use lettre::Tokio02Transport;
use rand::Rng;
use serde_derive::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};


@@ 176,7 178,15 @@ pub fn route_api() -> crate::RouteNode<()> {
            )
            .with_child("posts", posts::route_posts())
            .with_child("comments", comments::route_comments())
            .with_child("users", users::route_users()),
            .with_child("users", users::route_users())
            .with_child(
                "forgot_password",
                crate::RouteNode::new().with_child(
                    "keys",
                    crate::RouteNode::new()
                        .with_handler_async("POST", route_unstable_forgot_password_keys_create),
                ),
            ),
    )
}



@@ 709,6 719,100 @@ async fn route_unstable_misc_render_markdown(
        .body(output.into())?)
}

struct ForgotPasswordKey {
    value: i32,
}

impl ForgotPasswordKey {
    pub fn generate() -> Self {
        Self {
            value: rand::thread_rng().gen(),
        }
    }

    pub fn as_int(&self) -> i32 {
        self.value
    }

    pub fn to_str(&self) -> String {
        bs58::encode(&self.value.to_be_bytes()).into_string()
    }
}

async fn route_unstable_forgot_password_keys_create(
    _: (),
    ctx: Arc<crate::RouteContext>,
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let lang = crate::get_lang_for_req(&req);

    if ctx.mailer.is_none() {
        return Err(crate::Error::UserError(crate::simple_response(
            hyper::StatusCode::INTERNAL_SERVER_ERROR,
            lang.tr("email_not_configured", None).into_owned(),
        )));
    }

    #[derive(Deserialize)]
    struct ForgotPasswordBody<'a> {
        email_address: Cow<'a, str>,
    }

    let body = hyper::body::to_bytes(req.into_body()).await?;
    let body: ForgotPasswordBody = serde_json::from_slice(&body)?;

    let db = ctx.db_pool.get().await?;

    let user_row = db.query_opt("SELECT id, username, email_address FROM person WHERE local AND LOWER(email_address) = LOWER($1)", &[&body.email_address]).await?
        .ok_or_else(|| {
            crate::Error::UserError(crate::simple_response(
                hyper::StatusCode::BAD_REQUEST,
                lang.tr("no_such_local_user_by_email", None).into_owned(),
            ))
        })?;

    let user_id = UserLocalID(user_row.get(0));
    let username: &str = user_row.get(1);
    let user_email: &str = user_row.get(2);

    let user_email = lettre::Mailbox::new(None, user_email.parse()?);

    let key = ForgotPasswordKey::generate();
    db.execute(
        "INSERT INTO forgot_password_key (key, person, created) VALUES ($1, $2, current_timestamp)",
        &[&key.as_int(), &user_id],
    )
    .await?;

    let msg_body = lang
        .tr(
            "email_content_forgot_password",
            Some(&fluent::fluent_args!["key" => key.to_str(), "username" => username]),
        )
        .into_owned();

    let msg = lettre::Message::builder()
        .date_now()
        .subject("Forgot Password Request")
        .from(ctx.mail_from.as_ref().unwrap().clone())
        .to(user_email)
        .singlepart(
            lettre::message::SinglePart::binary()
                .header(lettre::message::header::ContentType::text_utf8())
                .body(msg_body),
        )?;

    crate::spawn_task(async move {
        ctx.mailer.as_ref().unwrap().send(msg).await?;

        Ok(())
    });

    Ok(hyper::Response::builder()
        .header(hyper::header::CONTENT_TYPE, "application/json")
        .body("{}".into())?)
}

async fn handle_common_posts_list(
    stream: impl futures::stream::TryStream<Ok = tokio_postgres::Row, Error = tokio_postgres::Error>
        + Send,