- Add centralized Zod schemas (server/utils/schemas.js) and validateBody utility for all API endpoints - Fix critical mass assignment in member creation: raw body no longer passed to new Member(), only validated fields (email, name, circle, contributionTier) are accepted - Apply Zod validation to login, profile patch, event registration, updates, verify-payment, and admin event creation endpoints - Fix logout cookie flags to match login (httpOnly: true, secure conditional on NODE_ENV) - Delete unauthenticated test/debug endpoints (test-connection, test-subscription, test-bot) - Remove sensitive console.log statements from Helcim and member endpoints - Remove unused bcryptjs dependency - Add 10MB file size limit on image uploads - Use runtime config for JWT secret across all endpoints - Add 38 validation tests (117 total, all passing)
96 lines
3.3 KiB
JavaScript
96 lines
3.3 KiB
JavaScript
import * as z from 'zod'
|
|
|
|
const privacyEnum = z.enum(['public', 'members', 'private'])
|
|
|
|
export const emailSchema = z.object({
|
|
email: z.string().trim().toLowerCase().email()
|
|
})
|
|
|
|
export const memberCreateSchema = z.object({
|
|
email: z.string().trim().toLowerCase().email(),
|
|
name: z.string().min(1).max(200),
|
|
circle: z.enum(['community', 'founder', 'practitioner']),
|
|
contributionTier: z.enum(['0', '5', '15', '30', '50'])
|
|
})
|
|
|
|
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(),
|
|
studio: z.string().max(200).optional(),
|
|
bio: z.string().max(5000).optional(),
|
|
location: z.string().max(200).optional(),
|
|
socialLinks: z.object({
|
|
mastodon: z.string().max(300).optional(),
|
|
linkedin: z.string().max(300).optional(),
|
|
website: z.string().max(300).optional(),
|
|
other: z.string().max(300).optional()
|
|
}).optional(),
|
|
offering: z.object({
|
|
text: z.string().max(2000).optional(),
|
|
tags: z.array(z.string().max(100)).max(20).optional()
|
|
}).optional(),
|
|
lookingFor: z.object({
|
|
text: z.string().max(2000).optional(),
|
|
tags: z.array(z.string().max(100)).max(20).optional()
|
|
}).optional(),
|
|
showInDirectory: z.boolean().optional(),
|
|
pronounsPrivacy: privacyEnum.optional(),
|
|
timeZonePrivacy: privacyEnum.optional(),
|
|
avatarPrivacy: privacyEnum.optional(),
|
|
studioPrivacy: privacyEnum.optional(),
|
|
bioPrivacy: privacyEnum.optional(),
|
|
locationPrivacy: privacyEnum.optional(),
|
|
socialLinksPrivacy: privacyEnum.optional(),
|
|
offeringPrivacy: privacyEnum.optional(),
|
|
lookingForPrivacy: privacyEnum.optional()
|
|
})
|
|
|
|
export const eventRegistrationSchema = z.object({
|
|
name: z.string().min(1).max(200),
|
|
email: z.string().trim().toLowerCase().email(),
|
|
dietary: z.boolean().optional()
|
|
})
|
|
|
|
export const updateCreateSchema = z.object({
|
|
content: z.string().min(1).max(50000),
|
|
images: z.array(z.string().url()).max(20).optional(),
|
|
privacy: z.enum(['public', 'members', 'private']).optional(),
|
|
commentsEnabled: z.boolean().optional()
|
|
})
|
|
|
|
export const paymentVerifySchema = z.object({
|
|
cardToken: z.string().min(1),
|
|
customerId: z.string().min(1)
|
|
})
|
|
|
|
export const adminEventCreateSchema = z.object({
|
|
title: z.string().min(1).max(500),
|
|
description: z.string().min(1).max(50000),
|
|
startDate: z.string().min(1),
|
|
endDate: z.string().min(1),
|
|
location: z.string().max(500).optional(),
|
|
maxAttendees: z.number().int().positive().optional(),
|
|
membersOnly: z.boolean().optional(),
|
|
registrationDeadline: z.string().optional(),
|
|
pricing: z.object({
|
|
paymentRequired: z.boolean().optional(),
|
|
isFree: z.boolean().optional()
|
|
}).optional(),
|
|
tickets: z.object({
|
|
enabled: z.boolean().optional(),
|
|
public: z.object({
|
|
available: z.boolean().optional(),
|
|
name: z.string().max(200).optional(),
|
|
description: z.string().max(2000).optional(),
|
|
price: z.number().min(0).optional(),
|
|
quantity: z.number().int().positive().optional(),
|
|
earlyBirdPrice: z.number().min(0).optional(),
|
|
earlyBirdDeadline: z.string().optional()
|
|
}).optional()
|
|
}).optional(),
|
|
image: z.string().url().optional(),
|
|
category: z.string().max(100).optional(),
|
|
tags: z.array(z.string().max(100)).max(20).optional(),
|
|
series: z.string().optional()
|
|
})
|