mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
feat: Define routes for listing and creating users
This commit is contained in:
@@ -62,6 +62,8 @@ zstd_compression = [
|
||||
"reqwest/zstd",
|
||||
]
|
||||
|
||||
admin_api = []
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
axum-client-ip.workspace = true
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod rooms;
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod ban;
|
||||
pub mod list;
|
||||
@@ -1,4 +1,5 @@
|
||||
mod lock;
|
||||
pub(crate) mod site;
|
||||
mod suspend;
|
||||
|
||||
pub(crate) use self::{lock::*, suspend::*};
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod rooms;
|
||||
pub(crate) mod users;
|
||||
@@ -1,12 +1,12 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result, info, utils::ReadyExt, warn};
|
||||
use conduwuit::{info, utils::ReadyExt, warn, Err, Result};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use ruma::{OwnedRoomAliasId, events::room::message::RoomMessageEventContent};
|
||||
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomAliasId};
|
||||
use ruminuwuity::admin::continuwuity::rooms;
|
||||
|
||||
use crate::{Ruma, client::leave_room};
|
||||
use crate::{client::leave_room, Ruma};
|
||||
|
||||
/// # `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,5 @@
|
||||
mod ban;
|
||||
mod list;
|
||||
|
||||
pub(crate) use ban::ban_room;
|
||||
pub(crate) use list::list_rooms;
|
||||
@@ -0,0 +1,42 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::Err;
|
||||
use futures::StreamExt;
|
||||
use ruminuwuity::admin::continuwuity::users;
|
||||
use tokio::join;
|
||||
|
||||
use crate::router::Ruma;
|
||||
|
||||
/// # `GET /_continuwuity/admin/v1/users`
|
||||
///
|
||||
/// Lists all users on this homeserver.
|
||||
pub(crate) async fn list_users_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<users::list::v1::Request>,
|
||||
) -> conduwuit::Result<users::list::v1::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
|
||||
if !services.users.is_admin(sender_user).await {
|
||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||
}
|
||||
|
||||
let mut users = Vec::new();
|
||||
while let Some(user_id) = services.users.list_local_users().next().await {
|
||||
let (deactivated, suspended, locked, admin, login_disabled) = join!(
|
||||
services.users.is_deactivated(&user_id),
|
||||
services.users.is_suspended(&user_id),
|
||||
services.users.is_locked(&user_id),
|
||||
services.users.is_admin(&user_id),
|
||||
services.users.is_login_disabled(&user_id),
|
||||
);
|
||||
users.push(users::list::v1::User {
|
||||
user_id: user_id.clone(),
|
||||
deactivated: deactivated.unwrap_or_default(),
|
||||
suspended: suspended.unwrap_or_default(),
|
||||
locked: locked.unwrap_or_default(),
|
||||
admin,
|
||||
login_disabled,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(users::list::v1::Response::new(users))
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod list;
|
||||
|
||||
pub(crate) use list::list_users_route;
|
||||
@@ -11,8 +11,6 @@ pub mod client;
|
||||
pub mod router;
|
||||
pub mod server;
|
||||
|
||||
pub mod admin;
|
||||
|
||||
pub(crate) use self::router::{Ruma, RumaResponse, State};
|
||||
|
||||
conduwuit::mod_ctor! {}
|
||||
|
||||
+14
-6
@@ -6,17 +6,19 @@ mod response;
|
||||
use std::str::FromStr;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
response::{IntoResponse, Redirect},
|
||||
routing::{any, get, post},
|
||||
Router,
|
||||
};
|
||||
use conduwuit::err;
|
||||
pub(super) use conduwuit_service::state::State;
|
||||
use http::{Uri, uri};
|
||||
use http::{uri, Uri};
|
||||
|
||||
use self::handler::RouterExt;
|
||||
pub(super) use self::{args::Args as Ruma, auth::ClientIdentity, response::RumaResponse};
|
||||
use crate::{admin, client, server};
|
||||
#[cfg(feature = "admin_api")]
|
||||
use crate::client::admin::site as admin_api;
|
||||
use crate::{client, server};
|
||||
|
||||
pub fn build(router: Router<State>, state: State) -> Router<State> {
|
||||
let config = &state.server.config;
|
||||
@@ -191,9 +193,7 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
|
||||
.ruma_route(&client::get_authorization_server_metadata_route)
|
||||
.merge(client::oauth::router(state))
|
||||
.route("/_conduwuit/server_version", get(client::conduwuit_server_version))
|
||||
.route("/_continuwuity/server_version", get(client::conduwuit_server_version))
|
||||
.ruma_route(&admin::rooms::ban::ban_room)
|
||||
.ruma_route(&admin::rooms::list::list_rooms);
|
||||
.route("/_continuwuity/server_version", get(client::conduwuit_server_version));
|
||||
|
||||
if config.allow_federation {
|
||||
router = router
|
||||
@@ -279,6 +279,14 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
|
||||
.route("/_matrix/media/r0/preview_url", any(redirect_legacy_preview));
|
||||
}
|
||||
|
||||
#[cfg(feature = "admin_api")]
|
||||
{
|
||||
router = router
|
||||
.ruma_route(&admin_api::users::list_users_route)
|
||||
.ruma_route(&admin_api::rooms::ban_room)
|
||||
.ruma_route(&admin_api::rooms::list_rooms)
|
||||
};
|
||||
|
||||
router
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ full = [
|
||||
"jemalloc_prof",
|
||||
"perf_measurements",
|
||||
"tokio_console",
|
||||
"conduwuit-api/admin_api",
|
||||
]
|
||||
|
||||
brotli_compression = [
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod rooms;
|
||||
pub mod users;
|
||||
|
||||
@@ -11,7 +11,7 @@ pub mod v1 {
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
unstable("org.continuwuity.admin") => "/_continuwuity/admin/rooms/list",
|
||||
1.0 => "/_continuwuity/admin/v1/rooms/list",
|
||||
1.0 => "/_continuwuity/admin/v1/rooms",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
pub mod v1 {
|
||||
use ruma::{
|
||||
OwnedMxcUri, OwnedRoomOrAliasId, OwnedUserId,
|
||||
api::{auth_scheme::AccessToken, request, response},
|
||||
metadata,
|
||||
};
|
||||
|
||||
metadata! {
|
||||
method: POST,
|
||||
rate_limited: false,
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
1.0 => "/_continuwuity/admin/v1/users/create",
|
||||
},
|
||||
}
|
||||
|
||||
#[request]
|
||||
pub struct Request {
|
||||
/// The user's localpart (the identifier between `@` and `:`). Cannot be
|
||||
/// blank.
|
||||
pub localpart: String,
|
||||
|
||||
/// The user's desired password. Cannot be blank.
|
||||
pub password: String,
|
||||
|
||||
/// The display name to set upon creation.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub display_name: Option<String>,
|
||||
|
||||
/// The avatar URI to set upon creation.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub avatar_url: Option<OwnedMxcUri>,
|
||||
|
||||
/// Suspends the user immediately upon creation. They can still log in.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub suspended: bool,
|
||||
|
||||
/// Locks the user immediately upon creation. They will receive
|
||||
/// M_USER_LOCKED upon login.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub locked: bool,
|
||||
|
||||
/// Disables the user's login immediately upon creation.
|
||||
///
|
||||
/// The user can still be used if an admin generates an access token for
|
||||
/// the account, but the user will not be able to use `POST
|
||||
/// /_matrix/client/v3/login`.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub login_disabled: bool,
|
||||
|
||||
/// Promotes the user to a server administrator immediately upon
|
||||
/// creation.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub admin: bool,
|
||||
|
||||
/// Skips joining rooms in the server's configured auto_join_rooms.
|
||||
///
|
||||
/// If this is false, all rooms in the config.toml's `auto_join_rooms`
|
||||
/// will be automatically joined upon creation. If `auto_join_rooms`
|
||||
/// is supplied in this request too, those rooms will be joined
|
||||
/// afterwards.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub skip_auto_join: bool,
|
||||
|
||||
/// Additional rooms to auto-join the new user to. If `skip_auto_join`
|
||||
/// is `true`, these rooms will still be joined.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub auto_join_rooms: Vec<OwnedRoomOrAliasId>,
|
||||
}
|
||||
|
||||
#[response]
|
||||
pub struct Response {
|
||||
/// The fully qualified user ID of the newly created user.
|
||||
pub user_id: OwnedUserId,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
#[must_use]
|
||||
pub fn new(localpart: String, password: String) -> Self {
|
||||
Self {
|
||||
localpart,
|
||||
password,
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
suspended: false,
|
||||
locked: false,
|
||||
login_disabled: false,
|
||||
admin: false,
|
||||
skip_auto_join: false,
|
||||
auto_join_rooms: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
#[must_use]
|
||||
pub fn new(user_id: OwnedUserId) -> Self { Self { user_id } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
pub mod v1 {
|
||||
use ruma::{
|
||||
OwnedUserId,
|
||||
api::{auth_scheme::AccessToken, request, response},
|
||||
metadata,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
metadata! {
|
||||
method: PUT,
|
||||
rate_limited: false,
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
1.0 => "/_continuwuity/admin/v1/users",
|
||||
}
|
||||
}
|
||||
|
||||
#[request]
|
||||
#[derive(Default)]
|
||||
pub struct Request {
|
||||
/// If true, includes deactivated users in the response.
|
||||
#[ruma_api(query)]
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub include_deactivated: bool,
|
||||
/// If true, includes locked users in the response.
|
||||
#[ruma_api(query)]
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub include_locked: bool,
|
||||
/// If true, includes suspended users in the response.
|
||||
#[ruma_api(query)]
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub include_suspended: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, serde::Serialize)]
|
||||
pub struct User {
|
||||
/// The full user ID of the user.
|
||||
pub user_id: OwnedUserId,
|
||||
|
||||
/// Whether this user is deactivated.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub deactivated: bool,
|
||||
|
||||
/// Whether this user is suspended.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub suspended: bool,
|
||||
|
||||
/// Whether this user is locked.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub locked: bool,
|
||||
|
||||
/// Whether this user is an admin.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub admin: bool,
|
||||
|
||||
/// Whether this user has their login disabled.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub login_disabled: bool,
|
||||
}
|
||||
|
||||
impl User {
|
||||
#[must_use]
|
||||
pub fn new(user_id: OwnedUserId) -> Self {
|
||||
Self {
|
||||
user_id,
|
||||
deactivated: false,
|
||||
suspended: false,
|
||||
locked: false,
|
||||
admin: false,
|
||||
login_disabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[response]
|
||||
#[derive(Default)]
|
||||
pub struct Response {
|
||||
pub users: Vec<User>,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
#[must_use]
|
||||
pub fn new() -> Self { Self::default() }
|
||||
}
|
||||
|
||||
impl Response {
|
||||
#[must_use]
|
||||
pub fn new(users: Vec<User>) -> Self { Self { users } }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assign::assign;
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn request_defaults() {
|
||||
let req = Request::new();
|
||||
assert!(!req.include_deactivated && !req.include_locked && !req.include_suspended);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_serialize_omits_default_values() {
|
||||
let user_id = OwnedUserId::try_from("@alice:example.org".to_owned()).unwrap();
|
||||
let user = User::new(user_id.clone());
|
||||
|
||||
let expected = json!({ "user_id": user_id.to_string() });
|
||||
assert_eq!(serde_json::to_value(&user).expect("failed to serialize user"), expected);
|
||||
|
||||
let suspended_user = assign!(user, {suspended: true});
|
||||
let expected2 = json!({ "user_id": "@alice:example.org", "suspended": true});
|
||||
assert_eq!(
|
||||
serde_json::to_value(&suspended_user).expect("failed to serialize user"),
|
||||
expected2
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_defaults() {
|
||||
let response = Response::default();
|
||||
assert!(response.users.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod create;
|
||||
pub mod list;
|
||||
Reference in New Issue
Block a user