Skip to main content

doido_auth/controllers/
registrations.rs

1//! Default registrations controller (`sign_up`).
2
3use crate::handlers::{register_user, sign_in};
4use crate::user::{AuthUser, RegisterableAuthUser};
5use doido_auth_macros::auth_controller;
6use doido_core::Result;
7use doido_model::password::HasSecurePassword;
8use serde::Deserialize;
9use serde::Serialize;
10use std::marker::PhantomData;
11
12/// Default registrations controller for [`auth_routes!`](crate::auth_routes).
13pub struct AuthRegistrations<U>(PhantomData<U>);
14
15#[derive(Debug, Deserialize)]
16pub struct SignUpForm {
17    pub email: String,
18    pub password: String,
19    #[serde(default)]
20    pub password_confirmation: Option<String>,
21}
22
23#[auth_controller]
24impl<U> AuthRegistrations<U>
25where
26    U: AuthUser + HasSecurePassword + RegisterableAuthUser + Serialize + Send + Sync + 'static,
27{
28    /// GET `{prefix}/sign_up` — registration form (HTML mode).
29    pub async fn new(ctx: doido_controller::Context) -> doido_controller::Response {
30        ctx.render("auth/sign_up", serde_json::json!({}))
31    }
32
33    /// POST `{prefix}/sign_up` — create an account and sign in.
34    pub async fn create(mut ctx: doido_controller::Context) -> Result<doido_controller::Response> {
35        let json = ctx.wants_json();
36        let form: SignUpForm = if json {
37            ctx.body_json().await?
38        } else {
39            ctx.form().await?
40        };
41
42        if let Some(ref confirm) = form.password_confirmation {
43            if form.password != *confirm {
44                return registration_error(ctx, json, "Password confirmation does not match");
45            }
46        }
47
48        let db = ctx.db().clone();
49        let user =
50            match register_user::<U, _, _>(&db, &form.email, &form.password, |email, digest| {
51                let db = db.clone();
52                async move { U::register(&db, email, digest).await }
53            })
54            .await
55            {
56                Ok(user) => user,
57                Err(crate::error::AuthError::EmailTaken) => {
58                    return registration_error(ctx, json, "Email has already been taken");
59                }
60                // `validatable`: surface validation failures as 422, not 500.
61                Err(crate::error::AuthError::Validation(msg)) => {
62                    return registration_error(ctx, json, &msg);
63                }
64                Err(e) => return Err(doido_core::anyhow::anyhow!(e.to_string())),
65            };
66
67        // `confirmable`: don't sign in yet — send a confirmation email and ask the
68        // user to confirm their address first.
69        if crate::confirmable::is_enabled() {
70            if let Some(token) =
71                crate::confirmable::generate_confirmation(ctx.db(), &form.email).await?
72            {
73                let _ = crate::confirmable::send_confirmation_email(&form.email, &token).await;
74            }
75            return if json {
76                Ok(ctx.json(serde_json::json!({ "status": "confirmation_sent" })))
77            } else {
78                Ok(ctx.render(
79                    "auth/sign_in",
80                    serde_json::json!({ "notice": "Please confirm your email to finish signing up." }),
81                ))
82            };
83        }
84
85        sign_in(ctx, &user)?;
86        if json {
87            Ok(ctx.json(user))
88        } else {
89            Ok(ctx.redirect_to("/"))
90        }
91    }
92}
93
94fn registration_error(
95    ctx: &doido_controller::Context,
96    json: bool,
97    message: &str,
98) -> Result<doido_controller::Response> {
99    if json {
100        Ok(ctx.status(422))
101    } else {
102        Ok(ctx.render("auth/sign_up", serde_json::json!({ "error": message })))
103    }
104}