mas_handlers/
session.rs

1// Copyright 2025 New Vector Ltd.
2//
3// SPDX-License-Identifier: AGPL-3.0-only
4// Please see LICENSE in the repository root for full details.
5
6//! Utilities for showing proposer HTML fallbacks when the user is logged out,
7//! locked or deactivated
8
9use axum::response::{Html, IntoResponse as _, Response};
10use mas_axum_utils::{SessionInfoExt, cookies::CookieJar, csrf::CsrfExt};
11use mas_data_model::BrowserSession;
12use mas_i18n::DataLocale;
13use mas_storage::{BoxRepository, Clock, RepositoryError};
14use mas_templates::{AccountInactiveContext, TemplateContext, Templates};
15use rand::RngCore;
16use thiserror::Error;
17
18#[derive(Debug, Error)]
19#[error(transparent)]
20pub enum SessionLoadError {
21    Template(#[from] mas_templates::TemplateError),
22    Repository(#[from] RepositoryError),
23}
24
25#[allow(clippy::large_enum_variant)]
26pub enum SessionOrFallback {
27    MaybeSession {
28        cookie_jar: CookieJar,
29        maybe_session: Option<BrowserSession>,
30    },
31    Fallback {
32        response: Response,
33    },
34}
35
36/// Load a session from the cookie jar, or fall back to an HTML error page if
37/// the account is locked, deactivated or logged out
38pub async fn load_session_or_fallback(
39    cookie_jar: CookieJar,
40    clock: &impl Clock,
41    rng: impl RngCore,
42    templates: &Templates,
43    locale: &DataLocale,
44    repo: &mut BoxRepository,
45) -> Result<SessionOrFallback, SessionLoadError> {
46    let (session_info, cookie_jar) = cookie_jar.session_info();
47    let Some(session_id) = session_info.current_session_id() else {
48        return Ok(SessionOrFallback::MaybeSession {
49            cookie_jar,
50            maybe_session: None,
51        });
52    };
53
54    let Some(session) = repo.browser_session().lookup(session_id).await? else {
55        // We looked up the session, but it was not found. Still update the cookie
56        let session_info = session_info.mark_session_ended();
57        let cookie_jar = cookie_jar.update_session_info(&session_info);
58        return Ok(SessionOrFallback::MaybeSession {
59            cookie_jar,
60            maybe_session: None,
61        });
62    };
63
64    if session.user.deactivated_at.is_some() {
65        // The account is deactivated, show the 'account deactivated' fallback
66        let (csrf_token, cookie_jar) = cookie_jar.csrf_token(clock, rng);
67        let ctx = AccountInactiveContext::new(session.user)
68            .with_csrf(csrf_token.form_value())
69            .with_language(locale.clone());
70        let fallback = templates.render_account_deactivated(&ctx)?;
71        let response = (cookie_jar, Html(fallback)).into_response();
72        return Ok(SessionOrFallback::Fallback { response });
73    }
74
75    if session.user.locked_at.is_some() {
76        // The account is locked, show the 'account locked' fallback
77        let (csrf_token, cookie_jar) = cookie_jar.csrf_token(clock, rng);
78        let ctx = AccountInactiveContext::new(session.user)
79            .with_csrf(csrf_token.form_value())
80            .with_language(locale.clone());
81        let fallback = templates.render_account_locked(&ctx)?;
82        let response = (cookie_jar, Html(fallback)).into_response();
83        return Ok(SessionOrFallback::Fallback { response });
84    }
85
86    if session.finished_at.is_some() {
87        // The session has finished, but the browser still has the cookie. This is
88        // likely a 'remote' logout, triggered either by an admin or from the
89        // user-management UI. In this case, we show the 'account logged out'
90        // fallback.
91        let (csrf_token, cookie_jar) = cookie_jar.csrf_token(clock, rng);
92        let ctx = AccountInactiveContext::new(session.user)
93            .with_csrf(csrf_token.form_value())
94            .with_language(locale.clone());
95        let fallback = templates.render_account_logged_out(&ctx)?;
96        let response = (cookie_jar, Html(fallback)).into_response();
97        return Ok(SessionOrFallback::Fallback { response });
98    }
99
100    Ok(SessionOrFallback::MaybeSession {
101        cookie_jar,
102        maybe_session: Some(session),
103    })
104}