Skip to main content

doido_auth/controllers/
sessions.rs

1//! Default sessions controller (`sign_in` / `sign_out`).
2
3use crate::handlers::{authenticate, sign_in, sign_out};
4use crate::user::AuthUser;
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 sessions controller for [`auth_routes!`](crate::auth_routes).
13pub struct AuthSessions<U>(PhantomData<U>);
14
15#[derive(Debug, Deserialize)]
16pub struct SignInForm {
17    pub email: String,
18    pub password: String,
19    /// `rememberable`: a truthy value issues a persistent remember cookie.
20    #[serde(default)]
21    pub remember: Option<String>,
22}
23
24fn is_truthy(value: &Option<String>) -> bool {
25    matches!(value.as_deref(), Some("1" | "true" | "on" | "yes"))
26}
27
28#[auth_controller]
29impl<U> AuthSessions<U>
30where
31    U: AuthUser + HasSecurePassword + Serialize + Send + Sync + 'static,
32{
33    /// GET `{prefix}/sign_in` — sign-in form (HTML mode).
34    pub async fn new(ctx: doido_controller::Context) -> doido_controller::Response {
35        ctx.render("auth/sign_in", serde_json::json!({}))
36    }
37
38    /// POST `{prefix}/sign_in` — authenticate and establish a session.
39    pub async fn create(mut ctx: doido_controller::Context) -> Result<doido_controller::Response> {
40        let json = ctx.wants_json();
41        let form: SignInForm = if json {
42            ctx.body_json().await?
43        } else {
44            ctx.form().await?
45        };
46
47        match authenticate::<U>(ctx.db(), &form.email, &form.password).await {
48            Ok(user) => {
49                // `trackable` module (best-effort — never blocks sign-in).
50                let _ = crate::trackable::record_sign_in(ctx.db(), &form.email, None).await;
51                // `rememberable` module: issue a persistent remember cookie.
52                if is_truthy(&form.remember) {
53                    let _ = crate::rememberable::record_remember(ctx.db(), &form.email).await;
54                    let max_age = crate::state::try_global()
55                        .map(|s| s.config.remember_for)
56                        .unwrap_or(1_209_600);
57                    let value = crate::rememberable::cookie_value(&user.id());
58                    ctx.cookies().set_signed_permanent(
59                        crate::rememberable::REMEMBER_COOKIE,
60                        value,
61                        max_age,
62                    );
63                }
64                sign_in(ctx, &user)?;
65                if json {
66                    Ok(ctx.json(user))
67                } else {
68                    Ok(ctx.redirect_to("/"))
69                }
70            }
71            Err(_) if json => Ok(ctx.status(401)),
72            Err(_) => Ok(ctx.render(
73                "auth/sign_in",
74                serde_json::json!({ "error": "Invalid email or password" }),
75            )),
76        }
77    }
78
79    /// DELETE `{prefix}/sign_out` — clear the session and any remember cookie.
80    pub async fn destroy(mut ctx: doido_controller::Context) -> Result<doido_controller::Response> {
81        // `rememberable`: expire the persistent cookie (Max-Age=0 deletes it).
82        ctx.cookies()
83            .set_signed_permanent(crate::rememberable::REMEMBER_COOKIE, "", 0);
84        sign_out(ctx)?;
85        if ctx.wants_json() {
86            Ok(ctx.status(204))
87        } else {
88            Ok(ctx.redirect_to("/"))
89        }
90    }
91}