Skip to content

Commit 5059ffa

Browse files
committed
feat: GET /users/search for the member picker
1 parent 6554051 commit 5059ffa

1 file changed

Lines changed: 89 additions & 0 deletions

File tree

api/src/routes/users.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use aruna_operations::read_user_document::{ReadUserDocumentError, ReadUserDocume
2929
use aruna_operations::register_or_get_oidc_user::{
3030
RegisterOrGetOidcUserInput, RegisterOrGetOidcUserOperation,
3131
};
32+
use aruna_operations::search_users::{SearchUsersInput, SearchUsersOperation};
3233
use aruna_operations::update_user::{UpdateUserInput, UpdateUserOperation};
3334
use axum::extract::{Path, Query, State};
3435
use axum::routing::{get, post};
@@ -50,6 +51,7 @@ use utoipa::{OpenApi, ToSchema};
5051
get_user_info,
5152
patch_user_info,
5253
list_users,
54+
search_users,
5355
get_user,
5456
update_user,
5557
)
@@ -62,6 +64,7 @@ pub fn router() -> Router<Arc<ServerState>> {
6264
.route("/users/token", get(get_token))
6365
.route("/users/info", get(get_user_info).patch(patch_user_info))
6466
.route("/users", get(list_users))
67+
.route("/users/search", get(search_users))
6568
.route("/users/{id}", get(get_user).patch(update_user))
6669
}
6770

@@ -101,6 +104,27 @@ pub struct ListUsersResponse {
101104
pub next_start_after: Option<String>,
102105
}
103106

107+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
108+
pub struct SearchUsersQuery {
109+
pub q: String,
110+
#[serde(default)]
111+
pub limit: Option<usize>,
112+
#[serde(default)]
113+
pub start_after: Option<String>,
114+
}
115+
116+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
117+
pub struct SearchUserResult {
118+
pub user_id: String,
119+
pub name: String,
120+
}
121+
122+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
123+
pub struct SearchUsersResponse {
124+
pub users: Vec<SearchUserResult>,
125+
pub next_start_after: Option<String>,
126+
}
127+
104128
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
105129
pub struct UserInfoRoleResponse {
106130
pub role_id: String,
@@ -150,6 +174,8 @@ pub type PatchUserInfoRequest = UpdateUserRequest;
150174

151175
const DEFAULT_LIST_USERS_LIMIT: usize = 100;
152176
const MAX_LIST_USERS_LIMIT: usize = 1_000;
177+
const MIN_SEARCH_QUERY_CHARS: usize = 2;
178+
const MAX_SEARCH_USERS_LIMIT: usize = 20;
153179

154180
impl From<User> for GetUserResponse {
155181
fn from(value: User) -> Self {
@@ -720,6 +746,69 @@ async fn list_users(
720746
))
721747
}
722748

749+
#[utoipa::path(
750+
get,
751+
path = "/users/search",
752+
tag = "users",
753+
params(
754+
("q" = String, Query, description = "Substring to match against user name or email; minimum 2 characters"),
755+
("limit" = Option<usize>, Query, description = "Maximum results, capped at 20"),
756+
("start_after" = Option<String>, Query, description = "Pagination cursor")
757+
),
758+
responses(
759+
(status = 200, description = "Matching users", body = SearchUsersResponse),
760+
(status = 400, description = "Query too short", body = ErrorResponse),
761+
(status = 401, description = "Unauthorized", body = ErrorResponse)
762+
),
763+
security(("bearer_auth" = []))
764+
)]
765+
async fn search_users(
766+
State(state): State<Arc<ServerState>>,
767+
Extension(auth): Extension<Option<AuthContext>>,
768+
Query(query): Query<SearchUsersQuery>,
769+
) -> ServerResult<(StatusCode, Json<SearchUsersResponse>)> {
770+
let auth = auth.ok_or(ServerError::Unauthorized)?;
771+
let realm_id = state.get_realm_id();
772+
if auth.realm_id != realm_id {
773+
return Err(ServerError::Forbidden);
774+
}
775+
let q = query.q.trim().to_string();
776+
if q.chars().count() < MIN_SEARCH_QUERY_CHARS {
777+
return Err(ServerError::BadRequest);
778+
}
779+
let limit = query
780+
.limit
781+
.unwrap_or(MAX_SEARCH_USERS_LIMIT)
782+
.clamp(1, MAX_SEARCH_USERS_LIMIT);
783+
784+
let output = drive(
785+
SearchUsersOperation::new(SearchUsersInput {
786+
realm_id,
787+
query: q,
788+
limit,
789+
start_after: query.start_after,
790+
}),
791+
&state.get_ctx(),
792+
)
793+
.await
794+
.map_err(|err| ServerError::InternalError(err.to_string()))?;
795+
796+
Ok((
797+
StatusCode::OK,
798+
Json(SearchUsersResponse {
799+
users: output
800+
.users
801+
.into_iter()
802+
.map(|user| SearchUserResult {
803+
user_id: user.user_id.to_string(),
804+
name: user.name,
805+
})
806+
.collect(),
807+
next_start_after: output.next_start_after,
808+
}),
809+
))
810+
}
811+
723812
#[utoipa::path(
724813
get,
725814
path = "/users/{id}",

0 commit comments

Comments
 (0)