From 02f4f9d88dc5797d598d10b33460c10443f8b554 Mon Sep 17 00:00:00 2001 From: timedout Date: Sat, 10 Oct 2026 23:11:37 +0100 Subject: [PATCH] feat(admin): Create user route --- src/api/admin/users/create.rs | 69 +++++++++++++++++++ src/api/admin/users/list.rs | 2 +- src/api/admin/users/mod.rs | 1 + src/api/router.rs | 3 +- src/ruminuwuity/Cargo.toml | 2 +- .../admin/continuwuity/users/create.rs | 58 ++++++++++++++++ .../admin/continuwuity/users/mod.rs | 1 + 7 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 src/api/admin/users/create.rs create mode 100644 src/ruminuwuity/admin/continuwuity/users/create.rs diff --git a/src/api/admin/users/create.rs b/src/api/admin/users/create.rs new file mode 100644 index 000000000..9f2bbe93b --- /dev/null +++ b/src/api/admin/users/create.rs @@ -0,0 +1,69 @@ +use axum::extract::State; +use conduwuit::{Err, error}; +use ruma::{api::client::profile::PropagateTo, profile::ProfileFieldValue}; +use ruminuwuity::admin::continuwuity::users::create::v1::{Request, Response}; +use service::users::{HashedPassword, ProfileFieldChange}; + +use crate::router::Ruma; + +/// # `POST /_continuwuity/admin/v1/users` +/// +/// Creates a new user on the homeserver. +pub(crate) async fn create_user( + State(services): State, + body: Ruma, +) -> conduwuit::Result { + let sender_user = body.identity.expect_sender_user()?; + if !services.users.is_admin(sender_user).await { + return Err!(Request(Forbidden("Only server administrators can use this endpoint"))); + } + + if !services.globals.user_is_local(&body.user_id) { + return Err!(Request(InvalidParam("User ID does not belong to this server."))); + } + if services.users.status(&body.user_id).await.is_found() { + return Err!(Request(UserInUse("An account with this user ID already exists."))); + } + + services + .users + .create_local_account( + &body.user_id, + body.password + .as_ref() + .and_then(|raw| HashedPassword::new(raw).ok()), + None, + None, + None, + ) + .await?; + if body.locked { + services + .users + .lock_account(&body.user_id, sender_user) + .await; + } + if body.suspended { + services + .users + .suspend_account(&body.user_id, sender_user) + .await; + } + for (key, value) in body.profile.clone().unwrap_or_default() { + services + .users + .set_profile_field( + &body.user_id, + ProfileFieldChange::Set(ProfileFieldValue::new(&key, value)?), + PropagateTo::All, + ) + .await + .inspect_err(|e| error!(error=?e, %key, "Failed to set profile field")) + .ok(); + } + if body.admin { + services.admin.make_user_admin(&body.user_id).await?; + } + + Ok(Response::default()) +} diff --git a/src/api/admin/users/list.rs b/src/api/admin/users/list.rs index d05d5d3b3..7fd2aae5c 100644 --- a/src/api/admin/users/list.rs +++ b/src/api/admin/users/list.rs @@ -11,7 +11,7 @@ use service::users::AccountStatus; use crate::router::Ruma; -/// # `PUT /_continuwuity/admin/v1/users` +/// # `GET /_continuwuity/admin/v1/users` /// /// Lists users on this homeserver pub(crate) async fn list_users( diff --git a/src/api/admin/users/mod.rs b/src/api/admin/users/mod.rs index d17e233fb..fa6613e51 100644 --- a/src/api/admin/users/mod.rs +++ b/src/api/admin/users/mod.rs @@ -1 +1,2 @@ +pub mod create; pub mod list; diff --git a/src/api/router.rs b/src/api/router.rs index 664af3232..022c98d5b 100644 --- a/src/api/router.rs +++ b/src/api/router.rs @@ -198,7 +198,8 @@ pub fn build(router: Router, state: State) -> Router { .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::users::list::list_users); + .ruma_route(&admin::users::list::list_users) + .ruma_route(&admin::users::create::create_user); if config.allow_federation { router = router diff --git a/src/ruminuwuity/Cargo.toml b/src/ruminuwuity/Cargo.toml index ee6c68a0c..3b45d2d56 100644 --- a/src/ruminuwuity/Cargo.toml +++ b/src/ruminuwuity/Cargo.toml @@ -11,7 +11,7 @@ version.workspace = true path = "mod.rs" [features] -default = ["client", "server"] +default = ["client", "server", "unstable-msc4484"] client = [] server = [] diff --git a/src/ruminuwuity/admin/continuwuity/users/create.rs b/src/ruminuwuity/admin/continuwuity/users/create.rs new file mode 100644 index 000000000..5e1ee716e --- /dev/null +++ b/src/ruminuwuity/admin/continuwuity/users/create.rs @@ -0,0 +1,58 @@ +pub mod v1 { + + use ruma::{ + OwnedUserId, + api::{OAuthClientScope, auth_scheme::AccessToken, request, response}, + metadata, + profile::UserProfile, + }; + + metadata! { + method: POST, + rate_limited: false, + authentication: AccessToken, + history: { + 1.0 => "/_continuwuity/admin/v1/users", + }, + required_client_scopes: [OAuthClientScope::ServerAdministration] + } + + #[request] + pub struct Request { + /// The full user ID to create. Must belong to the server. + pub user_id: OwnedUserId, + /// The password to set for the new user. + /// + /// If `None`, the user will not be able to log in. + #[serde(default, skip_serializing_if = "ruma::serde::is_default")] + pub password: Option, + /// The profile fields to set for the user. This can be used to set the + /// new user's display name and profile picture. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub profile: Option, + #[serde(default, skip_serializing_if = "ruma::serde::is_default")] + pub admin: bool, + #[serde(default, skip_serializing_if = "ruma::serde::is_default")] + pub suspended: bool, + #[serde(default, skip_serializing_if = "ruma::serde::is_default")] + pub locked: bool, + } + + #[derive(Default)] + #[response] + pub struct Response; + + impl Request { + #[must_use] + pub fn new(user_id: OwnedUserId) -> Self { + Self { + user_id, + password: None, + profile: None, + admin: false, + suspended: false, + locked: false, + } + } + } +} diff --git a/src/ruminuwuity/admin/continuwuity/users/mod.rs b/src/ruminuwuity/admin/continuwuity/users/mod.rs index d17e233fb..fa6613e51 100644 --- a/src/ruminuwuity/admin/continuwuity/users/mod.rs +++ b/src/ruminuwuity/admin/continuwuity/users/mod.rs @@ -1 +1,2 @@ +pub mod create; pub mod list;