diff --git a/src/api/client/oauth/mod.rs b/src/api/client/oauth/mod.rs index f8a30ec46..3bf6e92d2 100644 --- a/src/api/client/oauth/mod.rs +++ b/src/api/client/oauth/mod.rs @@ -1,7 +1,3 @@ -mod register_client; -mod server_metadata; -mod token; - use axum::{ Json, Router, extract::State, @@ -11,12 +7,17 @@ use serde_json::json; pub(crate) use server_metadata::*; +mod register_client; +mod server_metadata; +mod token; + const BASE_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/oauth2/"); const AUTH_CODE_PATH: &str = "grant/authorization_code"; const JWKS_URI_PATH: &str = "client/keys.json"; const CLIENT_REGISTER_PATH: &str = "client/register"; const TOKEN_REVOKE_PATH: &str = "client/revoke"; const TOKEN_PATH: &str = "grant/token"; +const ACCOUNT_MANAGEMENT_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/account/deeplink"); pub(crate) fn router() -> Router { Router::new().nest(BASE_PATH, oauth_router()) diff --git a/src/api/client/oauth/server_metadata.rs b/src/api/client/oauth/server_metadata.rs index e3aa1c1bb..ed310dc31 100644 --- a/src/api/client/oauth/server_metadata.rs +++ b/src/api/client/oauth/server_metadata.rs @@ -1,13 +1,19 @@ use axum::extract::State; use conduwuit::Result; -use ruma::{api::client::discovery::get_authorization_server_metadata, serde::Raw}; +use ruma::{ + api::client::discovery::get_authorization_server_metadata::{ + self, v1::AccountManagementAction, + }, + serde::Raw, +}; use serde_json::{Value, json}; use service::Services; use crate::{ Ruma, client::oauth::{ - AUTH_CODE_PATH, CLIENT_REGISTER_PATH, JWKS_URI_PATH, TOKEN_PATH, TOKEN_REVOKE_PATH, + ACCOUNT_MANAGEMENT_PATH, AUTH_CODE_PATH, CLIENT_REGISTER_PATH, JWKS_URI_PATH, TOKEN_PATH, + TOKEN_REVOKE_PATH, }, }; @@ -28,6 +34,15 @@ pub(crate) async fn authorization_server_metadata(services: &Services) -> Value .unwrap(); json!({ + "account_management_uri": endpoint_base.join(ACCOUNT_MANAGEMENT_PATH).unwrap(), + "account_management_actions_supported": [ + AccountManagementAction::AccountDeactivate, + AccountManagementAction::CrossSigningReset, + AccountManagementAction::DeviceDelete, + AccountManagementAction::DeviceView, + AccountManagementAction::DevicesList, + AccountManagementAction::Profile, + ], "authorization_endpoint": endpoint_base.join(AUTH_CODE_PATH).unwrap(), "code_challenge_methods_supported": ["S256"], "grant_types_supported": ["authorization_code", "refresh_token"], diff --git a/src/service/oauth/mod.rs b/src/service/oauth/mod.rs index 17e7c7c83..28cdfea77 100644 --- a/src/service/oauth/mod.rs +++ b/src/service/oauth/mod.rs @@ -314,8 +314,10 @@ pub async fn revoke_token(&self, token: String) -> Result<()> { .deserialized::() { (refresh_token_info.user_id, refresh_token_info.device_id) - } else if let Some(user) = self.services.users.find_from_token(&token).await { - user + } else if let Some((user_id, device_id, _)) = + self.services.users.find_from_token(&token).await + { + (user_id, device_id) } else { return Err!("Invalid token"); }; diff --git a/src/web/pages/account/mod.rs b/src/web/pages/account/mod.rs index 09facd9b5..781893a1d 100644 --- a/src/web/pages/account/mod.rs +++ b/src/web/pages/account/mod.rs @@ -1,13 +1,25 @@ -use axum::{Router, extract::State, response::Response, routing::get}; +use axum::{ + Router, + extract::{Query, State}, + response::Redirect, + routing::get, +}; use conduwuit_core::utils::{IterStream, ReadyExt, stream::TryExpect}; use conduwuit_service::threepid::EmailRequirement; use futures::StreamExt; -use ruma::{OwnedClientSecret, OwnedSessionId}; +use ruma::{ + OwnedClientSecret, OwnedDeviceId, OwnedSessionId, + api::client::discovery::get_authorization_server_metadata::v1::AccountManagementAction, +}; use serde::{Deserialize, Serialize}; use crate::{ WebError, - pages::components::{DeviceCard, DeviceCardStyle, UserCard}, + extract::Expect, + pages::{ + Result, + components::{DeviceCard, DeviceCardStyle, UserCard}, + }, response, session::{LoginTarget, User}, template, @@ -26,6 +38,7 @@ pub(crate) fn build() -> Router { Router::new() .route("/", get(get_account)) + .route("/deeplink", get(get_account_deeplink)) .merge(login::build()) .nest("/password/", password::build()) .nest("/email/", email::build()) @@ -49,10 +62,7 @@ struct Account use "account.html.j2" { } } -async fn get_account( - State(services): State, - user: User, -) -> Result { +async fn get_account(State(services): State, user: User) -> Result { let user_id = user.expect(LoginTarget::Account)?; let email_requirement = services.threepid.email_requirement(); @@ -97,3 +107,41 @@ async fn get_account( response!(Account::new(&services, user_card, email_requirement, email, device_cards)) } + +#[derive(Deserialize)] +struct AccountDeeplinkQuery { + action: Option, + device_id: Option, +} + +async fn get_account_deeplink( + Expect(Query(query)): Expect>, +) -> Result { + let redirect_target = match query.action.unwrap_or(AccountManagementAction::Profile) { + | AccountManagementAction::AccountDeactivate => "deactivate".to_owned(), + | AccountManagementAction::CrossSigningReset => "cross_signing_reset".to_owned(), + | AccountManagementAction::DeviceDelete => { + let Some(device_id) = query.device_id else { + return response!(WebError::BadRequest( + "A device ID is required for this action".to_owned() + )); + }; + + format!("device/{device_id}/delete") + }, + | AccountManagementAction::DeviceView => { + let Some(device_id) = query.device_id else { + return response!(WebError::BadRequest( + "A device ID is required for this action".to_owned() + )); + }; + + format!("device/{device_id}/") + }, + | AccountManagementAction::DevicesList => "#devices".to_owned(), + | AccountManagementAction::Profile => String::new(), + | _ => return response!(WebError::BadRequest("Unknown action".to_owned())), + }; + + response!(Redirect::to(&format!("{}/account/{}", crate::ROUTE_PREFIX, redirect_target))) +} diff --git a/src/web/pages/templates/_components/client_scopes.html.j2 b/src/web/pages/templates/_components/client_scopes.html.j2 index 05df28ff8..bf7df3aec 100644 --- a/src/web/pages/templates/_components/client_scopes.html.j2 +++ b/src/web/pages/templates/_components/client_scopes.html.j2 @@ -2,9 +2,9 @@ {% for scope in scopes %} {% match scope %} {% when Scope::ClientApi %} -
  • Interact with Matrix on your behalf
  • +
  • Send messages and interact with chatrooms on your behalf
  • {% when Scope::Device(_) %} -
  • Connect to your Matrix account
  • +
  • Access your Matrix account
  • {% endmatch %} {% endfor %} diff --git a/src/web/pages/templates/account.html.j2 b/src/web/pages/templates/account.html.j2 index 4e802e0d3..6fe4c8013 100644 --- a/src/web/pages/templates/account.html.j2 +++ b/src/web/pages/templates/account.html.j2 @@ -31,7 +31,7 @@ Your account
    Your devices ({{ devices.len() }}) -
    +
    {% for device in devices %} {{ device }} {% endfor %}