feat(ui, webserver): support list users in team management (#1005)
* feat(webserver): support list users * feat(ui, webserver): support list users in team management * fix lintr0.7
parent
ae4dc5f8d0
commit
58787e707b
|
|
@ -3,10 +3,11 @@
|
|||
import { CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
||||
import InvitationTable from './invitation-table'
|
||||
import UsersTable from './user-table'
|
||||
|
||||
export default function Team() {
|
||||
return (
|
||||
<div>
|
||||
<div className="xl:max-w-[750px]">
|
||||
<div>
|
||||
<CardHeader>
|
||||
<CardTitle>Invites</CardTitle>
|
||||
|
|
@ -19,7 +20,9 @@ export default function Team() {
|
|||
<CardHeader>
|
||||
<CardTitle>Users</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4"></CardContent>
|
||||
<CardContent className="p-4">
|
||||
<UsersTable />
|
||||
</CardContent>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import moment from 'moment'
|
||||
|
||||
import { graphql } from '@/lib/gql/generates'
|
||||
import { useAuthenticatedGraphQLQuery } from '@/lib/tabby/gql'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '@/components/ui/table'
|
||||
|
||||
const listUsers = graphql(/* GraphQL */ `
|
||||
query ListUsers {
|
||||
users {
|
||||
email
|
||||
isAdmin
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export default function UsersTable() {
|
||||
const { data, mutate } = useAuthenticatedGraphQLQuery(listUsers)
|
||||
const users = data?.users
|
||||
|
||||
return (
|
||||
users && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Joined</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map((x, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell className="w-[300px] font-medium">{x.email}</TableCell>
|
||||
<TableCell>{moment.utc(x.createdAt).fromNow()}</TableCell>
|
||||
<TableCell>{x.isAdmin ? 'Admin' : 'Member'}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@ type Query {
|
|||
isAdminInitialized: Boolean!
|
||||
invitations: [Invitation!]!
|
||||
me: User!
|
||||
users: [User!]!
|
||||
}
|
||||
|
||||
type Invitation {
|
||||
|
|
@ -56,6 +57,7 @@ type User {
|
|||
email: String!
|
||||
isAdmin: Boolean!
|
||||
authToken: String!
|
||||
createdAt: DateTimeUtc!
|
||||
}
|
||||
|
||||
type Worker {
|
||||
|
|
|
|||
|
|
@ -276,6 +276,8 @@ pub trait AuthenticationService: Send + Sync {
|
|||
async fn delete_invitation(&self, id: i32) -> Result<i32>;
|
||||
|
||||
async fn reset_user_auth_token(&self, email: &str) -> Result<()>;
|
||||
|
||||
async fn list_users(&self) -> Result<Vec<User>>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ pub mod worker;
|
|||
use std::sync::Arc;
|
||||
|
||||
use auth::AuthenticationService;
|
||||
use chrono::{DateTime, Utc};
|
||||
use juniper::{
|
||||
graphql_object, graphql_value, EmptySubscription, FieldError, GraphQLObject, IntoFieldError,
|
||||
Object, RootNode, ScalarValue, Value,
|
||||
|
|
@ -118,6 +119,15 @@ impl Query {
|
|||
Err(CoreError::Unauthorized("Not logged in"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn users(ctx: &Context) -> Result<Vec<User>> {
|
||||
if let Some(claims) = &ctx.claims {
|
||||
if claims.is_admin {
|
||||
return Ok(ctx.locator.auth().list_users().await?);
|
||||
}
|
||||
}
|
||||
Err(CoreError::Unauthorized("Only admin is able to query users"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, GraphQLObject)]
|
||||
|
|
@ -125,6 +135,7 @@ pub struct User {
|
|||
pub email: String,
|
||||
pub is_admin: bool,
|
||||
pub auth_token: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
|
|||
|
|
@ -296,6 +296,11 @@ impl AuthenticationService for DbConn {
|
|||
async fn reset_user_auth_token(&self, email: &str) -> Result<()> {
|
||||
self.reset_user_auth_token_by_email(email).await
|
||||
}
|
||||
|
||||
async fn list_users(&self) -> Result<Vec<User>> {
|
||||
let users = self.list_users().await?;
|
||||
Ok(users.into_iter().map(|x| x.into()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
fn password_hash(raw: &str) -> password_hash::Result<String> {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ impl From<User> for schema::User {
|
|||
email: val.email,
|
||||
is_admin: val.is_admin,
|
||||
auth_token: val.auth_token,
|
||||
created_at: val.created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -147,6 +148,19 @@ impl DbConn {
|
|||
Ok(users)
|
||||
}
|
||||
|
||||
pub async fn list_users(&self) -> Result<Vec<User>> {
|
||||
let users = self
|
||||
.conn
|
||||
.call(move |c| {
|
||||
let mut stmt = c.prepare(&User::select("true"))?;
|
||||
let user_iter = stmt.query_map([], User::from_row)?;
|
||||
Ok(user_iter.filter_map(|x| x.ok()).collect::<Vec<_>>())
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
pub async fn verify_auth_token(&self, token: &str) -> bool {
|
||||
let token = token.to_owned();
|
||||
let id: Result<i32, _> = self
|
||||
|
|
|
|||
Loading…
Reference in New Issue