You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-31 09:24:31 +03:00
admin: add APIs to list and get users
This commit is contained in:
@ -25,7 +25,9 @@ use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
mod call_context;
|
||||
mod model;
|
||||
mod params;
|
||||
mod response;
|
||||
mod v1;
|
||||
|
||||
use self::call_context::CallContext;
|
||||
|
||||
@ -36,7 +38,7 @@ where
|
||||
{
|
||||
let mut api = OpenApi::default();
|
||||
let router = ApiRouter::<S>::new()
|
||||
// TODO: add routes
|
||||
.nest("/api/admin/v1", self::v1::router())
|
||||
.finish_api_with(&mut api, |t| {
|
||||
t.title("Matrix Authentication Service admin API")
|
||||
.security_scheme(
|
||||
|
@ -12,10 +12,12 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
use ulid::Ulid;
|
||||
|
||||
/// A resource, with a type and an ID
|
||||
#[allow(dead_code)]
|
||||
pub trait Resource {
|
||||
/// The type of the resource
|
||||
const KIND: &'static str;
|
||||
@ -33,3 +35,72 @@ pub trait Resource {
|
||||
format!("{}/{}", Self::PATH, self.id())
|
||||
}
|
||||
}
|
||||
|
||||
/// A user
|
||||
#[derive(Serialize, JsonSchema)]
|
||||
pub struct User {
|
||||
#[serde(skip)]
|
||||
id: Ulid,
|
||||
|
||||
/// The username (localpart) of the user
|
||||
username: String,
|
||||
|
||||
/// When the user was created
|
||||
created_at: DateTime<Utc>,
|
||||
|
||||
/// When the user was locked. If null, the user is not locked.
|
||||
locked_at: Option<DateTime<Utc>>,
|
||||
|
||||
/// Whether the user can request admin privileges.
|
||||
can_request_admin: bool,
|
||||
}
|
||||
|
||||
impl User {
|
||||
/// Samples of users with different properties for examples in the schema
|
||||
pub fn samples() -> [Self; 3] {
|
||||
[
|
||||
Self {
|
||||
id: Ulid::from_bytes([0x01; 16]),
|
||||
username: "alice".to_owned(),
|
||||
created_at: DateTime::default(),
|
||||
locked_at: None,
|
||||
can_request_admin: false,
|
||||
},
|
||||
Self {
|
||||
id: Ulid::from_bytes([0x02; 16]),
|
||||
username: "bob".to_owned(),
|
||||
created_at: DateTime::default(),
|
||||
locked_at: None,
|
||||
can_request_admin: true,
|
||||
},
|
||||
Self {
|
||||
id: Ulid::from_bytes([0x03; 16]),
|
||||
username: "charlie".to_owned(),
|
||||
created_at: DateTime::default(),
|
||||
locked_at: Some(DateTime::default()),
|
||||
can_request_admin: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mas_data_model::User> for User {
|
||||
fn from(user: mas_data_model::User) -> Self {
|
||||
Self {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
created_at: user.created_at,
|
||||
locked_at: user.locked_at,
|
||||
can_request_admin: user.can_request_admin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resource for User {
|
||||
const KIND: &'static str = "user";
|
||||
const PATH: &'static str = "/api/admin/v1/users";
|
||||
|
||||
fn id(&self) -> Ulid {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
154
crates/handlers/src/admin/params.rs
Normal file
154
crates/handlers/src/admin/params.rs
Normal file
@ -0,0 +1,154 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Generated code from schemars violates this rule
|
||||
#![allow(clippy::str_to_string)]
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use aide::OperationIo;
|
||||
use async_trait::async_trait;
|
||||
use axum::{
|
||||
extract::{
|
||||
rejection::{PathRejection, QueryRejection},
|
||||
FromRequestParts, Path, Query,
|
||||
},
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
use axum_macros::FromRequestParts;
|
||||
use hyper::StatusCode;
|
||||
use mas_storage::pagination::PaginationDirection;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use ulid::Ulid;
|
||||
|
||||
use super::response::ErrorResponse;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Invalid ULID in path")]
|
||||
pub struct UlidPathParamRejection(#[from] PathRejection);
|
||||
|
||||
impl IntoResponse for UlidPathParamRejection {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse::from_error(&self)),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(JsonSchema, Debug, Clone, Copy, Deserialize)]
|
||||
struct UlidInPath {
|
||||
#[schemars(
|
||||
with = "String",
|
||||
title = "ULID",
|
||||
description = "A ULID as per https://github.com/ulid/spec",
|
||||
regex(pattern = r"^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$")
|
||||
)]
|
||||
id: Ulid,
|
||||
}
|
||||
|
||||
#[derive(FromRequestParts, OperationIo, Debug, Clone, Copy)]
|
||||
#[from_request(rejection(UlidPathParamRejection))]
|
||||
#[aide(input_with = "Path<UlidInPath>")]
|
||||
pub struct UlidPathParam(#[from_request(via(Path))] UlidInPath);
|
||||
|
||||
impl std::ops::Deref for UlidPathParam {
|
||||
type Target = Ulid;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0.id
|
||||
}
|
||||
}
|
||||
|
||||
/// The default page size if not specified
|
||||
const DEFAULT_PAGE_SIZE: usize = 10;
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Clone, Copy)]
|
||||
struct PaginationParams {
|
||||
/// Retrieve the items before the given ID
|
||||
#[serde(rename = "page[before]")]
|
||||
#[schemars(with = "Option<String>")]
|
||||
before: Option<Ulid>,
|
||||
|
||||
/// Retrieve the items after the given ID
|
||||
#[serde(rename = "page[after]")]
|
||||
#[schemars(with = "Option<String>")]
|
||||
after: Option<Ulid>,
|
||||
|
||||
/// Retrieve the first N items
|
||||
#[serde(rename = "page[first]")]
|
||||
first: Option<NonZeroUsize>,
|
||||
|
||||
/// Retrieve the last N items
|
||||
#[serde(rename = "page[last]")]
|
||||
last: Option<NonZeroUsize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PaginationRejection {
|
||||
#[error("Invalid pagination parameters")]
|
||||
Invalid(#[from] QueryRejection),
|
||||
|
||||
#[error("Cannot specify both `page[first]` and `page[last]` parameters")]
|
||||
FirstAndLast,
|
||||
}
|
||||
|
||||
impl IntoResponse for PaginationRejection {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse::from_error(&self)),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
/// An extractor for pagination parameters in the query string
|
||||
#[derive(OperationIo, Debug, Clone, Copy)]
|
||||
#[aide(input_with = "Query<PaginationParams>")]
|
||||
pub struct Pagination(pub mas_storage::Pagination);
|
||||
|
||||
#[async_trait]
|
||||
impl<S: Send + Sync> FromRequestParts<S> for Pagination {
|
||||
type Rejection = PaginationRejection;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut axum::http::request::Parts,
|
||||
state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let params = Query::<PaginationParams>::from_request_parts(parts, state).await?;
|
||||
|
||||
// Figure out the direction and the count out of the first and last parameters
|
||||
let (direction, count) = match (params.first, params.last) {
|
||||
// Make sure we don't specify both first and last
|
||||
(Some(_), Some(_)) => return Err(PaginationRejection::FirstAndLast),
|
||||
|
||||
// Default to forward pagination with a default page size
|
||||
(None, None) => (PaginationDirection::Forward, DEFAULT_PAGE_SIZE),
|
||||
|
||||
(Some(first), None) => (PaginationDirection::Forward, first.into()),
|
||||
(None, Some(last)) => (PaginationDirection::Backward, last.into()),
|
||||
};
|
||||
|
||||
Ok(Self(mas_storage::Pagination {
|
||||
before: params.before,
|
||||
after: params.after,
|
||||
direction,
|
||||
count,
|
||||
}))
|
||||
}
|
||||
}
|
37
crates/handlers/src/admin/v1/mod.rs
Normal file
37
crates/handlers/src/admin/v1/mod.rs
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use aide::axum::{routing::get_with, ApiRouter};
|
||||
use axum::extract::FromRequestParts;
|
||||
|
||||
use super::call_context::CallContext;
|
||||
|
||||
mod users;
|
||||
|
||||
pub fn router<S>() -> ApiRouter<S>
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
CallContext: FromRequestParts<S>,
|
||||
{
|
||||
ApiRouter::<S>::new()
|
||||
.api_route("/users", get_with(self::users::list, self::users::list_doc))
|
||||
.api_route(
|
||||
"/users/:id",
|
||||
get_with(self::users::get, self::users::get_doc),
|
||||
)
|
||||
.api_route(
|
||||
"/users/by-username/:username",
|
||||
get_with(self::users::by_username, self::users::by_username_doc),
|
||||
)
|
||||
}
|
87
crates/handlers/src/admin/v1/users/by_username.rs
Normal file
87
crates/handlers/src/admin/v1/users/by_username.rs
Normal file
@ -0,0 +1,87 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use aide::{transform::TransformOperation, OperationIo};
|
||||
use axum::{extract::Path, response::IntoResponse, Json};
|
||||
use hyper::StatusCode;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
admin::{
|
||||
call_context::CallContext,
|
||||
model::User,
|
||||
response::{ErrorResponse, SingleResponse},
|
||||
},
|
||||
impl_from_error_for_route,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error, OperationIo)]
|
||||
#[aide(output_with = "Json<ErrorResponse>")]
|
||||
pub enum RouteError {
|
||||
#[error(transparent)]
|
||||
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
|
||||
#[error("User with username {0:?} not found")]
|
||||
NotFound(String),
|
||||
}
|
||||
|
||||
impl_from_error_for_route!(mas_storage::RepositoryError);
|
||||
|
||||
impl IntoResponse for RouteError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let error = ErrorResponse::from_error(&self);
|
||||
let status = match self {
|
||||
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Self::NotFound(_) => StatusCode::NOT_FOUND,
|
||||
};
|
||||
(status, Json(error)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct UsernamePathParam {
|
||||
/// The username (localpart) of the user to get
|
||||
username: String,
|
||||
}
|
||||
|
||||
pub fn doc(operation: TransformOperation) -> TransformOperation {
|
||||
operation
|
||||
.description("Get a user by its username (localpart)")
|
||||
.response_with::<200, Json<SingleResponse<User>>, _>(|t| {
|
||||
let [sample, ..] = User::samples();
|
||||
let response =
|
||||
SingleResponse::new(sample, "/api/admin/v1/users/by-username/alice".to_owned());
|
||||
t.description("User was found").example(response)
|
||||
})
|
||||
.response_with::<404, RouteError, _>(|t| {
|
||||
let response = ErrorResponse::from_error(&RouteError::NotFound("alice".to_owned()));
|
||||
t.description("User was not found").example(response)
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "handler.admin.v1.users.by_username", skip_all, err)]
|
||||
pub async fn handler(
|
||||
CallContext { mut repo, .. }: CallContext,
|
||||
Path(UsernamePathParam { username }): Path<UsernamePathParam>,
|
||||
) -> Result<Json<SingleResponse<User>>, RouteError> {
|
||||
let self_path = format!("/api/admin/v1/users/by-username/{username}");
|
||||
let user = repo
|
||||
.user()
|
||||
.find_by_username(&username)
|
||||
.await?
|
||||
.ok_or(RouteError::NotFound(username))?;
|
||||
|
||||
Ok(Json(SingleResponse::new(User::from(user), self_path)))
|
||||
}
|
79
crates/handlers/src/admin/v1/users/get.rs
Normal file
79
crates/handlers/src/admin/v1/users/get.rs
Normal file
@ -0,0 +1,79 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use aide::{transform::TransformOperation, OperationIo};
|
||||
use axum::{response::IntoResponse, Json};
|
||||
use hyper::StatusCode;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{
|
||||
admin::{
|
||||
call_context::CallContext,
|
||||
model::User,
|
||||
params::UlidPathParam,
|
||||
response::{ErrorResponse, SingleResponse},
|
||||
},
|
||||
impl_from_error_for_route,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error, OperationIo)]
|
||||
#[aide(output_with = "Json<ErrorResponse>")]
|
||||
pub enum RouteError {
|
||||
#[error(transparent)]
|
||||
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
|
||||
#[error("User ID {0} not found")]
|
||||
NotFound(Ulid),
|
||||
}
|
||||
|
||||
impl_from_error_for_route!(mas_storage::RepositoryError);
|
||||
|
||||
impl IntoResponse for RouteError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let error = ErrorResponse::from_error(&self);
|
||||
let status = match self {
|
||||
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Self::NotFound(_) => StatusCode::NOT_FOUND,
|
||||
};
|
||||
(status, Json(error)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn doc(operation: TransformOperation) -> TransformOperation {
|
||||
operation
|
||||
.description("Get a user")
|
||||
.response_with::<200, Json<SingleResponse<User>>, _>(|t| {
|
||||
let [sample, ..] = User::samples();
|
||||
let response = SingleResponse::new_canonical(sample);
|
||||
t.description("User was found").example(response)
|
||||
})
|
||||
.response_with::<404, RouteError, _>(|t| {
|
||||
let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil()));
|
||||
t.description("User was not found").example(response)
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "handler.admin.v1.users.get", skip_all, err)]
|
||||
pub async fn handler(
|
||||
CallContext { mut repo, .. }: CallContext,
|
||||
id: UlidPathParam,
|
||||
) -> Result<Json<SingleResponse<User>>, RouteError> {
|
||||
let user = repo
|
||||
.user()
|
||||
.lookup(*id)
|
||||
.await?
|
||||
.ok_or(RouteError::NotFound(*id))?;
|
||||
|
||||
Ok(Json(SingleResponse::new_canonical(User::from(user))))
|
||||
}
|
135
crates/handlers/src/admin/v1/users/list.rs
Normal file
135
crates/handlers/src/admin/v1/users/list.rs
Normal file
@ -0,0 +1,135 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use aide::{transform::TransformOperation, OperationIo};
|
||||
use axum::{
|
||||
extract::{rejection::QueryRejection, Query},
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
use axum_macros::FromRequestParts;
|
||||
use hyper::StatusCode;
|
||||
use mas_storage::{user::UserFilter, Page};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
admin::{
|
||||
call_context::CallContext,
|
||||
model::{Resource, User},
|
||||
params::Pagination,
|
||||
response::{ErrorResponse, PaginatedResponse},
|
||||
},
|
||||
impl_from_error_for_route,
|
||||
};
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum UserStatus {
|
||||
/// The user is active
|
||||
Active,
|
||||
|
||||
/// The user is locked
|
||||
Locked,
|
||||
}
|
||||
|
||||
#[derive(FromRequestParts, Deserialize, JsonSchema, OperationIo)]
|
||||
#[aide(input_with = "Query<FilterParams>")]
|
||||
#[from_request(via(Query), rejection(RouteError))]
|
||||
pub struct FilterParams {
|
||||
#[serde(rename = "filter[can_request_admin]")]
|
||||
can_request_admin: Option<bool>,
|
||||
|
||||
#[serde(rename = "filter[status]")]
|
||||
status: Option<UserStatus>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a FilterParams> for UserFilter<'a> {
|
||||
fn from(val: &'a FilterParams) -> Self {
|
||||
let filter = UserFilter::default();
|
||||
|
||||
let filter = match val.can_request_admin {
|
||||
Some(true) => filter.can_request_admin_only(),
|
||||
Some(false) => filter.cannot_request_admin_only(),
|
||||
None => filter,
|
||||
};
|
||||
|
||||
let filter = match val.status {
|
||||
Some(UserStatus::Active) => filter.active_only(),
|
||||
Some(UserStatus::Locked) => filter.locked_only(),
|
||||
None => filter,
|
||||
};
|
||||
|
||||
filter
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, OperationIo)]
|
||||
#[aide(output_with = "Json<ErrorResponse>")]
|
||||
pub enum RouteError {
|
||||
#[error(transparent)]
|
||||
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
|
||||
#[error("Invalid filter parameters")]
|
||||
InvalidFilter(#[from] QueryRejection),
|
||||
}
|
||||
|
||||
impl_from_error_for_route!(mas_storage::RepositoryError);
|
||||
|
||||
impl IntoResponse for RouteError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let error = ErrorResponse::from_error(&self);
|
||||
let status = match self {
|
||||
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Self::InvalidFilter(_) => StatusCode::BAD_REQUEST,
|
||||
};
|
||||
(status, Json(error)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn doc(operation: TransformOperation) -> TransformOperation {
|
||||
operation
|
||||
.description("List users")
|
||||
.response_with::<200, Json<PaginatedResponse<User>>, _>(|t| {
|
||||
let users = User::samples();
|
||||
let pagination = mas_storage::Pagination::first(users.len());
|
||||
let page = Page {
|
||||
edges: users.into(),
|
||||
has_next_page: true,
|
||||
has_previous_page: false,
|
||||
};
|
||||
|
||||
t.description("Paginated response of users")
|
||||
.example(PaginatedResponse::new(page, pagination, 42, User::PATH))
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "handler.admin.v1.users.list", skip_all, err)]
|
||||
pub async fn handler(
|
||||
CallContext { mut repo, .. }: CallContext,
|
||||
Pagination(pagination): Pagination,
|
||||
filter: FilterParams,
|
||||
) -> Result<Json<PaginatedResponse<User>>, RouteError> {
|
||||
let filter = UserFilter::from(&filter);
|
||||
|
||||
let page = repo.user().list(filter, pagination).await?;
|
||||
let count = repo.user().count(filter).await?;
|
||||
|
||||
Ok(Json(PaginatedResponse::new(
|
||||
page.map(User::from),
|
||||
pagination,
|
||||
count,
|
||||
User::PATH,
|
||||
)))
|
||||
}
|
23
crates/handlers/src/admin/v1/users/mod.rs
Normal file
23
crates/handlers/src/admin/v1/users/mod.rs
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
mod by_username;
|
||||
mod get;
|
||||
mod list;
|
||||
|
||||
pub use self::{
|
||||
by_username::{doc as by_username_doc, handler as by_username},
|
||||
get::{doc as get_doc, handler as get},
|
||||
list::{doc as list_doc, handler as list},
|
||||
};
|
Reference in New Issue
Block a user