Better error pages when a user is deactivated or locked

This commit is contained in:
Quentin Gliech
2025-03-11 17:35:13 +01:00
parent 93b991ec95
commit 62a4aba08b
25 changed files with 522 additions and 128 deletions
+10 -5
View File
@@ -5,7 +5,7 @@
// Please see LICENSE in the repository root for full details.
use mas_data_model::BrowserSession;
use mas_storage::{RepositoryAccess, user::BrowserSessionRepository};
use mas_storage::RepositoryAccess;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
@@ -33,13 +33,12 @@ impl SessionInfo {
self
}
/// Load the [`BrowserSession`] from database
/// Load the active [`BrowserSession`] from database
///
/// # Errors
///
/// Returns an error if the session is not found or if the session is not
/// active anymore
pub async fn load_session<E>(
/// Returns an error if the underlying repository fails to load the session.
pub async fn load_active_session<E>(
&self,
repo: &mut impl RepositoryAccess<Error = E>,
) -> Result<Option<BrowserSession>, E> {
@@ -56,6 +55,12 @@ impl SessionInfo {
Ok(maybe_session)
}
/// Get the current session ID, if any
#[must_use]
pub fn current_session_id(&self) -> Option<Ulid> {
self.current
}
}
pub trait SessionInfoExt {
@@ -13,7 +13,7 @@ use axum::{
};
use chrono::Duration;
use mas_axum_utils::{
FancyError, SessionInfoExt,
FancyError,
cookies::CookieJar,
csrf::{CsrfExt, ProtectedForm},
};
@@ -28,7 +28,10 @@ use mas_templates::{CompatSsoContext, ErrorContext, TemplateContext, Templates};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::PreferredLanguage;
use crate::{
PreferredLanguage,
session::{SessionOrFallback, load_session_or_fallback},
};
#[derive(Serialize)]
struct AllParams<'s> {
@@ -61,10 +64,20 @@ pub async fn get(
Path(id): Path<Ulid>,
Query(params): Query<Params>,
) -> Result<Response, FancyError> {
let (session_info, cookie_jar) = cookie_jar.session_info();
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let (cookie_jar, maybe_session) = match load_session_or_fallback(
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
)
.await?
{
SessionOrFallback::MaybeSession {
cookie_jar,
maybe_session,
..
} => (cookie_jar, maybe_session),
SessionOrFallback::Fallback { response } => return Ok(response),
};
let maybe_session = session_info.load_session(&mut repo).await?;
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let Some(session) = maybe_session else {
// If there is no session, redirect to the login or register screen
@@ -126,10 +139,20 @@ pub async fn post(
Query(params): Query<Params>,
Form(form): Form<ProtectedForm<()>>,
) -> Result<Response, FancyError> {
let (session_info, cookie_jar) = cookie_jar.session_info();
cookie_jar.verify_form(&clock, form)?;
let (cookie_jar, maybe_session) = match load_session_or_fallback(
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
)
.await?
{
SessionOrFallback::MaybeSession {
cookie_jar,
maybe_session,
..
} => (cookie_jar, maybe_session),
SessionOrFallback::Fallback { response } => return Ok(response),
};
let maybe_session = session_info.load_session(&mut repo).await?;
cookie_jar.verify_form(&clock, form)?;
let Some(session) = maybe_session else {
// If there is no session, redirect to the login or register screen
+1 -1
View File
@@ -288,7 +288,7 @@ async fn get_requester(
RequestingEntity::OAuth2Session(Box::new((session, user)))
} else {
let maybe_session = session_info.load_session(&mut repo).await?;
let maybe_session = session_info.load_active_session(&mut repo).await?;
if let Some(session) = maybe_session.as_ref() {
activity_tracker
+1
View File
@@ -64,6 +64,7 @@ mod activity_tracker;
mod captcha;
mod preferred_language;
mod rate_limit;
mod session;
#[cfg(test)]
mod test_utils;
@@ -97,7 +97,7 @@ pub(crate) async fn get(
) -> Result<Response, RouteError> {
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info.load_session(&mut repo).await?;
let maybe_session = session_info.load_active_session(&mut repo).await?;
let user_agent = user_agent.map(|TypedHeader(ua)| ua.to_string());
@@ -176,7 +176,7 @@ pub(crate) async fn get(
let callback_destination = callback_destination.clone();
let locale = locale.clone();
async move {
let maybe_session = session_info.load_session(&mut repo).await?;
let maybe_session = session_info.load_active_session(&mut repo).await?;
let prompt = params.auth.prompt.as_deref().unwrap_or_default();
// Check if the request/request_uri/registration params are used. If so, reply
+73 -50
View File
@@ -1,4 +1,4 @@
// Copyright 2024 New Vector Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
@@ -11,7 +11,6 @@ use axum::{
use axum_extra::TypedHeader;
use hyper::StatusCode;
use mas_axum_utils::{
SessionInfoExt,
cookies::CookieJar,
csrf::{CsrfExt, ProtectedForm},
sentry::SentryEventID,
@@ -27,7 +26,10 @@ use mas_templates::{ConsentContext, PolicyViolationContext, TemplateContext, Tem
use thiserror::Error;
use ulid::Ulid;
use crate::{BoundActivityTracker, PreferredLanguage, impl_from_error_for_route};
use crate::{
BoundActivityTracker, PreferredLanguage, impl_from_error_for_route,
session::{SessionOrFallback, load_session_or_fallback},
};
#[derive(Debug, Error)]
pub enum RouteError {
@@ -54,6 +56,7 @@ impl_from_error_for_route!(mas_templates::TemplateError);
impl_from_error_for_route!(mas_storage::RepositoryError);
impl_from_error_for_route!(mas_policy::LoadError);
impl_from_error_for_route!(mas_policy::EvaluationError);
impl_from_error_for_route!(crate::session::SessionLoadError);
impl IntoResponse for RouteError {
fn into_response(self) -> axum::response::Response {
@@ -85,9 +88,18 @@ pub(crate) async fn get(
cookie_jar: CookieJar,
Path(grant_id): Path<Ulid>,
) -> Result<Response, RouteError> {
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info.load_session(&mut repo).await?;
let (cookie_jar, maybe_session) = match load_session_or_fallback(
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
)
.await?
{
SessionOrFallback::MaybeSession {
cookie_jar,
maybe_session,
..
} => (cookie_jar, maybe_session),
SessionOrFallback::Fallback { response } => return Ok(response),
};
let user_agent = user_agent.map(|ua| ua.to_string());
@@ -107,48 +119,48 @@ pub(crate) async fn get(
return Err(RouteError::GrantNotPending);
}
if let Some(session) = maybe_session {
activity_tracker
.record_browser_session(&clock, &session)
.await;
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let res = policy
.evaluate_authorization_grant(mas_policy::AuthorizationGrantInput {
user: Some(&session.user),
client: &client,
scope: &grant.scope,
grant_type: mas_policy::GrantType::AuthorizationCode,
requester: mas_policy::Requester {
ip_address: activity_tracker.ip(),
user_agent,
},
})
.await?;
if res.valid() {
let ctx = ConsentContext::new(grant, client)
.with_session(session)
.with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_consent(&ctx)?;
Ok((cookie_jar, Html(content)).into_response())
} else {
let ctx = PolicyViolationContext::for_authorization_grant(grant, client)
.with_session(session)
.with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_policy_violation(&ctx)?;
Ok((cookie_jar, Html(content)).into_response())
}
} else {
let Some(session) = maybe_session else {
let login = mas_router::Login::and_continue_grant(grant_id);
Ok((cookie_jar, url_builder.redirect(&login)).into_response())
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
};
activity_tracker
.record_browser_session(&clock, &session)
.await;
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let res = policy
.evaluate_authorization_grant(mas_policy::AuthorizationGrantInput {
user: Some(&session.user),
client: &client,
scope: &grant.scope,
grant_type: mas_policy::GrantType::AuthorizationCode,
requester: mas_policy::Requester {
ip_address: activity_tracker.ip(),
user_agent,
},
})
.await?;
if res.valid() {
let ctx = ConsentContext::new(grant, client)
.with_session(session)
.with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_consent(&ctx)?;
Ok((cookie_jar, Html(content)).into_response())
} else {
let ctx = PolicyViolationContext::for_authorization_grant(grant, client)
.with_session(session)
.with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_policy_violation(&ctx)?;
Ok((cookie_jar, Html(content)).into_response())
}
}
@@ -161,6 +173,8 @@ pub(crate) async fn get(
pub(crate) async fn post(
mut rng: BoxRng,
clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>,
mut policy: Policy,
mut repo: BoxRepository,
activity_tracker: BoundActivityTracker,
@@ -172,9 +186,18 @@ pub(crate) async fn post(
) -> Result<Response, RouteError> {
cookie_jar.verify_form(&clock, form)?;
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info.load_session(&mut repo).await?;
let (cookie_jar, maybe_session) = match load_session_or_fallback(
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
)
.await?
{
SessionOrFallback::MaybeSession {
cookie_jar,
maybe_session,
..
} => (cookie_jar, maybe_session),
SessionOrFallback::Fallback { response } => return Ok(response),
};
let user_agent = user_agent.map(|ua| ua.to_string());
+30 -8
View File
@@ -12,7 +12,7 @@ use axum::{
};
use axum_extra::TypedHeader;
use mas_axum_utils::{
FancyError, SessionInfoExt,
FancyError,
cookies::CookieJar,
csrf::{CsrfExt, ProtectedForm},
};
@@ -24,7 +24,10 @@ use serde::Deserialize;
use tracing::warn;
use ulid::Ulid;
use crate::{BoundActivityTracker, PreferredLanguage};
use crate::{
BoundActivityTracker, PreferredLanguage,
session::{SessionOrFallback, load_session_or_fallback},
};
#[derive(Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
@@ -51,10 +54,20 @@ pub(crate) async fn get(
cookie_jar: CookieJar,
Path(grant_id): Path<Ulid>,
) -> Result<Response, FancyError> {
let (session_info, cookie_jar) = cookie_jar.session_info();
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let (cookie_jar, maybe_session) = match load_session_or_fallback(
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
)
.await?
{
SessionOrFallback::MaybeSession {
cookie_jar,
maybe_session,
..
} => (cookie_jar, maybe_session),
SessionOrFallback::Fallback { response } => return Ok(response),
};
let maybe_session = session_info.load_session(&mut repo).await?;
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let user_agent = user_agent.map(|ua| ua.to_string());
@@ -137,12 +150,21 @@ pub(crate) async fn post(
Path(grant_id): Path<Ulid>,
Form(form): Form<ProtectedForm<ConsentForm>>,
) -> Result<Response, FancyError> {
let (session_info, cookie_jar) = cookie_jar.session_info();
let form = cookie_jar.verify_form(&clock, form)?;
let (cookie_jar, maybe_session) = match load_session_or_fallback(
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
)
.await?
{
SessionOrFallback::MaybeSession {
cookie_jar,
maybe_session,
..
} => (cookie_jar, maybe_session),
SessionOrFallback::Fallback { response } => return Ok(response),
};
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let maybe_session = session_info.load_session(&mut repo).await?;
let user_agent = user_agent.map(|TypedHeader(ua)| ua.to_string());
let Some(session) = maybe_session else {
+104
View File
@@ -0,0 +1,104 @@
// Copyright 2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//! Utilities for showing proposer HTML fallbacks when the user is logged out,
//! locked or deactivated
use axum::response::{Html, IntoResponse as _, Response};
use mas_axum_utils::{SessionInfoExt, cookies::CookieJar, csrf::CsrfExt};
use mas_data_model::BrowserSession;
use mas_i18n::DataLocale;
use mas_storage::{BoxRepository, Clock, RepositoryError};
use mas_templates::{AccountInactiveContext, TemplateContext, Templates};
use rand::RngCore;
use thiserror::Error;
#[derive(Debug, Error)]
#[error(transparent)]
pub enum SessionLoadError {
Template(#[from] mas_templates::TemplateError),
Repository(#[from] RepositoryError),
}
#[allow(clippy::large_enum_variant)]
pub enum SessionOrFallback {
MaybeSession {
cookie_jar: CookieJar,
maybe_session: Option<BrowserSession>,
},
Fallback {
response: Response,
},
}
/// Load a session from the cookie jar, or fall back to an HTML error page if
/// the account is locked, deactivated or logged out
pub async fn load_session_or_fallback(
cookie_jar: CookieJar,
clock: &impl Clock,
rng: impl RngCore,
templates: &Templates,
locale: &DataLocale,
repo: &mut BoxRepository,
) -> Result<SessionOrFallback, SessionLoadError> {
let (session_info, cookie_jar) = cookie_jar.session_info();
let Some(session_id) = session_info.current_session_id() else {
return Ok(SessionOrFallback::MaybeSession {
cookie_jar,
maybe_session: None,
});
};
let Some(session) = repo.browser_session().lookup(session_id).await? else {
// We looked up the session, but it was not found. Still update the cookie
let session_info = session_info.mark_session_ended();
let cookie_jar = cookie_jar.update_session_info(&session_info);
return Ok(SessionOrFallback::MaybeSession {
cookie_jar,
maybe_session: None,
});
};
if session.user.deactivated_at.is_some() {
// The account is deactivated, show the 'account deactivated' fallback
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(clock, rng);
let ctx = AccountInactiveContext::new(session.user)
.with_csrf(csrf_token.form_value())
.with_language(locale.clone());
let fallback = templates.render_account_deactivated(&ctx)?;
let response = (cookie_jar, Html(fallback)).into_response();
return Ok(SessionOrFallback::Fallback { response });
}
if session.user.locked_at.is_some() {
// The account is locked, show the 'account locked' fallback
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(clock, rng);
let ctx = AccountInactiveContext::new(session.user)
.with_csrf(csrf_token.form_value())
.with_language(locale.clone());
let fallback = templates.render_account_locked(&ctx)?;
let response = (cookie_jar, Html(fallback)).into_response();
return Ok(SessionOrFallback::Fallback { response });
}
if session.finished_at.is_some() {
// The session has finished, but the browser still has the cookie. This is
// likely a 'remote' logout, triggered either by an admin or from the
// user-management UI. In this case, we show the 'account logged out'
// fallback.
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(clock, rng);
let ctx = AccountInactiveContext::new(session.user)
.with_csrf(csrf_token.form_value())
.with_language(locale.clone());
let fallback = templates.render_account_logged_out(&ctx)?;
let response = (cookie_jar, Html(fallback)).into_response();
return Ok(SessionOrFallback::Fallback { response });
}
Ok(SessionOrFallback::MaybeSession {
cookie_jar,
maybe_session: Some(session),
})
}
+2 -2
View File
@@ -242,7 +242,7 @@ pub(crate) async fn get(
let (user_session_info, cookie_jar) = cookie_jar.session_info();
let (csrf_token, mut cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let maybe_user_session = user_session_info.load_session(&mut repo).await?;
let maybe_user_session = user_session_info.load_active_session(&mut repo).await?;
let response = match (maybe_user_session, link.user_id) {
(Some(session), Some(user_id)) if session.user.id == user_id => {
@@ -556,7 +556,7 @@ pub(crate) async fn post(
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let (user_session_info, cookie_jar) = cookie_jar.session_info();
let maybe_user_session = user_session_info.load_session(&mut repo).await?;
let maybe_user_session = user_session_info.load_active_session(&mut repo).await?;
let form_state = form.to_form_state();
let session = match (maybe_user_session, link.user_id, form) {
+20 -6
View File
@@ -8,13 +8,16 @@ use axum::{
extract::{Query, State},
response::{Html, IntoResponse},
};
use mas_axum_utils::{FancyError, SessionInfoExt, cookies::CookieJar};
use mas_axum_utils::{FancyError, cookies::CookieJar};
use mas_router::{PostAuthAction, UrlBuilder};
use mas_storage::{BoxClock, BoxRepository};
use mas_storage::{BoxClock, BoxRepository, BoxRng};
use mas_templates::{AppContext, TemplateContext, Templates};
use serde::Deserialize;
use crate::{BoundActivityTracker, PreferredLanguage};
use crate::{
BoundActivityTracker, PreferredLanguage,
session::{SessionOrFallback, load_session_or_fallback},
};
#[derive(Deserialize)]
pub struct Params {
@@ -31,13 +34,24 @@ pub async fn get(
Query(Params { action }): Query<Params>,
mut repo: BoxRepository,
clock: BoxClock,
mut rng: BoxRng,
cookie_jar: CookieJar,
) -> Result<impl IntoResponse, FancyError> {
let (session_info, cookie_jar) = cookie_jar.session_info();
let session = session_info.load_session(&mut repo).await?;
let (cookie_jar, maybe_session) = match load_session_or_fallback(
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
)
.await?
{
SessionOrFallback::MaybeSession {
cookie_jar,
maybe_session,
..
} => (cookie_jar, maybe_session),
SessionOrFallback::Fallback { response } => return Ok(response),
};
// TODO: keep the full path, not just the action
let Some(session) = session else {
let Some(session) = maybe_session else {
return Ok((
cookie_jar,
url_builder.redirect(&mas_router::Login::and_then(
+25 -10
View File
@@ -6,14 +6,18 @@
use axum::{
extract::State,
response::{Html, IntoResponse},
response::{Html, IntoResponse, Response},
};
use mas_axum_utils::{FancyError, SessionInfoExt, cookies::CookieJar, csrf::CsrfExt};
use mas_axum_utils::{FancyError, cookies::CookieJar, csrf::CsrfExt};
use mas_router::UrlBuilder;
use mas_storage::{BoxClock, BoxRepository, BoxRng};
use mas_templates::{IndexContext, TemplateContext, Templates};
use crate::{BoundActivityTracker, preferred_language::PreferredLanguage};
use crate::{
BoundActivityTracker,
preferred_language::PreferredLanguage,
session::{SessionOrFallback, load_session_or_fallback},
};
#[tracing::instrument(name = "handlers.views.index.get", skip_all, err)]
pub async fn get(
@@ -25,23 +29,34 @@ pub async fn get(
mut repo: BoxRepository,
cookie_jar: CookieJar,
PreferredLanguage(locale): PreferredLanguage,
) -> Result<impl IntoResponse, FancyError> {
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let (session_info, cookie_jar) = cookie_jar.session_info();
let session = session_info.load_session(&mut repo).await?;
) -> Result<Response, FancyError> {
let (cookie_jar, maybe_session) = match load_session_or_fallback(
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
)
.await?
{
SessionOrFallback::MaybeSession {
cookie_jar,
maybe_session,
..
} => (cookie_jar, maybe_session),
SessionOrFallback::Fallback { response } => return Ok(response),
};
if let Some(session) = session.as_ref() {
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
if let Some(session) = maybe_session.as_ref() {
activity_tracker
.record_browser_session(&clock, session)
.await;
}
let ctx = IndexContext::new(url_builder.oidc_discovery())
.maybe_with_session(session)
.maybe_with_session(maybe_session)
.with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_index(&ctx)?;
Ok((cookie_jar, Html(content)))
Ok((cookie_jar, Html(content)).into_response())
}
+14 -3
View File
@@ -38,6 +38,7 @@ use super::shared::OptionalPostAuthAction;
use crate::{
BoundActivityTracker, Limiter, PreferredLanguage, RequesterFingerprint, SiteConfig,
passwords::PasswordManager,
session::{SessionOrFallback, load_session_or_fallback},
};
#[derive(Debug, Deserialize, Serialize)]
@@ -64,10 +65,20 @@ pub(crate) async fn get(
Query(query): Query<OptionalPostAuthAction>,
cookie_jar: CookieJar,
) -> Result<Response, FancyError> {
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let (session_info, cookie_jar) = cookie_jar.session_info();
let (cookie_jar, maybe_session) = match load_session_or_fallback(
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
)
.await?
{
SessionOrFallback::MaybeSession {
cookie_jar,
maybe_session,
..
} => (cookie_jar, maybe_session),
SessionOrFallback::Fallback { response } => return Ok(response),
};
let maybe_session = session_info.load_session(&mut repo).await?;
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
if let Some(session) = maybe_session {
activity_tracker
+15 -9
View File
@@ -29,21 +29,27 @@ pub(crate) async fn post(
) -> Result<impl IntoResponse, FancyError> {
let form = cookie_jar.verify_form(&clock, form)?;
let (session_info, mut cookie_jar) = cookie_jar.session_info();
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info.load_session(&mut repo).await?;
if let Some(session_id) = session_info.current_session_id() {
let maybe_session = repo.browser_session().lookup(session_id).await?;
if let Some(session) = maybe_session {
if session.finished_at.is_none() {
activity_tracker
.record_browser_session(&clock, &session)
.await;
if let Some(session) = maybe_session {
activity_tracker
.record_browser_session(&clock, &session)
.await;
repo.browser_session().finish(&clock, session).await?;
cookie_jar = cookie_jar.update_session_info(&session_info.mark_session_ended());
repo.browser_session().finish(&clock, session).await?;
}
}
}
repo.save().await?;
// We always want to clear out the session cookie, even if the session was
// invalid
let cookie_jar = cookie_jar.update_session_info(&session_info.mark_session_ended());
let destination = if let Some(action) = form {
action.go_next(&url_builder)
} else {
+33 -8
View File
@@ -25,7 +25,11 @@ use serde::Deserialize;
use zeroize::Zeroizing;
use super::shared::OptionalPostAuthAction;
use crate::{BoundActivityTracker, PreferredLanguage, SiteConfig, passwords::PasswordManager};
use crate::{
BoundActivityTracker, PreferredLanguage, SiteConfig,
passwords::PasswordManager,
session::{SessionOrFallback, load_session_or_fallback},
};
#[derive(Deserialize, Debug)]
pub(crate) struct ReauthForm {
@@ -52,10 +56,18 @@ pub(crate) async fn get(
.into_response());
}
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info.load_session(&mut repo).await?;
let (cookie_jar, maybe_session) = match load_session_or_fallback(
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
)
.await?
{
SessionOrFallback::MaybeSession {
cookie_jar,
maybe_session,
..
} => (cookie_jar, maybe_session),
SessionOrFallback::Fallback { response } => return Ok(response),
};
let Some(session) = maybe_session else {
// If there is no session, redirect to the login screen, keeping the
@@ -64,6 +76,8 @@ pub(crate) async fn get(
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
};
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
activity_tracker
.record_browser_session(&clock, &session)
.await;
@@ -89,6 +103,8 @@ pub(crate) async fn get(
pub(crate) async fn post(
mut rng: BoxRng,
clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>,
State(password_manager): State<PasswordManager>,
State(url_builder): State<UrlBuilder>,
State(site_config): State<SiteConfig>,
@@ -104,9 +120,18 @@ pub(crate) async fn post(
let form = cookie_jar.verify_form(&clock, form)?;
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info.load_session(&mut repo).await?;
let (cookie_jar, maybe_session) = match load_session_or_fallback(
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
)
.await?
{
SessionOrFallback::MaybeSession {
cookie_jar,
maybe_session,
..
} => (cookie_jar, maybe_session),
SessionOrFallback::Fallback { response } => return Ok(response),
};
let Some(session) = maybe_session else {
// If there is no session, redirect to the login screen, keeping the
@@ -46,7 +46,7 @@ pub(crate) async fn get(
let (session_info, cookie_jar) = cookie_jar.session_info();
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let maybe_session = session_info.load_session(&mut repo).await?;
let maybe_session = session_info.load_active_session(&mut repo).await?;
if maybe_session.is_some() {
// TODO: redirect to continue whatever action was going on
return Ok((cookie_jar, url_builder.redirect(&mas_router::Index)).into_response());
@@ -100,7 +100,7 @@ pub(crate) async fn post(
let (session_info, cookie_jar) = cookie_jar.session_info();
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let maybe_session = session_info.load_session(&mut repo).await?;
let maybe_session = session_info.load_active_session(&mut repo).await?;
if maybe_session.is_some() {
// TODO: redirect to continue whatever action was going on
return Ok((cookie_jar, url_builder.redirect(&mas_router::Index)).into_response());
+2 -2
View File
@@ -56,7 +56,7 @@ pub(crate) async fn get(
let (session_info, cookie_jar) = cookie_jar.session_info();
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let maybe_session = session_info.load_session(&mut repo).await?;
let maybe_session = session_info.load_active_session(&mut repo).await?;
if maybe_session.is_some() {
// TODO: redirect to continue whatever action was going on
return Ok((cookie_jar, url_builder.redirect(&mas_router::Index)).into_response());
@@ -96,7 +96,7 @@ pub(crate) async fn post(
let (session_info, cookie_jar) = cookie_jar.session_info();
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let maybe_session = session_info.load_session(&mut repo).await?;
let maybe_session = session_info.load_active_session(&mut repo).await?;
if maybe_session.is_some() {
// TODO: redirect to continue whatever action was going on
return Ok((cookie_jar, url_builder.redirect(&mas_router::Index)).into_response());
+1 -1
View File
@@ -36,7 +36,7 @@ pub(crate) async fn get(
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info.load_session(&mut repo).await?;
let maybe_session = session_info.load_active_session(&mut repo).await?;
if let Some(session) = maybe_session {
activity_tracker
@@ -81,7 +81,7 @@ pub(crate) async fn get(
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info.load_session(&mut repo).await?;
let maybe_session = session_info.load_active_session(&mut repo).await?;
if maybe_session.is_some() {
let reply = query.action.go_next(&url_builder);
+27
View File
@@ -1590,6 +1590,33 @@ impl TemplateContext for DeviceConsentContext {
}
}
/// Context used by the `account/deactivated.html` and `account/locked.html`
/// templates
#[derive(Serialize)]
pub struct AccountInactiveContext {
user: User,
}
impl AccountInactiveContext {
/// Constructs a new context with an existing linked user
#[must_use]
pub fn new(user: User) -> Self {
Self { user }
}
}
impl TemplateContext for AccountInactiveContext {
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
where
Self: Sized,
{
User::samples(now, rng)
.into_iter()
.map(|user| AccountInactiveContext { user })
.collect()
}
}
/// Context used by the `form_post.html` template
#[derive(Serialize)]
pub struct FormPostContext<T> {
+17 -8
View File
@@ -34,14 +34,14 @@ mod macros;
pub use self::{
context::{
ApiDocContext, AppContext, CompatSsoContext, ConsentContext, DeviceConsentContext,
DeviceLinkContext, DeviceLinkFormField, EmailRecoveryContext, EmailVerificationContext,
EmptyContext, ErrorContext, FormPostContext, IndexContext, LoginContext, LoginFormField,
NotFoundContext, PasswordRegisterContext, PolicyViolationContext, PostAuthContext,
PostAuthContextInner, ReauthContext, ReauthFormField, RecoveryExpiredContext,
RecoveryFinishContext, RecoveryFinishFormField, RecoveryProgressContext,
RecoveryStartContext, RecoveryStartFormField, RegisterContext, RegisterFormField,
RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField,
AccountInactiveContext, ApiDocContext, AppContext, CompatSsoContext, ConsentContext,
DeviceConsentContext, DeviceLinkContext, DeviceLinkFormField, EmailRecoveryContext,
EmailVerificationContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
LoginContext, LoginFormField, NotFoundContext, PasswordRegisterContext,
PolicyViolationContext, PostAuthContext, PostAuthContextInner, ReauthContext,
ReauthFormField, RecoveryExpiredContext, RecoveryFinishContext, RecoveryFinishFormField,
RecoveryProgressContext, RecoveryStartContext, RecoveryStartFormField, RegisterContext,
RegisterFormField, RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField,
RegisterStepsEmailInUseContext, RegisterStepsVerifyEmailContext,
RegisterStepsVerifyEmailFormField, SiteBranding, SiteConfigExt, SiteFeatures,
TemplateContext, UpstreamExistingLinkContext, UpstreamRegister, UpstreamRegisterFormField,
@@ -413,6 +413,15 @@ register_templates! {
/// Render the device code consent page
pub fn render_device_consent(WithLanguage<WithCsrf<WithSession<DeviceConsentContext>>>) { "pages/device_consent.html" }
/// Render the 'account deactivated' page
pub fn render_account_deactivated(WithLanguage<WithCsrf<AccountInactiveContext>>) { "pages/account/deactivated.html" }
/// Render the 'account locked' page
pub fn render_account_locked(WithLanguage<WithCsrf<AccountInactiveContext>>) { "pages/account/locked.html" }
/// Render the 'account logged out' page
pub fn render_account_logged_out(WithLanguage<WithCsrf<AccountInactiveContext>>) { "pages/account/logged_out.html" }
}
impl Templates {
+26
View File
@@ -0,0 +1,26 @@
{#
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
-#}
{% extends "base.html" %}
{% block content %}
<main class="flex flex-col gap-6">
<header class="page-heading">
<div class="icon invalid">
{{ icon.delete() }}
</div>
<div class="header">
<h1 class="title">{{ _("mas.account.deactivated.heading") }}</h1>
{% set mxid = "@" + user.username + ":" + branding.server_name %}
<p class="text">{{ _("mas.account.deactivated.description", mxid=mxid) }}</p>
</div>
{{ logout.button(text=_("action.sign_in"), csrf_token=csrf_token) }}
</header>
</main>
{% endblock %}
+26
View File
@@ -0,0 +1,26 @@
{#
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
-#}
{% extends "base.html" %}
{% block content %}
<main class="flex flex-col gap-6">
<header class="page-heading">
<div class="icon invalid">
{{ icon.block() }}
</div>
<div class="header">
<h1 class="title">{{ _("mas.account.locked.heading") }}</h1>
{% set mxid = "@" + user.username + ":" + branding.server_name %}
<p class="text">{{ _("mas.account.locked.description", mxid=mxid) }}</p>
</div>
{{ logout.button(text=_("action.sign_in"), csrf_token=csrf_token) }}
</header>
</main>
{% endblock %}
+25
View File
@@ -0,0 +1,25 @@
{#
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
-#}
{% extends "base.html" %}
{% block content %}
<main class="flex flex-col gap-6">
<header class="page-heading">
<div class="icon invalid">
{{ icon.leave() }}
</div>
<div class="header">
<h1 class="title">{{ _("mas.account.logged_out.heading") }}</h1>
<p class="text">{{ _("mas.account.logged_out.description") }}</p>
</div>
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token) }}
</header>
</main>
{% endblock %}
+34 -2
View File
@@ -18,11 +18,11 @@
},
"sign_in": "Sign in",
"@sign_in": {
"context": "pages/index.html:30:26-45"
"context": "pages/account/deactivated.html:23:28-47, pages/account/locked.html:23:28-47, pages/index.html:30:26-45"
},
"sign_out": "Sign out",
"@sign_out": {
"context": "pages/consent.html:65:28-48, pages/device_consent.html:135:30-50, pages/index.html:28:28-48, pages/policy_violation.html:38:28-48, pages/sso.html:45:28-48, pages/upstream_oauth2/link_mismatch.html:24:24-44, pages/upstream_oauth2/suggest_link.html:32:26-46"
"context": "pages/account/logged_out.html:22:28-48, pages/consent.html:65:28-48, pages/device_consent.html:135:30-50, pages/index.html:28:28-48, pages/policy_violation.html:38:28-48, pages/sso.html:45:28-48, pages/upstream_oauth2/link_mismatch.html:24:24-44, pages/upstream_oauth2/suggest_link.html:32:26-46"
},
"skip": "Skip",
"@skip": {
@@ -110,6 +110,38 @@
}
},
"mas": {
"account": {
"deactivated": {
"description": "This account (<em>%(mxid)s</em>) has been deleted. If this is not expected, contact your server administrator.",
"@description": {
"context": "pages/account/deactivated.html:20:27-78"
},
"heading": "Account deleted",
"@heading": {
"context": "pages/account/deactivated.html:18:29-65"
}
},
"locked": {
"description": "This account (<em>%(mxid)s</em>) has been locked. If this is not expected, contact your server administrator.",
"@description": {
"context": "pages/account/locked.html:20:27-73"
},
"heading": "Account locked",
"@heading": {
"context": "pages/account/locked.html:18:29-60"
}
},
"logged_out": {
"description": "This session has been terminated. Sign out to be able to log back in",
"@description": {
"context": "pages/account/logged_out.html:19:27-66"
},
"heading": "Session terminated",
"@heading": {
"context": "pages/account/logged_out.html:18:29-64"
}
}
},
"back_to_homepage": "Go back to the homepage",
"@back_to_homepage": {
"context": "pages/404.html:16:29-54"