diff --git a/frontend/app/src/pages/main/v1/index.tsx b/frontend/app/src/pages/main/v1/index.tsx index 574bd5f89f..abdb383eba 100644 --- a/frontend/app/src/pages/main/v1/index.tsx +++ b/frontend/app/src/pages/main/v1/index.tsx @@ -6,7 +6,7 @@ import { Loading } from '@/components/v1/ui/loading'; import useCloud from '@/hooks/use-cloud'; import { useOrganizations } from '@/hooks/use-organizations'; import { useTenantDetails } from '@/hooks/use-tenant'; -import { queries } from '@/lib/api'; +import { queries, TenantMemberRole } from '@/lib/api'; import { MembershipsContextType, UserContextType, @@ -19,7 +19,7 @@ import { ReactNode, useEffect, useMemo } from 'react'; export function MainShell({ children }: { children?: ReactNode }) { const ctx = useOutletContext(); const { user, memberships } = ctx; - const { tenantId, isUserUniverseLoaded } = useTenantDetails(); + const { tenantId, isUserUniverseLoaded, membership } = useTenantDetails(); const { cloud, featureFlags, isCloudEnabled } = useCloud(tenantId); const managedWorkerEnabled = featureFlags?.['managed-worker'] === 'true'; const { getOrganizationIdForTenant } = useOrganizations(); @@ -29,6 +29,9 @@ export function MainShell({ children }: { children?: ReactNode }) { ? (getOrganizationIdForTenant(tenantId) ?? undefined) : undefined : undefined; + const canManageApiTokens = + membership === TenantMemberRole.OWNER || + membership === TenantMemberRole.ADMIN; useEffect(() => { if (!tenantId) { @@ -47,8 +50,15 @@ export function MainShell({ children }: { children?: ReactNode }) { managedWorkerEnabled, isCloudEnabled, orgId, + canManageApiTokens, }), - [cloud?.canBill, managedWorkerEnabled, isCloudEnabled, orgId], + [ + cloud?.canBill, + managedWorkerEnabled, + isCloudEnabled, + orgId, + canManageApiTokens, + ], ); const childCtx = useContextFromParent({ diff --git a/frontend/app/src/pages/main/v1/side-nav-items.tsx b/frontend/app/src/pages/main/v1/side-nav-items.tsx index 070f8554a6..2f2bb8ef53 100644 --- a/frontend/app/src/pages/main/v1/side-nav-items.tsx +++ b/frontend/app/src/pages/main/v1/side-nav-items.tsx @@ -24,6 +24,7 @@ export function sideNavItems(opts: { managedWorkerEnabled?: boolean; isCloudEnabled?: boolean; orgId?: string; + canManageApiTokens?: boolean; }): SideNavSection[] { return [ { @@ -279,16 +280,20 @@ export function sideNavItems(opts: { /> ), }, - { - key: 'settings-api-tokens', - name: 'API Tokens', - to: appRoutes.tenantSettingsApiTokensRoute.to, - icon: ({ collapsed }: { collapsed: boolean }) => ( - - ), - }, + ...(opts.canManageApiTokens + ? [ + { + key: 'settings-api-tokens', + name: 'API Tokens', + to: appRoutes.tenantSettingsApiTokensRoute.to, + icon: ({ collapsed }: { collapsed: boolean }) => ( + + ), + }, + ] + : []), ...(opts.canBill && opts.orgId ? [ { diff --git a/frontend/app/src/pages/main/v1/tenant-settings/api-tokens/index.tsx b/frontend/app/src/pages/main/v1/tenant-settings/api-tokens/index.tsx index cdfed3beec..7ab425e00f 100644 --- a/frontend/app/src/pages/main/v1/tenant-settings/api-tokens/index.tsx +++ b/frontend/app/src/pages/main/v1/tenant-settings/api-tokens/index.tsx @@ -5,23 +5,57 @@ import { RevokeTokenForm } from './components/revoke-token-form'; import { EmptyState } from '@/components/v1/molecules/empty-state/empty-state'; import RelativeDate from '@/components/v1/molecules/relative-date'; import { SimpleTable } from '@/components/v1/molecules/simple-table/simple-table'; +import { Alert, AlertDescription, AlertTitle } from '@/components/v1/ui/alert'; import { Button } from '@/components/v1/ui/button'; import { Dialog } from '@/components/v1/ui/dialog'; -import { useCurrentTenantId } from '@/hooks/use-tenant'; -import api, { APIToken, CreateAPITokenRequest, queries } from '@/lib/api'; +import { useCurrentTenantId, useTenantDetails } from '@/hooks/use-tenant'; +import api, { + APIToken, + CreateAPITokenRequest, + queries, + TenantMemberRole, +} from '@/lib/api'; import { useApiError } from '@/lib/hooks'; +import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; import { useMutation, useQuery } from '@tanstack/react-query'; import { useState, useMemo } from 'react'; export default function APITokens() { const { tenantId } = useCurrentTenantId(); + const { membership } = useTenantDetails(); + const canManageApiTokens = + membership === TenantMemberRole.OWNER || + membership === TenantMemberRole.ADMIN; const [showTokenDialog, setShowTokenDialog] = useState(false); const [revokeToken, setRevokeToken] = useState(null); const listTokensQuery = useQuery({ ...queries.tokens.list(tenantId), + enabled: canManageApiTokens, }); + if (!canManageApiTokens) { + return ( +
+
+ + + + + You do not have permission to manage API tokens. + + + Only tenant owners and admins can view or manage API tokens. + + +
+
+ ); + } + const tokenColumns = useMemo( () => [ {