mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-11-13 12:16:54 +00:00
feat(admin): List users endpoint
This commit is contained in:
14 files changed
+204
-10
No files matched your search
@@ -659,6 +659,9 @@ default-features = false
|
||||
[workspace.dependencies.ruminuwuity]
|
||||
package = "ruminuwuity"
|
||||
path = "src/ruminuwuity"
|
||||
features = [
|
||||
"unstable-msc4484"
|
||||
]
|
||||
|
||||
###############################################################################
|
||||
#
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod rooms;
|
||||
pub mod users;
|
||||
@@ -6,7 +6,7 @@ use ruminuwuity::admin::continuwuity::rooms;
|
||||
|
||||
use crate::{Ruma, client::leave_room};
|
||||
|
||||
/// # `PUT /_continuwuity/admin/rooms/{roomID}/ban`
|
||||
/// # `PUT /_continuwuity/admin/v1/rooms/{roomID}/ban`
|
||||
///
|
||||
/// Bans or unbans a room.
|
||||
pub(crate) async fn ban_room(
|
||||
|
||||
@@ -6,7 +6,7 @@ use ruminuwuity::admin::continuwuity::rooms;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET /_continuwuity/admin/rooms/list`
|
||||
/// # `GET /_continuwuity/admin/v1/rooms/list`
|
||||
///
|
||||
/// Lists all rooms known to this server, excluding banned ones.
|
||||
pub(crate) async fn list_rooms(
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err,
|
||||
utils::{TryFutureExtExt, stream::WidebandExt},
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use ruminuwuity::admin::continuwuity::users::list::v1::{QueryFilter, Request, Response, User};
|
||||
use service::users::AccountStatus;
|
||||
|
||||
use crate::router::Ruma;
|
||||
|
||||
/// # `PUT /_continuwuity/admin/v1/users`
|
||||
///
|
||||
/// Lists users on this homeserver
|
||||
pub(crate) async fn list_users(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<Request>,
|
||||
) -> conduwuit::Result<Response> {
|
||||
if !services
|
||||
.users
|
||||
.is_admin(body.identity.expect_sender_user()?)
|
||||
.await
|
||||
{
|
||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||
}
|
||||
|
||||
let limit = body.limit.unwrap_or(100);
|
||||
let offset = limit.saturating_mul(body.page.unwrap_or_default());
|
||||
let users = services
|
||||
.users
|
||||
.stream_local_users()
|
||||
.wide_filter_map(|user_id| {
|
||||
let body = &body;
|
||||
async move {
|
||||
let status = services.users.status(&user_id).await;
|
||||
assert_ne!(
|
||||
status,
|
||||
AccountStatus::NotFound,
|
||||
"Not found users should not appear in stream_local_users"
|
||||
);
|
||||
let (deactivated, locked, suspended, admin) = tokio::join!(
|
||||
std::future::ready(status == AccountStatus::Deactivated),
|
||||
services.users.is_locked(&user_id).unwrap_or_default(),
|
||||
services.users.is_suspended(&user_id).unwrap_or_default(),
|
||||
services.users.is_admin(&user_id),
|
||||
);
|
||||
// Exclude deactivated accounts by default
|
||||
if !deactivated && body.active == Some(QueryFilter::Exclude) {
|
||||
return None;
|
||||
}
|
||||
if deactivated
|
||||
&& body
|
||||
.deactivated
|
||||
.as_ref()
|
||||
.is_none_or(|d| *d == QueryFilter::Include)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// Exclude locked accounts by default
|
||||
if locked && body.locked != Some(QueryFilter::Include) {
|
||||
return None;
|
||||
}
|
||||
// Include suspended accounts by default
|
||||
if suspended && body.suspended == Some(QueryFilter::Exclude) {
|
||||
return None;
|
||||
}
|
||||
// Include admins by default
|
||||
if admin && body.admin == Some(QueryFilter::Exclude) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((user_id, deactivated, suspended, locked, admin))
|
||||
}
|
||||
})
|
||||
.skip(offset)
|
||||
.wide_then(|(user_id, deactivated, suspended, locked, admin)| async move {
|
||||
let (displayname, avatar_url) = tokio::join!(
|
||||
services.users.displayname(&user_id).ok(),
|
||||
services.users.avatar_url(&user_id).ok(),
|
||||
);
|
||||
let user = User {
|
||||
displayname,
|
||||
avatar_url,
|
||||
suspended,
|
||||
locked,
|
||||
deactivated,
|
||||
admin,
|
||||
};
|
||||
|
||||
(user_id, user)
|
||||
})
|
||||
.take(limit)
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
.await;
|
||||
|
||||
Ok(Response::new(users))
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod list;
|
||||
+2
-1
@@ -197,7 +197,8 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
|
||||
.merge(client::oauth::router(state))
|
||||
.route("/_continuwuity/server_version", get(client::continuwuity_server_version))
|
||||
.ruma_route(&admin::rooms::ban::ban_room)
|
||||
.ruma_route(&admin::rooms::list::list_rooms);
|
||||
.ruma_route(&admin::rooms::list::list_rooms)
|
||||
.ruma_route(&admin::users::list::list_users);
|
||||
|
||||
if config.allow_federation {
|
||||
router = router
|
||||
|
||||
@@ -18,6 +18,7 @@ server = []
|
||||
unstable-exhaustive-types = []
|
||||
unstable-msc3202 = []
|
||||
unstable-msc4203 = []
|
||||
unstable-msc4484 = ["ruma/unstable-msc4484"]
|
||||
|
||||
[dependencies]
|
||||
assign.workspace = true
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod rooms;
|
||||
pub mod users;
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod v1 {
|
||||
use ruma::{
|
||||
OwnedRoomAliasId, OwnedRoomId, OwnedUserId,
|
||||
api::{auth_scheme::AccessToken, request, response},
|
||||
api::{OAuthClientScope, auth_scheme::AccessToken, request, response},
|
||||
metadata,
|
||||
};
|
||||
|
||||
@@ -10,8 +10,9 @@ pub mod v1 {
|
||||
rate_limited: false,
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
1.0 => "/_continuwuity/admin/rooms/{room_id}/ban",
|
||||
}
|
||||
1.0 => "/_continuwuity/admin/v1/rooms/{room_id}/ban",
|
||||
},
|
||||
required_client_scopes: [OAuthClientScope::ServerAdministration]
|
||||
}
|
||||
|
||||
#[request]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod v1 {
|
||||
use ruma::{
|
||||
OwnedRoomId,
|
||||
api::{auth_scheme::AccessToken, request, response},
|
||||
api::{OAuthClientScope, auth_scheme::AccessToken, request, response},
|
||||
metadata,
|
||||
};
|
||||
|
||||
@@ -10,8 +10,9 @@ pub mod v1 {
|
||||
rate_limited: false,
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
1.0 => "/_continuwuity/admin/rooms/list",
|
||||
}
|
||||
1.0 => "/_continuwuity/admin/v1/rooms/list",
|
||||
},
|
||||
required_client_scopes: [OAuthClientScope::ServerAdministration]
|
||||
}
|
||||
|
||||
#[request]
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
pub mod v1 {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::{
|
||||
OwnedMxcUri, OwnedUserId,
|
||||
api::{OAuthClientScope, auth_scheme::AccessToken, request, response},
|
||||
metadata,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
metadata! {
|
||||
method: GET,
|
||||
rate_limited: false,
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
1.0 => "/_continuwuity/admin/v1/users",
|
||||
},
|
||||
required_client_scopes: [OAuthClientScope::ServerAdministration]
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Copy)]
|
||||
pub enum QueryFilter {
|
||||
/// Always include entities matching this criteria.
|
||||
#[serde(rename = "include")]
|
||||
Include,
|
||||
/// Never include entities matching this criteria.
|
||||
#[serde(rename = "exclude")]
|
||||
Exclude,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[request]
|
||||
#[serde(default)]
|
||||
pub struct Request {
|
||||
#[ruma_api(query)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active: Option<QueryFilter>,
|
||||
#[ruma_api(query)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deactivated: Option<QueryFilter>,
|
||||
#[ruma_api(query)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub suspended: Option<QueryFilter>,
|
||||
#[ruma_api(query)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub locked: Option<QueryFilter>,
|
||||
#[ruma_api(query)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub admin: Option<QueryFilter>,
|
||||
#[ruma_api(query)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub limit: Option<usize>,
|
||||
#[ruma_api(query)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub page: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct User {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub displayname: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub avatar_url: Option<OwnedMxcUri>,
|
||||
#[serde(skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub suspended: bool,
|
||||
#[serde(skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub locked: bool,
|
||||
#[serde(skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub deactivated: bool,
|
||||
#[serde(skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub admin: bool,
|
||||
}
|
||||
|
||||
#[response]
|
||||
pub struct Response {
|
||||
#[ruma_api(body)]
|
||||
pub users: BTreeMap<OwnedUserId, User>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
#[must_use]
|
||||
pub fn new(users: BTreeMap<OwnedUserId, User>) -> Self { Self { users } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod list;
|
||||
@@ -37,7 +37,7 @@ pub enum AccessTokenStatus {
|
||||
}
|
||||
|
||||
/// The status of a user account.
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, PartialEq, Debug, Eq)]
|
||||
pub enum AccountStatus {
|
||||
NotFound,
|
||||
Active,
|
||||
|
||||
Reference in new issue
Block a user