doido_auth/controllers/
passwords.rs1use crate::recoverable;
4use doido_auth_macros::auth_controller;
5use doido_core::Result;
6use serde::Deserialize;
7use std::marker::PhantomData;
8
9pub struct AuthPasswords<U>(PhantomData<U>);
11
12#[derive(Debug, Deserialize)]
13pub struct PasswordResetForm {
14 pub email: String,
15}
16
17#[derive(Debug, Deserialize)]
18pub struct PasswordUpdateForm {
19 pub password: String,
20 #[serde(default)]
21 pub password_confirmation: Option<String>,
22 pub reset_password_token: String,
23}
24
25#[derive(Debug, Deserialize)]
26pub struct EditQuery {
27 #[serde(default)]
28 pub reset_password_token: String,
29}
30
31#[auth_controller]
32impl<U> AuthPasswords<U>
33where
34 U: Send + Sync + 'static,
35{
36 pub async fn new(ctx: doido_controller::Context) -> doido_controller::Response {
38 ctx.render("auth/password_new", serde_json::json!({}))
39 }
40
41 pub async fn create(mut ctx: doido_controller::Context) -> Result<doido_controller::Response> {
44 let json = ctx.wants_json();
45 let form: PasswordResetForm = if json {
46 ctx.body_json().await?
47 } else {
48 ctx.form().await?
49 };
50
51 if let Some(token) = recoverable::request_reset(ctx.db(), &form.email).await? {
52 let _ = recoverable::send_reset_email(&form.email, &token).await;
53 }
54
55 if json {
56 Ok(ctx.json(serde_json::json!({ "status": "reset_email_sent" })))
57 } else {
58 Ok(ctx.render(
59 "auth/password_new",
60 serde_json::json!({ "notice": "If your email exists, reset instructions were sent." }),
61 ))
62 }
63 }
64
65 pub async fn edit(ctx: doido_controller::Context) -> doido_controller::Response {
68 let token = ctx
69 .params::<EditQuery>()
70 .map(|q| q.reset_password_token)
71 .unwrap_or_default();
72 ctx.render(
73 "auth/password_edit",
74 serde_json::json!({ "reset_password_token": token }),
75 )
76 }
77
78 pub async fn update(mut ctx: doido_controller::Context) -> Result<doido_controller::Response> {
80 let json = ctx.wants_json();
81 let form: PasswordUpdateForm = if json {
82 ctx.body_json().await?
83 } else {
84 ctx.form().await?
85 };
86
87 if let Some(ref confirm) = form.password_confirmation {
88 if &form.password != confirm {
89 return password_error(ctx, json, "Password confirmation does not match");
90 }
91 }
92
93 let reset =
94 recoverable::reset_password(ctx.db(), &form.reset_password_token, &form.password)
95 .await?;
96 if !reset {
97 return password_error(ctx, json, "Reset link is invalid or has expired");
98 }
99
100 if json {
101 Ok(ctx.json(serde_json::json!({ "status": "password_reset" })))
102 } else {
103 Ok(ctx.redirect_to("/users/sign_in"))
104 }
105 }
106}
107
108fn password_error(
109 ctx: &mut doido_controller::Context,
110 json: bool,
111 message: &str,
112) -> Result<doido_controller::Response> {
113 if json {
114 Ok(ctx.status(422))
115 } else {
116 Ok(ctx.render(
117 "auth/password_edit",
118 serde_json::json!({ "error": message }),
119 ))
120 }
121}