ghostguild-org/server/utils/schemas.js

425 lines
14 KiB
JavaScript

import * as z from 'zod'
import { ADMIN_ALERT_TYPES } from '../models/adminAlertDismissal.js'
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.string().max(500).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(),
showInDirectory: z.boolean().optional(),
notifications: z.object({
events: z.boolean().optional()
}).optional(),
craftTags: z.array(z.string().max(100)).max(16).optional(),
boardSlackHandle: z.string().max(200).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 paymentVerifySchema = z.object({
cardToken: z.string().min(1),
customerId: z.union([z.string(), z.number()]).transform(String)
})
// --- Helcim schemas ---
export const helcimCreatePlanSchema = z.object({
name: z.string().min(1).max(200),
amount: z.union([z.string().min(1), z.number().positive()]),
frequency: z.string().min(1).max(50),
currency: z.string().max(10).optional()
})
export const helcimCustomerSchema = z.object({
name: z.string().min(1).max(200),
email: z.string().trim().toLowerCase().email(),
circle: z.enum(['community', 'founder', 'practitioner']).optional(),
contributionTier: z.enum(['0', '5', '15', '30', '50']).optional(),
agreedToGuidelines: z.literal(true)
})
export const helcimInitializePaymentSchema = z.object({
amount: z.number().min(0).optional(),
customerCode: z.string().max(200).optional(),
metadata: z.object({
type: z.string().max(100).optional(),
eventTitle: z.string().max(500).optional(),
eventId: z.string().max(200).optional()
}).optional()
})
export const helcimSubscriptionSchema = z.object({
customerId: z.union([z.string().min(1), z.number()]),
contributionTier: z.enum(['0', '5', '15', '30', '50']),
customerCode: z.union([z.string().min(1).max(200), z.number()]).transform(String),
cardToken: z.string().max(500).optional().nullable(),
cadence: z.enum(['monthly', 'annual']).default('monthly')
})
export const helcimUpdateBillingSchema = z.object({
customerId: z.union([z.string().min(1), z.number()]),
billingAddress: z.object({
name: z.string().max(200).optional(),
street: z.string().min(1).max(500),
city: z.string().min(1).max(200),
province: z.string().max(200).optional(),
state: z.string().max(200).optional(),
country: z.string().min(1).max(100),
postalCode: z.string().min(1).max(20)
})
})
// --- Event ticket/registration schemas ---
export const ticketPurchaseSchema = z.object({
name: z.string().min(1).max(200),
email: z.string().trim().toLowerCase().email(),
transactionId: z.string().max(500).optional(),
createAccount: z.boolean().optional().default(true)
})
export const ticketReserveSchema = z.object({
email: z.string().trim().toLowerCase().email()
})
export const ticketEligibilitySchema = z.object({
email: z.string().trim().toLowerCase().email()
})
export const waitlistSchema = z.object({
name: z.string().max(200).optional(),
email: z.string().trim().toLowerCase().email(),
membershipLevel: z.string().max(100).optional()
})
export const waitlistDeleteSchema = z.object({
email: z.string().trim().toLowerCase().email()
})
export const cancelRegistrationSchema = z.object({
email: z.string().trim().toLowerCase().email()
})
export const checkRegistrationSchema = z.object({
email: z.string().trim().toLowerCase().email()
})
export const eventPaymentSchema = z.object({
name: z.string().min(1).max(200),
email: z.string().trim().toLowerCase().email(),
paymentToken: z.string().min(1).max(500)
})
// --- Member schemas ---
export const updateContributionSchema = z.object({
contributionTier: z.enum(['0', '5', '15', '30', '50']),
cadence: z.enum(['monthly', 'annual']).default('monthly')
})
export const updateCircleSchema = z.object({
circle: z.enum(['community', 'founder', 'practitioner'])
})
// --- Series ticket schemas ---
export const seriesTicketPurchaseSchema = z.object({
name: z.string().min(1).max(200),
email: z.string().trim().toLowerCase().email(),
paymentId: z.string().max(500).optional(),
ticketType: z.enum(['member', 'public', 'guest']),
})
export const seriesTicketEligibilitySchema = z.object({
email: z.string().trim().toLowerCase().email()
})
// --- Admin schemas ---
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()
})
export const adminEventUpdateSchema = 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().nullable(),
membersOnly: z.boolean().optional(),
registrationDeadline: z.string().optional().nullable(),
pricing: z.object({
paymentRequired: z.boolean().optional(),
isFree: z.boolean().optional(),
publicPrice: z.number().min(0).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().nullable(),
sold: z.number().int().min(0).optional(),
earlyBirdPrice: z.number().min(0).optional().nullable(),
earlyBirdDeadline: z.string().optional().nullable()
}).optional()
}).optional(),
image: z.string().url().optional().nullable(),
category: z.string().max(100).optional(),
tags: z.array(z.string().max(100)).max(20).optional(),
series: z.any().optional(),
slug: z.string().max(500).optional()
}).passthrough()
export const adminSeriesCreateSchema = z.object({
id: z.string().min(1).max(200),
title: z.string().min(1).max(500),
description: z.string().min(1).max(50000),
type: z.string().max(100).optional(),
totalEvents: z.number().int().positive().optional().nullable()
})
export const adminSeriesUpdateSchema = z.object({
id: z.string().min(1).max(200),
title: z.string().min(1).max(500),
description: z.string().max(50000).optional(),
type: z.string().max(100).optional(),
totalEvents: z.number().int().positive().optional().nullable()
})
export const adminSeriesItemUpdateSchema = z.object({
title: z.string().min(1).max(500).optional(),
description: z.string().max(50000).optional(),
type: z.string().max(100).optional(),
totalEvents: z.number().int().positive().optional().nullable(),
isActive: z.boolean().optional()
})
export const adminSeriesTicketsSchema = z.object({
id: z.string().min(1).max(200),
tickets: z.object({
enabled: z.boolean().optional(),
requiresSeriesTicket: z.boolean().optional(),
allowIndividualEventTickets: z.boolean().optional(),
currency: z.string().max(10).optional(),
member: z.object({
available: z.boolean().optional(),
isFree: z.boolean().optional(),
price: z.number().min(0).optional(),
name: z.string().max(200).optional(),
description: z.string().max(2000).optional(),
circleOverrides: z.record(z.any()).optional()
}).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().nullable(),
sold: z.number().int().min(0).optional(),
reserved: z.number().int().min(0).optional(),
earlyBirdPrice: z.number().min(0).optional().nullable(),
earlyBirdDeadline: z.string().optional().nullable()
}).optional(),
capacity: z.object({
total: z.number().int().positive().optional().nullable(),
reserved: z.number().int().min(0).optional()
}).optional(),
waitlist: z.object({
enabled: z.boolean().optional(),
maxSize: z.number().int().positive().optional().nullable(),
entries: z.array(z.any()).optional()
}).optional()
})
})
export const adminMemberCreateSchema = 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'])
})
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'])
})
export const bulkMemberImportSchema = z.object({
members: z.array(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'])
})).min(1).max(100)
})
export const memberInviteSchema = z.object({
memberIds: z.array(z.string().min(1)).min(1).max(100),
emailTemplate: z.string().min(1).max(10000)
})
// --- Pre-registrant schemas ---
export const preRegistrantStatusUpdateSchema = z.object({
status: z.enum(['pending', 'selected']),
adminNotes: z.string().max(2000).optional()
})
export const preRegistrantBulkStatusSchema = z.object({
ids: z.array(z.string().min(1)).min(1).max(100),
status: z.enum(['pending', 'selected'])
})
export const preRegistrantInviteSchema = z.object({
preRegistrantIds: z.array(z.string().min(1)).min(1).max(20),
emailTemplate: z.string().min(1).max(10000)
})
export const inviteVerifySchema = z.object({
token: z.string().min(1)
})
export const inviteAcceptSchema = z.object({
preRegistrationId: z.string().min(1),
name: z.string().min(1).max(200),
pronouns: z.string().max(100).optional(),
location: z.string().max(200).optional(),
circle: z.enum(['community', 'founder', 'practitioner']),
motivation: z.string().max(5000).optional(),
contributionTier: z.enum(['0', '5', '15', '30', '50']),
agreedToGuidelines: z.literal(true),
token: z.string().min(1)
})
// --- Onboarding schemas ---
export const onboardingTrackSchema = z.object({
goal: z.enum(['eventPageVisited', 'boardPageVisited', 'wikiClicked']).optional(),
skip: z.enum(['profileTags', 'visitEvent', 'board', 'wiki']).optional(),
}).refine((v) => v.goal || v.skip, {
message: 'Must provide goal or skip',
})
// --- Tag schemas ---
export const tagSuggestionSchema = z.object({
label: z.string().min(1).max(100),
pool: z.enum(['craft', 'cooperative'])
})
// --- Board post / channel schemas ---
export const boardPostCreateSchema = z.object({
title: z.string().trim().min(1).max(120),
seeking: z.string().max(500).optional(),
offering: z.string().max(500).optional(),
note: z.string().max(300).optional(),
tags: z.array(z.string().max(100)).optional().default([])
}).refine(
(data) => (data.seeking || '').trim().length > 0 || (data.offering || '').trim().length > 0,
{ message: 'At least one of seeking or offering must be provided', path: ['seeking'] }
)
export const boardPostUpdateSchema = z.object({
title: z.string().trim().min(1).max(120).optional(),
seeking: z.string().max(500).optional(),
offering: z.string().max(500).optional(),
note: z.string().max(300).optional(),
tags: z.array(z.string().max(100)).optional()
})
export const boardChannelCreateSchema = z.object({
name: z.string().trim().min(1).max(200),
slackChannelId: z.string().trim().min(1).max(50).regex(/^[A-Z0-9]+$/, 'Invalid Slack channel ID').optional(),
tagSlugs: z.array(z.string().max(100)).optional().default([])
})
export const boardChannelUpdateSchema = z.object({
name: z.string().trim().min(1).max(200).optional(),
slackChannelId: z.string().trim().min(1).max(50).regex(/^[A-Z0-9]+$/, 'Invalid Slack channel ID').optional(),
tagSlugs: z.array(z.string().max(100)).optional()
})
// --- Admin alert schemas ---
export const adminAlertDismissSchema = z.object({
alertType: z.enum(ADMIN_ALERT_TYPES),
signature: z.string().min(1).max(128)
})
export const adminAlertRestoreSchema = z.object({
alertTypes: z
.array(z.enum(ADMIN_ALERT_TYPES))
.min(1)
.max(ADMIN_ALERT_TYPES.length)
})
// --- Site content (key/value editable copy blocks) ---
export const SITE_CONTENT_KEYS = ['homepage.wiki_feature']
export const siteContentUpsertSchema = z.object({
title: z.string().max(300).optional(),
body: z.string().max(5000).optional()
})