feat: Add user creation endpoint

This commit is contained in:
timedout
2026-05-25 16:48:45 +01:00
committed by Ginger
parent cef2260dc7
commit f84f0c46b1
7 changed files with 138 additions and 8 deletions
+1 -1
View File
@@ -99,7 +99,7 @@ pub(crate) async fn register_route(
.users
.create_local_account(&user_id, password, identity.email)
.await;
services.users.join_auto_join_rooms(&user_id).await;
user_id
};
+119
View File
@@ -0,0 +1,119 @@
use axum::extract::State;
use conduwuit::{
Err, err, error, info,
utils::{IterStream, stream::BroadbandExt},
warn,
};
use futures::{FutureExt, StreamExt};
use ruma::UserId;
use ruminuwuity::admin::continuwuity::users;
use service::users::HashedPassword;
use crate::router::Ruma;
/// # `POST /_continuwuity/admin/v1/users/create`
///
/// Creates a new user.
pub(crate) async fn create_user_route(
State(services): State<crate::State>,
body: Ruma<users::create::v1::Request>,
) -> conduwuit::Result<users::create::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 user_id =
&UserId::parse_with_server_name(&body.localpart, services.globals.server_name())?;
if services.users.is_active_local(user_id).await {
return Err!(Conflict("A user with this username already exists"));
}
services
.users
.create_local_account(
user_id,
HashedPassword::new(&body.password)?,
body.email
.clone()
.map(lettre::Address::try_from)
.transpose()
.map_err(|e| err!(Request(BadJson("Invalid email address: {e}"))))?,
)
.await;
if body.suspended {
services.users.suspend_account(user_id, sender_user).await;
}
if body.locked {
services.users.lock_account(user_id, sender_user).await;
}
if body.login_disabled {
services.users.disable_login(user_id);
}
if let Some(ref value) = body.display_name {
services.users.set_profile_key(
user_id,
"displayname",
Some(serde_json::to_value(value)?),
);
}
if let Some(ref value) = body.avatar_url {
services
.users
.set_profile_key(user_id, "avatar_url", Some(serde_json::to_value(value)?));
}
if body.admin {
services
.admin
.make_user_admin(user_id)
.await
.inspect_err(|e| error!("failed to make new user {user_id} an admin: {e}"))
.ok();
}
if !body.skip_auto_join {
services.users.join_auto_join_rooms(user_id).await;
}
body.auto_join_rooms
.clone()
.into_iter()
.stream()
.broad_filter_map(|room| async move {
services
.rooms
.alias
.resolve_with_servers(&room, None)
.await
.inspect_err(|e| {
warn!(
"Failed to resolve room alias to room ID when attempting to auto join \
{room}: {e}"
);
})
.ok()
})
.for_each_concurrent(None, |(room_id, servers)| async move {
match services
.rooms
.membership
.join_room(
user_id,
&room_id,
Some("Automatically joining this room upon registration".to_owned()),
servers.as_ref(),
)
.boxed()
.await
{
| Err(e) => {
warn!("Failed to automatically join {user_id} to {room_id}: {e}");
},
| _ => {
info!("Automatically joined room {user_id} to {room_id}");
},
}
})
.await;
Ok(users::create::v1::Response::new(user_id.to_owned()))
}
+3 -1
View File
@@ -1,3 +1,5 @@
mod create;
mod list;
pub(crate) use list::list_users_route;
pub(crate) use create::*;
pub(crate) use list::*;
+1
View File
@@ -283,6 +283,7 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
{
router = router
.ruma_route(&admin_api::users::list_users_route)
.ruma_route(&admin_api::users::create_user_route)
.ruma_route(&admin_api::rooms::ban_room)
.ruma_route(&admin_api::rooms::legacy_list_rooms_route)
.ruma_route(&admin_api::rooms::list_rooms_route);
@@ -23,6 +23,10 @@ pub mod v1 {
/// The user's desired password. Cannot be blank.
pub password: String,
/// The user's email address, if any.
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
pub email: Option<String>,
/// The display name to set upon creation.
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
pub display_name: Option<String>,
@@ -80,6 +84,7 @@ pub mod v1 {
Self {
localpart,
password,
email: None,
display_name: None,
avatar_url: None,
suspended: false,
+8 -6
View File
@@ -229,6 +229,9 @@ impl Service {
}
/// Create a new account for a local human or bot user.
///
/// Does not automatically join the user to auto join rooms. Use
/// `join_auto_join_rooms` for that.
pub async fn create_local_account(
&self,
user_id: &UserId,
@@ -303,8 +306,11 @@ impl Service {
.ok();
}
}
info!("Created new user account for {user_id}");
}
// Autojoin the user to the configured autojoin rooms
/// Autojoin the user to the configured autojoin rooms
pub async fn join_auto_join_rooms(&self, user_id: &UserId) {
for room in &self.services.config.auto_join_rooms {
let Ok(room_id) = self.services.alias.resolve(room).await else {
error!(
@@ -320,9 +326,7 @@ impl Service {
.server_in_room(self.services.globals.server_name(), &room_id)
.await
{
warn!(
"Skipping room {room} to automatically join as we have never joined before."
);
warn!("Skipping auto-room {room} as we have never joined before.");
continue;
}
@@ -354,8 +358,6 @@ impl Service {
}
}
}
info!("Created new user account for {user_id}");
}
pub async fn determine_registration_user_id(
+1
View File
@@ -519,6 +519,7 @@ async fn complete_registration(
.registration_tokens
.mark_token_as_used(registration_token);
}
services.users.join_auto_join_rooms(&user_id).await;
let user_session = UserSession { user_id, last_login: SystemTime::now() };