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