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 axum::{Json, response::IntoResponse};
8use axum_extra::typed_header::TypedHeader;
9use headers::{Authorization, authorization::Bearer};
10use hyper::StatusCode;
11use mas_axum_utils::sentry::SentryEventID;
12use mas_data_model::TokenType;
13use mas_storage::{
14    BoxClock, BoxRepository, BoxRng, Clock, RepositoryAccess,
15    compat::{CompatAccessTokenRepository, CompatSessionRepository},
16    queue::{QueueJobRepositoryExt as _, SyncDevicesJob},
17};
18use thiserror::Error;
19
20use super::MatrixError;
21use crate::{BoundActivityTracker, impl_from_error_for_route};
22
23#[derive(Error, Debug)]
24pub enum RouteError {
25    #[error(transparent)]
26    Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
27
28    #[error("Missing access token")]
29    MissingAuthorization,
30
31    #[error("Invalid token format")]
32    TokenFormat(#[from] mas_data_model::TokenFormatError),
33
34    #[error("Invalid access token")]
35    InvalidAuthorization,
36}
37
38impl_from_error_for_route!(mas_storage::RepositoryError);
39
40impl IntoResponse for RouteError {
41    fn into_response(self) -> axum::response::Response {
42        let event_id = sentry::capture_error(&self);
43        let response = match self {
44            Self::Internal(_) => MatrixError {
45                errcode: "M_UNKNOWN",
46                error: "Internal error",
47                status: StatusCode::INTERNAL_SERVER_ERROR,
48            },
49            Self::MissingAuthorization => MatrixError {
50                errcode: "M_MISSING_TOKEN",
51                error: "Missing access token",
52                status: StatusCode::UNAUTHORIZED,
53            },
54            Self::InvalidAuthorization | Self::TokenFormat(_) => MatrixError {
55                errcode: "M_UNKNOWN_TOKEN",
56                error: "Invalid access token",
57                status: StatusCode::UNAUTHORIZED,
58            },
59        };
60
61        (SentryEventID::from(event_id), response).into_response()
62    }
63}
64
65#[tracing::instrument(name = "handlers.compat.logout.post", skip_all, err)]
66pub(crate) async fn post(
67    clock: BoxClock,
68    mut rng: BoxRng,
69    mut repo: BoxRepository,
70    activity_tracker: BoundActivityTracker,
71    maybe_authorization: Option<TypedHeader<Authorization<Bearer>>>,
72) -> Result<impl IntoResponse, RouteError> {
73    let TypedHeader(authorization) = maybe_authorization.ok_or(RouteError::MissingAuthorization)?;
74
75    let token = authorization.token();
76    let token_type = TokenType::check(token)?;
77
78    if token_type != TokenType::CompatAccessToken {
79        return Err(RouteError::InvalidAuthorization);
80    }
81
82    let token = repo
83        .compat_access_token()
84        .find_by_token(token)
85        .await?
86        .filter(|t| t.is_valid(clock.now()))
87        .ok_or(RouteError::InvalidAuthorization)?;
88
89    let session = repo
90        .compat_session()
91        .lookup(token.session_id)
92        .await?
93        .filter(|s| s.is_valid())
94        .ok_or(RouteError::InvalidAuthorization)?;
95
96    activity_tracker
97        .record_compat_session(&clock, &session)
98        .await;
99
100    let user = repo
101        .user()
102        .lookup(session.user_id)
103        .await?
104        // XXX: this is probably not the right error
105        .ok_or(RouteError::InvalidAuthorization)?;
106
107    // Schedule a job to sync the devices of the user with the homeserver
108    repo.queue_job()
109        .schedule_job(&mut rng, &clock, SyncDevicesJob::new(&user))
110        .await?;
111
112    repo.compat_session().finish(&clock, session).await?;
113
114    repo.save().await?;
115
116    Ok(Json(serde_json::json!({})))
117}