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:
1 parent
e915169e11
commit
eadc5e8a79
36 files changed
+255
-169
No files matched your search
@@ -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.",
|
||||||
|
|||||||
+1
-1
@@ -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"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
-26
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+36
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+7
-3
@@ -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: () => {
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+11
-6
@@ -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 (
|
||||||
File renamed without changes.
File renamed without changes.
+13
-9
@@ -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>
|
||||||
)
|
)
|
||||||
+13
-8
@@ -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} />
|
||||||
}
|
}
|
||||||
File renamed without changes.
+23
-14
@@ -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(
|
const query = {
|
||||||
apiKey.id,
|
|
||||||
{
|
|
||||||
...params,
|
...params,
|
||||||
},
|
fields: "id,*sales_channels",
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, isLoading, isError, error } = useAdminCustomQuery(
|
||||||
|
`/api-keys/${apiKey.id}`,
|
||||||
|
[adminPublishableApiKeysKeys.detailSalesChannels(apiKey.id, query)],
|
||||||
|
query,
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const count = sales_channels?.length || 0
|
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) => {
|
||||||
File renamed without changes.
File renamed without changes.
+5
-5
@@ -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))
|
||||||
)
|
)
|
||||||
+13
-7
@@ -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,20 +9,26 @@ 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) {
|
||||||
|
if (error) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw json("An unknown error has occured", 500)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteDrawer>
|
<RouteDrawer>
|
||||||
<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>
|
||||||
)
|
)
|
||||||
+11
-4
@@ -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, {
|
||||||
File renamed without changes.
File renamed without changes.
+4
-1
@@ -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>
|
||||||
)
|
)
|
||||||
+22
-10
@@ -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:
|
||||||
keepPreviousData: true,
|
"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 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")}
|
||||||
File renamed without changes.
+23
-11
@@ -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 (
|
||||||
+4
-8
@@ -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]
|
||||||
|
|
||||||
File renamed without changes.
File renamed without changes.
@@ -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 (
|
||||||
|
|||||||
Reference in new issue
Block a user