Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 27 additions & 21 deletions packages/framework/src/utils/middlewares/check-ownership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,42 @@ import {
type CheckResourceOwnershipByResourceIdOptions<Body> = {
entryPoint: string
filterField?: string
resourceId?: (req: AuthenticatedMedusaRequest<Body>) => string
resourceId?: (req: AuthenticatedMedusaRequest<Body>) => string | string[]
}

/**
* Middleware that verifies if the authenticated member owns/has access to the requested resource.
* Middleware that verifies if the authenticated member owns/has access to the requested resource(s).
* This is done by checking if the member's seller ID matches the resource's seller ID.
* Supports both single resource ID and arrays of resource IDs.
*
* @param options - Configuration options for the ownership check
* @param options.entryPoint - The entity type to verify ownership of (e.g. 'seller_product', 'service_zone')
* @param options.filterField - Field used to filter/lookup the resource (defaults to 'id')
* @param options.paramIdField - Request parameter containing the resource ID (defaults to 'id')
* @param options.resourceId - Function to extract resource ID(s) from the request (defaults to req.params.id)
*
* @throws {MedusaError} If the member does not own the resource
* @throws {MedusaError} If the member does not own any of the resources
*
* @example
* // Basic usage - check ownership of vendor product
* app.use(checkResourceOwnershipByParamId({
* // Basic usage - check ownership of single vendor product
* app.use(checkResourceOwnershipByResourceId({
* entryPoint: 'seller_product'
* }))
*
* @example
* // Custom field usage - check ownership of service zone
* app.use(checkResourceOwnershipByParamId({
* app.use(checkResourceOwnershipByResourceId({
* entryPoint: 'service_zone',
* filterField: 'service_zone_id',
* resourceId: (req) => req.params.zone_id
* }))
*
* @example
* // Batch usage - check ownership of multiple promotions
* app.use(checkResourceOwnershipByResourceId({
* entryPoint: 'seller_promotion',
* filterField: 'promotion_id',
* resourceId: (req) => [...(req.body.add || []), ...(req.body.remove || [])]
* }))
*/
export const checkResourceOwnershipByResourceId = <Body>({
entryPoint,
Expand All @@ -62,27 +71,24 @@ export const checkResourceOwnershipByResourceId = <Body>({
{ throwIfKeyNotFound: true }
)

const id = resourceId(req)
const ids = resourceId(req)
const idArray = Array.isArray(ids) ? ids : [ids]

const {
data: [resource]
} = await query.graph({
if (idArray.length === 0) {
next()
return
}

const { data: resources } = await query.graph({
entity: entryPoint,
fields: ['seller_id'],
filters: {
[filterField]: id
[filterField]: idArray,
seller_id: member.seller.id
}
})

if (!resource) {
res.status(404).json({
message: `${entryPoint} with ${filterField}: ${id} not found`,
type: MedusaError.Types.NOT_FOUND
})
return
}

if (member.seller.id !== resource.seller_id) {
if (resources.length !== idArray.length) {
res.status(403).json({
message: 'You are not allowed to perform this action',
type: MedusaError.Types.NOT_ALLOWED
Expand Down
Original file line number Diff line number Diff line change
@@ -1,101 +1,90 @@
import { addOrRemoveCampaignPromotionsWorkflow } from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"

import { addOrRemoveCampaignPromotionsWorkflow } from "@medusajs/core-flows"
import { LinkMethodRequest } from "@medusajs/framework/types"
import { refetchCampaign } from "@medusajs/medusa/api/admin/campaigns/helpers"

AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

import {
VendorAssignCampaignPromotionsType,
VendorGetCampaignsParamsType,
} from "../../validators"

/**
* @oas [post] /vendor/campaigns/{id}/promotions
* operationId: VendorPostCampaignsIdPromotions
* summary: Manage the Promotions of a Campaign
* description: Manage the promotions of a campaign, either by adding them or removing them from the campaign.
* operationId: "VendorAssignCampaignPromotions"
* summary: "Assign promotions to campaign"
* description: "Adds or removes promotions from a campaign for the authenticated vendor. The campaign and all promotions must belong to the vendor."
* x-authenticated: true
* parameters:
* - name: id
* in: path
* description: The campaign's ID.
* - in: path
* name: id
* required: true
* description: The ID of the campaign.
* schema:
* type: string
* - name: fields
* in: query
* description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default
* fields. without prefix it will replace the entire default fields.
* required: false
* schema:
* type: string
* title: fields
* description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default
* fields. without prefix it will replace the entire default fields.
* externalDocs:
* url: "#select-fields-and-relations"
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* required: false
* description: Comma-separated fields to include in the response.
* requestBody:
* content:
* application/json:
* schema:
* type: object
* description: The promotions to add or remove from the campaign.
* properties:
* add:
* type: array
* description: The promotions to add to the campaign.
* items:
* type: string
* title: add
* description: A promotion's ID.
* description: Array of promotion IDs to add to the campaign.
* remove:
* type: array
* description: The promotions to remove from the campaign.
* items:
* type: string
* title: remove
* description: A promotion's ID.
* tags:
* - Vendor Campaigns
* description: Array of promotion IDs to remove from the campaign.
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/VendorCampaign"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*
*/
* type: object
* properties:
* campaign:
* $ref: "#/components/schemas/VendorCampaign"
* tags:
* - Vendor Campaigns
* security:
* - api_token: []
* - cookie_auth: []
*/
export const POST = async (
req: AuthenticatedMedusaRequest<
VendorAssignCampaignPromotionsType,
VendorGetCampaignsParamsType
>,
res: MedusaResponse
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { id } = req.params
const { add = [], remove = [] } = req.validatedBody

await addOrRemoveCampaignPromotionsWorkflow(req.scope).run({
input: { id, add, remove },
})

const {
data: [campaign],
} = await query.graph({
entity: "campaign",
fields: req.queryConfig.fields,
filters: {
id: req.params.id,
},
})

export const POST = async (
req: AuthenticatedMedusaRequest<LinkMethodRequest>,
res: MedusaResponse
) => {
const { id } = req.params
const { add, remove } = req.validatedBody
await addOrRemoveCampaignPromotionsWorkflow(req.scope).run({
input: { id, add, remove },
})

const campaign = await refetchCampaign(
req.params.id,
req.scope,
req.queryConfig.fields
)

res.status(200).json({ campaign })
}
res.status(200).json({ campaign })
}
27 changes: 19 additions & 8 deletions packages/modules/b2c-core/src/api/vendor/campaigns/middlewares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,17 @@ import {
validateAndTransformQuery
} from '@medusajs/framework'

import { checkResourceOwnershipByResourceId, filterBySellerId } from '@mercurjs/framework'
import sellerCampaign from '../../../links/seller-campaign'
import {
checkResourceOwnershipByResourceId,
filterBySellerId
} from '../../../shared/infra/http/middlewares'
import sellerPromotion from '../../../links/seller-promotion'
import { vendorCampaignQueryConfig } from './query-config'
import {
VendorAssignCampaignPromotions,
VendorAssignCampaignPromotionsType,
VendorCreateCampaign,
VendorGetCampaignsParams,
VendorUpdateCampaign
} from './validators'
import { createLinkBody } from '@medusajs/medusa/api/utils/validators'

export const vendorCampaignsMiddlewares: MiddlewareRoute[] = [
{
Expand Down Expand Up @@ -80,14 +79,26 @@ export const vendorCampaignsMiddlewares: MiddlewareRoute[] = [
]
},
{
method: ["POST"],
matcher: "/vendor/campaigns/:id/promotions",
method: ['POST'],
matcher: '/vendor/campaigns/:id/promotions',
middlewares: [
validateAndTransformBody(createLinkBody()),
validateAndTransformBody(VendorAssignCampaignPromotions),
validateAndTransformQuery(
VendorGetCampaignsParams,
vendorCampaignQueryConfig.retrieve
),
checkResourceOwnershipByResourceId({
entryPoint: sellerCampaign.entryPoint,
filterField: 'campaign_id'
}),
checkResourceOwnershipByResourceId<VendorAssignCampaignPromotionsType>({
entryPoint: sellerPromotion.entryPoint,
filterField: 'promotion_id',
resourceId: (req) => {
const body = (req.validatedBody ?? req.body) as VendorAssignCampaignPromotionsType
return [...(body?.add || []), ...(body?.remove || [])]
}
})
],
},
]
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from 'zod'

import { CampaignBudgetType, isPresent } from '@medusajs/framework/utils'
import { createFindParams } from '@medusajs/medusa/api/utils/validators'
import { createFindParams, createLinkBody } from '@medusajs/medusa/api/utils/validators'

export type VendorGetCampaignsParamsType = z.infer<
typeof VendorGetCampaignsParams
Expand All @@ -11,6 +11,13 @@ export const VendorGetCampaignsParams = createFindParams({
limit: 50
})


export const VendorAssignCampaignPromotions = createLinkBody();

export type VendorAssignCampaignPromotionsType = z.infer<
typeof VendorAssignCampaignPromotions
>;

/**
* @schema VendorCreateCampaignBudget
* type: object
Expand Down
Loading