Skip to content
This repository has been archived by the owner on Sep 10, 2024. It is now read-only.

Commit

Permalink
graphql: users query to list users with a few filters
Browse files Browse the repository at this point in the history
  • Loading branch information
sandhose committed Jul 5, 2024
1 parent 8a1ac9c commit f849b48
Show file tree
Hide file tree
Showing 4 changed files with 377 additions and 2 deletions.
104 changes: 102 additions & 2 deletions crates/handlers/src/graphql/query/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use async_graphql::{Context, Object, ID};
use async_graphql::{
connection::{query, Connection, Edge, OpaqueCursor},
Context, Enum, Object, ID,
};
use mas_storage::{user::UserFilter, Pagination};

use crate::graphql::{
model::{NodeType, User},
model::{Cursor, NodeCursor, NodeType, PreloadedTotalCount, User},
state::ContextExt as _,
UserId,
};
Expand Down Expand Up @@ -72,4 +76,100 @@ impl UserQuery {

Ok(Some(User(user)))
}

/// Get a list of users.
///
/// This is only available to administrators.
async fn users(
&self,
ctx: &Context<'_>,

#[graphql(name = "state", desc = "List only users with the given state.")]
state_param: Option<UserState>,

#[graphql(
name = "canRequestAdmin",
desc = "List only users with the given 'canRequestAdmin' value"
)]
can_request_admin_param: Option<bool>,

#[graphql(desc = "Returns the elements in the list that come after the cursor.")]
after: Option<String>,
#[graphql(desc = "Returns the elements in the list that come before the cursor.")]
before: Option<String>,
#[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
#[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
) -> Result<Connection<Cursor, User, PreloadedTotalCount>, async_graphql::Error> {
let requester = ctx.requester();
if !requester.is_admin() {
return Err(async_graphql::Error::new("Unauthorized"));
}

let state = ctx.state();
let mut repo = state.repository().await?;

query(
after,
before,
first,
last,
|after, before, first, last| async move {
let after_id = after
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::User))
.transpose()?;
let before_id = before
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::User))
.transpose()?;
let pagination = Pagination::try_new(before_id, after_id, first, last)?;

// Build the query filter
let filter = UserFilter::new();
let filter = match can_request_admin_param {
Some(true) => filter.can_request_admin_only(),
Some(false) => filter.cannot_request_admin_only(),
None => filter,
};
let filter = match state_param {
Some(UserState::Active) => filter.active_only(),
Some(UserState::Locked) => filter.locked_only(),
None => filter,
};

let page = repo.user().list(filter, pagination).await?;

// Preload the total count if requested
let count = if ctx.look_ahead().field("totalCount").exists() {
Some(repo.user().count(filter).await?)
} else {
None
};

repo.cancel().await?;

let mut connection = Connection::with_additional_fields(
page.has_previous_page,
page.has_next_page,
PreloadedTotalCount(count),
);
connection.edges.extend(
page.edges.into_iter().map(|p| {
Edge::new(OpaqueCursor(NodeCursor(NodeType::User, p.id)), User(p))
}),
);

Ok::<_, async_graphql::Error>(connection)
},
)
.await
}
}

/// The state of a user.
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
enum UserState {
/// The user is active.
Active,

/// The user is locked.
Locked,
}
78 changes: 78 additions & 0 deletions frontend/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,37 @@ type Query {
"""
userByUsername(username: String!): User
"""
Get a list of users.
This is only available to administrators.
"""
users(
"""
List only users with the given state.
"""
state: UserState
"""
List only users with the given 'canRequestAdmin' value
"""
canRequestAdmin: Boolean
"""
Returns the elements in the list that come after the cursor.
"""
after: String
"""
Returns the elements in the list that come before the cursor.
"""
before: String
"""
Returns the first *n* elements from the list.
"""
first: Int
"""
Returns the last *n* elements from the list.
"""
last: Int
): UserConnection!
"""
Fetch an upstream OAuth 2.0 link by its ID.
"""
upstreamOauth2Link(id: ID!): UpstreamOAuth2Link
Expand Down Expand Up @@ -1727,6 +1758,39 @@ type UserAgent {
deviceType: DeviceType!
}

type UserConnection {
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
"""
A list of edges.
"""
edges: [UserEdge!]!
"""
A list of nodes.
"""
nodes: [User!]!
"""
Identifies the total count of items in the connection.
"""
totalCount: Int!
}

"""
An edge in a connection.
"""
type UserEdge {
"""
The item at the end of the edge
"""
node: User!
"""
A cursor for use in pagination
"""
cursor: String!
}

"""
A user email address
"""
Expand Down Expand Up @@ -1797,6 +1861,20 @@ enum UserEmailState {
CONFIRMED
}

"""
The state of a user.
"""
enum UserState {
"""
The user is active.
"""
ACTIVE
"""
The user is locked.
"""
LOCKED
}

"""
The input for the `verifyEmail` mutation
"""
Expand Down
46 changes: 46 additions & 0 deletions frontend/src/gql/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,12 @@ export type Query = {
userByUsername?: Maybe<User>;
/** Fetch a user email by its ID. */
userEmail?: Maybe<UserEmail>;
/**
* Get a list of users.
*
* This is only available to administrators.
*/
users: UserConnection;
/** Get the viewer */
viewer: Viewer;
/** Get the viewer's session */
Expand Down Expand Up @@ -815,6 +821,17 @@ export type QueryUserEmailArgs = {
id: Scalars['ID']['input'];
};


/** The query root of the GraphQL interface. */
export type QueryUsersArgs = {
after?: InputMaybe<Scalars['String']['input']>;
before?: InputMaybe<Scalars['String']['input']>;
canRequestAdmin?: InputMaybe<Scalars['Boolean']['input']>;
first?: InputMaybe<Scalars['Int']['input']>;
last?: InputMaybe<Scalars['Int']['input']>;
state?: InputMaybe<UserState>;
};

/** The input for the `removeEmail` mutation */
export type RemoveEmailInput = {
/** The ID of the email address to remove */
Expand Down Expand Up @@ -1212,6 +1229,27 @@ export type UserAgent = {
version?: Maybe<Scalars['String']['output']>;
};

export type UserConnection = {
__typename?: 'UserConnection';
/** A list of edges. */
edges: Array<UserEdge>;
/** A list of nodes. */
nodes: Array<User>;
/** Information to aid in pagination. */
pageInfo: PageInfo;
/** Identifies the total count of items in the connection. */
totalCount: Scalars['Int']['output'];
};

/** An edge in a connection. */
export type UserEdge = {
__typename?: 'UserEdge';
/** A cursor for use in pagination */
cursor: Scalars['String']['output'];
/** The item at the end of the edge */
node: User;
};

/** A user email address */
export type UserEmail = CreationEvent & Node & {
__typename?: 'UserEmail';
Expand Down Expand Up @@ -1257,6 +1295,14 @@ export enum UserEmailState {
Pending = 'PENDING'
}

/** The state of a user. */
export enum UserState {
/** The user is active. */
Active = 'ACTIVE',
/** The user is locked. */
Locked = 'LOCKED'
}

/** The input for the `verifyEmail` mutation */
export type VerifyEmailInput = {
/** The verification code */
Expand Down
Loading

0 comments on commit f849b48

Please sign in to comment.