Add Zod validation, fix mass assignment, remove test endpoints and dead code
- 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)
This commit is contained in:
parent
26c300c357
commit
b7279f57d1
41 changed files with 467 additions and 321 deletions
96
server/utils/schemas.js
Normal file
96
server/utils/schemas.js
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
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()
|
||||
})
|
||||
12
server/utils/validateBody.js
Normal file
12
server/utils/validateBody.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export async function validateBody(event, schema) {
|
||||
const body = await readBody(event)
|
||||
const result = schema.safeParse(body)
|
||||
if (!result.success) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Validation failed',
|
||||
data: result.error.flatten().fieldErrors
|
||||
})
|
||||
}
|
||||
return result.data
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue