feat: Implement a web-based account management dashboard

This commit is contained in:
Ginger
2026-04-27 16:47:08 -04:00
parent 02948960fa
commit 6b0b8344d4
72 changed files with 2554 additions and 677 deletions
+11 -10
View File
@@ -5,35 +5,36 @@ use serde::{Deserialize, Deserializer, Serialize};
use url::Url;
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ClientMetadata {
#[serde(default)]
application_type: ApplicationType,
pub application_type: ApplicationType,
#[serde(default, skip_serializing_if = "Option::is_none")]
client_name: Option<String>,
pub client_name: Option<String>,
client_uri: Url,
pub client_uri: Url,
#[serde(default, deserialize_with = "btreeset_skip_err")]
grant_types: BTreeSet<GrantType>,
pub grant_types: BTreeSet<GrantType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
logo_uri: Option<Url>,
pub logo_uri: Option<Url>,
#[serde(default, skip_serializing_if = "Option::is_none")]
policy_uri: Option<Url>,
pub policy_uri: Option<Url>,
#[serde(default)]
redirect_uris: Vec<Url>,
pub redirect_uris: Vec<Url>,
#[serde(default, deserialize_with = "btreeset_skip_err")]
response_types: BTreeSet<ResponseType>,
pub response_types: BTreeSet<ResponseType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
token_endpoint_auth_method: Option<String>,
pub token_endpoint_auth_method: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
tos_uri: Option<Url>,
pub tos_uri: Option<Url>,
}
impl ClientMetadata {
+54 -2
View File
@@ -1,8 +1,13 @@
use std::sync::Arc;
use std::{
collections::HashMap,
sync::{Arc, Mutex},
time::{Duration, SystemTime},
};
use base64::Engine;
use conduwuit::{Result, utils::hash::sha256};
use database::{Deserialized, Json, Map};
use ruma::{DeviceId, OwnedUserId, UserId};
use crate::{Dep, config, oauth::client_metadata::ClientMetadata};
@@ -11,6 +16,7 @@ pub mod client_metadata;
pub struct Service {
services: Services,
db: Data,
tickets: Mutex<HashMap<String, HashMap<OAuthTicket, SystemTime>>>,
}
struct Data {
@@ -21,6 +27,22 @@ struct Services {
config: Dep<config::Service>,
}
/// A time-limited grant for a client to perform some sensitive action.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum OAuthTicket {
CrossSigningReset,
}
impl OAuthTicket {
const MAX_AGE: Duration = Duration::from_mins(10);
pub fn ticket_issue_path(&self) -> &'static str {
match self {
| Self::CrossSigningReset => "/account/cross_signing_reset",
}
}
}
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
@@ -30,6 +52,7 @@ impl crate::Service for Service {
db: Data {
clientid_clientmetadata: args.db["clientid_clientmetadata"].clone(),
},
tickets: Mutex::default(),
}))
}
@@ -61,7 +84,7 @@ impl Service {
Ok(client_id)
}
async fn get_client_registration(&self, client_id: &str) -> Option<ClientMetadata> {
pub async fn get_client_registration(&self, client_id: &str) -> Option<ClientMetadata> {
self.db
.clientid_clientmetadata
.get(client_id)
@@ -69,4 +92,33 @@ impl Service {
.deserialized()
.ok()
}
pub async fn get_client_id_for_device(&self, _device_id: &DeviceId) -> Option<String> {
None // TODO
}
/// Issue a ticket for `localpart` to perform some action.
pub fn issue_ticket(&self, localpart: String, ticket: OAuthTicket) {
self.tickets
.lock()
.expect("should be able to lock tickets")
.entry(localpart)
.or_default()
.insert(ticket, SystemTime::now());
}
/// Try to consume an unexpired ticket for `localpart`.
pub fn try_consume_ticket(&self, localpart: &str, ticket: OAuthTicket) -> bool {
let now = SystemTime::now();
self.tickets
.lock()
.expect("should be able to lock tickets")
.get_mut(localpart)
.and_then(|tickets| tickets.remove(&ticket))
.is_some_and(|issued| {
now.duration_since(issued)
.is_ok_and(|duration| duration < OAuthTicket::MAX_AGE)
})
}
}