mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
feat: Allow devices to be removed from the account panel
This commit is contained in:
@@ -7,10 +7,16 @@ use axum::{
|
||||
extract::State,
|
||||
routing::method_routing::{get, post},
|
||||
};
|
||||
use const_str::concat;
|
||||
use serde_json::json;
|
||||
pub(crate) use server_metadata::*;
|
||||
|
||||
const BASE_PATH: &str = const_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 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";
|
||||
|
||||
pub(crate) fn router() -> Router<crate::State> {
|
||||
Router::new().nest(BASE_PATH, oauth_router())
|
||||
@@ -24,9 +30,9 @@ pub(crate) fn router() -> Router<crate::State> {
|
||||
|
||||
fn oauth_router() -> Router<crate::State> {
|
||||
Router::new()
|
||||
.route(CLIENT_REGISTER_PATH, post(register_client::register_client_route))
|
||||
.route(concat!("/", CLIENT_REGISTER_PATH), post(register_client::register_client_route))
|
||||
// TODO(unspecced): used by old versions of the matrix-js-sdk
|
||||
.route(JWKS_URI_PATH, get(async || Json(json!({"keys": []}))))
|
||||
.route(TOKEN_PATH, post(token::token_route))
|
||||
.route(TOKEN_REVOKE_PATH, post(token::revoke_token_route))
|
||||
.route(concat!("/", JWKS_URI_PATH), get(async || Json(json!({"keys": []}))))
|
||||
.route(concat!("/", TOKEN_PATH), post(token::token_route))
|
||||
.route(concat!("/", TOKEN_REVOKE_PATH), post(token::revoke_token_route))
|
||||
}
|
||||
|
||||
@@ -4,13 +4,12 @@ use ruma::{api::client::discovery::get_authorization_server_metadata, serde::Raw
|
||||
use serde_json::{Value, json};
|
||||
use service::Services;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
pub(super) const AUTH_CODE_PATH: &str = "grant/authorization_code";
|
||||
pub(super) const JWKS_URI_PATH: &str = "client/keys.json";
|
||||
pub(super) const CLIENT_REGISTER_PATH: &str = "client/register";
|
||||
pub(super) const TOKEN_REVOKE_PATH: &str = "client/revoke";
|
||||
pub(super) const TOKEN_PATH: &str = "grant/token";
|
||||
use crate::{
|
||||
Ruma,
|
||||
client::oauth::{
|
||||
AUTH_CODE_PATH, CLIENT_REGISTER_PATH, JWKS_URI_PATH, TOKEN_PATH, TOKEN_REVOKE_PATH,
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) async fn get_authorization_server_metadata_route(
|
||||
State(services): State<crate::State>,
|
||||
|
||||
+3
-1
@@ -3,7 +3,7 @@ use std::{any::Any, sync::Once, time::Duration};
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
Router,
|
||||
extract::rejection::{FormRejection, QueryRejection},
|
||||
extract::rejection::{FormRejection, PathRejection, QueryRejection},
|
||||
http::{HeaderValue, StatusCode, header},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
@@ -37,6 +37,8 @@ enum WebError {
|
||||
#[error("{0}")]
|
||||
FormRejection(#[from] FormRejection),
|
||||
#[error("{0}")]
|
||||
PathRejection(#[from] PathRejection),
|
||||
#[error("{0}")]
|
||||
BadRequest(String),
|
||||
|
||||
#[error("This page does not exist.")]
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{Path, State},
|
||||
routing::on,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use ruma::OwnedDeviceId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
WebError,
|
||||
extract::{Expect, PostForm},
|
||||
pages::{GET_POST, Result, components::DeviceCard},
|
||||
response,
|
||||
session::{LoginTarget, User},
|
||||
template,
|
||||
};
|
||||
|
||||
pub(crate) fn build() -> Router<crate::State> {
|
||||
Router::new().route("/{device}/remove", on(GET_POST, route_remove_device))
|
||||
}
|
||||
|
||||
template! {
|
||||
struct RemoveDevice use "remove_device.html.j2" {
|
||||
body: RemoveDeviceBody
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum RemoveDeviceBody {
|
||||
Form {
|
||||
device_card: Box<DeviceCard>,
|
||||
last_device: bool,
|
||||
},
|
||||
Success,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub(crate) struct RemoveDevicePath {
|
||||
pub device: OwnedDeviceId,
|
||||
}
|
||||
|
||||
async fn route_remove_device(
|
||||
State(services): State<crate::State>,
|
||||
user: User,
|
||||
Expect(Path(query)): Expect<Path<RemoveDevicePath>>,
|
||||
PostForm(form): PostForm<()>,
|
||||
) -> Result {
|
||||
let user_id = user.expect(LoginTarget::RemoveDevice(query.clone()))?;
|
||||
|
||||
let Ok(device) = services
|
||||
.users
|
||||
.get_device_metadata(&user_id, &query.device)
|
||||
.await
|
||||
else {
|
||||
return response!(WebError::BadRequest("Unknown device".to_owned()));
|
||||
};
|
||||
|
||||
if form.is_some() {
|
||||
services
|
||||
.users
|
||||
.remove_device(&user_id, &device.device_id)
|
||||
.await;
|
||||
|
||||
response!(RemoveDevice::new(&services, RemoveDeviceBody::Success))
|
||||
} else {
|
||||
let device_card = DeviceCard::for_device(&services, &user_id, device, false).await;
|
||||
let last_device = services.users.all_devices_metadata(&user_id).count().await <= 1;
|
||||
|
||||
response!(RemoveDevice::new(&services, RemoveDeviceBody::Form {
|
||||
device_card: Box::new(device_card),
|
||||
last_device
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use axum::{Router, extract::State, response::Response, routing::get};
|
||||
use conduwuit_core::utils::{IterStream, stream::TryExpect};
|
||||
use conduwuit_service::threepid::EmailRequirement;
|
||||
use futures::StreamExt;
|
||||
use ruma::{OwnedClientSecret, OwnedSessionId};
|
||||
@@ -12,11 +13,12 @@ use crate::{
|
||||
template,
|
||||
};
|
||||
|
||||
mod cross_signing_reset;
|
||||
mod deactivate;
|
||||
mod email;
|
||||
mod login;
|
||||
mod password;
|
||||
pub(crate) mod cross_signing_reset;
|
||||
pub(crate) mod deactivate;
|
||||
pub(crate) mod device;
|
||||
pub(crate) mod email;
|
||||
pub(crate) mod login;
|
||||
pub(crate) mod password;
|
||||
|
||||
pub(crate) fn build() -> Router<crate::State> {
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
@@ -29,6 +31,7 @@ pub(crate) fn build() -> Router<crate::State> {
|
||||
.nest("/email/", email::build())
|
||||
.nest("/cross_signing_reset", cross_signing_reset::build())
|
||||
.nest("/deactivate", deactivate::build())
|
||||
.nest("/device/", device::build())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
@@ -64,11 +67,24 @@ async fn get_account(
|
||||
let mut devices: Vec<_> = services
|
||||
.users
|
||||
.all_device_ids(&user_id)
|
||||
.then(async |device_id| DeviceCard::for_device(&services, &user_id, device_id).await)
|
||||
.then(async |device_id| {
|
||||
services
|
||||
.users
|
||||
.get_device_metadata(&user_id, &device_id)
|
||||
.await
|
||||
})
|
||||
.expect_ok()
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
devices.sort_unstable_by(|a, b| a.last_seen_ts.cmp(&b.last_seen_ts).reverse());
|
||||
|
||||
response!(Account::new(&services, user_card, email_requirement, email, devices))
|
||||
let device_cards = devices
|
||||
.into_iter()
|
||||
.stream()
|
||||
.then(async |device| DeviceCard::for_device(&services, &user_id, device, true).await)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
response!(Account::new(&services, user_card, email_requirement, email, device_cards))
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use askama::{Template, filters::HtmlSafe};
|
||||
use base64::Engine;
|
||||
use conduwuit_core::{result::FlatOk, utils};
|
||||
use conduwuit_service::{Services, media::mxc::Mxc, oauth::client_metadata::ClientMetadata};
|
||||
use ruma::{OwnedDeviceId, OwnedUserId, UserId};
|
||||
use ruma::{OwnedDeviceId, OwnedUserId, UserId, api::client::device::Device};
|
||||
|
||||
pub(super) mod form;
|
||||
|
||||
@@ -87,8 +87,8 @@ pub(super) struct DeviceCard {
|
||||
pub display_name: Option<String>,
|
||||
pub avatar: Avatar,
|
||||
pub last_active: String,
|
||||
pub last_seen_ts: Option<u64>,
|
||||
pub oauth_metadata: Option<ClientMetadata>,
|
||||
pub show_actions: bool,
|
||||
}
|
||||
|
||||
impl HtmlSafe for DeviceCard {}
|
||||
@@ -97,18 +97,13 @@ impl DeviceCard {
|
||||
pub(super) async fn for_device(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
device_id: OwnedDeviceId,
|
||||
device: Device,
|
||||
show_actions: bool,
|
||||
) -> Self {
|
||||
let device = services
|
||||
.users
|
||||
.get_device_metadata(user_id, &device_id)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let oauth_metadata = async {
|
||||
let client_id = services
|
||||
.oauth
|
||||
.get_client_id_for_device(user_id, &device_id)
|
||||
.get_client_id_for_device(user_id, &device.device_id)
|
||||
.await?;
|
||||
|
||||
Some(
|
||||
@@ -124,11 +119,7 @@ impl DeviceCard {
|
||||
let display_name = oauth_metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.client_name.clone())
|
||||
.or_else(|| {
|
||||
device
|
||||
.as_ref()
|
||||
.and_then(|device| device.display_name.clone())
|
||||
});
|
||||
.or_else(|| device.display_name.clone());
|
||||
|
||||
let avatar = {
|
||||
let avatar_src = oauth_metadata
|
||||
@@ -153,9 +144,7 @@ impl DeviceCard {
|
||||
Avatar { avatar_type }
|
||||
};
|
||||
|
||||
let last_seen_ts = device.as_ref().and_then(|device| device.last_seen_ts);
|
||||
|
||||
let last_active = last_seen_ts.map_or_else(
|
||||
let last_active = device.last_seen_ts.map_or_else(
|
||||
|| "unknown".to_owned(),
|
||||
|last_seen_ts| {
|
||||
last_seen_ts
|
||||
@@ -169,12 +158,12 @@ impl DeviceCard {
|
||||
);
|
||||
|
||||
Self {
|
||||
device_id,
|
||||
device_id: device.device_id,
|
||||
display_name,
|
||||
avatar,
|
||||
last_active,
|
||||
last_seen_ts: last_seen_ts.map(|last_seen_ts| last_seen_ts.as_secs().into()),
|
||||
oauth_metadata,
|
||||
show_actions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,5 +21,10 @@
|
||||
<p>
|
||||
Last active: {{ last_active }}
|
||||
</p>
|
||||
{% if show_actions %}
|
||||
<p>
|
||||
<a href="device/{{ device_id }}/remove">Remove</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{% extends "_layout.html.j2" %}
|
||||
|
||||
{%- block title -%}
|
||||
Remove a device
|
||||
{%- endblock -%}
|
||||
|
||||
{%- block content -%}
|
||||
<div class="panel">
|
||||
<h1>Remove a device <a class="back" href="{{ crate::ROUTE_PREFIX }}/account/">Back</a></h1>
|
||||
{% match body %}
|
||||
{% when RemoveDeviceBody::Form { device_card, last_device } %}
|
||||
{{ device_card }}
|
||||
<p>
|
||||
Do you want to remove this device from your account? It will be signed out immediately.
|
||||
</p>
|
||||
{% if last_device %}
|
||||
<p>
|
||||
<em class="negative">This is your last device.</em> If you remove it, you will need to use
|
||||
your recovery key to recover your encrypted messages when you log in on a new device.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
<em class="negative">This may be a destructive action.</em> Make sure you have access to
|
||||
another confirmed device, or your recovery key, to avoid losing access to your encrypted messages.
|
||||
</p>
|
||||
{% endif %}
|
||||
<form method="post">
|
||||
<button type="submit">Remove this device</button>
|
||||
</form>
|
||||
{% when RemoveDeviceBody::Success %}
|
||||
<p>
|
||||
The device has been signed out successfully.
|
||||
</p>
|
||||
{% endmatch %}
|
||||
</div>
|
||||
{%- endblock -%}
|
||||
@@ -10,7 +10,7 @@ use ruma::{OwnedUserId, UserId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tower_sessions::Session;
|
||||
|
||||
use crate::{ROUTE_PREFIX, WebError};
|
||||
use crate::{ROUTE_PREFIX, WebError, pages::account::device::RemoveDevicePath};
|
||||
|
||||
pub(crate) mod store;
|
||||
|
||||
@@ -32,6 +32,7 @@ pub(crate) enum LoginTarget {
|
||||
ChangeEmail,
|
||||
CrossSigningReset,
|
||||
Deactivate,
|
||||
RemoveDevice(RemoveDevicePath),
|
||||
}
|
||||
|
||||
impl PartialEq for LoginTarget {
|
||||
@@ -51,6 +52,8 @@ impl LoginTarget {
|
||||
| Self::ChangeEmail => "account/email/change/".into(),
|
||||
| Self::CrossSigningReset => "account/cross_signing_reset".into(),
|
||||
| Self::Deactivate => "account/deactivate".into(),
|
||||
| Self::RemoveDevice(path) =>
|
||||
format!("account/device/{}/remove", path.device,).into(),
|
||||
};
|
||||
|
||||
format!("{ROUTE_PREFIX}/{path}")
|
||||
|
||||
Reference in New Issue
Block a user