diff --git a/ee/tabby-ui/app/(dashboard)/team/components/team.tsx b/ee/tabby-ui/app/(dashboard)/team/components/team.tsx
index 7639703..b4546ac 100644
--- a/ee/tabby-ui/app/(dashboard)/team/components/team.tsx
+++ b/ee/tabby-ui/app/(dashboard)/team/components/team.tsx
@@ -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 (
-
+
Invites
@@ -19,7 +20,9 @@ export default function Team() {
Users
-
+
+
+
)
diff --git a/ee/tabby-ui/app/(dashboard)/team/components/user-table.tsx b/ee/tabby-ui/app/(dashboard)/team/components/user-table.tsx
new file mode 100644
index 0000000..d663183
--- /dev/null
+++ b/ee/tabby-ui/app/(dashboard)/team/components/user-table.tsx
@@ -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 && (
+
+
+
+ Email
+ Joined
+ Role
+
+
+
+ {users.map((x, i) => (
+
+ {x.email}
+ {moment.utc(x.createdAt).fromNow()}
+ {x.isAdmin ? 'Admin' : 'Member'}
+
+ ))}
+
+
+ )
+ )
+}
diff --git a/ee/tabby-webserver/graphql/schema.graphql b/ee/tabby-webserver/graphql/schema.graphql
index 8559f98..5c41e35 100644
--- a/ee/tabby-webserver/graphql/schema.graphql
+++ b/ee/tabby-webserver/graphql/schema.graphql
@@ -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 {
diff --git a/ee/tabby-webserver/src/schema/auth.rs b/ee/tabby-webserver/src/schema/auth.rs
index 4e12ab7..7a48e7f 100644
--- a/ee/tabby-webserver/src/schema/auth.rs
+++ b/ee/tabby-webserver/src/schema/auth.rs
@@ -276,6 +276,8 @@ pub trait AuthenticationService: Send + Sync {
async fn delete_invitation(&self, id: i32) -> Result
;
async fn reset_user_auth_token(&self, email: &str) -> Result<()>;
+
+ async fn list_users(&self) -> Result>;
}
#[cfg(test)]
diff --git a/ee/tabby-webserver/src/schema/mod.rs b/ee/tabby-webserver/src/schema/mod.rs
index ccba4d7..f7f3a29 100644
--- a/ee/tabby-webserver/src/schema/mod.rs
+++ b/ee/tabby-webserver/src/schema/mod.rs
@@ -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> {
+ 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,
}
#[derive(Default)]
diff --git a/ee/tabby-webserver/src/service/auth.rs b/ee/tabby-webserver/src/service/auth.rs
index 98f81bb..9b2d9d5 100644
--- a/ee/tabby-webserver/src/service/auth.rs
+++ b/ee/tabby-webserver/src/service/auth.rs
@@ -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> {
+ let users = self.list_users().await?;
+ Ok(users.into_iter().map(|x| x.into()).collect())
+ }
}
fn password_hash(raw: &str) -> password_hash::Result {
diff --git a/ee/tabby-webserver/src/service/db/users.rs b/ee/tabby-webserver/src/service/db/users.rs
index 64e24ff..7efdd87 100644
--- a/ee/tabby-webserver/src/service/db/users.rs
+++ b/ee/tabby-webserver/src/service/db/users.rs
@@ -48,6 +48,7 @@ impl From 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> {
+ 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::>())
+ })
+ .await?;
+
+ Ok(users)
+ }
+
pub async fn verify_auth_token(&self, token: &str) -> bool {
let token = token.to_owned();
let id: Result = self