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
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -17,10 +17,10 @@ logs
|
|||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
docs/*
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
/docs/
|
||||
scripts/*.js
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
if (import.meta.server) return
|
||||
if (process.server) return
|
||||
|
||||
const { isAuthenticated, memberData, checkMemberStatus } = useAuth()
|
||||
|
||||
|
|
|
|||
10
package-lock.json
generated
10
package-lock.json
generated
|
|
@ -14,7 +14,6 @@
|
|||
"@nuxt/ui": "^4.0.0",
|
||||
"@nuxtjs/plausible": "^3.0.1",
|
||||
"@slack/web-api": "^7.10.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"chrono-node": "^2.8.4",
|
||||
"cloudinary": "^2.7.0",
|
||||
"eslint": "^9.34.0",
|
||||
|
|
@ -7606,15 +7605,6 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"bin": {
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@
|
|||
"@nuxt/ui": "^4.0.0",
|
||||
"@nuxtjs/plausible": "^3.0.1",
|
||||
"@slack/web-api": "^7.10.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"chrono-node": "^2.8.4",
|
||||
"chrono-node": "^2.8.4",
|
||||
"cloudinary": "^2.7.0",
|
||||
"eslint": "^9.34.0",
|
||||
"isomorphic-dompurify": "^3.0.0",
|
||||
|
|
|
|||
|
|
@ -1,20 +1,14 @@
|
|||
import Event from "../../models/event.js";
|
||||
import { connectDB } from "../../utils/mongoose.js";
|
||||
import { requireAdmin } from "../../utils/auth.js";
|
||||
import { validateBody } from "../../utils/validateBody.js";
|
||||
import { adminEventCreateSchema } from "../../utils/schemas.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const admin = await requireAdmin(event);
|
||||
|
||||
const body = await readBody(event);
|
||||
|
||||
// Validate required fields
|
||||
if (!body.title || !body.description || !body.startDate || !body.endDate) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Missing required fields",
|
||||
});
|
||||
}
|
||||
const body = await validateBody(event, adminEventCreateSchema);
|
||||
|
||||
await connectDB();
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import jwt from "jsonwebtoken";
|
|||
import { Resend } from "resend";
|
||||
import Member from "../../models/member.js";
|
||||
import { connectDB } from "../../utils/mongoose.js";
|
||||
import { validateBody } from "../../utils/validateBody.js";
|
||||
import { emailSchema } from "../../utils/schemas.js";
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
|
|
@ -10,14 +12,7 @@ export default defineEventHandler(async (event) => {
|
|||
// Connect to database
|
||||
await connectDB();
|
||||
|
||||
const { email } = await readBody(event);
|
||||
|
||||
if (!email) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Email is required",
|
||||
});
|
||||
}
|
||||
const { email } = await validateBody(event, emailSchema);
|
||||
|
||||
const GENERIC_MESSAGE = "If this email is registered, we've sent a login link.";
|
||||
|
||||
|
|
@ -31,10 +26,11 @@ export default defineEventHandler(async (event) => {
|
|||
};
|
||||
}
|
||||
|
||||
// Generate magic link token
|
||||
// Generate magic link token (use runtime config for consistency with verify/requireAuth)
|
||||
const config = useRuntimeConfig(event);
|
||||
const token = jwt.sign(
|
||||
{ memberId: member._id },
|
||||
process.env.JWT_SECRET,
|
||||
config.jwtSecret,
|
||||
{ expiresIn: "15m" },
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
export default defineEventHandler(async (event) => {
|
||||
// Clear the auth token cookie
|
||||
// Clear the auth token cookie (flags must match login for proper clearing)
|
||||
setCookie(event, 'auth-token', '', {
|
||||
httpOnly: false, // Match the original cookie settings
|
||||
secure: false, // Don't require HTTPS in development
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 0 // Expire immediately
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET)
|
||||
const decoded = jwt.verify(token, useRuntimeConfig().jwtSecret)
|
||||
const member = await Member.findById(decoded.memberId).select('-__v')
|
||||
|
||||
if (!member) {
|
||||
|
|
|
|||
|
|
@ -18,8 +18,9 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
try {
|
||||
// Verify the JWT token
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET)
|
||||
// Verify the JWT token (use runtime config for consistency with login/requireAuth)
|
||||
const config = useRuntimeConfig(event)
|
||||
const decoded = jwt.verify(token, config.jwtSecret)
|
||||
const member = await Member.findById(decoded.memberId)
|
||||
|
||||
if (!member) {
|
||||
|
|
@ -32,7 +33,7 @@ export default defineEventHandler(async (event) => {
|
|||
// Create a new session token for the authenticated user
|
||||
const sessionToken = jwt.sign(
|
||||
{ memberId: member._id, email: member.email },
|
||||
process.env.JWT_SECRET,
|
||||
config.jwtSecret,
|
||||
{ expiresIn: '7d' }
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import Event from "../../../models/event.js";
|
|||
import Member from "../../../models/member.js";
|
||||
import { connectDB } from "../../../utils/mongoose.js";
|
||||
import { sendEventRegistrationEmail } from "../../../utils/resend.js";
|
||||
import { validateBody } from "../../../utils/validateBody.js";
|
||||
import { eventRegistrationSchema } from "../../../utils/schemas.js";
|
||||
import mongoose from "mongoose";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
|
|
@ -9,7 +11,7 @@ export default defineEventHandler(async (event) => {
|
|||
// Ensure database connection
|
||||
await connectDB();
|
||||
const identifier = getRouterParam(event, "id");
|
||||
const body = await readBody(event);
|
||||
const body = await validateBody(event, eventRegistrationSchema);
|
||||
|
||||
if (!identifier) {
|
||||
throw createError({
|
||||
|
|
@ -18,14 +20,6 @@ export default defineEventHandler(async (event) => {
|
|||
});
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!body.name || !body.email) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Name and email are required",
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch the event - try by slug first, then by ID
|
||||
let eventData;
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,6 @@ export default defineEventHandler(async (event) => {
|
|||
// Optional: Verify the transaction with Helcim API
|
||||
// This adds extra security to ensure the transaction is legitimate
|
||||
// For now, we trust the transaction ID from HelcimPay.js
|
||||
console.log("Payment completed with transaction ID:", transactionId);
|
||||
}
|
||||
|
||||
// Create registration
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
|
||||
console.log('Creating payment plan:', body.name)
|
||||
|
||||
const response = await fetch(`${HELCIM_API_BASE}/payment-plans`, {
|
||||
method: 'POST',
|
||||
|
|
@ -44,7 +43,6 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
const planData = await response.json()
|
||||
console.log('Payment plan created:', planData)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default defineEventHandler(async (event) => {
|
|||
// Decode JWT token
|
||||
let decoded
|
||||
try {
|
||||
decoded = jwt.verify(token, process.env.JWT_SECRET)
|
||||
decoded = jwt.verify(token, useRuntimeConfig().jwtSecret)
|
||||
} catch (err) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export default defineEventHandler(async (event) => {
|
|||
email: body.email,
|
||||
helcimCustomerId: customerData.id
|
||||
},
|
||||
process.env.JWT_SECRET,
|
||||
config.jwtSecret,
|
||||
{ expiresIn: '24h' }
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default defineEventHandler(async (event) => {
|
|||
// Decode JWT token
|
||||
let decoded
|
||||
try {
|
||||
decoded = jwt.verify(token, process.env.JWT_SECRET)
|
||||
decoded = jwt.verify(token, useRuntimeConfig().jwtSecret)
|
||||
} catch (err) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
|
|
@ -59,7 +59,6 @@ export default defineEventHandler(async (event) => {
|
|||
const existingCustomer = searchData.customers.find(c => c.email === member.email)
|
||||
|
||||
if (existingCustomer) {
|
||||
console.log('Found existing Helcim customer:', existingCustomer.id)
|
||||
|
||||
// Update member record with customer ID if not already set
|
||||
if (!member.helcimCustomerId) {
|
||||
|
|
@ -77,12 +76,11 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
}
|
||||
} catch (searchError) {
|
||||
console.log('Error searching for customer:', searchError)
|
||||
console.error('Error searching for customer:', searchError)
|
||||
// Continue to create new customer
|
||||
}
|
||||
|
||||
// No existing customer found, create new one
|
||||
console.log('Creating new Helcim customer for:', member.email)
|
||||
const createResponse = await fetch(`${HELCIM_API_BASE}/customers`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -107,7 +105,6 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
const customerData = await createResponse.json()
|
||||
console.log('Created Helcim customer:', customerData.id)
|
||||
|
||||
// Update member record with customer ID
|
||||
member.helcimCustomerId = customerData.id
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ export default defineEventHandler(async (event) => {
|
|||
const config = useRuntimeConfig(event)
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
|
||||
console.log('Fetching payment plans from Helcim...')
|
||||
|
||||
const response = await fetch(`${HELCIM_API_BASE}/payment-plans`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
|
@ -18,17 +16,13 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch payment plans:', response.status, response.statusText)
|
||||
const errorText = await response.text()
|
||||
console.error('Response body:', errorText)
|
||||
|
||||
throw createError({
|
||||
statusCode: response.status,
|
||||
statusMessage: `Failed to fetch payment plans: ${errorText}`
|
||||
statusMessage: 'Failed to fetch payment plans'
|
||||
})
|
||||
}
|
||||
|
||||
const plansData = await response.json()
|
||||
console.log('Payment plans retrieved:', JSON.stringify(plansData, null, 2))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ export default defineEventHandler(async (event) => {
|
|||
const config = useRuntimeConfig(event)
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
|
||||
console.log('Fetching existing subscriptions from Helcim...')
|
||||
|
||||
const response = await fetch(`${HELCIM_API_BASE}/subscriptions`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
|
@ -18,17 +16,13 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch subscriptions:', response.status, response.statusText)
|
||||
const errorText = await response.text()
|
||||
console.error('Response body:', errorText)
|
||||
|
||||
throw createError({
|
||||
statusCode: response.status,
|
||||
statusMessage: `Failed to fetch subscriptions: ${errorText}`
|
||||
statusMessage: 'Failed to fetch subscriptions'
|
||||
})
|
||||
}
|
||||
|
||||
const subscriptionsData = await response.json()
|
||||
console.log('Existing subscriptions:', JSON.stringify(subscriptionsData, null, 2))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
// Test Helcim API connection
|
||||
const HELCIM_API_BASE = 'https://api.helcim.com/v2'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const config = useRuntimeConfig(event)
|
||||
|
||||
// Log token info (safely)
|
||||
const tokenInfo = {
|
||||
hasToken: !!config.public.helcimToken,
|
||||
tokenLength: config.public.helcimToken ? config.public.helcimToken.length : 0,
|
||||
tokenPrefix: config.public.helcimToken ? config.public.helcimToken.substring(0, 10) : null
|
||||
}
|
||||
|
||||
console.log('Helcim Token Info:', tokenInfo)
|
||||
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
|
||||
// Try connection test endpoint
|
||||
const response = await $fetch(`${HELCIM_API_BASE}/connection-test`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'api-token': helcimToken
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Helcim API connection successful',
|
||||
tokenInfo,
|
||||
connectionResponse: response
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Helcim test error:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || 'Failed to connect to Helcim API',
|
||||
statusCode: error.statusCode,
|
||||
tokenInfo: {
|
||||
hasToken: !!useRuntimeConfig().public.helcimToken,
|
||||
tokenLength: useRuntimeConfig().public.helcimToken ? useRuntimeConfig().public.helcimToken.length : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
// Test minimal subscription creation to understand required fields
|
||||
const HELCIM_API_BASE = 'https://api.helcim.com/v2'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const config = useRuntimeConfig(event)
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
|
||||
// Generate a 25-character idempotency key
|
||||
const idempotencyKey = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`.substring(0, 25)
|
||||
|
||||
// Test with minimal fields first
|
||||
const testRequest1 = {
|
||||
customerCode: 'CST1020', // Use a recent customer code
|
||||
planId: 20162
|
||||
}
|
||||
|
||||
console.log('Testing subscription with minimal fields:', testRequest1)
|
||||
|
||||
try {
|
||||
const response1 = await fetch(`${HELCIM_API_BASE}/subscriptions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'content-type': 'application/json',
|
||||
'api-token': helcimToken,
|
||||
'idempotency-key': idempotencyKey + 'a'
|
||||
},
|
||||
body: JSON.stringify(testRequest1)
|
||||
})
|
||||
|
||||
const result1 = await response1.text()
|
||||
console.log('Test 1 - Status:', response1.status)
|
||||
console.log('Test 1 - Response:', result1)
|
||||
|
||||
if (!response1.ok) {
|
||||
// Try with paymentPlanId instead
|
||||
const testRequest2 = {
|
||||
customerCode: 'CST1020',
|
||||
paymentPlanId: 20162
|
||||
}
|
||||
|
||||
console.log('Testing subscription with paymentPlanId:', testRequest2)
|
||||
|
||||
const response2 = await fetch(`${HELCIM_API_BASE}/subscriptions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'content-type': 'application/json',
|
||||
'api-token': helcimToken,
|
||||
'idempotency-key': idempotencyKey + 'b'
|
||||
},
|
||||
body: JSON.stringify(testRequest2)
|
||||
})
|
||||
|
||||
const result2 = await response2.text()
|
||||
console.log('Test 2 - Status:', response2.status)
|
||||
console.log('Test 2 - Response:', result2)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Test error:', error)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Check server logs for test results'
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in test endpoint:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
// Verify payment token from HelcimPay.js
|
||||
import { requireAuth } from '../../utils/auth.js'
|
||||
import { validateBody } from '../../utils/validateBody.js'
|
||||
import { paymentVerifySchema } from '../../utils/schemas.js'
|
||||
|
||||
const HELCIM_API_BASE = 'https://api.helcim.com/v2'
|
||||
|
||||
|
|
@ -7,15 +9,7 @@ export default defineEventHandler(async (event) => {
|
|||
try {
|
||||
await requireAuth(event)
|
||||
const config = useRuntimeConfig(event)
|
||||
const body = await readBody(event)
|
||||
|
||||
// Validate required fields
|
||||
if (!body.cardToken || !body.customerId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Card token and customer ID are required'
|
||||
})
|
||||
}
|
||||
const body = await validateBody(event, paymentVerifySchema)
|
||||
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
|
||||
|
|
@ -48,13 +42,13 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
// Verify the card token exists for this customer
|
||||
const cardExists = Array.isArray(cards) && cards.some(card =>
|
||||
card.cardToken === body.cardToken || card.id
|
||||
card.cardToken === body.cardToken
|
||||
)
|
||||
|
||||
if (!cardExists && Array.isArray(cards) && cards.length === 0) {
|
||||
if (!cardExists) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'No payment method found for this customer'
|
||||
statusMessage: 'Payment method not found or does not belong to this customer'
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default defineEventHandler(async (event) => {
|
|||
// Decode JWT token
|
||||
let decoded;
|
||||
try {
|
||||
decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
decoded = jwt.verify(token, config.jwtSecret);
|
||||
} catch (err) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
import Member from '../../models/member.js'
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
import { getSlackService } from '../../utils/slack.ts'
|
||||
import { validateBody } from '../../utils/validateBody.js'
|
||||
import { memberCreateSchema } from '../../utils/schemas.js'
|
||||
// Simple payment check function to avoid import issues
|
||||
const requiresPayment = (contributionValue) => contributionValue !== '0'
|
||||
|
||||
|
|
@ -14,7 +16,7 @@ async function inviteToSlack(member) {
|
|||
return
|
||||
}
|
||||
|
||||
console.log(`Processing Slack invitation for ${member.email}...`)
|
||||
console.warn(`Processing Slack invitation for member`)
|
||||
|
||||
const inviteResult = await slackService.inviteUserToSlack(
|
||||
member.email,
|
||||
|
|
@ -45,13 +47,13 @@ async function inviteToSlack(member) {
|
|||
inviteResult.status
|
||||
)
|
||||
|
||||
console.log(`Successfully processed Slack invitation for ${member.email}: ${inviteResult.status}`)
|
||||
console.warn(`Slack invitation processed: ${inviteResult.status}`)
|
||||
} else {
|
||||
// Update member record to reflect failed invitation
|
||||
member.slackInviteStatus = 'failed'
|
||||
await member.save()
|
||||
|
||||
console.error(`Failed to process Slack invitation for ${member.email}: ${inviteResult.error}`)
|
||||
console.error(`Failed to process Slack invitation: ${inviteResult.error}`)
|
||||
// Don't throw error - member creation should still succeed
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -73,32 +75,30 @@ export default defineEventHandler(async (event) => {
|
|||
// Ensure database is connected
|
||||
await connectDB()
|
||||
|
||||
const body = await readBody(event)
|
||||
|
||||
const validatedData = await validateBody(event, memberCreateSchema)
|
||||
|
||||
try {
|
||||
// Check if member already exists
|
||||
const existingMember = await Member.findOne({ email: body.email })
|
||||
const existingMember = await Member.findOne({ email: validatedData.email })
|
||||
if (existingMember) {
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
statusMessage: 'A member with this email already exists'
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
statusMessage: 'A member with this email already exists'
|
||||
})
|
||||
}
|
||||
|
||||
const member = new Member(body)
|
||||
|
||||
const member = new Member(validatedData)
|
||||
await member.save()
|
||||
|
||||
// Send Slack invitation for new members
|
||||
await inviteToSlack(member)
|
||||
|
||||
// TODO: Process payment with Helcim if not free tier
|
||||
if (requiresPayment(body.contributionTier)) {
|
||||
if (requiresPayment(validatedData.contributionTier)) {
|
||||
// Payment processing will be added here
|
||||
console.log('Payment processing needed for tier:', body.contributionTier)
|
||||
}
|
||||
|
||||
|
||||
// TODO: Send welcome email
|
||||
console.log('Welcome email should be sent to:', body.email)
|
||||
|
||||
return { success: true, member }
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
if (token) {
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const decoded = jwt.verify(token, useRuntimeConfig().jwtSecret);
|
||||
currentMemberId = decoded.memberId;
|
||||
isAuthenticated = true;
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
let memberId;
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const decoded = jwt.verify(token, useRuntimeConfig().jwtSecret);
|
||||
memberId = decoded.memberId;
|
||||
} catch (err) {
|
||||
throw createError({
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import Member from "../../models/member.js";
|
||||
import { requireAuth } from "../../utils/auth.js";
|
||||
import { validateBody } from "../../utils/validateBody.js";
|
||||
import { memberProfileUpdateSchema } from "../../utils/schemas.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const authedMember = await requireAuth(event);
|
||||
const memberId = authedMember._id;
|
||||
|
||||
const body = await readBody(event);
|
||||
const body = await validateBody(event, memberProfileUpdateSchema);
|
||||
|
||||
// Define allowed profile fields
|
||||
const allowedFields = [
|
||||
// Profile fields from validated body
|
||||
const profileFields = [
|
||||
"pronouns",
|
||||
"timeZone",
|
||||
"avatar",
|
||||
|
|
@ -19,7 +21,7 @@ export default defineEventHandler(async (event) => {
|
|||
"showInDirectory",
|
||||
];
|
||||
|
||||
// Define privacy fields
|
||||
// Privacy fields from validated body
|
||||
const privacyFields = [
|
||||
"pronounsPrivacy",
|
||||
"timeZonePrivacy",
|
||||
|
|
@ -32,10 +34,10 @@ export default defineEventHandler(async (event) => {
|
|||
"lookingForPrivacy",
|
||||
];
|
||||
|
||||
// Build update object
|
||||
// Build update object from validated data
|
||||
const updateData = {};
|
||||
|
||||
allowedFields.forEach((field) => {
|
||||
profileFields.forEach((field) => {
|
||||
if (body[field] !== undefined) {
|
||||
updateData[field] = body[field];
|
||||
}
|
||||
|
|
@ -73,7 +75,7 @@ export default defineEventHandler(async (event) => {
|
|||
if (!member) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
message: "Member not found",
|
||||
statusMessage: "Member not found",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +101,7 @@ export default defineEventHandler(async (event) => {
|
|||
console.error("Profile update error:", error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: "Failed to update profile",
|
||||
statusMessage: "Failed to update profile",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export default defineEventHandler(async (event) => {
|
|||
// Decode JWT token
|
||||
let decoded;
|
||||
try {
|
||||
decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
decoded = jwt.verify(token, useRuntimeConfig().jwtSecret);
|
||||
} catch (err) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
if (token) {
|
||||
try {
|
||||
jwt.verify(token, process.env.JWT_SECRET);
|
||||
jwt.verify(token, useRuntimeConfig().jwtSecret);
|
||||
isAuthenticated = true;
|
||||
} catch (err) {
|
||||
isAuthenticated = false;
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
import { WebClient } from '@slack/web-api'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
if (!config.slackBotToken) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Slack bot token not configured'
|
||||
}
|
||||
}
|
||||
|
||||
const client = new WebClient(config.slackBotToken)
|
||||
|
||||
try {
|
||||
// Test basic API access
|
||||
const authTest = await client.auth.test()
|
||||
console.log('Auth test result:', authTest)
|
||||
|
||||
// Test if admin API is available
|
||||
let adminApiAvailable = false
|
||||
let adminError = null
|
||||
|
||||
try {
|
||||
// Try to call admin.users.list to test admin API access
|
||||
await client.admin.users.list({ limit: 1 })
|
||||
adminApiAvailable = true
|
||||
} catch (error: any) {
|
||||
adminError = error.data?.error || error.message
|
||||
console.log('Admin API test failed:', adminError)
|
||||
}
|
||||
|
||||
// Test channel access if channel ID is configured
|
||||
let channelAccess = false
|
||||
let channelError = null
|
||||
|
||||
if (config.slackVettingChannelId) {
|
||||
try {
|
||||
const channelInfo = await client.conversations.info({
|
||||
channel: config.slackVettingChannelId
|
||||
})
|
||||
channelAccess = !!channelInfo.channel
|
||||
} catch (error: any) {
|
||||
channelError = error.data?.error || error.message
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
botInfo: {
|
||||
user: authTest.user,
|
||||
team: authTest.team,
|
||||
url: authTest.url
|
||||
},
|
||||
adminApiAvailable,
|
||||
adminError: adminApiAvailable ? null : adminError,
|
||||
channelAccess,
|
||||
channelError: channelAccess ? null : channelError,
|
||||
channelId: config.slackVettingChannelId || 'Not configured'
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.data?.error || error.message || 'Unknown error'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -12,7 +12,7 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
let memberId;
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const decoded = jwt.verify(token, useRuntimeConfig().jwtSecret);
|
||||
memberId = decoded.memberId;
|
||||
} catch (err) {
|
||||
throw createError({ statusCode: 401, statusMessage: "Invalid token" });
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
let memberId;
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const decoded = jwt.verify(token, useRuntimeConfig().jwtSecret);
|
||||
memberId = decoded.memberId;
|
||||
} catch (err) {
|
||||
throw createError({
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export default defineEventHandler(async (event) => {
|
|||
// Check if user is authenticated
|
||||
if (token) {
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const decoded = jwt.verify(token, useRuntimeConfig().jwtSecret);
|
||||
memberId = decoded.memberId;
|
||||
} catch (err) {
|
||||
// Token invalid, continue as non-member
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
let memberId;
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const decoded = jwt.verify(token, useRuntimeConfig().jwtSecret);
|
||||
memberId = decoded.memberId;
|
||||
} catch (err) {
|
||||
throw createError({
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default defineEventHandler(async (event) => {
|
|||
// Check if user is authenticated
|
||||
if (token) {
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const decoded = jwt.verify(token, useRuntimeConfig().jwtSecret);
|
||||
memberId = decoded.memberId;
|
||||
} catch (err) {
|
||||
// Token invalid, continue as non-member
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import jwt from "jsonwebtoken";
|
||||
import Update from "../../models/update.js";
|
||||
import { connectDB } from "../../utils/mongoose.js";
|
||||
import { validateBody } from "../../utils/validateBody.js";
|
||||
import { updateCreateSchema } from "../../utils/schemas.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await connectDB();
|
||||
|
|
@ -16,7 +18,7 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
let memberId;
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const decoded = jwt.verify(token, useRuntimeConfig().jwtSecret);
|
||||
memberId = decoded.memberId;
|
||||
} catch (err) {
|
||||
throw createError({
|
||||
|
|
@ -25,14 +27,7 @@ export default defineEventHandler(async (event) => {
|
|||
});
|
||||
}
|
||||
|
||||
const body = await readBody(event);
|
||||
|
||||
if (!body.content || !body.content.trim()) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Content is required",
|
||||
});
|
||||
}
|
||||
const body = await validateBody(event, updateCreateSchema);
|
||||
|
||||
try {
|
||||
const update = await Update.create({
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
let memberId;
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const decoded = jwt.verify(token, useRuntimeConfig().jwtSecret);
|
||||
memberId = decoded.memberId;
|
||||
} catch (err) {
|
||||
throw createError({
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default defineEventHandler(async (event) => {
|
|||
// Check if user is authenticated
|
||||
if (token) {
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const decoded = jwt.verify(token, useRuntimeConfig().jwtSecret);
|
||||
currentMemberId = decoded.memberId;
|
||||
} catch (err) {
|
||||
// Token invalid, continue as non-member
|
||||
|
|
|
|||
|
|
@ -39,6 +39,15 @@ export default defineEventHandler(async (event) => {
|
|||
})
|
||||
}
|
||||
|
||||
// Validate file size (10MB limit)
|
||||
const maxSize = 10 * 1024 * 1024
|
||||
if (fileData.data.length > maxSize) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'File too large. Maximum size is 10MB.'
|
||||
})
|
||||
}
|
||||
|
||||
// Convert buffer to base64 for Cloudinary upload
|
||||
const base64File = `data:${fileData.type};base64,${fileData.data.toString('base64')}`
|
||||
|
||||
|
|
|
|||
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
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@ describe('auth login endpoint', () => {
|
|||
|
||||
await expect(loginHandler(event)).rejects.toMatchObject({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Email is required'
|
||||
statusMessage: 'Validation failed'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
273
tests/server/api/validation.test.js
Normal file
273
tests/server/api/validation.test.js
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { createMockEvent } from '../helpers/createMockEvent.js'
|
||||
import {
|
||||
emailSchema,
|
||||
memberCreateSchema,
|
||||
memberProfileUpdateSchema,
|
||||
eventRegistrationSchema,
|
||||
updateCreateSchema,
|
||||
paymentVerifySchema,
|
||||
adminEventCreateSchema
|
||||
} from '../../../server/utils/schemas.js'
|
||||
import { validateBody } from '../../../server/utils/validateBody.js'
|
||||
|
||||
// --- Schema unit tests ---
|
||||
|
||||
describe('emailSchema', () => {
|
||||
it('accepts a valid email', () => {
|
||||
const result = emailSchema.safeParse({ email: 'test@example.com' })
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data.email).toBe('test@example.com')
|
||||
})
|
||||
|
||||
it('rejects a malformed email', () => {
|
||||
const result = emailSchema.safeParse({ email: 'not-an-email' })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects missing email', () => {
|
||||
const result = emailSchema.safeParse({})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('trims and lowercases email', () => {
|
||||
const result = emailSchema.safeParse({ email: ' Test@EXAMPLE.COM ' })
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data.email).toBe('test@example.com')
|
||||
})
|
||||
})
|
||||
|
||||
describe('memberCreateSchema', () => {
|
||||
const validMember = {
|
||||
email: 'new@example.com',
|
||||
name: 'Test User',
|
||||
circle: 'community',
|
||||
contributionTier: '0'
|
||||
}
|
||||
|
||||
it('accepts valid member data', () => {
|
||||
const result = memberCreateSchema.safeParse(validMember)
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects role field (mass assignment)', () => {
|
||||
const result = memberCreateSchema.safeParse({ ...validMember, role: 'admin' })
|
||||
expect(result.success).toBe(true)
|
||||
// role should NOT be in the output
|
||||
expect(result.data).not.toHaveProperty('role')
|
||||
})
|
||||
|
||||
it('rejects status field (mass assignment)', () => {
|
||||
const result = memberCreateSchema.safeParse({ ...validMember, status: 'active' })
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data).not.toHaveProperty('status')
|
||||
})
|
||||
|
||||
it('rejects helcimCustomerId field (mass assignment)', () => {
|
||||
const result = memberCreateSchema.safeParse({ ...validMember, helcimCustomerId: 'cust_123' })
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data).not.toHaveProperty('helcimCustomerId')
|
||||
})
|
||||
|
||||
it('rejects _id field (mass assignment)', () => {
|
||||
const result = memberCreateSchema.safeParse({ ...validMember, _id: '507f1f77bcf86cd799439011' })
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data).not.toHaveProperty('_id')
|
||||
})
|
||||
|
||||
it('rejects invalid circle enum', () => {
|
||||
const result = memberCreateSchema.safeParse({ ...validMember, circle: 'superadmin' })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects invalid contributionTier enum', () => {
|
||||
const result = memberCreateSchema.safeParse({ ...validMember, contributionTier: '999' })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects missing required fields', () => {
|
||||
const result = memberCreateSchema.safeParse({ email: 'test@example.com' })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('lowercases email', () => {
|
||||
const result = memberCreateSchema.safeParse({ ...validMember, email: 'NEW@Example.COM' })
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data.email).toBe('new@example.com')
|
||||
})
|
||||
})
|
||||
|
||||
describe('eventRegistrationSchema', () => {
|
||||
it('accepts valid registration', () => {
|
||||
const result = eventRegistrationSchema.safeParse({ name: 'Jane', email: 'jane@example.com' })
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects missing name', () => {
|
||||
const result = eventRegistrationSchema.safeParse({ email: 'jane@example.com' })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects malformed email', () => {
|
||||
const result = eventRegistrationSchema.safeParse({ name: 'Jane', email: 'bad' })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('lowercases email', () => {
|
||||
const result = eventRegistrationSchema.safeParse({ name: 'Jane', email: 'JANE@Example.COM' })
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data.email).toBe('jane@example.com')
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateCreateSchema', () => {
|
||||
it('accepts valid content', () => {
|
||||
const result = updateCreateSchema.safeParse({ content: 'Hello world' })
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects empty content', () => {
|
||||
const result = updateCreateSchema.safeParse({ content: '' })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects content exceeding 50000 chars', () => {
|
||||
const result = updateCreateSchema.safeParse({ content: 'a'.repeat(50001) })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts content at exactly 50000 chars', () => {
|
||||
const result = updateCreateSchema.safeParse({ content: 'a'.repeat(50000) })
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('validates images are URLs', () => {
|
||||
const result = updateCreateSchema.safeParse({
|
||||
content: 'test',
|
||||
images: ['not-a-url']
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts valid images array', () => {
|
||||
const result = updateCreateSchema.safeParse({
|
||||
content: 'test',
|
||||
images: ['https://example.com/img.png']
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects more than 20 images', () => {
|
||||
const images = Array.from({ length: 21 }, (_, i) => `https://example.com/img${i}.png`)
|
||||
const result = updateCreateSchema.safeParse({ content: 'test', images })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('validates privacy enum', () => {
|
||||
const result = updateCreateSchema.safeParse({ content: 'test', privacy: 'invalid' })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('paymentVerifySchema', () => {
|
||||
it('accepts valid card token and customer ID', () => {
|
||||
const result = paymentVerifySchema.safeParse({ cardToken: 'tok_123', customerId: 'cust_456' })
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects missing cardToken', () => {
|
||||
const result = paymentVerifySchema.safeParse({ customerId: 'cust_456' })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects empty cardToken', () => {
|
||||
const result = paymentVerifySchema.safeParse({ cardToken: '', customerId: 'cust_456' })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('adminEventCreateSchema', () => {
|
||||
const validEvent = {
|
||||
title: 'Test Event',
|
||||
description: 'A test event',
|
||||
startDate: '2026-04-01T10:00:00Z',
|
||||
endDate: '2026-04-01T12:00:00Z'
|
||||
}
|
||||
|
||||
it('accepts valid event data', () => {
|
||||
const result = adminEventCreateSchema.safeParse(validEvent)
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects missing title', () => {
|
||||
const { title, ...rest } = validEvent
|
||||
const result = adminEventCreateSchema.safeParse(rest)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects missing dates', () => {
|
||||
const { startDate, endDate, ...rest } = validEvent
|
||||
const result = adminEventCreateSchema.safeParse({ ...rest, title: 'Test' })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('memberProfileUpdateSchema', () => {
|
||||
it('rejects role in profile update', () => {
|
||||
const result = memberProfileUpdateSchema.safeParse({ role: 'admin', bio: 'test' })
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data).not.toHaveProperty('role')
|
||||
})
|
||||
|
||||
it('rejects status in profile update', () => {
|
||||
const result = memberProfileUpdateSchema.safeParse({ status: 'active', bio: 'test' })
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data).not.toHaveProperty('status')
|
||||
})
|
||||
|
||||
it('validates privacy enum values', () => {
|
||||
const result = memberProfileUpdateSchema.safeParse({ bioPrivacy: 'invalid' })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts valid privacy values', () => {
|
||||
const result = memberProfileUpdateSchema.safeParse({ bioPrivacy: 'public' })
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// --- validateBody integration tests ---
|
||||
|
||||
describe('validateBody', () => {
|
||||
it('returns validated data on success', async () => {
|
||||
const event = createMockEvent({
|
||||
method: 'POST',
|
||||
body: { email: 'test@example.com' }
|
||||
})
|
||||
const data = await validateBody(event, emailSchema)
|
||||
expect(data.email).toBe('test@example.com')
|
||||
})
|
||||
|
||||
it('throws 400 on validation failure', async () => {
|
||||
const event = createMockEvent({
|
||||
method: 'POST',
|
||||
body: { email: 'bad' }
|
||||
})
|
||||
await expect(validateBody(event, emailSchema)).rejects.toMatchObject({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Validation failed'
|
||||
})
|
||||
})
|
||||
|
||||
it('strips unknown fields from output', async () => {
|
||||
const event = createMockEvent({
|
||||
method: 'POST',
|
||||
body: { email: 'test@example.com', name: 'Test', circle: 'community', contributionTier: '0', role: 'admin', _id: 'fake' }
|
||||
})
|
||||
const data = await validateBody(event, memberCreateSchema)
|
||||
expect(data).not.toHaveProperty('role')
|
||||
expect(data).not.toHaveProperty('_id')
|
||||
expect(data.email).toBe('test@example.com')
|
||||
expect(data.name).toBe('Test')
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue