mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +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::{
|
use axum::{
|
||||||
Json, Router,
|
Json, Router,
|
||||||
extract::State,
|
extract::State,
|
||||||
@@ -11,12 +7,17 @@ use const_str::concat;
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
pub(crate) use server_metadata::*;
|
pub(crate) use server_metadata::*;
|
||||||
|
|
||||||
|
mod register_client;
|
||||||
|
mod server_metadata;
|
||||||
|
mod token;
|
||||||
|
|
||||||
const BASE_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/oauth2/");
|
const BASE_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/oauth2/");
|
||||||
const AUTH_CODE_PATH: &str = "grant/authorization_code";
|
const AUTH_CODE_PATH: &str = "grant/authorization_code";
|
||||||
const JWKS_URI_PATH: &str = "client/keys.json";
|
const JWKS_URI_PATH: &str = "client/keys.json";
|
||||||
const CLIENT_REGISTER_PATH: &str = "client/register";
|
const CLIENT_REGISTER_PATH: &str = "client/register";
|
||||||
const TOKEN_REVOKE_PATH: &str = "client/revoke";
|
const TOKEN_REVOKE_PATH: &str = "client/revoke";
|
||||||
const TOKEN_PATH: &str = "grant/token";
|
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> {
|
pub(crate) fn router() -> Router<crate::State> {
|
||||||
Router::new().nest(BASE_PATH, oauth_router())
|
Router::new().nest(BASE_PATH, oauth_router())
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use conduwuit::Result;
|
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 serde_json::{Value, json};
|
||||||
use service::Services;
|
use service::Services;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Ruma,
|
Ruma,
|
||||||
client::oauth::{
|
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();
|
.unwrap();
|
||||||
|
|
||||||
json!({
|
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(),
|
"authorization_endpoint": endpoint_base.join(AUTH_CODE_PATH).unwrap(),
|
||||||
"code_challenge_methods_supported": ["S256"],
|
"code_challenge_methods_supported": ["S256"],
|
||||||
"grant_types_supported": ["authorization_code", "refresh_token"],
|
"grant_types_supported": ["authorization_code", "refresh_token"],
|
||||||
|
|||||||
@@ -314,8 +314,10 @@ impl Service {
|
|||||||
.deserialized::<RefreshTokenInfo>()
|
.deserialized::<RefreshTokenInfo>()
|
||||||
{
|
{
|
||||||
(refresh_token_info.user_id, refresh_token_info.device_id)
|
(refresh_token_info.user_id, refresh_token_info.device_id)
|
||||||
} else if let Some(user) = self.services.users.find_from_token(&token).await {
|
} else if let Some((user_id, device_id, _)) =
|
||||||
user
|
self.services.users.find_from_token(&token).await
|
||||||
|
{
|
||||||
|
(user_id, device_id)
|
||||||
} else {
|
} else {
|
||||||
return Err!("Invalid token");
|
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_core::utils::{IterStream, ReadyExt, stream::TryExpect};
|
||||||
use conduwuit_service::threepid::EmailRequirement;
|
use conduwuit_service::threepid::EmailRequirement;
|
||||||
use futures::StreamExt;
|
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 serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
WebError,
|
WebError,
|
||||||
pages::components::{DeviceCard, DeviceCardStyle, UserCard},
|
extract::Expect,
|
||||||
|
pages::{
|
||||||
|
Result,
|
||||||
|
components::{DeviceCard, DeviceCardStyle, UserCard},
|
||||||
|
},
|
||||||
response,
|
response,
|
||||||
session::{LoginTarget, User},
|
session::{LoginTarget, User},
|
||||||
template,
|
template,
|
||||||
@@ -26,6 +38,7 @@ pub(crate) fn build() -> Router<crate::State> {
|
|||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(get_account))
|
.route("/", get(get_account))
|
||||||
|
.route("/deeplink", get(get_account_deeplink))
|
||||||
.merge(login::build())
|
.merge(login::build())
|
||||||
.nest("/password/", password::build())
|
.nest("/password/", password::build())
|
||||||
.nest("/email/", email::build())
|
.nest("/email/", email::build())
|
||||||
@@ -49,10 +62,7 @@ template! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_account(
|
async fn get_account(State(services): State<crate::State>, user: User) -> Result {
|
||||||
State(services): State<crate::State>,
|
|
||||||
user: User,
|
|
||||||
) -> Result<Response, WebError> {
|
|
||||||
let user_id = user.expect(LoginTarget::Account)?;
|
let user_id = user.expect(LoginTarget::Account)?;
|
||||||
|
|
||||||
let email_requirement = services.threepid.email_requirement();
|
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))
|
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 %}
|
{% for scope in scopes %}
|
||||||
{% match scope %}
|
{% match scope %}
|
||||||
{% when Scope::ClientApi %}
|
{% 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(_) %}
|
{% when Scope::Device(_) %}
|
||||||
<li>Connect to your Matrix account</li>
|
<li>Access your Matrix account</li>
|
||||||
{% endmatch %}
|
{% endmatch %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Your account
|
|||||||
<section>
|
<section>
|
||||||
<details>
|
<details>
|
||||||
<summary>Your devices ({{ devices.len() }})</summary>
|
<summary>Your devices ({{ devices.len() }})</summary>
|
||||||
<div class="card-list">
|
<div class="card-list" id="devices"car>
|
||||||
{% for device in devices %}
|
{% for device in devices %}
|
||||||
{{ device }}
|
{{ device }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
Reference in New Issue
Block a user