matrix_sdk/authentication/oauth/
account_management_url.rs

1// Copyright 2025 Kévin Commaille
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Types and functions related to the account management URL.
16//!
17//! This is a Matrix extension introduced in [MSC4191](https://github.com/matrix-org/matrix-spec-proposals/pull/4191).
18
19use ruma::{
20    api::client::discovery::get_authorization_server_metadata::msc2965::AccountManagementAction,
21    OwnedDeviceId,
22};
23use url::Url;
24
25/// An account management action that a user can take, including a device ID for
26/// the actions that support it.
27///
28/// The actions are defined in [MSC4191].
29///
30/// [MSC4191]: https://github.com/matrix-org/matrix-spec-proposals/pull/4191
31#[derive(Debug, Clone, PartialEq, Eq)]
32#[non_exhaustive]
33pub enum AccountManagementActionFull {
34    /// `org.matrix.profile`
35    ///
36    /// The user wishes to view their profile (name, avatar, contact details).
37    Profile,
38
39    /// `org.matrix.sessions_list`
40    ///
41    /// The user wishes to view a list of their sessions.
42    SessionsList,
43
44    /// `org.matrix.session_view`
45    ///
46    /// The user wishes to view the details of a specific session.
47    SessionView {
48        /// The ID of the session to view the details of.
49        device_id: OwnedDeviceId,
50    },
51
52    /// `org.matrix.session_end`
53    ///
54    /// The user wishes to end/log out of a specific session.
55    SessionEnd {
56        /// The ID of the session to end.
57        device_id: OwnedDeviceId,
58    },
59
60    /// `org.matrix.account_deactivate`
61    ///
62    /// The user wishes to deactivate their account.
63    AccountDeactivate,
64
65    /// `org.matrix.cross_signing_reset`
66    ///
67    /// The user wishes to reset their cross-signing keys.
68    CrossSigningReset,
69}
70
71impl AccountManagementActionFull {
72    /// Get the [`AccountManagementAction`] matching this
73    /// [`AccountManagementActionFull`].
74    pub fn action_type(&self) -> AccountManagementAction {
75        match self {
76            Self::Profile => AccountManagementAction::Profile,
77            Self::SessionsList => AccountManagementAction::SessionsList,
78            Self::SessionView { .. } => AccountManagementAction::SessionView,
79            Self::SessionEnd { .. } => AccountManagementAction::SessionEnd,
80            Self::AccountDeactivate => AccountManagementAction::AccountDeactivate,
81            Self::CrossSigningReset => AccountManagementAction::CrossSigningReset,
82        }
83    }
84
85    /// Append this action to the query of the given URL.
86    fn append_to_url(&self, url: &mut Url) {
87        let mut query_pairs = url.query_pairs_mut();
88        query_pairs.append_pair("action", self.action_type().as_str());
89
90        match self {
91            Self::SessionView { device_id } | Self::SessionEnd { device_id } => {
92                query_pairs.append_pair("device_id", device_id.as_str());
93            }
94            _ => {}
95        }
96    }
97}
98
99/// Builder for the URL for accessing the account management capabilities, as
100/// defined in [MSC4191].
101///
102/// This type can be instantiated with [`OAuth::account_management_url()`] and
103/// [`OAuth::fetch_account_management_url()`].
104///
105/// [`AccountManagementUrlBuilder::build()`] returns a URL to be opened in a web
106/// browser where the end-user will be able to access the account management
107/// capabilities of the issuer.
108///
109/// # Example
110///
111/// ```no_run
112/// use matrix_sdk::authentication::oauth::AccountManagementActionFull;
113/// # _ = async {
114/// # let client: matrix_sdk::Client = unimplemented!();
115/// let oauth = client.oauth();
116///
117/// // Get the account management URL from the server metadata.
118/// let Some(url_builder) = oauth.account_management_url().await? else {
119///     println!("The server doesn't advertise an account management URL");
120///     return Ok(());
121/// };
122///
123/// // The user wants to see the list of sessions.
124/// let url =
125///     url_builder.action(AccountManagementActionFull::SessionsList).build();
126///
127/// println!("See your sessions at: {url}");
128/// # anyhow::Ok(()) };
129/// ```
130///
131/// [MSC4191]: https://github.com/matrix-org/matrix-spec-proposals/pull/4191
132/// [`OAuth::account_management_url()`]: super::OAuth::account_management_url
133/// [`OAuth::fetch_account_management_url()`]: super::OAuth::fetch_account_management_url
134#[derive(Debug, Clone)]
135pub struct AccountManagementUrlBuilder {
136    account_management_uri: Url,
137    action: Option<AccountManagementActionFull>,
138}
139
140impl AccountManagementUrlBuilder {
141    /// Construct an [`AccountManagementUrlBuilder`] for the given URL.
142    pub(super) fn new(account_management_uri: Url) -> Self {
143        Self { account_management_uri, action: None }
144    }
145
146    /// Set the action that the user wishes to take.
147    pub fn action(mut self, action: AccountManagementActionFull) -> Self {
148        self.action = Some(action);
149        self
150    }
151
152    /// Build the URL to present to the end user.
153    pub fn build(self) -> Url {
154        // Add our parameters to the query, because the URL might already have one.
155        let mut account_management_uri = self.account_management_uri;
156
157        if let Some(action) = &self.action {
158            action.append_to_url(&mut account_management_uri);
159        }
160
161        account_management_uri
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use ruma::owned_device_id;
168    use url::Url;
169
170    use super::{AccountManagementActionFull, AccountManagementUrlBuilder};
171
172    #[test]
173    fn test_build_account_management_url_actions() {
174        let base_url = Url::parse("https://example.org").unwrap();
175        let device_id = owned_device_id!("ABCDEFG");
176
177        let url = AccountManagementUrlBuilder::new(base_url.clone()).build();
178        assert_eq!(url, base_url);
179
180        let url = AccountManagementUrlBuilder::new(base_url.clone())
181            .action(AccountManagementActionFull::Profile)
182            .build();
183        assert_eq!(url.as_str(), "https://example.org/?action=org.matrix.profile");
184
185        let url = AccountManagementUrlBuilder::new(base_url.clone())
186            .action(AccountManagementActionFull::SessionsList)
187            .build();
188        assert_eq!(url.as_str(), "https://example.org/?action=org.matrix.sessions_list");
189
190        let url = AccountManagementUrlBuilder::new(base_url.clone())
191            .action(AccountManagementActionFull::SessionView { device_id: device_id.clone() })
192            .build();
193        assert_eq!(
194            url.as_str(),
195            "https://example.org/?action=org.matrix.session_view&device_id=ABCDEFG"
196        );
197
198        let url = AccountManagementUrlBuilder::new(base_url.clone())
199            .action(AccountManagementActionFull::SessionEnd { device_id })
200            .build();
201        assert_eq!(
202            url.as_str(),
203            "https://example.org/?action=org.matrix.session_end&device_id=ABCDEFG"
204        );
205
206        let url = AccountManagementUrlBuilder::new(base_url.clone())
207            .action(AccountManagementActionFull::AccountDeactivate)
208            .build();
209        assert_eq!(url.as_str(), "https://example.org/?action=org.matrix.account_deactivate");
210
211        let url = AccountManagementUrlBuilder::new(base_url)
212            .action(AccountManagementActionFull::CrossSigningReset)
213            .build();
214        assert_eq!(url.as_str(), "https://example.org/?action=org.matrix.cross_signing_reset");
215    }
216
217    #[test]
218    fn test_build_account_management_url_with_query() {
219        let base_url = Url::parse("https://example.org/?sid=123456").unwrap();
220
221        let url = AccountManagementUrlBuilder::new(base_url.clone())
222            .action(AccountManagementActionFull::Profile)
223            .build();
224        assert_eq!(url.as_str(), "https://example.org/?sid=123456&action=org.matrix.profile");
225
226        let url = AccountManagementUrlBuilder::new(base_url)
227            .action(AccountManagementActionFull::SessionView {
228                device_id: owned_device_id!("ABCDEFG"),
229            })
230            .build();
231        assert_eq!(
232            url.as_str(),
233            "https://example.org/?sid=123456&action=org.matrix.session_view&device_id=ABCDEFG"
234        );
235    }
236}