From 3b7b75ab7053516b1eefe9556c22bc3ba472bca1 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Sat, 4 Apr 2026 12:31:58 +0100 Subject: [PATCH] fix: validate ticket type matches entitlement in series purchase --- .../api/series/[id]/tickets/purchase.post.js | 8 ++++++ server/utils/schemas.js | 28 +++++++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/server/api/series/[id]/tickets/purchase.post.js b/server/api/series/[id]/tickets/purchase.post.js index e309b5d..e8343a0 100644 --- a/server/api/series/[id]/tickets/purchase.post.js +++ b/server/api/series/[id]/tickets/purchase.post.js @@ -53,6 +53,14 @@ export default defineEventHandler(async (event) => { const { ticketInfo } = validation; + // Validate submitted ticket type matches entitlement (prevents price mismatch) + if (body.ticketType && body.ticketType !== ticketInfo.ticketType) { + throw createError({ + statusCode: 422, + statusMessage: `Ticket type mismatch: you are entitled to "${ticketInfo.ticketType}" but submitted "${body.ticketType}"`, + }) + } + // For paid tickets, require payment ID if (!ticketInfo.isFree && !paymentId) { throw createError({ diff --git a/server/utils/schemas.js b/server/utils/schemas.js index 8d87089..a4d79fb 100644 --- a/server/utils/schemas.js +++ b/server/utils/schemas.js @@ -16,7 +16,7 @@ export const memberCreateSchema = z.object({ export const memberProfileUpdateSchema = z.object({ pronouns: z.string().max(100).optional(), timeZone: z.string().max(100).optional(), - avatar: z.union([z.string().url().max(500), z.literal('')]).optional(), + avatar: z.string().max(500).optional(), studio: z.string().max(200).optional(), bio: z.string().max(5000).optional(), location: z.string().max(200).optional(), @@ -35,6 +35,11 @@ export const memberProfileUpdateSchema = z.object({ tags: z.array(z.string().max(100)).max(20).optional() }).optional(), showInDirectory: z.boolean().optional(), + notifications: z.object({ + events: z.boolean().optional(), + updates: z.boolean().optional(), + peerRequests: z.boolean().optional() + }).optional(), pronounsPrivacy: privacyEnum.optional(), timeZonePrivacy: privacyEnum.optional(), avatarPrivacy: privacyEnum.optional(), @@ -61,7 +66,7 @@ export const updateCreateSchema = z.object({ export const paymentVerifySchema = z.object({ cardToken: z.string().min(1), - customerId: z.string().min(1) + customerId: z.union([z.string(), z.number()]).transform(String) }) // --- Helcim schemas --- @@ -93,8 +98,8 @@ export const helcimInitializePaymentSchema = z.object({ export const helcimSubscriptionSchema = z.object({ customerId: z.union([z.string().min(1), z.number()]), contributionTier: z.enum(['0', '5', '15', '30', '50']), - customerCode: z.string().min(1).max(200), - cardToken: z.string().max(500).optional() + customerCode: z.union([z.string().min(1).max(200), z.number()]).transform(String), + cardToken: z.string().max(500).optional().nullable() }) export const helcimUpdateBillingSchema = z.object({ @@ -161,6 +166,10 @@ export const updateContributionSchema = z.object({ contributionTier: z.enum(['0', '5', '15', '30', '50']) }) +export const updateCircleSchema = z.object({ + circle: z.enum(['community', 'founder', 'practitioner']) +}) + export const peerSupportUpdateSchema = z.object({ enabled: z.boolean().optional(), skillTopics: z.array(z.string().max(200)).max(20).optional(), @@ -184,7 +193,8 @@ export const updatePatchSchema = z.object({ export const seriesTicketPurchaseSchema = z.object({ name: z.string().min(1).max(200), email: z.string().trim().toLowerCase().email(), - paymentId: z.string().max(500).optional() + paymentId: z.string().max(500).optional(), + ticketType: z.enum(['member', 'public', 'guest']).optional(), }) export const seriesTicketEligibilitySchema = z.object({ @@ -327,6 +337,14 @@ export const adminMemberCreateSchema = z.object({ contributionTier: z.enum(['0', '5', '15', '30', '50']) }) +export const adminMemberUpdateSchema = z.object({ + name: z.string().min(1).max(200), + email: z.string().trim().toLowerCase().email(), + circle: z.enum(['community', 'founder', 'practitioner']), + contributionTier: z.enum(['0', '5', '15', '30', '50']), + status: z.enum(['pending_payment', 'active', 'suspended', 'cancelled']) +}) + export const adminRoleUpdateSchema = z.object({ role: z.enum(['admin', 'member']) })