mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-05-12 22:14:54 +00:00
feat: Add support for account management deeplinks
This commit is contained in:
@@ -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<crate::State> {
|
||||
Router::new().nest(BASE_PATH, oauth_router())
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -314,8 +314,10 @@ pub async fn revoke_token(&self, token: String) -> Result<()> {
|
||||
.deserialized::<RefreshTokenInfo>()
|
||||
{
|
||||
(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");
|
||||
};
|
||||
|
||||
@@ -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<crate::State> {
|
||||
|
||||
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<crate::State>,
|
||||
user: User,
|
||||
) -> Result<Response, WebError> {
|
||||
async fn get_account(State(services): State<crate::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<AccountManagementAction>,
|
||||
device_id: Option<OwnedDeviceId>,
|
||||
}
|
||||
|
||||
async fn get_account_deeplink(
|
||||
Expect(Query(query)): Expect<Query<AccountDeeplinkQuery>>,
|
||||
) -> 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)))
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
{% for scope in scopes %}
|
||||
{% match scope %}
|
||||
{% when Scope::ClientApi %}
|
||||
<li>Interact with Matrix on your behalf</li>
|
||||
<li>Send messages and interact with chatrooms on your behalf</li>
|
||||
{% when Scope::Device(_) %}
|
||||
<li>Connect to your Matrix account</li>
|
||||
<li>Access your Matrix account</li>
|
||||
{% endmatch %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -31,7 +31,7 @@ Your account
|
||||
<section>
|
||||
<details>
|
||||
<summary>Your devices ({{ devices.len() }})</summary>
|
||||
<div class="card-list">
|
||||
<div class="card-list" id="devices"car>
|
||||
{% for device in devices %}
|
||||
{{ device }}
|
||||
{% endfor %}
|
||||
|
||||
Reference in New Issue
Block a user