From bc3e676332eae0559a8707d90103834d3ef6eb72 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 5 Dec 2023 17:22:28 +0100 Subject: [PATCH] Implement account management discovery as per MSC2965 --- crates/handlers/src/oauth2/discovery.rs | 13 ++++++ crates/router/src/endpoints.rs | 17 +++++++- crates/router/src/url_builder.rs | 6 +++ frontend/src/routing/actions.test.ts | 57 +++++++++++++++++++++++++ frontend/src/routing/actions.ts | 8 +++- 5 files changed, 99 insertions(+), 2 deletions(-) diff --git a/crates/handlers/src/oauth2/discovery.rs b/crates/handlers/src/oauth2/discovery.rs index 80d013014..19f3ef3a7 100644 --- a/crates/handlers/src/oauth2/discovery.rs +++ b/crates/handlers/src/oauth2/discovery.rs @@ -34,6 +34,10 @@ struct DiscoveryResponse { #[serde(rename = "org.matrix.matrix-authentication-service.graphql_endpoint")] graphql_endpoint: url::Url, + + // As per MSC2965 + account_management_uri: url::Url, + account_management_actions_supported: Vec, } #[tracing::instrument(name = "handlers.oauth2.discovery.get", skip_all)] @@ -168,6 +172,15 @@ pub(crate) async fn get( Json(DiscoveryResponse { standard, graphql_endpoint: url_builder.graphql_endpoint(), + account_management_uri: url_builder.account_management_uri(), + // This needs to be kept in sync with what is supported in the frontend, + // see frontend/src/routing/actions.ts + account_management_actions_supported: vec![ + "org.matrix.profile".to_owned(), + "org.matrix.sessions_list".to_owned(), + "org.matrix.session_view".to_owned(), + "org.matrix.session_end".to_owned(), + ], }) } diff --git a/crates/router/src/endpoints.rs b/crates/router/src/endpoints.rs index c6dcde8b0..bb6f43baf 100644 --- a/crates/router/src/endpoints.rs +++ b/crates/router/src/endpoints.rs @@ -429,11 +429,26 @@ impl AccountAddEmail { /// Actions parameters as defined by MSC2965 #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case", tag = "action")] +#[serde(tag = "action")] pub enum AccountAction { + #[serde(rename = "org.matrix.profile")] + OrgMatrixProfile, + #[serde(rename = "profile")] Profile, + + #[serde(rename = "org.matrix.sessions_list")] + OrgMatrixSessionsList, + #[serde(rename = "sessions_list")] SessionsList, + + #[serde(rename = "org.matrix.session_view")] + OrgMatrixSessionView { device_id: String }, + #[serde(rename = "session_view")] SessionView { device_id: String }, + + #[serde(rename = "org.matrix.session_end")] + OrgMatrixSessionEnd { device_id: String }, + #[serde(rename = "session_end")] SessionEnd { device_id: String }, } diff --git a/crates/router/src/url_builder.rs b/crates/router/src/url_builder.rs index 2883461e1..e64768434 100644 --- a/crates/router/src/url_builder.rs +++ b/crates/router/src/url_builder.rs @@ -195,6 +195,12 @@ impl UrlBuilder { pub fn upstream_oauth_authorize(&self, id: Ulid) -> Url { self.absolute_url_for(&crate::endpoints::UpstreamOAuth2Authorize::new(id)) } + + /// Account management URI + #[must_use] + pub fn account_management_uri(&self) -> Url { + self.absolute_url_for(&crate::endpoints::Account::default()) + } } #[cfg(test)] diff --git a/frontend/src/routing/actions.test.ts b/frontend/src/routing/actions.test.ts index b6956031b..e64aa7656 100644 --- a/frontend/src/routing/actions.test.ts +++ b/frontend/src/routing/actions.test.ts @@ -95,4 +95,61 @@ describe("getRouteActionRedirection()", () => { searchParams: new URLSearchParams(), }); }); + + it("redirects to session detail when location has a action=org.matrix.session_end", () => { + const searchParams = new URLSearchParams(); + searchParams.set("action", "org.matrix.session_end"); + searchParams.set("device_id", "test-device-id"); + searchParams.set("something_else", "should-remain"); + expect( + getRouteActionRedirection({ pathname: "/account/", searchParams }), + ).toEqual({ + route: { + type: "session", + id: "test-device-id", + }, + searchParams: new URLSearchParams("?something_else=should-remain"), + }); + }); + + it("redirects to session detail when location has a action=org.matrix.session_view", () => { + const searchParams = new URLSearchParams(); + searchParams.set("action", "org.matrix.session_view"); + searchParams.set("device_id", "test-device-id"); + expect( + getRouteActionRedirection({ pathname: "/account/", searchParams }), + ).toEqual({ + route: { + type: "session", + id: "test-device-id", + }, + searchParams: new URLSearchParams(), + }); + }); + + it("redirects to sessions overview when location has a action=org.matrix.sessions_list", () => { + const searchParams = new URLSearchParams(); + searchParams.set("action", "org.matrix.sessions_list"); + expect( + getRouteActionRedirection({ pathname: "/account/", searchParams }), + ).toEqual({ + route: { + type: "sessions-overview", + }, + searchParams: new URLSearchParams(), + }); + }); + + it("redirects to profile when location has a action=org.matrix.profile", () => { + const searchParams = new URLSearchParams(); + searchParams.set("action", "org.matrix.profile"); + expect( + getRouteActionRedirection({ pathname: "/account/", searchParams }), + ).toEqual({ + route: { + type: "profile", + }, + searchParams: new URLSearchParams(), + }); + }); }); diff --git a/frontend/src/routing/actions.ts b/frontend/src/routing/actions.ts index 81a97fa60..834c0e71d 100644 --- a/frontend/src/routing/actions.ts +++ b/frontend/src/routing/actions.ts @@ -32,11 +32,17 @@ export const getRouteActionRedirection = ( } => { // Clone the search params so we can modify them const searchParams = new URLSearchParams(location.searchParams?.toString()); - const action = searchParams.get("action"); + let action = searchParams.get("action"); const deviceId = searchParams.get("device_id"); searchParams.delete("action"); searchParams.delete("device_id"); + // Actions are actually prefixed with org.matrix. in the latest version of MSC2965 + // but we still want to support non-prefixed actions for backwards compatibility + if (action) { + action = action.replace(/^org.matrix./, ""); + } + let route: Route; switch (action) { case RouteAction.EndSession: