diff --git a/apps/backend/src/api/openapi/vendor/seller.ts b/apps/backend/src/api/openapi/vendor/seller.ts index 743062bc..f28c4ddd 100644 --- a/apps/backend/src/api/openapi/vendor/seller.ts +++ b/apps/backend/src/api/openapi/vendor/seller.ts @@ -169,3 +169,54 @@ * type: boolean * description: Whether the invite has been accepted. */ + +/** + * @schema SellerApiKey + * title: "Api key" + * description: "A seller api key details" + * properties: + * id: + * type: string + * description: The unique identifier of the api key. + * title: + * type: string + * description: The api key title. + * redacted: + * type: string + * description: The redacted api key value. + * created_by: + * type: string + * description: The identity that created the api key. + * revoked_by: + * type: string + * description: The identity that revoked the api key. + * revoked_at: + * type: string + * format: date-time + * description: The date with timezone at which the invite expires. + */ + +/** + * @schema SellerApiKeyExplicit + * title: "Api key explicit" + * description: "A seller api key with explicit token value" + * properties: + * id: + * type: string + * description: The unique identifier of the api key. + * title: + * type: string + * description: The api key title. + * redacted: + * type: string + * description: The redacted api key value. + * seller_id: + * type: string + * description: The seller id associated with the api key. + * token: + * type: string + * description: Explicit api key value. + * created_by: + * type: string + * description: The identity that created the api key. + */ diff --git a/apps/backend/src/api/vendor/api-keys/[id]/route.ts b/apps/backend/src/api/vendor/api-keys/[id]/route.ts new file mode 100644 index 00000000..d28d6a3a --- /dev/null +++ b/apps/backend/src/api/vendor/api-keys/[id]/route.ts @@ -0,0 +1,122 @@ +import { + AuthenticatedMedusaRequest, + MedusaResponse, + container +} from '@medusajs/framework' +import { ContainerRegistrationKeys } from '@medusajs/framework/utils' + +import { revokeSellerApiKeyWorkflow } from '../../../../workflows/seller/workflows' + +/** + * @oas [get] /vendor/api-keys/{id} + * operationId: "VendorGetSellerApiKeyById" + * summary: "Get an api key by id" + * description: "Retrieves an api key by id for the authenticated vendor." + * x-authenticated: true + * parameters: + * - in: path + * name: id + * required: true + * description: The ID of the API key. + * schema: + * type: string + * responses: + * "200": + * description: OK + * content: + * application/json: + * schema: + * type: object + * properties: + * api_key: + * $ref: "#/components/schemas/SellerApiKey" + * tags: + * - Seller + * security: + * - api_token: [] + * - cookie_auth: [] + */ +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const query = container.resolve(ContainerRegistrationKeys.QUERY) + + const { + data: [api_key] + } = await query.graph({ + entity: 'seller_api_key', + fields: [ + 'id', + 'title', + 'redacted', + 'created_by', + 'revoked_at', + 'revoked_by' + ], + filters: { id: req.params.id } + }) + + res.json({ api_key }) +} + +/** + * @oas [delete] /vendor/api-keys/{id} + * operationId: "VendorRevokeSellerApiKeyById" + * summary: "Revoke an api key by id" + * description: "Revokes an api key by id for the authenticated vendor." + * x-authenticated: true + * parameters: + * - in: path + * name: id + * required: true + * description: The ID of the API key. + * schema: + * type: string + * responses: + * "200": + * description: OK + * content: + * application/json: + * schema: + * type: object + * properties: + * api_key: + * $ref: "#/components/schemas/SellerApiKey" + * tags: + * - Seller + * security: + * - api_token: [] + * - cookie_auth: [] + */ +export const DELETE = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const query = container.resolve(ContainerRegistrationKeys.QUERY) + + await revokeSellerApiKeyWorkflow.run({ + container: req.scope, + input: { + id: req.params.id, + revoked_by: req.auth_context.actor_id + } + }) + + const { + data: [api_key] + } = await query.graph({ + entity: 'seller_api_key', + fields: [ + 'id', + 'title', + 'redacted', + 'created_by', + 'revoked_at', + 'revoked_by' + ], + filters: { id: req.params.id } + }) + + res.json({ api_key }) +} diff --git a/apps/backend/src/api/vendor/api-keys/middlewares.ts b/apps/backend/src/api/vendor/api-keys/middlewares.ts new file mode 100644 index 00000000..78012736 --- /dev/null +++ b/apps/backend/src/api/vendor/api-keys/middlewares.ts @@ -0,0 +1,37 @@ +import { + MiddlewareRoute, + validateAndTransformBody, + validateAndTransformQuery +} from '@medusajs/framework' + +import { + checkResourceOwnershipByResourceId, + filterBySellerId +} from '../../../shared/infra/http/middlewares' +import { + VendorCreateSellerApiKey, + VendorGetSellerApiKeysParams +} from './validators' + +export const vendorApiKeyMiddlewares: MiddlewareRoute[] = [ + { + method: ['GET'], + matcher: '/vendor/api-keys', + middlewares: [ + validateAndTransformQuery(VendorGetSellerApiKeysParams, {}), + filterBySellerId() + ] + }, + { + method: ['POST'], + matcher: '/vendor/api-keys', + middlewares: [validateAndTransformBody(VendorCreateSellerApiKey)] + }, + { + method: ['DELETE', 'GET'], + matcher: '/vendor/api-keys/:id', + middlewares: [ + checkResourceOwnershipByResourceId({ entryPoint: 'seller_api_key' }) + ] + } +] diff --git a/apps/backend/src/api/vendor/api-keys/route.ts b/apps/backend/src/api/vendor/api-keys/route.ts new file mode 100644 index 00000000..b8425e15 --- /dev/null +++ b/apps/backend/src/api/vendor/api-keys/route.ts @@ -0,0 +1,115 @@ +import { + AuthenticatedMedusaRequest, + MedusaResponse, + container +} from '@medusajs/framework' +import { ContainerRegistrationKeys } from '@medusajs/framework/utils' + +import { fetchSellerByAuthContext } from '../../../shared/infra/http/utils' +import { createSellerApiKeyWorkflow } from '../../../workflows/seller/workflows' +import { VendorCreateSellerApiKeyType } from './validators' + +/** + * @oas [get] /vendor/api-keys + * operationId: "VendorGetSellerMyApiKeys" + * summary: "Get api keys of the current seller" + * description: "Retrieves the api keys associated with the seller." + * x-authenticated: true + * responses: + * "200": + * description: OK + * content: + * application/json: + * schema: + * type: object + * properties: + * api_keys: + * type: array + * items: + * $ref: "#/components/schemas/SellerApiKey" + * count: + * type: integer + * description: The total number of items available + * offset: + * type: integer + * description: The number of items skipped before these items + * limit: + * type: integer + * description: The number of items per page + * tags: + * - Seller + * security: + * - api_token: [] + * - cookie_auth: [] + */ +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const query = container.resolve(ContainerRegistrationKeys.QUERY) + + const { data: api_keys, metadata } = await query.graph({ + entity: 'seller_api_key', + fields: [ + 'id', + 'title', + 'redacted', + 'created_by', + 'revoked_at', + 'revoked_by' + ], + filters: req.filterableFields + }) + + res.json({ + api_keys, + count: metadata?.count, + skip: metadata?.skip, + take: metadata?.take + }) +} + +/** + * @oas [post] /vendor/api-keys + * operationId: "VendorCreateApiKey" + * summary: "Create seller api key" + * description: "Creates a seller api key" + * x-authenticated: true + * requestBody: + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/VendorCreateSellerApiKey" + * responses: + * "201": + * description: Created + * content: + * application/json: + * schema: + * type: object + * properties: + * api_key: + * $ref: "#/components/schemas/SellerApiKeyExplicit" + * tags: + * - Seller + * security: + * - api_token: [] + * - cookie_auth: [] + */ +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) + + const { result: api_key } = await createSellerApiKeyWorkflow.run({ + container: req.scope, + input: { + ...req.validatedBody, + seller_id: seller.id, + created_by: req.auth_context.actor_id + } + }) + + res.status(201).json({ api_key }) +} diff --git a/apps/backend/src/api/vendor/api-keys/validators.ts b/apps/backend/src/api/vendor/api-keys/validators.ts new file mode 100644 index 00000000..6193f265 --- /dev/null +++ b/apps/backend/src/api/vendor/api-keys/validators.ts @@ -0,0 +1,29 @@ +import { z } from 'zod' + +import { createFindParams } from '@medusajs/medusa/api/utils/validators' + +/** + * @schema VendorCreateSellerApiKey + * title: "Create api key" + * description: "A schema for the api key creation." + * x-resourceId: VendorCreateSellerApiKey + * type: object + * properties: + * title: + * type: string + * description: The title of the key + */ +export type VendorCreateSellerApiKeyType = z.infer< + typeof VendorCreateSellerApiKey +> +export const VendorCreateSellerApiKey = z.object({ + title: z.string().max(50) +}) + +export type VendorGetSellerApiKeysParamsType = z.infer< + typeof VendorGetSellerApiKeysParams +> +export const VendorGetSellerApiKeysParams = createFindParams({ + offset: 0, + limit: 50 +}) diff --git a/apps/backend/src/api/vendor/campaigns/route.ts b/apps/backend/src/api/vendor/campaigns/route.ts index 7ce8780d..2b53cd4d 100644 --- a/apps/backend/src/api/vendor/campaigns/route.ts +++ b/apps/backend/src/api/vendor/campaigns/route.ts @@ -2,7 +2,7 @@ import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' import { ContainerRegistrationKeys } from '@medusajs/framework/utils' import sellerCampaign from '../../../links/seller-campaign' -import { fetchSellerByAuthActorId } from '../../../shared/infra/http/utils' +import { fetchSellerByAuthContext } from '../../../shared/infra/http/utils' import { createVendorCampaignWorkflow } from '../../../workflows/campaigns/workflows' import { VendorCreateCampaignType } from './validators' @@ -118,10 +118,7 @@ export const POST = async ( res: MedusaResponse ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const seller = await fetchSellerByAuthActorId( - req.auth_context?.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const { result } = await createVendorCampaignWorkflow.run({ container: req.scope, diff --git a/apps/backend/src/api/vendor/customers/[id]/orders/route.ts b/apps/backend/src/api/vendor/customers/[id]/orders/route.ts index c2e74a2c..752c8642 100644 --- a/apps/backend/src/api/vendor/customers/[id]/orders/route.ts +++ b/apps/backend/src/api/vendor/customers/[id]/orders/route.ts @@ -2,7 +2,7 @@ import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' import { getOrdersListWorkflow } from '@medusajs/medusa/core-flows' import { selectCustomerOrders } from '../../../../../modules/seller/utils' -import { fetchSellerByAuthActorId } from '../../../../../shared/infra/http/utils' +import { fetchSellerByAuthContext } from '../../../../../shared/infra/http/utils' /** * @oas [get] /vendor/customers/{id}/orders @@ -66,10 +66,7 @@ export const GET = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const { orders: orderIds, count } = await selectCustomerOrders( req.scope, diff --git a/apps/backend/src/api/vendor/customers/route.ts b/apps/backend/src/api/vendor/customers/route.ts index 66597e6d..b9374528 100644 --- a/apps/backend/src/api/vendor/customers/route.ts +++ b/apps/backend/src/api/vendor/customers/route.ts @@ -1,7 +1,7 @@ import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' import { selectSellerCustomers } from '../../../modules/seller/utils' -import { fetchSellerByAuthActorId } from '../../../shared/infra/http/utils' +import { fetchSellerByAuthContext } from '../../../shared/infra/http/utils' /** * @oas [get] /vendor/customers @@ -56,10 +56,7 @@ export const GET = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const { customers, count } = await selectSellerCustomers( req.scope, diff --git a/apps/backend/src/api/vendor/fulfillment-sets/[id]/service-zones/route.ts b/apps/backend/src/api/vendor/fulfillment-sets/[id]/service-zones/route.ts index 650028f3..d083b7eb 100644 --- a/apps/backend/src/api/vendor/fulfillment-sets/[id]/service-zones/route.ts +++ b/apps/backend/src/api/vendor/fulfillment-sets/[id]/service-zones/route.ts @@ -1,5 +1,5 @@ import { SELLER_MODULE } from 'src/modules/seller' -import { fetchSellerByAuthActorId } from 'src/shared/infra/http/utils' +import { fetchSellerByAuthContext } from 'src/shared/infra/http/utils' import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' import { ContainerRegistrationKeys, Modules } from '@medusajs/framework/utils' @@ -48,10 +48,7 @@ export const POST = async ( const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) const remoteLink = req.scope.resolve(ContainerRegistrationKeys.REMOTE_LINK) - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const { result: [serviceZone] diff --git a/apps/backend/src/api/vendor/invites/route.ts b/apps/backend/src/api/vendor/invites/route.ts index 8d456265..2ebdb347 100644 --- a/apps/backend/src/api/vendor/invites/route.ts +++ b/apps/backend/src/api/vendor/invites/route.ts @@ -1,4 +1,4 @@ -import { fetchSellerByAuthActorId } from '#/shared/infra/http/utils' +import { fetchSellerByAuthContext } from '#/shared/infra/http/utils' import { inviteMemberWorkflow } from '#/workflows/member/workflows' import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' @@ -39,10 +39,7 @@ export const POST = async ( ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const { result: created } = await inviteMemberWorkflow(req.scope).run({ input: { diff --git a/apps/backend/src/api/vendor/middlewares.ts b/apps/backend/src/api/vendor/middlewares.ts index b361691b..0431f4bf 100644 --- a/apps/backend/src/api/vendor/middlewares.ts +++ b/apps/backend/src/api/vendor/middlewares.ts @@ -1,7 +1,9 @@ import { unlessBaseUrl } from '#/shared/infra/http/utils' -import { MiddlewareRoute, authenticate } from '@medusajs/framework' +import { MiddlewareRoute } from '@medusajs/framework' +import { authenticateVendor } from '../../shared/infra/http/middlewares/authenticate-vendor' +import { vendorApiKeyMiddlewares } from './api-keys/middlewares' import { vendorCampaignsMiddlewares } from './campaigns/middlewares' import { vendorCors } from './cors' import { vendorCustomerGroupsMiddlewares } from './customer-groups/middlewares' @@ -46,7 +48,7 @@ export const vendorMiddlewares: MiddlewareRoute[] = [ matcher: '/vendor/sellers', method: ['POST'], middlewares: [ - authenticate('seller', ['bearer', 'session'], { + authenticateVendor({ allowUnregistered: true }) ] @@ -54,14 +56,14 @@ export const vendorMiddlewares: MiddlewareRoute[] = [ { matcher: '/vendor/invites/accept', method: ['POST'], - middlewares: [authenticate('seller', ['bearer', 'session'])] + middlewares: [authenticateVendor()] }, { matcher: '/vendor/*', middlewares: [ unlessBaseUrl( /^\/vendor\/(sellers|invites\/accept)$/, - authenticate('seller', ['bearer', 'session'], { + authenticateVendor({ allowUnregistered: false }) ) @@ -96,5 +98,6 @@ export const vendorMiddlewares: MiddlewareRoute[] = [ ...vendorCampaignsMiddlewares, ...vendorStatisticsMiddlewares, ...vendorFulfillmentProvidersMiddlewares, - ...vendorReturnsMiddlewares + ...vendorReturnsMiddlewares, + ...vendorApiKeyMiddlewares ] diff --git a/apps/backend/src/api/vendor/orders/route.ts b/apps/backend/src/api/vendor/orders/route.ts index 76e11377..a826d1d3 100644 --- a/apps/backend/src/api/vendor/orders/route.ts +++ b/apps/backend/src/api/vendor/orders/route.ts @@ -1,5 +1,5 @@ import sellerOrderLink from '#/links/seller-order' -import { fetchSellerByAuthActorId } from '#/shared/infra/http/utils' +import { fetchSellerByAuthContext } from '#/shared/infra/http/utils' import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' import { OrderDTO } from '@medusajs/framework/types' @@ -107,10 +107,7 @@ export const GET = async ( ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const { data: orderRelations } = await query.graph({ entity: sellerOrderLink.entryPoint, diff --git a/apps/backend/src/api/vendor/payout-account/onboarding/route.ts b/apps/backend/src/api/vendor/payout-account/onboarding/route.ts index 1475768b..86ee2dbd 100644 --- a/apps/backend/src/api/vendor/payout-account/onboarding/route.ts +++ b/apps/backend/src/api/vendor/payout-account/onboarding/route.ts @@ -1,4 +1,4 @@ -import { fetchSellerByAuthActorId } from '#/shared/infra/http/utils' +import { fetchSellerByAuthContext } from '#/shared/infra/http/utils' import { createOnboardingForSellerWorkflow } from '#/workflows/seller/workflows' import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' @@ -38,10 +38,7 @@ export const POST = async ( res: MedusaResponse ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const { result } = await createOnboardingForSellerWorkflow(req.scope).run({ context: { transactionId: seller.id }, diff --git a/apps/backend/src/api/vendor/payout-account/route.ts b/apps/backend/src/api/vendor/payout-account/route.ts index b69d580a..d089fe0b 100644 --- a/apps/backend/src/api/vendor/payout-account/route.ts +++ b/apps/backend/src/api/vendor/payout-account/route.ts @@ -1,5 +1,5 @@ import sellerPayoutAccountLink from '#/links/seller-payout-account' -import { fetchSellerByAuthActorId } from '#/shared/infra/http/utils' +import { fetchSellerByAuthContext } from '#/shared/infra/http/utils' import { createPayoutAccountForSellerWorkflow } from '#/workflows/seller/workflows' import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' @@ -89,10 +89,7 @@ export const POST = async ( res: MedusaResponse ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const { result } = await createPayoutAccountForSellerWorkflow(req.scope).run({ context: { transactionId: seller.id }, diff --git a/apps/backend/src/api/vendor/price-lists/[id]/prices/route.ts b/apps/backend/src/api/vendor/price-lists/[id]/prices/route.ts index d8394db4..c368b170 100644 --- a/apps/backend/src/api/vendor/price-lists/[id]/prices/route.ts +++ b/apps/backend/src/api/vendor/price-lists/[id]/prices/route.ts @@ -1,7 +1,7 @@ import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' import { ContainerRegistrationKeys } from '@medusajs/framework/utils' -import { fetchSellerByAuthActorId } from '../../../../../shared/infra/http/utils' +import { fetchSellerByAuthContext } from '../../../../../shared/infra/http/utils' import { createVendorPriceListPricesWorkflow } from '../../../../../workflows/price-list/workflows' import { VendorCreatePriceListPriceType } from '../../validators' @@ -52,10 +52,7 @@ export const POST = async ( const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) const id = req.params.id - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) await createVendorPriceListPricesWorkflow.run({ container: req.scope, diff --git a/apps/backend/src/api/vendor/price-lists/route.ts b/apps/backend/src/api/vendor/price-lists/route.ts index e7d2599d..c03cae44 100644 --- a/apps/backend/src/api/vendor/price-lists/route.ts +++ b/apps/backend/src/api/vendor/price-lists/route.ts @@ -2,7 +2,7 @@ import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' import { ContainerRegistrationKeys } from '@medusajs/framework/utils' import sellerPriceList from '../../../links/seller-price-list' -import { fetchSellerByAuthActorId } from '../../../shared/infra/http/utils' +import { fetchSellerByAuthContext } from '../../../shared/infra/http/utils' import { createVendorPriceListWorkflow } from '../../../workflows/price-list/workflows' import { VendorCreatePriceListType } from './validators' @@ -114,10 +114,7 @@ export const POST = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const { result: [price_list] diff --git a/apps/backend/src/api/vendor/products/route.ts b/apps/backend/src/api/vendor/products/route.ts index bc562ca4..ea84d5c0 100644 --- a/apps/backend/src/api/vendor/products/route.ts +++ b/apps/backend/src/api/vendor/products/route.ts @@ -7,7 +7,7 @@ import { ContainerRegistrationKeys } from '@medusajs/framework/utils' import { createProductsWorkflow } from '@medusajs/medusa/core-flows' import sellerProductLink from '../../../links/seller-product' -import { fetchSellerByAuthActorId } from '../../../shared/infra/http/utils' +import { fetchSellerByAuthContext } from '../../../shared/infra/http/utils' import { assignBrandToProductWorkflow } from '../../../workflows/brand/workflows' import { VendorCreateProductType, @@ -126,10 +126,7 @@ export const POST = async ( ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const seller = await fetchSellerByAuthActorId( - req.auth_context?.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const brand_name = req.validatedBody.brand_name delete req.validatedBody['brand_name'] diff --git a/apps/backend/src/api/vendor/promotions/route.ts b/apps/backend/src/api/vendor/promotions/route.ts index e795a62e..b1f87edd 100644 --- a/apps/backend/src/api/vendor/promotions/route.ts +++ b/apps/backend/src/api/vendor/promotions/route.ts @@ -2,7 +2,7 @@ import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' import { ContainerRegistrationKeys } from '@medusajs/framework/utils' import sellerPromotion from '../../../links/seller-promotion' -import { fetchSellerByAuthActorId } from '../../../shared/infra/http/utils' +import { fetchSellerByAuthContext } from '../../../shared/infra/http/utils' import { createVendorPromotionWorkflow } from '../../../workflows/promotions/workflows' import { VendorCreatePromotionType } from './validators' @@ -111,10 +111,7 @@ export const POST = async ( res: MedusaResponse ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const seller = await fetchSellerByAuthActorId( - req.auth_context?.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const { result } = await createVendorPromotionWorkflow.run({ container: req.scope, diff --git a/apps/backend/src/api/vendor/requests/route.ts b/apps/backend/src/api/vendor/requests/route.ts index f57c079d..88c26350 100644 --- a/apps/backend/src/api/vendor/requests/route.ts +++ b/apps/backend/src/api/vendor/requests/route.ts @@ -2,7 +2,7 @@ import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' import { ContainerRegistrationKeys } from '@medusajs/framework/utils' import sellerRequest from '../../../links/seller-request' -import { fetchSellerByAuthActorId } from '../../../shared/infra/http/utils' +import { fetchSellerByAuthContext } from '../../../shared/infra/http/utils' import { createProductRequestWorkflow, createRequestWorkflow @@ -103,10 +103,7 @@ export const POST = async ( ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const workflow = req.validatedBody.request.type === 'product' diff --git a/apps/backend/src/api/vendor/reservations/[id]/route.ts b/apps/backend/src/api/vendor/reservations/[id]/route.ts index ab6e008a..3d592d0e 100644 --- a/apps/backend/src/api/vendor/reservations/[id]/route.ts +++ b/apps/backend/src/api/vendor/reservations/[id]/route.ts @@ -12,7 +12,7 @@ import { } from '@medusajs/framework/utils' import sellerStockLocation from '../../../../links/seller-stock-location' -import { fetchSellerByAuthActorId } from '../../../../shared/infra/http/utils' +import { fetchSellerByAuthContext } from '../../../../shared/infra/http/utils' import { VendorUpdateReservationType } from '../validators' /** @@ -118,10 +118,7 @@ export const POST = async ( const { id } = req.params if (req.validatedBody.location_id) { - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const { data: [relation] diff --git a/apps/backend/src/api/vendor/reservations/middlewares.ts b/apps/backend/src/api/vendor/reservations/middlewares.ts index 9252505e..11f294fa 100644 --- a/apps/backend/src/api/vendor/reservations/middlewares.ts +++ b/apps/backend/src/api/vendor/reservations/middlewares.ts @@ -14,7 +14,7 @@ import { import sellerInventoryItem from '../../../links/seller-inventory-item' import { filterBySellerId } from '../../../shared/infra/http/middlewares' -import { fetchSellerByAuthActorId } from '../../../shared/infra/http/utils' +import { fetchSellerByAuthContext } from '../../../shared/infra/http/utils' import { vendorReservationQueryConfig } from './query-config' import { VendorGetReservationParams, @@ -57,10 +57,7 @@ const checkReservationOwnership = () => { } }) - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) if (!seller || !relation || seller.id !== relation.seller_id) { res.status(403).json({ diff --git a/apps/backend/src/api/vendor/sellers/me/onboarding/route.ts b/apps/backend/src/api/vendor/sellers/me/onboarding/route.ts index aab7f08e..9d8698fc 100644 --- a/apps/backend/src/api/vendor/sellers/me/onboarding/route.ts +++ b/apps/backend/src/api/vendor/sellers/me/onboarding/route.ts @@ -1,7 +1,7 @@ import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' import { ContainerRegistrationKeys } from '@medusajs/framework/utils' -import { fetchSellerByAuthActorId } from '../../../../../shared/infra/http/utils' +import { fetchSellerByAuthContext } from '../../../../../shared/infra/http/utils' import { recalculateOnboardingWorkflow } from '../../../../../workflows/seller/workflows' /** @@ -32,10 +32,7 @@ export const GET = async ( res: MedusaResponse ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const { data: [onboarding] @@ -78,10 +75,7 @@ export const POST = async ( res: MedusaResponse ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) await recalculateOnboardingWorkflow.run({ container: req.scope, diff --git a/apps/backend/src/api/vendor/sellers/me/route.ts b/apps/backend/src/api/vendor/sellers/me/route.ts index 4eac8cd6..fc96d10e 100644 --- a/apps/backend/src/api/vendor/sellers/me/route.ts +++ b/apps/backend/src/api/vendor/sellers/me/route.ts @@ -1,7 +1,7 @@ import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' import { ContainerRegistrationKeys } from '@medusajs/framework/utils' -import { fetchSellerByAuthActorId } from '../../../../shared/infra/http/utils/seller' +import { fetchSellerByAuthContext } from '../../../../shared/infra/http/utils/seller' import { updateSellerWorkflow } from '../../../../workflows/seller/workflows' import { VendorUpdateSellerType } from '../validators' @@ -31,8 +31,8 @@ export const GET = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, + const seller = await fetchSellerByAuthContext( + req.auth_context, req.scope, req.queryConfig.fields ) @@ -72,10 +72,7 @@ export const POST = async ( res: MedusaResponse ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const { id } = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const { id } = await fetchSellerByAuthContext(req.auth_context, req.scope) await updateSellerWorkflow(req.scope).run({ input: { diff --git a/apps/backend/src/api/vendor/shipping-options/route.ts b/apps/backend/src/api/vendor/shipping-options/route.ts index 828c0b26..a9eacb4c 100644 --- a/apps/backend/src/api/vendor/shipping-options/route.ts +++ b/apps/backend/src/api/vendor/shipping-options/route.ts @@ -5,7 +5,7 @@ import { ContainerRegistrationKeys, Modules } from '@medusajs/framework/utils' import { createShippingOptionsWorkflow } from '@medusajs/medusa/core-flows' import { SELLER_MODULE } from '../../../modules/seller' -import { fetchSellerByAuthActorId } from '../../../shared/infra/http/utils' +import { fetchSellerByAuthContext } from '../../../shared/infra/http/utils' import { VendorCreateShippingOptionType } from './validators' /** @@ -42,10 +42,7 @@ export const POST = async ( const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) const remoteLink = req.scope.resolve(ContainerRegistrationKeys.REMOTE_LINK) - const seller = await fetchSellerByAuthActorId( - req.auth_context?.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const { result } = await createShippingOptionsWorkflow(req.scope).run({ input: [ diff --git a/apps/backend/src/api/vendor/statistics/route.ts b/apps/backend/src/api/vendor/statistics/route.ts index 814f5555..dab82566 100644 --- a/apps/backend/src/api/vendor/statistics/route.ts +++ b/apps/backend/src/api/vendor/statistics/route.ts @@ -4,7 +4,7 @@ import { selectCustomersChartData, selectOrdersChartData } from '../../../modules/seller/utils' -import { fetchSellerByAuthActorId } from '../../../shared/infra/http/utils' +import { fetchSellerByAuthContext } from '../../../shared/infra/http/utils' /** * @oas [get] /vendor/statistics @@ -47,10 +47,7 @@ export const GET = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const orders = await selectOrdersChartData(req.scope, seller.id, [ (req.validatedQuery.time_from as Date).toISOString(), diff --git a/apps/backend/src/api/vendor/stock-locations/[id]/fulfillment-sets/route.ts b/apps/backend/src/api/vendor/stock-locations/[id]/fulfillment-sets/route.ts index 68c87275..9253cf27 100644 --- a/apps/backend/src/api/vendor/stock-locations/[id]/fulfillment-sets/route.ts +++ b/apps/backend/src/api/vendor/stock-locations/[id]/fulfillment-sets/route.ts @@ -1,4 +1,4 @@ -import { fetchSellerByAuthActorId } from '#/shared/infra/http/utils' +import { fetchSellerByAuthContext } from '#/shared/infra/http/utils' import { createLocationFulfillmentSetAndAssociateWithSellerWorkflow } from '#/workflows/fulfillment-set/workflows' import { @@ -54,10 +54,7 @@ export const POST = async ( ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) await createLocationFulfillmentSetAndAssociateWithSellerWorkflow( req.scope diff --git a/apps/backend/src/api/vendor/stock-locations/route.ts b/apps/backend/src/api/vendor/stock-locations/route.ts index 3d4142d7..3f044ea0 100644 --- a/apps/backend/src/api/vendor/stock-locations/route.ts +++ b/apps/backend/src/api/vendor/stock-locations/route.ts @@ -1,6 +1,6 @@ import sellerStockLocationLink from '#/links/seller-stock-location' import { SELLER_MODULE } from '#/modules/seller' -import { fetchSellerByAuthActorId } from '#/shared/infra/http/utils' +import { fetchSellerByAuthContext } from '#/shared/infra/http/utils' import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' import { ContainerRegistrationKeys, Modules } from '@medusajs/framework/utils' @@ -47,10 +47,7 @@ export const POST = async ( ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) const remoteLink = req.scope.resolve(ContainerRegistrationKeys.REMOTE_LINK) - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const { result } = await createStockLocationsWorkflow(req.scope).run({ input: { locations: [req.validatedBody] } diff --git a/apps/backend/src/modules/seller/migrations/.snapshot-mercur.json b/apps/backend/src/modules/seller/migrations/.snapshot-mercur.json index 57321e0e..da7e87a8 100644 --- a/apps/backend/src/modules/seller/migrations/.snapshot-mercur.json +++ b/apps/backend/src/modules/seller/migrations/.snapshot-mercur.json @@ -509,6 +509,141 @@ }, "nativeEnums": {} }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "seller_id": { + "name": "seller_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "token": { + "name": "token", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "redacted": { + "name": "redacted", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "title": { + "name": "title", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "created_by": { + "name": "created_by", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "seller_api_key", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_seller_api_key_deleted_at", + "columnNames": [], + "composite": false, + "constraint": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_seller_api_key_deleted_at\" ON \"seller_api_key\" (deleted_at) WHERE deleted_at IS NULL" + }, + { + "keyName": "seller_api_key_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {}, + "nativeEnums": {} + }, { "columns": { "id": { diff --git a/apps/backend/src/modules/seller/migrations/Migration20250320120006.ts b/apps/backend/src/modules/seller/migrations/Migration20250320120006.ts new file mode 100644 index 00000000..f9493bd2 --- /dev/null +++ b/apps/backend/src/modules/seller/migrations/Migration20250320120006.ts @@ -0,0 +1,14 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250320120006 extends Migration { + + override async up(): Promise { + this.addSql(`create table if not exists "seller_api_key" ("id" text not null, "seller_id" text not null, "token" text not null, "redacted" text not null, "title" text not null, "created_by" text not null, "revoked_by" text null, "revoked_at" timestamptz null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "seller_api_key_pkey" primary key ("id"));`); + this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_seller_api_key_deleted_at" ON "seller_api_key" (deleted_at) WHERE deleted_at IS NULL;`); + } + + override async down(): Promise { + this.addSql(`drop table if exists "seller_api_key" cascade;`); + } + +} diff --git a/apps/backend/src/modules/seller/models/index.ts b/apps/backend/src/modules/seller/models/index.ts index d54c8b71..b8506abb 100644 --- a/apps/backend/src/modules/seller/models/index.ts +++ b/apps/backend/src/modules/seller/models/index.ts @@ -2,3 +2,4 @@ export * from './seller' export * from './member' export * from './invite' export * from './onboarding' +export * from './seller-api-key' diff --git a/apps/backend/src/modules/seller/models/seller-api-key.ts b/apps/backend/src/modules/seller/models/seller-api-key.ts new file mode 100644 index 00000000..e0f40e71 --- /dev/null +++ b/apps/backend/src/modules/seller/models/seller-api-key.ts @@ -0,0 +1,12 @@ +import { model } from '@medusajs/framework/utils' + +export const SellerApiKey = model.define('seller_api_key', { + id: model.id({ prefix: 'selapi' }).primaryKey(), + seller_id: model.text(), + token: model.text(), + redacted: model.text().searchable(), + title: model.text().searchable(), + created_by: model.text(), + revoked_by: model.text().nullable(), + revoked_at: model.dateTime().nullable() +}) diff --git a/apps/backend/src/modules/seller/service.ts b/apps/backend/src/modules/seller/service.ts index a11c42cb..70b10643 100644 --- a/apps/backend/src/modules/seller/service.ts +++ b/apps/backend/src/modules/seller/service.ts @@ -1,3 +1,4 @@ +import crypto, { createHash } from 'crypto' import jwt, { JwtPayload } from 'jsonwebtoken' import { ConfigModule } from '@medusajs/framework' @@ -10,7 +11,13 @@ import { } from '@medusajs/framework/utils' import { SELLER_MODULE } from '.' -import { Member, MemberInvite, Seller, SellerOnboarding } from './models' +import { + Member, + MemberInvite, + Seller, + SellerApiKey, + SellerOnboarding +} from './models' import { MemberInviteDTO } from './types' type InjectedDependencies = { @@ -28,7 +35,8 @@ class SellerModuleService extends MedusaService({ MemberInvite, Member, Seller, - SellerOnboarding + SellerOnboarding, + SellerApiKey }) { private readonly config_: SellerModuleConfig private readonly httpConfig_: ConfigModule['projectConfig']['http'] @@ -121,6 +129,22 @@ class SellerModuleService extends MedusaService({ }) } + public async generateSecretKey() { + const plainToken = 'ssk_' + crypto.randomBytes(32).toString('hex') + const redacted = [plainToken.slice(0, 6), plainToken.slice(-3)].join('***') + const hashedToken = this.calculateHash(plainToken) + + return { + plainToken, + hashedToken, + redacted + } + } + + public calculateHash(token: string): string { + return createHash('sha256').update(token).digest('hex') + } + async isOnboardingCompleted(seller_id: string): Promise { const { onboarding } = await this.retrieveSeller(seller_id, { relations: ['onboarding'] diff --git a/apps/backend/src/modules/seller/types/common.ts b/apps/backend/src/modules/seller/types/common.ts index 7cdca16d..0c2cafed 100644 --- a/apps/backend/src/modules/seller/types/common.ts +++ b/apps/backend/src/modules/seller/types/common.ts @@ -53,3 +53,16 @@ export type MemberInviteDTO = { expires_at: Date accepted: boolean } + +export type SellerApiKeyDTO = { + id: string + seller_id: string + token: string + redacted: string + title: string + created_by: string + revoked_by: string | null + revoked_at: Date | null + created_at: Date + updated_at: Date | null +} diff --git a/apps/backend/src/modules/seller/types/mutations.ts b/apps/backend/src/modules/seller/types/mutations.ts index 86c2063a..6a9d9201 100644 --- a/apps/backend/src/modules/seller/types/mutations.ts +++ b/apps/backend/src/modules/seller/types/mutations.ts @@ -60,3 +60,9 @@ export interface AcceptMemberInviteDTO { export interface UpdateMemberInviteDTO extends Partial { id: string } + +export interface CreateSellerApiKeyDTO { + seller_id: string + title: string + created_by: string +} diff --git a/apps/backend/src/shared/infra/http/middlewares/authenticate-vendor.ts b/apps/backend/src/shared/infra/http/middlewares/authenticate-vendor.ts new file mode 100644 index 00000000..b11ccb17 --- /dev/null +++ b/apps/backend/src/shared/infra/http/middlewares/authenticate-vendor.ts @@ -0,0 +1,69 @@ +import { NextFunction } from 'express' + +import { + AuthenticatedMedusaRequest, + MedusaRequest, + MedusaResponse, + authenticate +} from '@medusajs/framework' + +import { SELLER_MODULE } from '../../../../modules/seller' +import SellerModuleService from '../../../../modules/seller/service' + +async function authenticateWithApiKey( + req: MedusaRequest, + res: MedusaResponse, + next: NextFunction +) { + const service = req.scope.resolve(SELLER_MODULE) + const token = req.headers.authorization?.split(' ')[1] || '' + + let normalizedToken = token + if (!token.startsWith('ssk_')) { + normalizedToken = Buffer.from(token, 'base64').toString('utf-8') + } + + if (normalizedToken.endsWith(':')) { + normalizedToken = normalizedToken.slice(0, -1) + } + + const [api_key] = await service.listSellerApiKeys({ + token: service.calculateHash(normalizedToken) + }) + + if (!api_key || api_key.revoked_at !== null || api_key.deleted_at !== null) { + return res.status(401).json({ message: 'Invalid api key!' }) + } + + const req_ = req as AuthenticatedMedusaRequest + req_.auth_context = { + actor_id: api_key.id, + actor_type: 'seller-api-key', + auth_identity_id: '', + app_metadata: {} + } + + return next() +} + +export function authenticateVendor( + options: { allowUnauthenticated?: boolean; allowUnregistered?: boolean } = {} +) { + return async ( + req: MedusaRequest, + res: MedusaResponse, + next: NextFunction + ) => { + const authorization = req.headers.authorization + + if (authorization && authorization.toLowerCase().startsWith('basic')) { + return authenticateWithApiKey(req, res, next) + } + + return authenticate('seller', ['bearer', 'session'], options)( + req, + res, + next + ) + } +} diff --git a/apps/backend/src/shared/infra/http/middlewares/check-ownership.ts b/apps/backend/src/shared/infra/http/middlewares/check-ownership.ts index 829b7cc6..ba7982b6 100644 --- a/apps/backend/src/shared/infra/http/middlewares/check-ownership.ts +++ b/apps/backend/src/shared/infra/http/middlewares/check-ownership.ts @@ -6,6 +6,8 @@ import { MedusaError } from '@medusajs/framework/utils' +import { fetchSellerByAuthContext } from '../utils' + type CheckResourceOwnershipByResourceIdOptions = { entryPoint: string filterField?: string @@ -49,18 +51,7 @@ export const checkResourceOwnershipByResourceId = ({ ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const { - data: [member] - } = await query.graph( - { - entity: 'member', - fields: ['seller.id'], - filters: { - id: req.auth_context.actor_id - } - }, - { throwIfKeyNotFound: true } - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) const id = resourceId(req) @@ -82,7 +73,7 @@ export const checkResourceOwnershipByResourceId = ({ return } - if (member.seller.id !== resource.seller_id) { + if (seller.id !== resource.seller_id) { res.status(403).json({ message: 'You are not allowed to perform this action', type: MedusaError.Types.NOT_ALLOWED diff --git a/apps/backend/src/shared/infra/http/middlewares/filter-by-seller-id.ts b/apps/backend/src/shared/infra/http/middlewares/filter-by-seller-id.ts index d6040cca..d313b49b 100644 --- a/apps/backend/src/shared/infra/http/middlewares/filter-by-seller-id.ts +++ b/apps/backend/src/shared/infra/http/middlewares/filter-by-seller-id.ts @@ -2,17 +2,14 @@ import { NextFunction } from 'express' import { AuthenticatedMedusaRequest } from '@medusajs/framework/http' -import { fetchSellerByAuthActorId } from '../utils/seller' +import { fetchSellerByAuthContext } from '../utils/seller' /** * @desc Adds a seller id to the filterable fields */ export function filterBySellerId() { return async (req: AuthenticatedMedusaRequest, _, next: NextFunction) => { - const seller = await fetchSellerByAuthActorId( - req.auth_context.actor_id, - req.scope - ) + const seller = await fetchSellerByAuthContext(req.auth_context, req.scope) req.filterableFields.seller_id = seller.id diff --git a/apps/backend/src/shared/infra/http/utils/seller.ts b/apps/backend/src/shared/infra/http/utils/seller.ts index 298da915..1dfb6d26 100644 --- a/apps/backend/src/shared/infra/http/utils/seller.ts +++ b/apps/backend/src/shared/infra/http/utils/seller.ts @@ -1,7 +1,10 @@ import { SellerDTO } from '#/modules/seller/types' -import { MedusaContainer } from '@medusajs/framework' -import { ContainerRegistrationKeys } from '@medusajs/framework/utils' +import { AuthContext, MedusaContainer } from '@medusajs/framework' +import { + ContainerRegistrationKeys, + MedusaError +} from '@medusajs/framework/utils' export const fetchSellerByAuthActorId = async ( authActorId: string, @@ -23,3 +26,41 @@ export const fetchSellerByAuthActorId = async ( }) return seller } + +export const fetchSellerByAuthContext = async ( + ctx: AuthContext, + scope: MedusaContainer, + fields: string[] = ['id'] +): Promise => { + const query = scope.resolve(ContainerRegistrationKeys.QUERY) + + if (ctx.actor_type === 'seller') { + return fetchSellerByAuthActorId(ctx.actor_id, scope, fields) + } + + if (ctx.actor_type === 'seller-api-key') { + const { + data: [api_key] + } = await query.graph({ + entity: 'seller_api_key', + fields: ['seller_id'], + filters: { + id: ctx.actor_id + } + }) + + const { + data: [seller] + } = await query.graph({ + entity: 'seller', + filters: { + id: api_key.seller_id + }, + fields + }) + + return seller + } + + throw new MedusaError(MedusaError.Types.UNAUTHORIZED, 'Invalid actor type!') +} diff --git a/apps/backend/src/workflows/seller/steps/create-seller-api-key.ts b/apps/backend/src/workflows/seller/steps/create-seller-api-key.ts new file mode 100644 index 00000000..9d2350cb --- /dev/null +++ b/apps/backend/src/workflows/seller/steps/create-seller-api-key.ts @@ -0,0 +1,30 @@ +import { StepResponse, createStep } from '@medusajs/framework/workflows-sdk' + +import { SELLER_MODULE } from '../../../modules/seller' +import SellerModuleService from '../../../modules/seller/service' +import { CreateSellerApiKeyDTO } from '../../../modules/seller/types' + +export const createSellerApiKeyStep = createStep( + 'create-seller-api-key', + async (input: CreateSellerApiKeyDTO, { container }) => { + const service = container.resolve(SELLER_MODULE) + + const tokenDetails = await service.generateSecretKey() + + const apiKey = await service.createSellerApiKeys({ + ...input, + token: tokenDetails.hashedToken, + redacted: tokenDetails.redacted + }) + + return new StepResponse( + { + id: apiKey.id, + token: tokenDetails.plainToken, + redacted: tokenDetails.redacted, + ...input + }, + apiKey.id + ) + } +) diff --git a/apps/backend/src/workflows/seller/steps/index.ts b/apps/backend/src/workflows/seller/steps/index.ts index e98d8004..e8f0e9e1 100644 --- a/apps/backend/src/workflows/seller/steps/index.ts +++ b/apps/backend/src/workflows/seller/steps/index.ts @@ -9,3 +9,5 @@ export * from './create-seller-onboarding' export * from './recalculate-onboarding' export * from './get-seller-products' export * from './validate-products-to-import' +export * from './create-seller-api-key' +export * from './revoke-seller-api-key' diff --git a/apps/backend/src/workflows/seller/steps/revoke-seller-api-key.ts b/apps/backend/src/workflows/seller/steps/revoke-seller-api-key.ts new file mode 100644 index 00000000..a6fc41c9 --- /dev/null +++ b/apps/backend/src/workflows/seller/steps/revoke-seller-api-key.ts @@ -0,0 +1,18 @@ +import { StepResponse, createStep } from '@medusajs/framework/workflows-sdk' + +import { SELLER_MODULE } from '../../../modules/seller' +import SellerModuleService from '../../../modules/seller/service' + +export const revokeSellerApiKeyStep = createStep( + 'revoke-seller-api-key', + async (input: { id: string; revoked_by: string }, { container }) => { + const service = container.resolve(SELLER_MODULE) + + const apiKey = await service.updateSellerApiKeys({ + ...input, + revoked_at: new Date() + }) + + return new StepResponse(apiKey, apiKey.id) + } +) diff --git a/apps/backend/src/workflows/seller/workflows/create-seller-api-key.ts b/apps/backend/src/workflows/seller/workflows/create-seller-api-key.ts new file mode 100644 index 00000000..62802bcc --- /dev/null +++ b/apps/backend/src/workflows/seller/workflows/create-seller-api-key.ts @@ -0,0 +1,13 @@ +import { WorkflowResponse, createWorkflow } from '@medusajs/workflows-sdk' + +import { CreateSellerApiKeyDTO } from '../../../modules/seller/types' +import { createSellerApiKeyStep } from '../steps' + +export const createSellerApiKeyWorkflow = createWorkflow( + 'create-seller-api-key', + function (input: CreateSellerApiKeyDTO) { + const apiKey = createSellerApiKeyStep(input) + + return new WorkflowResponse(apiKey) + } +) diff --git a/apps/backend/src/workflows/seller/workflows/index.ts b/apps/backend/src/workflows/seller/workflows/index.ts index 45ac3ce0..cadf7290 100644 --- a/apps/backend/src/workflows/seller/workflows/index.ts +++ b/apps/backend/src/workflows/seller/workflows/index.ts @@ -6,3 +6,5 @@ export * from './create-payout-account-for-seller' export * from './recalculate-onboarding' export * from './export-seller-products' export * from './import-seller-products' +export * from './create-seller-api-key' +export * from './revoke-seller-api-key' diff --git a/apps/backend/src/workflows/seller/workflows/revoke-seller-api-key.ts b/apps/backend/src/workflows/seller/workflows/revoke-seller-api-key.ts new file mode 100644 index 00000000..fe15862d --- /dev/null +++ b/apps/backend/src/workflows/seller/workflows/revoke-seller-api-key.ts @@ -0,0 +1,12 @@ +import { WorkflowResponse, createWorkflow } from '@medusajs/workflows-sdk' + +import { revokeSellerApiKeyStep } from '../steps' + +export const revokeSellerApiKeyWorkflow = createWorkflow( + 'revoke-seller-api-key', + function (input: { id: string; revoked_by: string }) { + const apiKey = revokeSellerApiKeyStep(input) + + return new WorkflowResponse(apiKey) + } +) diff --git a/packages/http-client/src/index.ts b/packages/http-client/src/index.ts index 525c6082..e685bbdc 100644 --- a/packages/http-client/src/index.ts +++ b/packages/http-client/src/index.ts @@ -24041,6 +24041,47 @@ export interface ReviewRemoveRequest { }; } +/** + * Api key + * A seller api key details + */ +export interface SellerApiKey { + /** The unique identifier of the api key. */ + id?: string; + /** The api key title. */ + title?: string; + /** The redacted api key value. */ + redacted?: string; + /** The identity that created the api key. */ + created_by?: string; + /** The identity that revoked the api key. */ + revoked_by?: string; + /** + * The date with timezone at which the invite expires. + * @format date-time + */ + revoked_at?: string; +} + +/** + * Api key explicit + * A seller api key with explicit token value + */ +export interface SellerApiKeyExplicit { + /** The unique identifier of the api key. */ + id?: string; + /** The api key title. */ + title?: string; + /** The redacted api key value. */ + redacted?: string; + /** The seller id associated with the api key. */ + seller_id?: string; + /** Explicit api key value. */ + token?: string; + /** The identity that created the api key. */ + created_by?: string; +} + /** * Create Order Return Request * A schema for the creation of order return request. @@ -24634,6 +24675,15 @@ export interface VendorCreateSeller { }; } +/** + * Create api key + * A schema for the api key creation. + */ +export interface VendorCreateSellerApiKey { + /** The title of the key */ + title?: string; +} + export interface VendorCreateServiceZone { /** The name of the service zone. */ name: string; @@ -56644,6 +56694,109 @@ export class Api extends HttpClient + this.request< + { + api_keys?: SellerApiKey[]; + /** The total number of items available */ + count?: number; + /** The number of items skipped before these items */ + offset?: number; + /** The number of items per page */ + limit?: number; + }, + any + >({ + path: `/vendor/api-keys`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Creates a seller api key + * + * @tags Seller + * @name VendorCreateApiKey + * @summary Create seller api key + * @request POST:/vendor/api-keys + * @secure + */ + vendorCreateApiKey: (data: VendorCreateSellerApiKey, params: RequestParams = {}) => + this.request< + { + /** A seller api key with explicit token value */ + api_key?: SellerApiKeyExplicit; + }, + any + >({ + path: `/vendor/api-keys`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * @description Retrieves an api key by id for the authenticated vendor. + * + * @tags Seller + * @name VendorGetSellerApiKeyById + * @summary Get an api key by id + * @request GET:/vendor/api-keys/{id} + * @secure + */ + vendorGetSellerApiKeyById: (id: string, params: RequestParams = {}) => + this.request< + { + /** A seller api key details */ + api_key?: SellerApiKey; + }, + any + >({ + path: `/vendor/api-keys/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Revokes an api key by id for the authenticated vendor. + * + * @tags Seller + * @name VendorRevokeSellerApiKeyById + * @summary Revoke an api key by id + * @request DELETE:/vendor/api-keys/{id} + * @secure + */ + vendorRevokeSellerApiKeyById: (id: string, params: RequestParams = {}) => + this.request< + { + /** A seller api key details */ + api_key?: SellerApiKey; + }, + any + >({ + path: `/vendor/api-keys/${id}`, + method: "DELETE", + secure: true, + format: "json", + ...params, + }), + /** * @description Retrieves a list of campaigns for the authenticated vendor. *