diff options
author | Colin Reeder <vpzomtrrfrt@gmail.com> | 2020-09-26 18:29:31 -0600 |
---|---|---|
committer | Colin Reeder <vpzomtrrfrt@gmail.com> | 2020-09-26 18:29:31 -0600 |
commit | 3d1b91c031d2e67e2f9265dc8366c684ce286d28 (patch) | |
tree | dfe94a273b694fbfbfd208c07a2e89ab67311627 | |
parent | 03f0616a7c2b00ac0e47247795308c0a35a12e88 (diff) |
Add Forgot Password
-rw-r--r-- | res/lang/en.ftl | 7 | ||||
-rw-r--r-- | src/routes/forgot_password.rs | 305 | ||||
-rw-r--r-- | src/routes/mod.rs | 5 |
3 files changed, 317 insertions, 0 deletions
diff --git a/res/lang/en.ftl b/res/lang/en.ftl index 49218f8..35a2167 100644 --- a/res/lang/en.ftl +++ b/res/lang/en.ftl @@ -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: diff --git a/src/routes/forgot_password.rs b/src/routes/forgot_password.rs new file mode 100644 index 0000000..14b827d --- /dev/null +++ b/src/routes/forgot_password.rs @@ -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), + ) +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index d7684a7..fe959a1 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -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() |