feat: Admin V2 API keys (#6883)

Add API key management UI for V2

This PR only adds publishable API key UI. Secret API key management will come in a follow-up PR.
This commit is contained in:
Oli Juhl authored and GitHub committed 2024-04-05 09:55:59 +00:00
1 parent e915169e11
commit eadc5e8a79
36 files changed
+265 -179

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
---
"@medusajs/medusa": patch
"medusa-react": patch
"@medusajs/admin": patch
---
feat: Admin V2 API keys
@@ -803,9 +803,10 @@
"deleteSalesChannelWarning": "You are about to delete the sales channel {{name}}. This action cannot be undone." "deleteSalesChannelWarning": "You are about to delete the sales channel {{name}}. This action cannot be undone."
}, },
"apiKeyManagement": { "apiKeyManagement": {
"domain": "API Key Management", "domainTitle": "API Keys",
"domain": "{{ keyType }} API Keys",
"createKey": "Create key", "createKey": "Create key",
"createPublishableApiKey": "Create Publishable API Key", "createPublishableApiKey": "Create {{ keyType }} API Key",
"editKey": "Edit key", "editKey": "Edit key",
"revoke": "Revoke", "revoke": "Revoke",
"publishableApiKeyHint": "Publishable API keys are used to limit the scope of requests to specific sales channels.", "publishableApiKeyHint": "Publishable API keys are used to limit the scope of requests to specific sales channels.",
@@ -64,7 +64,7 @@ const useDeveloperRoutes = (): NavItemProps[] => {
return useMemo( return useMemo(
() => [ () => [
{ {
label: t("apiKeyManagement.domain"), label: t("apiKeyManagement.domainTitle"),
to: "/settings/api-key-management", to: "/settings/api-key-management",
}, },
{ {
@@ -111,7 +111,7 @@ const useLinks = (): CommandGroupProps[] => {
label: t("salesChannels.domain"), label: t("salesChannels.domain"),
}, },
{ {
label: t("apiKeyManagement.domain"), label: t("apiKeyManagement.domainTitle"),
}, },
], ],
}, },
@@ -0,0 +1,4 @@
export function upperCaseFirst(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
@@ -768,58 +768,6 @@ export const v1Routes: RouteObject[] = [
}, },
], ],
}, },
{
path: "api-key-management",
element: <Outlet />,
handle: {
crumb: () => "API Key Management",
},
children: [
{
path: "",
lazy: () =>
import(
"../../routes/api-key-management/api-key-management-list"
),
children: [
{
path: "create",
lazy: () =>
import(
"../../routes/api-key-management/api-key-management-create"
),
},
],
},
{
path: ":id",
lazy: () =>
import(
"../../routes/api-key-management/api-key-management-detail"
),
handle: {
crumb: (data: AdminPublishableApiKeysRes) =>
data.publishable_api_key.title,
},
children: [
{
path: "edit",
lazy: () =>
import(
"../../routes/api-key-management/api-key-management-edit"
),
},
{
path: "add-sales-channels",
lazy: () =>
import(
"../../routes/api-key-management/api-key-management-add-sales-channels"
),
},
],
},
],
},
...settingsExtensions, ...settingsExtensions,
], ],
}, },
@@ -8,6 +8,7 @@ import { Outlet } from "react-router-dom"
import { SearchProvider } from "../search-provider" import { SearchProvider } from "../search-provider"
import { SettingsLayout } from "../../components/layout/settings-layout" import { SettingsLayout } from "../../components/layout/settings-layout"
import { SidebarProvider } from "../sidebar-provider" import { SidebarProvider } from "../sidebar-provider"
import { ApiKeyDTO } from "@medusajs/types"
import { Spinner } from "@medusajs/icons" import { Spinner } from "@medusajs/icons"
import { useV2Session } from "../../lib/api-v2" import { useV2Session } from "../../lib/api-v2"
@@ -332,6 +333,57 @@ export const v2Routes: RouteObject[] = [
}, },
], ],
}, },
{
path: "api-key-management",
element: <Outlet />,
handle: {
crumb: () => "API Key Management",
},
children: [
{
path: "",
lazy: () =>
import(
"../../v2-routes/api-key-management/api-key-management-list"
),
children: [
{
path: "create",
lazy: () =>
import(
"../../v2-routes/api-key-management/api-key-management-create"
),
},
],
},
{
path: ":id",
lazy: () =>
import(
"../../v2-routes/api-key-management/api-key-management-detail"
),
handle: {
crumb: (data: { api_key: ApiKeyDTO }) => data.api_key.title,
},
children: [
{
path: "edit",
lazy: () =>
import(
"../../v2-routes/api-key-management/api-key-management-edit"
),
},
{
path: "add-sales-channels",
lazy: () =>
import(
"../../v2-routes/api-key-management/api-key-management-add-sales-channels"
),
},
],
},
],
},
], ],
}, },
], ],
@@ -1,26 +0,0 @@
import { useAdminPublishableApiKeySalesChannels } from "medusa-react"
import { useParams } from "react-router-dom"
import { RouteFocusModal } from "../../../components/route-modal"
import { AddSalesChannelsToApiKeyForm } from "./components"
export const ApiKeyManagementAddSalesChannels = () => {
const { id } = useParams()
const { sales_channels, isLoading, isError, error } =
useAdminPublishableApiKeySalesChannels(id!)
if (isError) {
throw error
}
return (
<RouteFocusModal>
{!isLoading && sales_channels && (
<AddSalesChannelsToApiKeyForm
apiKey={id!}
preSelected={sales_channels.map((sc) => sc.id)}
/>
)}
</RouteFocusModal>
)
}
@@ -0,0 +1,36 @@
import { adminPublishableApiKeysKeys, useAdminCustomQuery } from "medusa-react"
import { useParams } from "react-router-dom"
import { RouteFocusModal } from "../../../components/route-modal"
import { AddSalesChannelsToApiKeyForm } from "./components"
export const ApiKeyManagementAddSalesChannels = () => {
const { id } = useParams()
const { data, isLoading, isError, error } = useAdminCustomQuery(
`/api-keys/${id}`,
[adminPublishableApiKeysKeys.detailSalesChannels(id!)],
{
fields: "id,*sales_channels",
},
{
keepPreviousData: true,
}
)
if (isError) {
throw error
}
const salesChannels = data?.api_key?.sales_channels
return (
<RouteFocusModal>
{!isLoading && salesChannels && (
<AddSalesChannelsToApiKeyForm
apiKey={id!}
preSelected={salesChannels.map((sc) => sc.id)}
/>
)}
</RouteFocusModal>
)
}
@@ -18,7 +18,9 @@ import {
useReactTable, useReactTable,
} from "@tanstack/react-table" } from "@tanstack/react-table"
import { import {
adminPublishableApiKeysKeys,
useAdminAddPublishableKeySalesChannelsBatch, useAdminAddPublishableKeySalesChannelsBatch,
useAdminCustomPost,
useAdminSalesChannels, useAdminSalesChannels,
} from "medusa-react" } from "medusa-react"
import { useEffect, useMemo, useState } from "react" import { useEffect, useMemo, useState } from "react"
@@ -61,8 +63,10 @@ export const AddSalesChannelsToApiKeyForm = ({
const { setValue } = form const { setValue } = form
const { mutateAsync, isLoading: isMutating } = const { mutateAsync, isLoading: isMutating } = useAdminCustomPost(
useAdminAddPublishableKeySalesChannelsBatch(apiKey) `/api-keys/${apiKey}/sales-channels/batch/add`,
[adminPublishableApiKeysKeys.detailSalesChannels(apiKey)]
)
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({ const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
@@ -128,7 +132,7 @@ export const AddSalesChannelsToApiKeyForm = ({
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync( await mutateAsync(
{ {
sales_channel_ids: values.sales_channel_ids.map((p) => ({ id: p })), sales_channel_ids: values.sales_channel_ids,
}, },
{ {
onSuccess: () => { onSuccess: () => {
@@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Button, Heading, Input, Text } from "@medusajs/ui" import { Button, Heading, Input, Text } from "@medusajs/ui"
import { useAdminCreatePublishableApiKey } from "medusa-react" import { adminPublishableApiKeysKeys, useAdminCustomPost } from "medusa-react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
@@ -26,14 +26,19 @@ export const CreatePublishableApiKeyForm = () => {
resolver: zodResolver(CreatePublishableApiKeySchema), resolver: zodResolver(CreatePublishableApiKeySchema),
}) })
const { mutateAsync, isLoading } = useAdminCreatePublishableApiKey() const { mutateAsync, isLoading } = useAdminCustomPost(`/admin/api-keys`, [
adminPublishableApiKeysKeys.lists(),
])
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(values, { await mutateAsync(
onSuccess: ({ publishable_api_key }) => { { title: values.title, type: "publishable" },
handleSuccess(`/settings/api-key-management/${publishable_api_key.id}`) {
}, onSuccess: () => {
}) handleSuccess(`/settings/api-key-management`)
},
}
)
}) })
return ( return (
@@ -1,4 +1,4 @@
import { useAdminPublishableApiKey } from "medusa-react" import { adminPublishableApiKeysKeys, useAdminCustomQuery } from "medusa-react"
import { Outlet, json, useLoaderData, useParams } from "react-router-dom" import { Outlet, json, useLoaderData, useParams } from "react-router-dom"
import { JsonViewSection } from "../../../components/common/json-view-section" import { JsonViewSection } from "../../../components/common/json-view-section"
import { ApiKeyGeneralSection } from "./components/api-key-general-section" import { ApiKeyGeneralSection } from "./components/api-key-general-section"
@@ -11,16 +11,20 @@ export const ApiKeyManagementDetail = () => {
> >
const { id } = useParams() const { id } = useParams()
const { publishable_api_key, isLoading, isError, error } = const { data, isLoading, isError, error } = useAdminCustomQuery(
useAdminPublishableApiKey(id!, { `/api-keys/${id}`,
initialData, [adminPublishableApiKeysKeys.detail(id!)],
}) undefined,
{
initialData: initialData,
}
)
if (isLoading) { if (isLoading) {
return <div>Loading...</div> return <div>Loading...</div>
} }
if (isError || !publishable_api_key) { if (isError || !data?.api_key) {
if (error) { if (error) {
throw error throw error
} }
@@ -30,9 +34,9 @@ export const ApiKeyManagementDetail = () => {
return ( return (
<div className="flex flex-col gap-y-2"> <div className="flex flex-col gap-y-2">
<ApiKeyGeneralSection apiKey={publishable_api_key} /> <ApiKeyGeneralSection apiKey={data?.api_key} />
<ApiKeySalesChannelSection apiKey={publishable_api_key} /> <ApiKeySalesChannelSection apiKey={data?.api_key} />
<JsonViewSection data={publishable_api_key} /> <JsonViewSection data={data?.api_key} />
<Outlet /> <Outlet />
</div> </div>
) )
@@ -9,6 +9,7 @@ import {
usePrompt, usePrompt,
} from "@medusajs/ui" } from "@medusajs/ui"
import { import {
useAdminCustomQuery,
useAdminDeletePublishableApiKey, useAdminDeletePublishableApiKey,
useAdminRevokePublishableApiKey, useAdminRevokePublishableApiKey,
useAdminUser, useAdminUser,
@@ -18,9 +19,10 @@ import { useNavigate } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu" import { ActionMenu } from "../../../../../components/common/action-menu"
import { Skeleton } from "../../../../../components/common/skeleton" import { Skeleton } from "../../../../../components/common/skeleton"
import { UserLink } from "../../../../../components/common/user-link" import { UserLink } from "../../../../../components/common/user-link"
import { ApiKeyDTO } from "@medusajs/types"
type ApiKeyGeneralSectionProps = { type ApiKeyGeneralSectionProps = {
apiKey: PublishableApiKey apiKey: ApiKeyDTO
} }
export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => { export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
@@ -121,10 +123,10 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
</Text> </Text>
<div className="bg-ui-bg-subtle border-ui-border-base box-border flex w-fit cursor-default items-center gap-x-0.5 overflow-hidden rounded-full border pl-2 pr-1"> <div className="bg-ui-bg-subtle border-ui-border-base box-border flex w-fit cursor-default items-center gap-x-0.5 overflow-hidden rounded-full border pl-2 pr-1">
<Text size="xsmall" leading="compact" className="truncate"> <Text size="xsmall" leading="compact" className="truncate">
{apiKey.id} {apiKey.redacted}
</Text> </Text>
<Copy <Copy
content={apiKey.id} content={apiKey.token}
variant="mini" variant="mini"
className="text-ui-fg-subtle" className="text-ui-fg-subtle"
/> />
@@ -149,9 +151,12 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
} }
const ActionBy = ({ userId }: { userId: string | null }) => { const ActionBy = ({ userId }: { userId: string | null }) => {
const { user, isLoading, isError, error } = useAdminUser(userId!, { const { data, isLoading, isError, error } = useAdminCustomQuery(
enabled: !!userId, `/users/${userId}`,
}) [],
{},
{ enabled: !!userId }
)
if (!userId) { if (!userId) {
return ( return (
@@ -174,7 +179,7 @@ const ActionBy = ({ userId }: { userId: string | null }) => {
) )
} }
if (!user) { if (!data?.user) {
return ( return (
<Text size="small" className="text-ui-fg-subtle"> <Text size="small" className="text-ui-fg-subtle">
- -
@@ -182,5 +187,5 @@ const ActionBy = ({ userId }: { userId: string | null }) => {
) )
} }
return <UserLink {...user} /> return <UserLink {...data.user} />
} }
@@ -20,7 +20,9 @@ import {
useReactTable, useReactTable,
} from "@tanstack/react-table" } from "@tanstack/react-table"
import { import {
useAdminPublishableApiKeySalesChannels, adminPublishableApiKeysKeys,
useAdminCustomPost,
useAdminCustomQuery,
useAdminRemovePublishableKeySalesChannelsBatch, useAdminRemovePublishableKeySalesChannelsBatch,
} from "medusa-react" } from "medusa-react"
import { useMemo, useState } from "react" import { useMemo, useState } from "react"
@@ -34,9 +36,10 @@ import {
import { Query } from "../../../../../components/filtering/query" import { Query } from "../../../../../components/filtering/query"
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
import { useQueryParams } from "../../../../../hooks/use-query-params" import { useQueryParams } from "../../../../../hooks/use-query-params"
import { ApiKeyDTO } from "@medusajs/types"
type ApiKeySalesChannelSectionProps = { type ApiKeySalesChannelSectionProps = {
apiKey: PublishableApiKey apiKey: ApiKeyDTO
} }
const PAGE_SIZE = 10 const PAGE_SIZE = 10
@@ -64,23 +67,28 @@ export const ApiKeySalesChannelSection = ({
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}) const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const params = useQueryParams(["q"]) const params = useQueryParams(["q"])
const { sales_channels, isLoading, isError, error } =
useAdminPublishableApiKeySalesChannels(
apiKey.id,
{
...params,
},
{
keepPreviousData: true,
}
)
const count = sales_channels?.length || 0 const query = {
...params,
fields: "id,*sales_channels",
}
const { data, isLoading, isError, error } = useAdminCustomQuery(
`/api-keys/${apiKey.id}`,
[adminPublishableApiKeysKeys.detailSalesChannels(apiKey.id, query)],
query,
{
keepPreviousData: true,
}
)
const salesChannels = data?.api_key?.sales_channels
const count = salesChannels?.length || 0
const columns = useColumns() const columns = useColumns()
const table = useReactTable({ const table = useReactTable({
data: sales_channels ?? [], data: salesChannels ?? [],
columns, columns,
pageCount: Math.ceil(count / PAGE_SIZE), pageCount: Math.ceil(count / PAGE_SIZE),
state: { state: {
@@ -97,8 +105,9 @@ export const ApiKeySalesChannelSection = ({
}, },
}) })
const { mutateAsync } = useAdminRemovePublishableKeySalesChannelsBatch( const { mutateAsync } = useAdminCustomPost(
apiKey.id `/api-keys/${apiKey.id}/sales-channels/batch/remove`,
[adminPublishableApiKeysKeys.detailSalesChannels(apiKey.id)]
) )
const handleRemove = async () => { const handleRemove = async () => {
@@ -119,7 +128,7 @@ export const ApiKeySalesChannelSection = ({
await mutateAsync( await mutateAsync(
{ {
sales_channel_ids: keys.map((k) => ({ id: k })), sales_channel_ids: keys,
}, },
{ {
onSuccess: () => { onSuccess: () => {
@@ -129,7 +138,7 @@ export const ApiKeySalesChannelSection = ({
) )
} }
const noRecords = !isLoading && !sales_channels?.length && !params.q const noRecords = !isLoading && !salesChannels?.length && !params.q
if (isError) { if (isError) {
throw error throw error
@@ -155,7 +164,7 @@ export const ApiKeySalesChannelSection = ({
<NoRecords /> <NoRecords />
) : ( ) : (
<div> <div>
{!isLoading && sales_channels?.length !== 0 ? ( {!isLoading && salesChannels?.length !== 0 ? (
<Table> <Table>
<Table.Header className="border-t-0"> <Table.Header className="border-t-0">
{table.getHeaderGroups().map((headerGroup) => { {table.getHeaderGroups().map((headerGroup) => {
@@ -1,13 +1,13 @@
import { AdminPublishableApiKeysRes } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js" import { Response } from "@medusajs/medusa-js"
import { adminProductKeys } from "medusa-react" import { adminPublishableApiKeysKeys } from "medusa-react"
import { LoaderFunctionArgs } from "react-router-dom" import { LoaderFunctionArgs } from "react-router-dom"
import { ApiKeyDTO } from "@medusajs/types"
import { medusa, queryClient } from "../../../lib/medusa" import { medusa, queryClient } from "../../../lib/medusa"
const apiKeyDetailQuery = (id: string) => ({ const apiKeyDetailQuery = (id: string) => ({
queryKey: adminProductKeys.detail(id), queryKey: adminPublishableApiKeysKeys.detail(id),
queryFn: async () => medusa.admin.publishableApiKeys.retrieve(id), queryFn: async () => medusa.admin.custom.get(`/api-keys/${id}`),
}) })
export const apiKeyLoader = async ({ params }: LoaderFunctionArgs) => { export const apiKeyLoader = async ({ params }: LoaderFunctionArgs) => {
@@ -15,7 +15,7 @@ export const apiKeyLoader = async ({ params }: LoaderFunctionArgs) => {
const query = apiKeyDetailQuery(id!) const query = apiKeyDetailQuery(id!)
return ( return (
queryClient.getQueryData<Response<AdminPublishableApiKeysRes>>( queryClient.getQueryData<Response<{ api_key: ApiKeyDTO }>>(
query.queryKey query.queryKey
) ?? (await queryClient.fetchQuery(query)) ) ?? (await queryClient.fetchQuery(query))
) )
@@ -1,7 +1,7 @@
import { Heading } from "@medusajs/ui" import { Heading } from "@medusajs/ui"
import { useAdminPublishableApiKey } from "medusa-react" import { adminPublishableApiKeysKeys, useAdminCustomQuery } from "medusa-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom" import { json, useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal" import { RouteDrawer } from "../../../components/route-modal"
import { EditApiKeyForm } from "./components/edit-api-key-form" import { EditApiKeyForm } from "./components/edit-api-key-form"
@@ -9,11 +9,17 @@ export const ApiKeyManagementEdit = () => {
const { id } = useParams() const { id } = useParams()
const { t } = useTranslation() const { t } = useTranslation()
const { publishable_api_key, isLoading, isError, error } = const { data, isLoading, isError, error } = useAdminCustomQuery(
useAdminPublishableApiKey(id!) `/api-keys/${id}`,
[adminPublishableApiKeysKeys.detail(id!)]
)
if (isError) { if (isError || !data?.api_key) {
throw error if (error) {
throw error
}
throw json("An unknown error has occured", 500)
} }
return ( return (
@@ -21,8 +27,8 @@ export const ApiKeyManagementEdit = () => {
<RouteDrawer.Header> <RouteDrawer.Header>
<Heading>{t("apiKeyManagement.editKey")}</Heading> <Heading>{t("apiKeyManagement.editKey")}</Heading>
</RouteDrawer.Header> </RouteDrawer.Header>
{!isLoading && publishable_api_key && ( {!isLoading && !!data?.api_key && (
<EditApiKeyForm apiKey={publishable_api_key} /> <EditApiKeyForm apiKey={data.api_key} />
)} )}
</RouteDrawer> </RouteDrawer>
) )
@@ -1,7 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import type { PublishableApiKey } from "@medusajs/medusa"
import { Button, Input } from "@medusajs/ui" import { Button, Input } from "@medusajs/ui"
import { useAdminUpdatePublishableApiKey } from "medusa-react" import { adminPublishableApiKeysKeys, useAdminCustomPost } from "medusa-react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
@@ -11,9 +10,10 @@ import {
RouteDrawer, RouteDrawer,
useRouteModal, useRouteModal,
} from "../../../../../components/route-modal" } from "../../../../../components/route-modal"
import { ApiKeyDTO } from "@medusajs/types"
type EditApiKeyFormProps = { type EditApiKeyFormProps = {
apiKey: PublishableApiKey apiKey: ApiKeyDTO
} }
const EditApiKeySchema = zod.object({ const EditApiKeySchema = zod.object({
@@ -31,7 +31,14 @@ export const EditApiKeyForm = ({ apiKey }: EditApiKeyFormProps) => {
resolver: zodResolver(EditApiKeySchema), resolver: zodResolver(EditApiKeySchema),
}) })
const { mutateAsync, isLoading } = useAdminUpdatePublishableApiKey(apiKey.id) const { mutateAsync, isLoading } = useAdminCustomPost(
`/api-keys/${apiKey.id}`,
[
adminPublishableApiKeysKeys.lists(),
adminPublishableApiKeysKeys.detail(apiKey.id),
adminPublishableApiKeysKeys.details(),
]
)
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(data, { await mutateAsync(data, {
@@ -1,10 +1,13 @@
import { Outlet } from "react-router-dom" import { Outlet } from "react-router-dom"
import { ApiKeyManagementListTable } from "./components/api-key-management-list-table" import { ApiKeyManagementListTable } from "./components/api-key-management-list-table"
// TODO: Add secret API keys
export const ApiKeyManagementList = () => { export const ApiKeyManagementList = () => {
return ( return (
<div className="flex flex-col gap-y-2"> <div className="flex flex-col gap-y-2">
<ApiKeyManagementListTable /> <ApiKeyManagementListTable keyType="publishable" />
{/* <ApiKeyManagementListTable keyType="secret" /> */}
<Outlet /> <Outlet />
</div> </div>
) )
@@ -1,5 +1,5 @@
import { Button, Container, Heading } from "@medusajs/ui" import { Button, Container, Heading } from "@medusajs/ui"
import { useAdminPublishableApiKeys } from "medusa-react" import { adminPublishableApiKeysKeys, useAdminCustomQuery } from "medusa-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import { DataTable } from "../../../../../components/table/data-table" import { DataTable } from "../../../../../components/table/data-table"
@@ -7,30 +7,40 @@ import { useDataTable } from "../../../../../hooks/use-data-table"
import { useApiKeyManagementTableColumns } from "./use-api-key-management-table-columns" import { useApiKeyManagementTableColumns } from "./use-api-key-management-table-columns"
import { useApiKeyManagementTableFilters } from "./use-api-key-management-table-filters" import { useApiKeyManagementTableFilters } from "./use-api-key-management-table-filters"
import { useApiKeyManagementTableQuery } from "./use-api-key-management-table-query" import { useApiKeyManagementTableQuery } from "./use-api-key-management-table-query"
import { upperCaseFirst } from "../../../../../lib/uppercase-first"
const PAGE_SIZE = 20 const PAGE_SIZE = 20
export const ApiKeyManagementListTable = () => { export const ApiKeyManagementListTable = ({
keyType,
}: {
keyType: "secret" | "publishable"
}) => {
const { t } = useTranslation() const { t } = useTranslation()
const { searchParams, raw } = useApiKeyManagementTableQuery({ const { searchParams, raw } = useApiKeyManagementTableQuery({
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
}) })
const { publishable_api_keys, count, isLoading, isError, error } =
useAdminPublishableApiKeys( const query = {
{ ...searchParams,
...searchParams, type: keyType,
}, fields:
{ "id,title,redacted,token,type,created_at,updated_at,revoked_at,last_used_at,created_by,revoked_by",
keepPreviousData: true, }
}
) // @ts-ignore
const { data, count, isLoading, isError, error } = useAdminCustomQuery(
"/api-keys",
[adminPublishableApiKeysKeys.list(query)],
query
)
const filters = useApiKeyManagementTableFilters() const filters = useApiKeyManagementTableFilters()
const columns = useApiKeyManagementTableColumns() const columns = useApiKeyManagementTableColumns()
const { table } = useDataTable({ const { table } = useDataTable({
data: publishable_api_keys || [], data: data?.api_keys || [],
columns, columns,
count, count,
enablePagination: true, enablePagination: true,
@@ -49,7 +59,9 @@ export const ApiKeyManagementListTable = () => {
return ( return (
<Container className="divide-y p-0"> <Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4"> <div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("apiKeyManagement.domain")}</Heading> <Heading level="h2">
{t(`apiKeyManagement.domain`, { keyType: upperCaseFirst(keyType) })}
</Heading>
<Link to="create"> <Link to="create">
<Button variant="secondary" size="small"> <Button variant="secondary" size="small">
{t("actions.create")} {t("actions.create")}
@@ -3,6 +3,9 @@ import { PublishableApiKey } from "@medusajs/medusa"
import { Copy, Text, usePrompt } from "@medusajs/ui" import { Copy, Text, usePrompt } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table" import { createColumnHelper } from "@tanstack/react-table"
import { import {
adminPublishableApiKeysKeys,
useAdminCustomDelete,
useAdminCustomPost,
useAdminDeletePublishableApiKey, useAdminDeletePublishableApiKey,
useAdminRevokePublishableApiKey, useAdminRevokePublishableApiKey,
} from "medusa-react" } from "medusa-react"
@@ -12,8 +15,9 @@ import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu" import { ActionMenu } from "../../../../../components/common/action-menu"
import { DateCell } from "../../../../../components/table/table-cells/common/date-cell" import { DateCell } from "../../../../../components/table/table-cells/common/date-cell"
import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell" import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
import { ApiKeyDTO } from "@medusajs/types"
const columnHelper = createColumnHelper<PublishableApiKey>() const columnHelper = createColumnHelper<ApiKeyDTO>()
export const useApiKeyManagementTableColumns = () => { export const useApiKeyManagementTableColumns = () => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -28,9 +32,9 @@ export const useApiKeyManagementTableColumns = () => {
</div> </div>
), ),
}), }),
columnHelper.accessor("id", { columnHelper.accessor("redacted", {
header: "Key", header: "Token",
cell: ({ getValue }) => { cell: ({ getValue, row }) => {
const token = getValue() const token = getValue()
return ( return (
@@ -42,7 +46,7 @@ export const useApiKeyManagementTableColumns = () => {
{token} {token}
</Text> </Text>
<Copy <Copy
content={token} content={row.original.token}
variant="mini" variant="mini"
className="text-ui-fg-subtle" className="text-ui-fg-subtle"
/> />
@@ -73,7 +77,7 @@ export const useApiKeyManagementTableColumns = () => {
columnHelper.display({ columnHelper.display({
id: "actions", id: "actions",
cell: ({ row }) => { cell: ({ row }) => {
return <ApiKeyActions apiKey={row.original} /> return <ApiKeyActions apiKey={row.original as any} />
}, },
}), }),
], ],
@@ -82,11 +86,19 @@ export const useApiKeyManagementTableColumns = () => {
} }
const ApiKeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => { const ApiKeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => {
const { mutateAsync: revokeAsync } = useAdminRevokePublishableApiKey( const { mutateAsync: revokeAsync } = useAdminCustomPost(
apiKey.id `/api-keys/${apiKey.id}/revoke`,
[
adminPublishableApiKeysKeys.lists(),
adminPublishableApiKeysKeys.detail(apiKey.id),
]
) )
const { mutateAsync: deleteAsync } = useAdminDeletePublishableApiKey( const { mutateAsync: deleteAsync } = useAdminCustomDelete(
apiKey.id `/api-keys/${apiKey.id}`,
[
adminPublishableApiKeysKeys.lists(),
adminPublishableApiKeysKeys.detail(apiKey.id),
]
) )
const { t } = useTranslation() const { t } = useTranslation()
@@ -123,7 +135,7 @@ const ApiKeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => {
return return
} }
await revokeAsync() await revokeAsync({})
} }
return ( return (
@@ -7,14 +7,10 @@ export const useApiKeyManagementTableFilters = () => {
let filters: Filter[] = [] let filters: Filter[] = []
const dateFilters: Filter[] = [ const dateFilters: Filter[] = [
{ label: t("fields.createdAt"), key: "created_at" }, { label: t("fields.createdAt"), key: "created_at", type: "date" },
{ label: t("fields.updatedAt"), key: "updated_at" }, { label: t("fields.updatedAt"), key: "updated_at", type: "date" },
{ label: t("fields.revokedAt"), key: "revoked_at" }, { label: t("fields.revokedAt"), key: "revoked_at", type: "date" },
].map((f) => ({ ]
key: f.key,
label: f.label,
type: "date",
}))
filters = [...filters, ...dateFilters] filters = [...filters, ...dateFilters]
+1
View File
@@ -1,3 +1,4 @@
export * from "./admin" export * from "./admin"
export * from "./store" export * from "./store"
export * from "./utils" export * from "./utils"
@@ -30,7 +30,7 @@ export const GET = async (
const [apiKey] = await remoteQuery(queryObject) const [apiKey] = await remoteQuery(queryObject)
res.status(200).json({ apiKey }) res.status(200).json({ api_key: apiKey })
} }
export const POST = async ( export const POST = async (