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.
1415//! 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).
1819use ruma::{
20 api::client::discovery::get_authorization_server_metadata::msc2965::AccountManagementAction,
21 OwnedDeviceId,
22};
23use url::Url;
2425/// 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).
37Profile,
3839/// `org.matrix.sessions_list`
40 ///
41 /// The user wishes to view a list of their sessions.
42SessionsList,
4344/// `org.matrix.session_view`
45 ///
46 /// The user wishes to view the details of a specific session.
47SessionView {
48/// The ID of the session to view the details of.
49device_id: OwnedDeviceId,
50 },
5152/// `org.matrix.session_end`
53 ///
54 /// The user wishes to end/log out of a specific session.
55SessionEnd {
56/// The ID of the session to end.
57device_id: OwnedDeviceId,
58 },
5960/// `org.matrix.account_deactivate`
61 ///
62 /// The user wishes to deactivate their account.
63AccountDeactivate,
6465/// `org.matrix.cross_signing_reset`
66 ///
67 /// The user wishes to reset their cross-signing keys.
68CrossSigningReset,
69}
7071impl AccountManagementActionFull {
72/// Get the [`AccountManagementAction`] matching this
73 /// [`AccountManagementActionFull`].
74pub fn action_type(&self) -> AccountManagementAction {
75match self {
76Self::Profile => AccountManagementAction::Profile,
77Self::SessionsList => AccountManagementAction::SessionsList,
78Self::SessionView { .. } => AccountManagementAction::SessionView,
79Self::SessionEnd { .. } => AccountManagementAction::SessionEnd,
80Self::AccountDeactivate => AccountManagementAction::AccountDeactivate,
81Self::CrossSigningReset => AccountManagementAction::CrossSigningReset,
82 }
83 }
8485/// Append this action to the query of the given URL.
86fn append_to_url(&self, url: &mut Url) {
87let mut query_pairs = url.query_pairs_mut();
88 query_pairs.append_pair("action", self.action_type().as_str());
8990match self {
91Self::SessionView { device_id } | Self::SessionEnd { device_id } => {
92 query_pairs.append_pair("device_id", device_id.as_str());
93 }
94_ => {}
95 }
96 }
97}
9899/// 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}
139140impl AccountManagementUrlBuilder {
141/// Construct an [`AccountManagementUrlBuilder`] for the given URL.
142pub(super) fn new(account_management_uri: Url) -> Self {
143Self { account_management_uri, action: None }
144 }
145146/// Set the action that the user wishes to take.
147pub fn action(mut self, action: AccountManagementActionFull) -> Self {
148self.action = Some(action);
149self
150}
151152/// Build the URL to present to the end user.
153pub fn build(self) -> Url {
154// Add our parameters to the query, because the URL might already have one.
155let mut account_management_uri = self.account_management_uri;
156157if let Some(action) = &self.action {
158 action.append_to_url(&mut account_management_uri);
159 }
160161 account_management_uri
162 }
163}
164165#[cfg(test)]
166mod tests {
167use ruma::owned_device_id;
168use url::Url;
169170use super::{AccountManagementActionFull, AccountManagementUrlBuilder};
171172#[test]
173fn test_build_account_management_url_actions() {
174let base_url = Url::parse("https://example.org").unwrap();
175let device_id = owned_device_id!("ABCDEFG");
176177let url = AccountManagementUrlBuilder::new(base_url.clone()).build();
178assert_eq!(url, base_url);
179180let url = AccountManagementUrlBuilder::new(base_url.clone())
181 .action(AccountManagementActionFull::Profile)
182 .build();
183assert_eq!(url.as_str(), "https://example.org/?action=org.matrix.profile");
184185let url = AccountManagementUrlBuilder::new(base_url.clone())
186 .action(AccountManagementActionFull::SessionsList)
187 .build();
188assert_eq!(url.as_str(), "https://example.org/?action=org.matrix.sessions_list");
189190let url = AccountManagementUrlBuilder::new(base_url.clone())
191 .action(AccountManagementActionFull::SessionView { device_id: device_id.clone() })
192 .build();
193assert_eq!(
194 url.as_str(),
195"https://example.org/?action=org.matrix.session_view&device_id=ABCDEFG"
196);
197198let url = AccountManagementUrlBuilder::new(base_url.clone())
199 .action(AccountManagementActionFull::SessionEnd { device_id })
200 .build();
201assert_eq!(
202 url.as_str(),
203"https://example.org/?action=org.matrix.session_end&device_id=ABCDEFG"
204);
205206let url = AccountManagementUrlBuilder::new(base_url.clone())
207 .action(AccountManagementActionFull::AccountDeactivate)
208 .build();
209assert_eq!(url.as_str(), "https://example.org/?action=org.matrix.account_deactivate");
210211let url = AccountManagementUrlBuilder::new(base_url)
212 .action(AccountManagementActionFull::CrossSigningReset)
213 .build();
214assert_eq!(url.as_str(), "https://example.org/?action=org.matrix.cross_signing_reset");
215 }
216217#[test]
218fn test_build_account_management_url_with_query() {
219let base_url = Url::parse("https://example.org/?sid=123456").unwrap();
220221let url = AccountManagementUrlBuilder::new(base_url.clone())
222 .action(AccountManagementActionFull::Profile)
223 .build();
224assert_eq!(url.as_str(), "https://example.org/?sid=123456&action=org.matrix.profile");
225226let url = AccountManagementUrlBuilder::new(base_url)
227 .action(AccountManagementActionFull::SessionView {
228 device_id: owned_device_id!("ABCDEFG"),
229 })
230 .build();
231assert_eq!(
232 url.as_str(),
233"https://example.org/?sid=123456&action=org.matrix.session_view&device_id=ABCDEFG"
234);
235 }
236}