~vpzom/hitide

3d1b91c031d2e67e2f9265dc8366c684ce286d28 — Colin Reeder 28 days ago 03f0616
Add Forgot Password
3 files changed, 317 insertions(+), 0 deletions(-)

M res/lang/en.ftl
A src/routes/forgot_password.rs
M src/routes/mod.rs
M res/lang/en.ftl => res/lang/en.ftl +7 -0
@@ 30,6 30,13 @@ fetch = Fetch
follow = Follow
follow_request_sent = Follow request sent!
follow_undo = Unfollow
forgot_password = Forgot Password
forgot_password_code_info = You should have received a code via email.
forgot_password_code_prompt = Enter it here:
forgot_password_complete = Password successfully reset.
forgot_password_email_prompt = Email Address:
forgot_password_info = If your account has an attached email address, you can reset your password here.
forgot_password_new_password_prompt = New Password:
home_follow_prompt1 = Why not
home_follow_prompt2 = follow some communities?
liked_by = Liked by:

A src/routes/forgot_password.rs => src/routes/forgot_password.rs +305 -0
@@ 0,0 1,305 @@
use crate::routes::{
    fetch_base_data, for_client, get_cookie_map_for_headers, get_cookie_map_for_req, html_response,
    res_to_error, CookieMap, HTPage,
};
use serde_derive::Deserialize;
use std::borrow::Cow;
use std::sync::Arc;

async fn page_forgot_password(
    _: (),
    ctx: Arc<crate::RouteContext>,
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let cookies = get_cookie_map_for_req(&req)?;

    page_forgot_password_inner(ctx, req.headers(), &cookies, None).await
}

async fn page_forgot_password_inner(
    ctx: Arc<crate::RouteContext>,
    headers: &hyper::header::HeaderMap,
    cookies: &CookieMap<'_>,
    display_error: Option<String>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let lang = crate::get_lang_for_headers(headers);
    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, headers, &cookies).await?;

    let title = lang.tr("forgot_password", None);

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} lang={&lang} title={&title}>
            <h1>{title.as_ref()}</h1>
            <form method={"POST"} action={"/forgot_password/submit"}>
                <p>{lang.tr("forgot_password_info", None)}</p>
                {
                    display_error.map(|msg| {
                        render::rsx! {
                            <div class={"errorBox"}>{msg}</div>
                        }
                    })
                }
                <div>
                    <label>
                        {lang.tr("forgot_password_email_prompt", None)}
                        {" "}
                        <input type={"email"} name={"email_address"} required={"required"} />
                    </label>
                </div>
                <button type={"submit"}>{lang.tr("submit", None)}</button>
            </form>
        </HTPage>
    }))
}

async fn page_forgot_password_code(
    _: (),
    ctx: Arc<crate::RouteContext>,
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let cookies = get_cookie_map_for_req(&req)?;

    page_forgot_password_code_inner(ctx, req.headers(), &cookies, None).await
}

async fn page_forgot_password_code_inner(
    ctx: Arc<crate::RouteContext>,
    headers: &hyper::header::HeaderMap,
    cookies: &CookieMap<'_>,
    display_error: Option<String>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let lang = crate::get_lang_for_headers(headers);
    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, headers, &cookies).await?;

    let title = lang.tr("forgot_password", None);

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} lang={&lang} title={&title}>
            <h1>{title.as_ref()}</h1>
            <form method={"POST"} action={"/forgot_password/code/submit"}>
                <p>{lang.tr("forgot_password_code_info", None)}</p>
                {
                    display_error.map(|msg| {
                        render::rsx! {
                            <div class={"errorBox"}>{msg}</div>
                        }
                    })
                }
                <div>
                    <label>
                        {lang.tr("forgot_password_code_prompt", None)}
                        {" "}
                        <input type={"text"} name={"key"} required={"required"} />
                    </label>
                </div>
                <button type={"submit"}>{lang.tr("submit", None)}</button>
            </form>
        </HTPage>
    }))
}

async fn page_forgot_password_code_reset_inner(
    key: &str,
    ctx: Arc<crate::RouteContext>,
    headers: &hyper::header::HeaderMap,
    cookies: &CookieMap<'_>,
    display_error: Option<String>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let lang = crate::get_lang_for_headers(headers);
    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, headers, &cookies).await?;

    let title = lang.tr("forgot_password", None);

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} lang={&lang} title={&title}>
            <h1>{title.as_ref()}</h1>
            <form method={"POST"} action={"/forgot_password/code/submit"}>
                {
                    display_error.map(|msg| {
                        render::rsx! {
                            <div class={"errorBox"}>{msg}</div>
                        }
                    })
                }
                <input type={"hidden"} name={"key"} value={key} />
                <div>
                    <label>
                        {lang.tr("forgot_password_new_password_prompt", None)}
                        {" "}
                        <input type={"password"} name={"new_password"} required={"required"} />
                    </label>
                </div>
                <button type={"submit"}>{lang.tr("submit", None)}</button>
            </form>
        </HTPage>
    }))
}

