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:
7
.changeset/purple-bats-clap.md
Normal file
7
.changeset/purple-bats-clap.md
Normal 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."
|
||||
},
|
||||
"apiKeyManagement": {
|
||||
"domain": "API Key Management",
|
||||
"domainTitle": "API Keys",
|
||||
"domain": "{{ keyType }} API Keys",
|
||||
"createKey": "Create key",
|
||||
"createPublishableApiKey": "Create Publishable API Key",
|
||||
"createPublishableApiKey": "Create {{ keyType }} API Key",
|
||||
"editKey": "Edit key",
|
||||
"revoke": "Revoke",
|
||||
"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(
|
||||
() => [
|
||||
{
|
||||
label: t("apiKeyManagement.domain"),
|
||||
label: t("apiKeyManagement.domainTitle"),
|
||||
to: "/settings/api-key-management",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -111,7 +111,7 @@ const useLinks = (): CommandGroupProps[] => {
|
||||
label: t("salesChannels.domain"),
|
||||
},
|
||||
{
|
||||
label: t("apiKeyManagement.domain"),
|
||||
label: t("apiKeyManagement.domainTitle"),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
4
packages/admin-next/dashboard/src/lib/uppercase-first.ts
Normal file
4
packages/admin-next/dashboard/src/lib/uppercase-first.ts
Normal file
@@ -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,
|
||||
],
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Outlet } from "react-router-dom"
|
||||
import { SearchProvider } from "../search-provider"
|
||||
import { SettingsLayout } from "../../components/layout/settings-layout"
|
||||
import { SidebarProvider } from "../sidebar-provider"
|
||||
import { ApiKeyDTO } from "@medusajs/types"
|
||||
import { Spinner } from "@medusajs/icons"
|
||||
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,
|
||||
} from "@tanstack/react-table"
|
||||
import {
|
||||
adminPublishableApiKeysKeys,
|
||||
useAdminAddPublishableKeySalesChannelsBatch,
|
||||
useAdminCustomPost,
|
||||
useAdminSalesChannels,
|
||||
} from "medusa-react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
@@ -61,8 +63,10 @@ export const AddSalesChannelsToApiKeyForm = ({
|
||||
|
||||
const { setValue } = form
|
||||
|
||||
const { mutateAsync, isLoading: isMutating } =
|
||||
useAdminAddPublishableKeySalesChannelsBatch(apiKey)
|
||||
const { mutateAsync, isLoading: isMutating } = useAdminCustomPost(
|
||||
`/api-keys/${apiKey}/sales-channels/batch/add`,
|
||||
[adminPublishableApiKeysKeys.detailSalesChannels(apiKey)]
|
||||
)
|
||||
|
||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
@@ -128,7 +132,7 @@ export const AddSalesChannelsToApiKeyForm = ({
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
await mutateAsync(
|
||||
{
|
||||
sales_channel_ids: values.sales_channel_ids.map((p) => ({ id: p })),
|
||||
sales_channel_ids: values.sales_channel_ids,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
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 { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
@@ -26,14 +26,19 @@ export const CreatePublishableApiKeyForm = () => {
|
||||
resolver: zodResolver(CreatePublishableApiKeySchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminCreatePublishableApiKey()
|
||||
const { mutateAsync, isLoading } = useAdminCustomPost(`/admin/api-keys`, [
|
||||
adminPublishableApiKeysKeys.lists(),
|
||||
])
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
await mutateAsync(values, {
|
||||
onSuccess: ({ publishable_api_key }) => {
|
||||
handleSuccess(`/settings/api-key-management/${publishable_api_key.id}`)
|
||||
},
|
||||
})
|
||||
await mutateAsync(
|
||||
{ title: values.title, type: "publishable" },
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleSuccess(`/settings/api-key-management`)
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
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 { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { ApiKeyGeneralSection } from "./components/api-key-general-section"
|
||||
@@ -11,16 +11,20 @@ export const ApiKeyManagementDetail = () => {
|
||||
>
|
||||
|
||||
const { id } = useParams()
|
||||
const { publishable_api_key, isLoading, isError, error } =
|
||||
useAdminPublishableApiKey(id!, {
|
||||
initialData,
|
||||
})
|
||||
const { data, isLoading, isError, error } = useAdminCustomQuery(
|
||||
`/api-keys/${id}`,
|
||||
[adminPublishableApiKeysKeys.detail(id!)],
|
||||
undefined,
|
||||
{
|
||||
initialData: initialData,
|
||||
}
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isError || !publishable_api_key) {
|
||||
if (isError || !data?.api_key) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
@@ -30,9 +34,9 @@ export const ApiKeyManagementDetail = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<ApiKeyGeneralSection apiKey={publishable_api_key} />
|
||||
<ApiKeySalesChannelSection apiKey={publishable_api_key} />
|
||||
<JsonViewSection data={publishable_api_key} />
|
||||
<ApiKeyGeneralSection apiKey={data?.api_key} />
|
||||
<ApiKeySalesChannelSection apiKey={data?.api_key} />
|
||||
<JsonViewSection data={data?.api_key} />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
useAdminCustomQuery,
|
||||
useAdminDeletePublishableApiKey,
|
||||
useAdminRevokePublishableApiKey,
|
||||
useAdminUser,
|
||||
@@ -18,9 +19,10 @@ import { useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { Skeleton } from "../../../../../components/common/skeleton"
|
||||
import { UserLink } from "../../../../../components/common/user-link"
|
||||
import { ApiKeyDTO } from "@medusajs/types"
|
||||
|
||||
type ApiKeyGeneralSectionProps = {
|
||||
apiKey: PublishableApiKey
|
||||
apiKey: ApiKeyDTO
|
||||
}
|
||||
|
||||
export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
@@ -121,10 +123,10 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
</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">
|
||||
<Text size="xsmall" leading="compact" className="truncate">
|
||||
{apiKey.id}
|
||||
{apiKey.redacted}
|
||||
</Text>
|
||||
<Copy
|
||||
content={apiKey.id}
|
||||
content={apiKey.token}
|
||||
variant="mini"
|
||||
className="text-ui-fg-subtle"
|
||||
/>
|
||||
@@ -149,9 +151,12 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
}
|
||||
|
||||
const ActionBy = ({ userId }: { userId: string | null }) => {
|
||||
const { user, isLoading, isError, error } = useAdminUser(userId!, {
|
||||
enabled: !!userId,
|
||||
})
|
||||
const { data, isLoading, isError, error } = useAdminCustomQuery(
|
||||
`/users/${userId}`,
|
||||
[],
|
||||
{},
|
||||
{ enabled: !!userId }
|
||||
)
|
||||
|
||||
if (!userId) {
|
||||
return (
|
||||
@@ -174,7 +179,7 @@ const ActionBy = ({ userId }: { userId: string | null }) => {
|
||||
)
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
if (!data?.user) {
|
||||
return (
|
||||
<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,
|
||||
} from "@tanstack/react-table"
|
||||
import {
|
||||
useAdminPublishableApiKeySalesChannels,
|
||||
adminPublishableApiKeysKeys,
|
||||
useAdminCustomPost,
|
||||
useAdminCustomQuery,
|
||||
useAdminRemovePublishableKeySalesChannelsBatch,
|
||||
} from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
@@ -34,9 +36,10 @@ import {
|
||||
import { Query } from "../../../../../components/filtering/query"
|
||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
import { ApiKeyDTO } from "@medusajs/types"
|
||||
|
||||
type ApiKeySalesChannelSectionProps = {
|
||||
apiKey: PublishableApiKey
|
||||
apiKey: ApiKeyDTO
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
@@ -64,23 +67,28 @@ export const ApiKeySalesChannelSection = ({
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
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 table = useReactTable({
|
||||
data: sales_channels ?? [],
|
||||
data: salesChannels ?? [],
|
||||
columns,
|
||||
pageCount: Math.ceil(count / PAGE_SIZE),
|
||||
state: {
|
||||
@@ -97,8 +105,9 @@ export const ApiKeySalesChannelSection = ({
|
||||
},
|
||||
})
|
||||
|
||||
const { mutateAsync } = useAdminRemovePublishableKeySalesChannelsBatch(
|
||||
apiKey.id
|
||||
const { mutateAsync } = useAdminCustomPost(
|
||||
`/api-keys/${apiKey.id}/sales-channels/batch/remove`,
|
||||
[adminPublishableApiKeysKeys.detailSalesChannels(apiKey.id)]
|
||||
)
|
||||
|
||||
const handleRemove = async () => {
|
||||
@@ -119,7 +128,7 @@ export const ApiKeySalesChannelSection = ({
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
sales_channel_ids: keys.map((k) => ({ id: k })),
|
||||
sales_channel_ids: keys,
|
||||
},
|
||||
{
|
||||
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) {
|
||||
throw error
|
||||
@@ -155,7 +164,7 @@ export const ApiKeySalesChannelSection = ({
|
||||
<NoRecords />
|
||||
) : (
|
||||
<div>
|
||||
{!isLoading && sales_channels?.length !== 0 ? (
|
||||
{!isLoading && salesChannels?.length !== 0 ? (
|
||||
<Table>
|
||||
<Table.Header className="border-t-0">
|
||||
{table.getHeaderGroups().map((headerGroup) => {
|
||||
@@ -1,13 +1,13 @@
|
||||
import { AdminPublishableApiKeysRes } from "@medusajs/medusa"
|
||||
import { Response } from "@medusajs/medusa-js"
|
||||
import { adminProductKeys } from "medusa-react"
|
||||
import { adminPublishableApiKeysKeys } from "medusa-react"
|
||||
import { LoaderFunctionArgs } from "react-router-dom"
|
||||
|
||||
import { ApiKeyDTO } from "@medusajs/types"
|
||||
import { medusa, queryClient } from "../../../lib/medusa"
|
||||
|
||||
const apiKeyDetailQuery = (id: string) => ({
|
||||
queryKey: adminProductKeys.detail(id),
|
||||
queryFn: async () => medusa.admin.publishableApiKeys.retrieve(id),
|
||||
queryKey: adminPublishableApiKeysKeys.detail(id),
|
||||
queryFn: async () => medusa.admin.custom.get(`/api-keys/${id}`),
|
||||
})
|
||||
|
||||
export const apiKeyLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||
@@ -15,7 +15,7 @@ export const apiKeyLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const query = apiKeyDetailQuery(id!)
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<Response<AdminPublishableApiKeysRes>>(
|
||||
queryClient.getQueryData<Response<{ api_key: ApiKeyDTO }>>(
|
||||
query.queryKey
|
||||
) ?? (await queryClient.fetchQuery(query))
|
||||
)
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Heading } from "@medusajs/ui"
|
||||
import { useAdminPublishableApiKey } from "medusa-react"
|
||||
import { adminPublishableApiKeysKeys, useAdminCustomQuery } from "medusa-react"
|
||||
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 { EditApiKeyForm } from "./components/edit-api-key-form"
|
||||
|
||||
@@ -9,11 +9,17 @@ export const ApiKeyManagementEdit = () => {
|
||||
const { id } = useParams()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { publishable_api_key, isLoading, isError, error } =
|
||||
useAdminPublishableApiKey(id!)
|
||||
const { data, isLoading, isError, error } = useAdminCustomQuery(
|
||||
`/api-keys/${id}`,
|
||||
[adminPublishableApiKeysKeys.detail(id!)]
|
||||
)
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
if (isError || !data?.api_key) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw json("An unknown error has occured", 500)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -21,8 +27,8 @@ export const ApiKeyManagementEdit = () => {
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t("apiKeyManagement.editKey")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
{!isLoading && publishable_api_key && (
|
||||
<EditApiKeyForm apiKey={publishable_api_key} />
|
||||
{!isLoading && !!data?.api_key && (
|
||||
<EditApiKeyForm apiKey={data.api_key} />
|
||||
)}
|
||||
</RouteDrawer>
|
||||
)
|
||||
@@ -1,7 +1,6 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import type { PublishableApiKey } from "@medusajs/medusa"
|
||||
import { Button, Input } from "@medusajs/ui"
|
||||
import { useAdminUpdatePublishableApiKey } from "medusa-react"
|
||||
import { adminPublishableApiKeysKeys, useAdminCustomPost } from "medusa-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
@@ -11,9 +10,10 @@ import {
|
||||
RouteDrawer,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { ApiKeyDTO } from "@medusajs/types"
|
||||
|
||||
type EditApiKeyFormProps = {
|
||||
apiKey: PublishableApiKey
|
||||
apiKey: ApiKeyDTO
|
||||
}
|
||||
|
||||
const EditApiKeySchema = zod.object({
|
||||
@@ -31,7 +31,14 @@ export const EditApiKeyForm = ({ apiKey }: EditApiKeyFormProps) => {
|
||||
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) => {
|
||||
await mutateAsync(data, {
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { ApiKeyManagementListTable } from "./components/api-key-management-list-table"
|
||||
|
||||
// TODO: Add secret API keys
|
||||
|
||||
export const ApiKeyManagementList = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<ApiKeyManagementListTable />
|
||||
<ApiKeyManagementListTable keyType="publishable" />
|
||||
{/* <ApiKeyManagementListTable keyType="secret" /> */}
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button, Container, Heading } from "@medusajs/ui"
|
||||
import { useAdminPublishableApiKeys } from "medusa-react"
|
||||
import { adminPublishableApiKeysKeys, useAdminCustomQuery } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
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 { useApiKeyManagementTableFilters } from "./use-api-key-management-table-filters"
|
||||
import { useApiKeyManagementTableQuery } from "./use-api-key-management-table-query"
|
||||
import { upperCaseFirst } from "../../../../../lib/uppercase-first"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const ApiKeyManagementListTable = () => {
|
||||
export const ApiKeyManagementListTable = ({
|
||||
keyType,
|
||||
}: {
|
||||
keyType: "secret" | "publishable"
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { searchParams, raw } = useApiKeyManagementTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
const { publishable_api_keys, count, isLoading, isError, error } =
|
||||
useAdminPublishableApiKeys(
|
||||
{
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const query = {
|
||||
...searchParams,
|
||||
type: keyType,
|
||||
fields:
|
||||
"id,title,redacted,token,type,created_at,updated_at,revoked_at,last_used_at,created_by,revoked_by",
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const { data, count, isLoading, isError, error } = useAdminCustomQuery(
|
||||
"/api-keys",
|
||||
[adminPublishableApiKeysKeys.list(query)],
|
||||
query
|
||||
)
|
||||
|
||||
const filters = useApiKeyManagementTableFilters()
|
||||
const columns = useApiKeyManagementTableColumns()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: publishable_api_keys || [],
|
||||
data: data?.api_keys || [],
|
||||
columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
@@ -49,7 +59,9 @@ export const ApiKeyManagementListTable = () => {
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<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">
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.create")}
|
||||
@@ -3,6 +3,9 @@ import { PublishableApiKey } from "@medusajs/medusa"
|
||||
import { Copy, Text, usePrompt } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import {
|
||||
adminPublishableApiKeysKeys,
|
||||
useAdminCustomDelete,
|
||||
useAdminCustomPost,
|
||||
useAdminDeletePublishableApiKey,
|
||||
useAdminRevokePublishableApiKey,
|
||||
} from "medusa-react"
|
||||
@@ -12,8 +15,9 @@ import { useTranslation } from "react-i18next"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { DateCell } from "../../../../../components/table/table-cells/common/date-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 = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -28,9 +32,9 @@ export const useApiKeyManagementTableColumns = () => {
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("id", {
|
||||
header: "Key",
|
||||
cell: ({ getValue }) => {
|
||||
columnHelper.accessor("redacted", {
|
||||
header: "Token",
|
||||
cell: ({ getValue, row }) => {
|
||||
const token = getValue()
|
||||
|
||||
return (
|
||||
@@ -42,7 +46,7 @@ export const useApiKeyManagementTableColumns = () => {
|
||||
{token}
|
||||
</Text>
|
||||
<Copy
|
||||
content={token}
|
||||
content={row.original.token}
|
||||
variant="mini"
|
||||
className="text-ui-fg-subtle"
|
||||
/>
|
||||
@@ -73,7 +77,7 @@ export const useApiKeyManagementTableColumns = () => {
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
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 { mutateAsync: revokeAsync } = useAdminRevokePublishableApiKey(
|
||||
apiKey.id
|
||||
const { mutateAsync: revokeAsync } = useAdminCustomPost(
|
||||
`/api-keys/${apiKey.id}/revoke`,
|
||||
[
|
||||
adminPublishableApiKeysKeys.lists(),
|
||||
adminPublishableApiKeysKeys.detail(apiKey.id),
|
||||
]
|
||||
)
|
||||
const { mutateAsync: deleteAsync } = useAdminDeletePublishableApiKey(
|
||||
apiKey.id
|
||||
const { mutateAsync: deleteAsync } = useAdminCustomDelete(
|
||||
`/api-keys/${apiKey.id}`,
|
||||
[
|
||||
adminPublishableApiKeysKeys.lists(),
|
||||
adminPublishableApiKeysKeys.detail(apiKey.id),
|
||||
]
|
||||
)
|
||||
|
||||
const { t } = useTranslation()
|
||||
@@ -123,7 +135,7 @@ const ApiKeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => {
|
||||
return
|
||||
}
|
||||
|
||||
await revokeAsync()
|
||||
await revokeAsync({})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -7,14 +7,10 @@ export const useApiKeyManagementTableFilters = () => {
|
||||
let filters: Filter[] = []
|
||||
|
||||
const dateFilters: Filter[] = [
|
||||
{ label: t("fields.createdAt"), key: "created_at" },
|
||||
{ label: t("fields.updatedAt"), key: "updated_at" },
|
||||
{ label: t("fields.revokedAt"), key: "revoked_at" },
|
||||
].map((f) => ({
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
type: "date",
|
||||
}))
|
||||
{ label: t("fields.createdAt"), key: "created_at", type: "date" },
|
||||
{ label: t("fields.updatedAt"), key: "updated_at", type: "date" },
|
||||
{ label: t("fields.revokedAt"), key: "revoked_at", type: "date" },
|
||||
]
|
||||
|
||||
filters = [...filters, ...dateFilters]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./admin"
|
||||
export * from "./store"
|
||||
export * from "./utils"
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export const GET = async (
|
||||
|
||||
const [apiKey] = await remoteQuery(queryObject)
|
||||
|
||||
res.status(200).json({ apiKey })
|
||||
res.status(200).json({ api_key: apiKey })
|
||||
}
|
||||
|
||||
export const POST = async (
|
||||
|
||||
Reference in New Issue
Block a user