feat: Add support for MSC4484

This commit is contained in:
Ginger authored and Ellis Git committed 2026-10-05 14:22:53 +00:00
1 parent 3a20ad7ff2
commit 823ee9f6e1
11 files changed
+190 -76

No files matched your search

Generated
+11 -11
View File
@@ -4947,7 +4947,7 @@ dependencies = [
[[package]]
name = "ruma"
version = "0.16.0"
source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
dependencies = [
"assign",
"js_int",
@@ -4966,7 +4966,7 @@ dependencies = [
[[package]]
name = "ruma-appservice-api"
version = "0.16.0"
source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
dependencies = [
"http",
"js_int",
@@ -4979,7 +4979,7 @@ dependencies = [
[[package]]
name = "ruma-client-api"
version = "0.24.0"
source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
dependencies = [
"as_variant",
"assign",
@@ -5001,7 +5001,7 @@ dependencies = [
[[package]]
name = "ruma-common"
version = "0.19.0"
source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
dependencies = [
"as_variant",
"base64 0.22.1",
@@ -5034,7 +5034,7 @@ dependencies = [
[[package]]
name = "ruma-events"
version = "0.34.0"
source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
dependencies = [
"as_variant",
"indexmap 2.14.2",
@@ -5055,7 +5055,7 @@ dependencies = [
[[package]]
name = "ruma-federation-api"
version = "0.15.0"
source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
dependencies = [
"bytes",
"headers",
@@ -5078,7 +5078,7 @@ dependencies = [
[[package]]
name = "ruma-identifiers-validation"
version = "0.12.1"
source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
dependencies = [
"js_int",
"thiserror 2.0.21",
@@ -5087,7 +5087,7 @@ dependencies = [
[[package]]
name = "ruma-macros"
version = "0.19.0"
source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
dependencies = [
"as_variant",
"cfg-if",
@@ -5103,7 +5103,7 @@ dependencies = [
[[package]]
name = "ruma-push-gateway-api"
version = "0.15.0"
source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
dependencies = [
"js_int",
"ruma-common",
@@ -5115,7 +5115,7 @@ dependencies = [
[[package]]
name = "ruma-signatures"
version = "0.21.0"
source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
dependencies = [
"base64 0.22.1",
"ed25519-dalek 3.0.0",
@@ -5132,7 +5132,7 @@ dependencies = [
[[package]]
name = "ruma-state-res"
version = "0.17.0"
source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
dependencies = [
"js_int",
"ruma-common",
+1 -1
View File
@@ -348,7 +348,7 @@ version = "1.1.1"
# Used for matrix spec type definitions and helpers
[workspace.dependencies.ruma]
git = "https://github.com/ruma/ruma.git"
rev = "e927699d374114843f9f41ee133a5ced08449229"
rev = "fe8570dbca23f4263165748ed898aad126b23d85"
features = [
"appservice-api-c",
"client-api",
+57 -14
View File
@@ -1,4 +1,7 @@
use std::any::{Any, TypeId};
use std::{
any::{Any, TypeId},
collections::BTreeSet,
};
use conduwuit::{
Err, Error, Result, err,
@@ -9,7 +12,7 @@ use ruma::{
DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedServerName, OwnedUserId, UInt,
UserId,
api::{
IncomingRequest,
IncomingRequest, OAuthClientScope,
auth_scheme::{
AccessToken, AccessTokenOptional, AppserviceToken, AppserviceTokenOptional,
AuthScheme, NoAccessToken, NoAuthentication,
@@ -84,7 +87,7 @@ impl ClientIdentity {
pub(crate) trait CheckAuth: AuthScheme {
type Identity: Send;
fn check<R: IncomingRequest + Any>(
fn check<R: IncomingRequest<Authentication = Self> + Any>(
services: &Services,
incoming_request: &hyper::Request<&[u8]>,
authentication: Self::Output,
@@ -95,7 +98,7 @@ pub(crate) trait CheckAuth: AuthScheme {
impl CheckAuth for ServerSignatures {
type Identity = OwnedServerName;
async fn check<R: IncomingRequest + Any>(
async fn check<R: IncomingRequest<Authentication = Self> + Any>(
services: &Services,
incoming_request: &hyper::Request<&[u8]>,
authentication: Self::Output,
@@ -155,29 +158,42 @@ impl CheckAuth for ServerSignatures {
impl CheckAuth for AccessToken {
type Identity = ClientIdentity;
async fn check<R: IncomingRequest + Any>(
async fn check<R: IncomingRequest<Authentication = Self> + Any>(
services: &Services,
_incoming_request: &hyper::Request<&[u8]>,
authentication: Self::Output,
query: AuthQueryParams,
) -> Result<Self::Identity> {
check_access_token(services, &authentication, query, TypeId::of::<R>()).await
check_access_token(
services,
&authentication,
query,
TypeId::of::<R>(),
R::required_client_scopes(),
)
.await
}
}
impl CheckAuth for AccessTokenOptional {
type Identity = Option<ClientIdentity>;
async fn check<R: IncomingRequest + Any>(
async fn check<R: IncomingRequest<Authentication = Self> + Any>(
services: &Services,
_incoming_request: &hyper::Request<&[u8]>,
authentication: Self::Output,
query: AuthQueryParams,
) -> Result<Self::Identity> {
if let Some(authentication) = authentication {
check_access_token(services, &authentication, query, TypeId::of::<R>())
.await
.map(Some)
check_access_token(
services,
&authentication,
query,
TypeId::of::<R>(),
R::required_client_scopes(),
)
.await
.map(Some)
} else {
Ok(None)
}
@@ -200,7 +216,7 @@ impl CheckAuth for AppserviceToken {
impl CheckAuth for AppserviceTokenOptional {
type Identity = Option<RegistrationInfo>;
async fn check<R: IncomingRequest + Any>(
async fn check<R: IncomingRequest<Authentication = Self> + Any>(
services: &Services,
_incoming_request: &hyper::Request<&[u8]>,
authentication: Self::Output,
@@ -219,7 +235,7 @@ impl CheckAuth for AppserviceTokenOptional {
impl CheckAuth for NoAuthentication {
type Identity = ();
fn check<R: IncomingRequest + Any>(
fn check<R: IncomingRequest<Authentication = Self> + Any>(
_services: &Services,
_incoming_request: &hyper::Request<&[u8]>,
_authentication: Self::Output,
@@ -232,7 +248,7 @@ impl CheckAuth for NoAuthentication {
impl CheckAuth for NoAccessToken {
type Identity = Option<ClientIdentity>;
async fn check<R: IncomingRequest + Any>(
async fn check<R: IncomingRequest<Authentication = Self> + Any>(
services: &Services,
incoming_request: &hyper::Request<&[u8]>,
_authentication: Self::Output,
@@ -245,7 +261,7 @@ impl CheckAuth for NoAccessToken {
})?;
if let Some(authentication) = authentication {
check_access_token(services, &authentication, query, TypeId::of::<R>())
check_access_token(services, &authentication, query, TypeId::of::<R>(), &[])
.await
.map(Some)
} else {
@@ -259,6 +275,7 @@ async fn check_access_token(
token: &str,
query: AuthQueryParams,
route: TypeId,
required_scopes: &[OAuthClientScope],
) -> Result<ClientIdentity> {
if token.is_empty() {
return Err!(Request(Unauthorized("Empty access token.")));
@@ -292,6 +309,32 @@ async fn check_access_token(
}
}
// Make sure the user has the right scopes to use the route
let user_scopes = if let Some(session_info) = services
.oauth
.get_session_info_for_device(&sender_user, &sender_device)
.await
{
session_info.scopes
} else {
let mut scopes = BTreeSet::from_iter([OAuthClientScope::ApiFullAccess]);
if services.admin.user_is_admin(&sender_user).await {
scopes.insert(OAuthClientScope::ServerAdministration);
}
scopes
};
if !required_scopes
.iter()
.any(|scope| user_scopes.contains(scope))
{
return Err!(Request(Forbidden(
"You do not have permission to access this endpoint."
)));
}
Ok(ClientIdentity::User { sender_user, sender_device })
} else if let Ok(appservice_info) = services.appservice.find_from_token(token).await {
let Ok(sender_user) = query.user_id.clone().map_or_else(
+2
View File
@@ -48,5 +48,7 @@ pub fn unstable_features() -> BTreeMap<String, bool> {
("org.matrix.msc4174".to_owned(), true),
// Membership-based invite blocking (https://github.com/matrix-org/matrix-spec-proposals/pull/4494)
("uk.timedout.msc4494".to_owned(), true),
// server administration OAuth scope (https://github.com/matrix-org/matrix-spec-proposals/pull/4484)
("org.continuwuity.msc4484.unstable".to_owned(), true),
])
}
+1 -1
View File
@@ -147,7 +147,7 @@ pub enum ResponseType {
/// Deserialize a BTreeSet from a sequence, skipping items which fail to
/// deserialize. This is used as a deserialize helper for ClientMetadata to
/// ignore unknown enum variants in a few fields.
fn btreeset_skip_err<'de, D, V>(de: D) -> Result<BTreeSet<V>, D::Error>
pub(super) fn btreeset_skip_err<'de, D, V>(de: D) -> Result<BTreeSet<V>, D::Error>
where
D: Deserializer<'de>,
V: Deserialize<'de> + Hash + Eq + Ord,
+40 -24
View File
@@ -1,14 +1,14 @@
use std::{
borrow::Cow,
collections::BTreeSet,
collections::HashSet,
error::Error,
fmt::{Debug, Display},
hash::Hash,
mem::discriminant,
};
use regex::Regex;
use ruma::OwnedDeviceId;
use regex::regex;
use ruma::{OwnedDeviceId, api::OAuthClientScope};
use serde::{Deserialize, Serialize};
use url::Url;
@@ -82,30 +82,42 @@ pub enum Prompt {
Unknown,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialOrd, Ord)]
pub enum Scope {
#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum RequestedScope {
Device(OwnedDeviceId),
ClientApi,
ServerAdministration,
}
impl PartialEq for Scope {
impl RequestedScope {
#[must_use]
pub fn as_client_scope(&self) -> Option<OAuthClientScope> {
match self {
| Self::ClientApi => Some(OAuthClientScope::ApiFullAccess),
| Self::Device(_) => None,
| Self::ServerAdministration => Some(OAuthClientScope::ServerAdministration),
}
}
}
impl PartialEq for RequestedScope {
fn eq(&self, other: &Self) -> bool { discriminant(self) == discriminant(other) }
}
impl Eq for Scope {}
impl Eq for RequestedScope {}
impl Hash for Scope {
impl Hash for RequestedScope {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { discriminant(self).hash(state); }
}
impl Display for Scope {
impl Display for RequestedScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let urn = match self {
| Self::ClientApi => "urn:matrix:client:api:*".to_owned(),
| Self::Device(device_id) => format!("urn:matrix:client:device:{device_id}"),
};
f.write_str(&urn)
match self {
| Self::ClientApi => write!(f, "urn:matrix:client:api:*"),
| Self::Device(device_id) => write!(f, "urn:matrix:client:device:{device_id}"),
| Self::ServerAdministration =>
write!(f, "urn:matrix:client:cc.c10y.msc4484.server_administration"),
}
}
}
@@ -113,22 +125,26 @@ impl Display for Scope {
pub struct RawScopes(String);
impl RawScopes {
pub fn to_scopes(&self) -> Result<BTreeSet<Scope>, String> {
#[allow(clippy::trivial_regex)]
pub fn to_scopes(&self) -> Result<HashSet<RequestedScope>, String> {
let client_api_token_regex =
Regex::new(r"urn:matrix:(client|org.matrix.msc2967.client):api:\*").unwrap();
let device_token_regex = Regex::new(
r"urn:matrix:(client|org.matrix.msc2967.client):device:([a-zA-Z0-9-._~]{5,})",
)
.unwrap();
regex!(r"urn:matrix:(client|org.matrix.msc2967.client):api:\*");
let device_token_regex =
regex!(r"urn:matrix:(client|org.matrix.msc2967.client):device:([a-zA-Z0-9-._~]{5,})");
let server_administration_regex =
regex!(r"urn:matrix:client:cc.c10y.msc4484.server_administration");
let mut scopes = BTreeSet::new();
let mut scopes = HashSet::new();
for token in self.0.split(' ') {
let scope_was_new = {
if client_api_token_regex.is_match(token) {
scopes.insert(Scope::ClientApi)
scopes.insert(RequestedScope::ClientApi)
} else if server_administration_regex.is_match(token) {
scopes.insert(RequestedScope::ServerAdministration)
} else if let Some(captures) = device_token_regex.captures(token) {
scopes.insert(Scope::Device(captures.get(2).unwrap().as_str().into()))
scopes
.insert(RequestedScope::Device(captures.get(2).unwrap().as_str().into()))
} else if token == "openid" {
// TODO(unspecced): Element sets this scope but doesn't use
// it for anything
+39 -12
View File
@@ -1,5 +1,5 @@
use std::{
collections::{BTreeSet, HashMap},
collections::{BTreeSet, HashMap, HashSet},
sync::{Arc, Mutex},
time::{Duration, SystemTime},
};
@@ -13,18 +13,18 @@ use database::{Deserialized, Json, Map};
use itertools::Itertools;
use lru_cache::LruCache;
use rand::distr::{Distribution, slice::Choose};
use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId};
use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId, api::OAuthClientScope};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{
Dep, config,
Dep, admin, config,
oauth::{
client_metadata::{ApplicationType, ClientMetadata, GrantType, ResponseType},
grant::{
AuthorizationCodeQuery, AuthorizationCodeResponse, CodeChallengeMethod,
DeviceCodeRequest, DeviceCodeResponse, ErrorCode, OAuthError, ResponseMode, Scope,
TokenRequest, TokenRequestType, TokenResponse, TokenType,
DeviceCodeRequest, DeviceCodeResponse, ErrorCode, OAuthError, RequestedScope,
ResponseMode, TokenRequest, TokenRequestType, TokenResponse, TokenType,
},
},
users::{self, DeviceToken},
@@ -48,6 +48,7 @@ struct Data {
}
struct Services {
admin: Dep<admin::Service>,
users: Dep<users::Service>,
config: Dep<config::Service>,
}
@@ -55,7 +56,9 @@ struct Services {
#[derive(Debug, Deserialize, Serialize)]
pub struct SessionInfo {
pub client_id: String,
pub scopes: BTreeSet<Scope>,
// Ignore unknown scopes, old databases might have a device scope in here
#[serde(deserialize_with = "client_metadata::btreeset_skip_err")]
pub scopes: BTreeSet<OAuthClientScope>,
current_refresh_token: String,
}
@@ -68,7 +71,7 @@ struct RefreshTokenInfo {
struct PendingAuthCodeGrant {
authorizing_user: OwnedUserId,
requested_scopes: BTreeSet<Scope>,
requested_scopes: HashSet<RequestedScope>,
client_name: Option<String>,
expected_client_id: String,
expected_redirect_uri: Url,
@@ -92,7 +95,7 @@ impl PendingAuthCodeGrant {
struct PendingDeviceCodeGrant {
state: DeviceCodeGrantState,
requested_scopes: BTreeSet<Scope>,
requested_scopes: HashSet<RequestedScope>,
client_name: Option<String>,
client_id: String,
requested_at: SystemTime,
@@ -124,7 +127,7 @@ impl PendingDeviceCodeGrant {
pub struct DeviceCodeGrantInfo {
pub device_code: String,
pub client_metadata: ClientMetadata,
pub requested_scopes: BTreeSet<Scope>,
pub requested_scopes: HashSet<RequestedScope>,
}
/// A time-limited grant for a client to perform some sensitive action.
@@ -148,6 +151,7 @@ impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
services: Services {
admin: args.depend::<admin::Service>("admin"),
users: args.depend::<users::Service>("users"),
config: args.depend::<config::Service>("config"),
},
@@ -540,17 +544,20 @@ impl Service {
async fn create_session(
&self,
authorizing_user: OwnedUserId,
requested_scopes: BTreeSet<Scope>,
requested_scopes: HashSet<RequestedScope>,
client_name: Option<String>,
client_id: String,
) -> Result<TokenResponse, OAuthError> {
self.check_requested_scopes(&authorizing_user, &requested_scopes)
.await?;
let access_token = DeviceToken::new_random().with_max_age(Self::ACCESS_TOKEN_MAX_AGE);
let refresh_token = Self::generate_token();
let device_id = requested_scopes
.iter()
.find_map(|scope| {
if let Scope::Device(device_id) = scope {
if let RequestedScope::Device(device_id) = scope {
Some(device_id.to_owned())
} else {
None
@@ -599,7 +606,10 @@ impl Service {
Json(SessionInfo {
client_id: client_id.clone(),
current_refresh_token: refresh_token.clone(),
scopes: requested_scopes.clone(),
scopes: requested_scopes
.iter()
.filter_map(RequestedScope::as_client_scope)
.collect(),
}),
);
@@ -700,6 +710,23 @@ impl Service {
}
}
async fn check_requested_scopes(
&self,
authorizing_user: &UserId,
requested_scopes: &HashSet<RequestedScope>,
) -> Result<(), OAuthError> {
if requested_scopes.contains(&RequestedScope::ServerAdministration)
&& !self.services.admin.user_is_admin(authorizing_user).await
{
return Err(OAuthError::new(
ErrorCode::AccessDenied,
"You are not a server administrator.".to_owned(),
));
}
Ok(())
}
/// Issue a ticket for `localpart` to perform some action.
pub fn issue_ticket(&self, localpart: String, ticket: OAuthTicket) {
self.tickets
+5 -5
View File
@@ -2,11 +2,11 @@ use std::{collections::BTreeSet, time::SystemTime};
use askama::{Template, filters::HtmlSafe};
use conduwuit_core::utils;
use conduwuit_service::{
Services,
oauth::{client_metadata::ClientMetadata, grant::Scope},
use conduwuit_service::{Services, oauth::client_metadata::ClientMetadata};
use ruma::{
OwnedDeviceId, OwnedUserId, UserId,
api::{OAuthClientScope, client::device::Device},
};
use ruma::{OwnedDeviceId, OwnedUserId, UserId, api::client::device::Device};
pub(super) mod form;
@@ -202,7 +202,7 @@ impl DeviceCard {
#[derive(Debug, Template)]
#[template(path = "_components/client_scopes.html.j2")]
pub(super) struct ClientScopes {
pub scopes: BTreeSet<Scope>,
pub scopes: BTreeSet<OAuthClientScope>,
}
impl HtmlSafe for ClientScopes {}
+16 -4
View File
@@ -6,9 +6,9 @@ use axum::{
};
use conduwuit_service::oauth::{
client_metadata::ClientMetadata,
grant::{AuthorizationCodeQuery, DeviceCodeVerifyQuery, Prompt},
grant::{AuthorizationCodeQuery, DeviceCodeVerifyQuery, Prompt, RequestedScope},
};
use ruma::OwnedUserId;
use ruma::{OwnedUserId, api::OAuthClientScope};
use serde::{Deserialize, de::IgnoredAny};
use crate::{
@@ -102,7 +102,13 @@ async fn route_authorization_code(
return Err(WebError::BadRequest("Invalid client ID".to_owned()));
};
let scopes = query.scope.to_scopes().map_err(WebError::BadRequest)?;
let scopes = query
.scope
.to_scopes()
.map_err(WebError::BadRequest)?
.iter()
.filter_map(RequestedScope::as_client_scope)
.collect();
let user_avatar = Avatar::for_local_user(&services, &user_id).await;
@@ -183,6 +189,12 @@ async fn route_device_code(
));
};
let scopes = grant_info
.requested_scopes
.iter()
.filter_map(RequestedScope::as_client_scope)
.collect();
let user_avatar = Avatar::for_local_user(&services, &user_id).await;
response!(Grant::new(
@@ -196,7 +208,7 @@ async fn route_device_code(
user_id,
user_avatar,
grant_info.client_metadata,
ClientScopes { scopes: grant_info.requested_scopes },
ClientScopes { scopes },
Some(grant_info.device_code),
))
},
@@ -1,10 +1,12 @@
<ul>
{% for scope in scopes %}
{% match scope %}
{% when Scope::ClientApi %}
<li>Send messages and interact with chatrooms on your behalf</li>
{% when Scope::Device(_) %}
<li>Access your Matrix account</li>
{% when OAuthClientScope::ApiFullAccess %}
<li>Access and control all features of your Matrix account</li>
{% when OAuthClientScope::ServerAdministration %}
<li><em class="danger">Access your homeserver's administrator tools</em></li>
{% when _ %}
<li>missingno</li>
{% endmatch %}
{% endfor %}
</ul>
+12
View File
@@ -21,14 +21,26 @@ Authorize client
</div>
{{ Avatar::for_client(client_metadata) }}
</div>
<div class="identity">
Signed in as <code>{{ user_id }}</code>. <a href="{{ crate::ROUTE_PREFIX }}/account/logout?{{ logout_query }}">Switch accounts</a>
</div>
<p>
<b>{{ client_name }}</b> (<a href="{{ client_metadata.client_uri }}">{{ client_metadata.client_uri.domain().unwrap() }}</a>) would like
your permission to:
{{ scopes }}
</p>
{% if scopes.scopes.contains(&OAuthClientScope::ServerAdministration) %}
<p class="card danger">
⚠️ {{ client_name }} wants to be able to administrate your homeserver. If it is untrustworthy,
it could use its administrator powers to cause severe damage to your homeserver's users and rooms.
Make sure you trust it fully before you continue.
</p>
{% endif %}
{% match (&client_metadata.policy_uri, &client_metadata.tos_uri) %}
{% when (Some(policy_uri), Some(tos_uri)) %}
<p>