mas_handlers/compat/
logout.rs1use 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 .ok_or(RouteError::InvalidAuthorization)?;
119
120 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}