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 { CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
|
||||||
import InvitationTable from './invitation-table'
|
import InvitationTable from './invitation-table'
|
||||||
|
import UsersTable from './user-table'
|
||||||
|
|
||||||
export default function Team() {
|
export default function Team() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="xl:max-w-[750px]">
|
||||||
<div>
|
<div>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Invites</CardTitle>
|
<CardTitle>Invites</CardTitle>
|
||||||
|
|
@ -19,7 +20,9 @@ export default function Team() {
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Users</CardTitle>
|
<CardTitle>Users</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-4"></CardContent>
|
<CardContent className="p-4">
|
||||||
|
<UsersTable />
|
||||||
|
</CardContent>
|
||||||
</div>
|
</div>
|
||||||
</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!
|
isAdminInitialized: Boolean!
|
||||||
invitations: [Invitation!]!
|
invitations: [Invitation!]!
|
||||||
me: User!
|
me: User!
|
||||||
|
users: [User!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Invitation {
|
type Invitation {
|
||||||
|
|
@ -56,6 +57,7 @@ type User {
|
||||||
email: String!
|
email: String!
|
||||||
isAdmin: Boolean!
|
isAdmin: Boolean!
|
||||||
authToken: String!
|
authToken: String!
|
||||||
|
createdAt: DateTimeUtc!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Worker {
|
type Worker {
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,8 @@ pub trait AuthenticationService: Send + Sync {
|
||||||
async fn delete_invitation(&self, id: i32) -> Result<i32>;
|
async fn delete_invitation(&self, id: i32) -> Result<i32>;
|
||||||
|
|
||||||
async fn reset_user_auth_token(&self, email: &str) -> Result<()>;
|
async fn reset_user_auth_token(&self, email: &str) -> Result<()>;
|
||||||
|
|
||||||
|
async fn list_users(&self) -> Result<Vec<User>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ pub mod worker;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use auth::AuthenticationService;
|
use auth::AuthenticationService;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use juniper::{
|
use juniper::{
|
||||||
graphql_object, graphql_value, EmptySubscription, FieldError, GraphQLObject, IntoFieldError,
|
graphql_object, graphql_value, EmptySubscription, FieldError, GraphQLObject, IntoFieldError,
|
||||||
Object, RootNode, ScalarValue, Value,
|
Object, RootNode, ScalarValue, Value,
|
||||||
|
|
@ -118,6 +119,15 @@ impl Query {
|
||||||
Err(CoreError::Unauthorized("Not logged in"))
|
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)]
|
#[derive(Debug, GraphQLObject)]
|
||||||
|
|
@ -125,6 +135,7 @@ pub struct User {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub is_admin: bool,
|
pub is_admin: bool,
|
||||||
pub auth_token: String,
|
pub auth_token: String,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
|
||||||
|
|
@ -296,6 +296,11 @@ impl AuthenticationService for DbConn {
|
||||||
async fn reset_user_auth_token(&self, email: &str) -> Result<()> {
|
async fn reset_user_auth_token(&self, email: &str) -> Result<()> {
|
||||||
self.reset_user_auth_token_by_email(email).await
|
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> {
|
fn password_hash(raw: &str) -> password_hash::Result<String> {
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ impl From<User> for schema::User {
|
||||||
email: val.email,
|
email: val.email,
|
||||||
is_admin: val.is_admin,
|
is_admin: val.is_admin,
|
||||||
auth_token: val.auth_token,
|
auth_token: val.auth_token,
|
||||||
|
created_at: val.created_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -147,6 +148,19 @@ impl DbConn {
|
||||||
Ok(users)
|
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 {
|
pub async fn verify_auth_token(&self, token: &str) -> bool {
|
||||||
let token = token.to_owned();
|
let token = token.to_owned();
|
||||||
let id: Result<i32, _> = self
|
let id: Result<i32, _> = self
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue