@@ -29,6 +29,7 @@ use aruna_operations::read_user_document::{ReadUserDocumentError, ReadUserDocume
2929use aruna_operations:: register_or_get_oidc_user:: {
3030 RegisterOrGetOidcUserInput , RegisterOrGetOidcUserOperation ,
3131} ;
32+ use aruna_operations:: search_users:: { SearchUsersInput , SearchUsersOperation } ;
3233use aruna_operations:: update_user:: { UpdateUserInput , UpdateUserOperation } ;
3334use axum:: extract:: { Path , Query , State } ;
3435use 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 ) ]
105129pub struct UserInfoRoleResponse {
106130 pub role_id : String ,
@@ -150,6 +174,8 @@ pub type PatchUserInfoRequest = UpdateUserRequest;
150174
151175const DEFAULT_LIST_USERS_LIMIT : usize = 100 ;
152176const MAX_LIST_USERS_LIMIT : usize = 1_000 ;
177+ const MIN_SEARCH_QUERY_CHARS : usize = 2 ;
178+ const MAX_SEARCH_USERS_LIMIT : usize = 20 ;
153179
154180impl 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