mas_handlers/compat/
logout.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use std::sync::LazyLock;
8
9use axum::{Json, response::IntoResponse};
10use axum_extra::typed_header::TypedHeader;
11use headers::{Authorization, authorization::Bearer};
12use hyper::StatusCode;
13use mas_axum_utils::sentry::SentryEventID;
14use mas_data_model::TokenType;
15use mas_storage::{
16    BoxClock, BoxRepository, BoxRng, Clock, RepositoryAccess,
17    compat::{CompatAccessTokenRepository, CompatSessionRepository},
18    queue::{QueueJobRepositoryExt as _, SyncDevicesJob},
19};
20use opentelemetry::{Key, KeyValue, metrics::Counter};
21use thiserror::Error;
22
23use super::MatrixError;
24use crate::{BoundActivityTracker, METER, impl_from_error_for_route};
25
26static LOGOUT_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
27    METER
28        .u64_counter("mas.compat.logout_request")
29        .with_description("How many compatibility logout request have happened")
30        .with_unit("{request}")
31        .build()
32});
33const RESULT: Key = Key::from_static_str("result");
34
35#[derive(Error, Debug)]
36pub enum RouteError {
37    #[error(transparent)]
38    Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
39
40    #[error("Missing access token")]
41    MissingAuthorization,
42
43    #[error("Invalid token format")]
44    TokenFormat(#[from] mas_data_model::TokenFormatError),
45
46    #[error("Invalid access token")]
47    InvalidAuthorization,
48}
49
50impl_from_error_for_route!(mas_storage::RepositoryError);
51
52impl IntoResponse for RouteError {
53    fn into_response(self) -> axum::response::Response {
54        let event_id = sentry::capture_error(&self);
55        LOGOUT_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
56        let response = match self {
57            Self::Internal(_) => MatrixError {
58                errcode: "M_UNKNOWN",
59                error: "Internal error",
60                status: StatusCode::INTERNAL_SERVER_ERROR,
61            },
62            Self::MissingAuthorization => MatrixError {
63                errcode: "M_MISSING_TOKEN",
64                error: "Missing access token",
65                status: StatusCode::UNAUTHORIZED,
66            },
67            Self::InvalidAuthorization | Self::TokenFormat(_) => MatrixError {
68                errcode: "M_UNKNOWN_TOKEN",
69                error: "Invalid access token",
70                status: StatusCode::UNAUTHORIZED,
71            },
72        };
73
74        (SentryEventID::from(event_id), response).into_response()
75    }
76}
77
78#[tracing::instrument(name = "handlers.compat.logout.post", skip_all, err)]
79pub(crate) async fn post(
80    clock: BoxClock,
81    mut rng: BoxRng,
82    mut repo: BoxRepository,
83    activity_tracker: BoundActivityTracker,
84    maybe_authorization: Option<TypedHeader<Authorization<Bearer>>>,
85) -> Result<impl IntoResponse, RouteError> {
86    let TypedHeader(authorization) = maybe_authorization.ok_or(RouteError::MissingAuthorization)?;
87
88    let token = authorization.token();
89    let token_type = TokenType::check(token)?;
90
91    if token_type != TokenType::CompatAccessToken {
92        return Err(RouteError::InvalidAuthorization);
93    }
94
95    let token = repo
96        .compat_access_token()
97        .find_by_token(token)
98        .await?
99        .filter(|t| t.is_valid(clock.now()))
100        .ok_or(RouteError::InvalidAuthorization)?;
101
102    let session = repo
103        .compat_session()
104        .lookup(token.session_id)
105        .await?
106        .filter(|s| s.is_valid())
107        .ok_or(RouteError::InvalidAuthorization)?;
108
109    activity_tracker
110        .record_compat_session(&clock, &session)
111        .await;
112
113    let user = repo
114        .user()
115        .lookup(session.user_id)
116        .await?
117        // XXX: this is probably not the right error
118        .ok_or(RouteError::InvalidAuthorization)?;
119
120    // Schedule a job to sync the devices of the user with the homeserver
121    repo.queue_job()
122        .schedule_job(&mut rng, &clock, SyncDevicesJob::new(&user))
123        .await?;
124
125    repo.compat_session().finish(&clock, session).await?;
126
127    repo.save().await?;
128
129    LOGOUT_COUNTER.add(1, &[KeyValue::new(RESULT, "success")]);
130
131    Ok(Json(serde_json::json!({})))
132}