async fn handler_forgot_password_code_submit(
    _: (),
    ctx: Arc<crate::RouteContext>,
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    #[derive(Deserialize)]
    struct CodeSubmitBody<'a> {
        key: Cow<'a, str>,
        new_password: Option<Cow<'a, str>>,
    }

    let (req_parts, body) = req.into_parts();

    let cookies = get_cookie_map_for_headers(&req_parts.headers)?;

    let body = hyper::body::to_bytes(body).await?;
    let body: CodeSubmitBody = serde_urlencoded::from_bytes(&body)?;

    if let Some(new_password) = body.new_password {
        let api_res = res_to_error(
            ctx.http_client
                .request(for_client(
                    hyper::Request::post(format!(
                        "{}/api/unstable/forgot_password/keys/{}/reset",
                        ctx.backend_host,
                        urlencoding::encode(&body.key),
                    ))
                    .body(
                        serde_json::to_vec(&serde_json::json!({ "new_password": new_password }))?
                            .into(),
                    )?,
                    &req_parts.headers,
                    &cookies,
                )?)
                .await?,
        )
        .await;

        match api_res {
            Ok(_) => {
                let base_data = fetch_base_data(
                    &ctx.backend_host,
                    &ctx.http_client,
                    &req_parts.headers,
                    &cookies,
                )
                .await?;

                let lang = crate::get_lang_for_headers(&req_parts.headers);

                let title = lang.tr("forgot_password", None);

                Ok(html_response(render::html! {
                    <HTPage base_data={&base_data} lang={&lang} title={&title}>
                        <h1>{title.as_ref()}</h1>
                        <p>
                            {lang.tr("forgot_password_complete", None)}{" "}
                            <a href={"/login"}>{lang.tr("login", None)}</a>
                        </p>
                    </HTPage>
                }))
            }
            Err(crate::Error::RemoteError((_, message))) => {
                page_forgot_password_code_reset_inner(
                    &body.key,
                    ctx,
                    &req_parts.headers,
                    &cookies,
                    Some(message),
                )
                .await
            }
            Err(other) => Err(other),
        }
    } else {
        let api_res = res_to_error(
            ctx.http_client
                .request(for_client(
                    hyper::Request::get(format!(
                        "{}/api/unstable/forgot_password/keys/{}",
                        ctx.backend_host,
                        urlencoding::encode(&body.key),
                    ))
                    .body(Default::default())?,
                    &req_parts.headers,
                    &cookies,
                )?)
                .await?,
        )
        .await;

        match api_res {
            Ok(_) => {
                page_forgot_password_code_reset_inner(
                    &body.key,
                    ctx,
                    &req_parts.headers,
                    &cookies,
                    None,
                )
                .await
            }
            Err(crate::Error::RemoteError((_, message))) => {
                page_forgot_password_code_inner(ctx, &req_parts.headers, &cookies, Some(message))
                    .await
            }
            Err(other) => Err(other),
        }
    }
}

async fn handler_forgot_password_submit(
    _: (),
    ctx: Arc<crate::RouteContext>,
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let (req_parts, body) = req.into_parts();

    let cookies = get_cookie_map_for_headers(&req_parts.headers)?;

    let body = hyper::body::to_bytes(body).await?;
    let body: serde_json::Value = serde_urlencoded::from_bytes(&body)?;

    let api_res = res_to_error(
        ctx.http_client
            .request(for_client(
                hyper::Request::post(format!(
                    "{}/api/unstable/forgot_password/keys",
                    ctx.backend_host,
                ))
                .body(serde_json::to_vec(&body)?.into())?,
                &req_parts.headers,
                &cookies,
            )?)
            .await?,
    )
    .await;

    match api_res {
        Ok(_) => Ok(hyper::Response::builder()
            .status(hyper::StatusCode::SEE_OTHER)
            .header(hyper::header::LOCATION, "/forgot_password/code")
            .body("Request submitted.".into())?),
        Err(crate::Error::RemoteError((_, message))) => {
            page_forgot_password_inner(ctx, &req_parts.headers, &cookies, Some(message)).await
        }
        Err(other) => Err(other),
    }
}

pub fn route_forgot_password() -> crate::RouteNode<()> {
    crate::RouteNode::new()
        .with_handler_async("GET", page_forgot_password)
        .with_child(
            "code",
            crate::RouteNode::new()
                .with_handler_async("GET", page_forgot_password_code)
                .with_child(
                    "submit",
                    crate::RouteNode::new()
                        .with_handler_async("POST", handler_forgot_password_code_submit),
                ),
        )
        .with_child(
            "submit",
            crate::RouteNode::new().with_handler_async("POST", handler_forgot_password_submit),
        )
}

M src/routes/mod.rs => src/routes/mod.rs +5 -0
@@ 14,6 14,7 @@ use crate::util::author_is_me;
use crate::PageBaseData;

mod communities;
mod forgot_password;
mod posts;
mod r#static;



@@ 648,6 649,9 @@ async fn page_login_inner(
            <p>
                {lang.tr("or_start", None)}{" "}<a href={"/signup"}>{lang.tr("login_signup_link", None)}</a>
            </p>
            <p>
                <a href={"/forgot_password"}>{lang.tr("forgot_password", None)}</a>
            </p>
        </HTPage>
    }))
}


@@ 1568,6 1572,7 @@ pub fn route_root() -> crate::RouteNode<()> {
            ),
        )
        .with_child("communities", communities::route_communities())
        .with_child("forgot_password", forgot_password::route_forgot_password())
        .with_child(
            "login",
            crate::RouteNode::new()