Implement OWASP ASVS L1 security remediation (Phases 0-2)

Auth: Add requireAuth/requireAdmin guards with JWT cookie verification,
member status checks (suspended/cancelled = 403), and admin role
enforcement. Apply to all admin, upload, and payment endpoints. Add
role field to Member model.

CSRF: Double-submit cookie middleware with client plugin. Exempt
webhook and magic-link verify routes.

Headers: X-Content-Type-Options, X-Frame-Options, X-XSS-Protection,
Referrer-Policy, Permissions-Policy on all responses. HSTS and CSP
(Helcim/Cloudinary/Plausible sources) in production only.

Rate limiting: Auth 5/5min, payment 10/min, upload 10/min, general
100/min via rate-limiter-flexible, keyed by client IP.

XSS: DOMPurify sanitization on marked() output with tag/attr
allowlists. escapeHtml() utility for email template interpolation.

Anti-enumeration: Login returns identical response for existing and
non-existing emails. Remove 404 handling from login UI components.

Mass assignment: Remove helcimCustomerId from profile allowedFields.

Session: 7-day token expiry, refresh endpoint, httpOnly+secure cookies.

Environment: Validate required secrets on startup via server plugin.
Remove JWT_SECRET hardcoded fallback.
This commit is contained in:
Jennie Robinson Faber 2026-03-01 12:53:18 +00:00
parent 29c96a207e
commit 26c300c357
41 changed files with 566 additions and 380 deletions

View file

@ -1,26 +1,13 @@
import Member from '../../models/member.js'
import Event from '../../models/event.js'
import { connectDB } from '../../utils/mongoose.js'
import jwt from 'jsonwebtoken'
import { requireAdmin } from '../../utils/auth.js'
export default defineEventHandler(async (event) => {
try {
// TODO: Temporarily disabled auth for testing - enable when authentication is set up
// Basic auth check
// const token = getCookie(event, 'auth-token') || getHeader(event, 'authorization')?.replace('Bearer ', '')
// if (!token) {
// throw createError({
// statusCode: 401,
// statusMessage: 'Authentication required'
// })
// }
// const config = useRuntimeConfig()
// jwt.verify(token, config.jwtSecret)
await requireAdmin(event)
await connectDB()
// Get stats
const totalMembers = await Member.countDocuments()
const now = new Date()
@ -28,21 +15,21 @@ export default defineEventHandler(async (event) => {
startDate: { $lte: now },
endDate: { $gte: now }
})
// Calculate monthly revenue from member contributions
const members = await Member.find({}, 'contributionTier').lean()
const monthlyRevenue = members.reduce((total, member) => {
return total + parseInt(member.contributionTier || '0')
}, 0)
const pendingSlackInvites = await Member.countDocuments({ slackInvited: false })
// Get recent members (last 5)
const recentMembers = await Member.find()
.sort({ createdAt: -1 })
.limit(5)
.lean()
// Get upcoming events (next 5)
const upcomingEvents = await Event.find({
startDate: { $gte: now }
@ -50,7 +37,7 @@ export default defineEventHandler(async (event) => {
.sort({ startDate: 1 })
.limit(5)
.lean()
return {
stats: {
totalMembers,
@ -62,9 +49,10 @@ export default defineEventHandler(async (event) => {
upcomingEvents
}
} catch (error) {
if (error.statusCode) throw error
throw createError({
statusCode: 500,
statusMessage: 'Failed to fetch dashboard data'
})
}
})
})

View file

@ -1,34 +1,22 @@
import Event from '../../models/event.js'
import { connectDB } from '../../utils/mongoose.js'
import jwt from 'jsonwebtoken'
import { requireAdmin } from '../../utils/auth.js'
export default defineEventHandler(async (event) => {
try {
// TODO: Temporarily disabled auth for testing - enable when authentication is set up
// Basic auth check - you may want to implement proper admin role checking
// const token = getCookie(event, 'auth-token') || getHeader(event, 'authorization')?.replace('Bearer ', '')
// if (!token) {
// throw createError({
// statusCode: 401,
// statusMessage: 'Authentication required'
// })
// }
// const config = useRuntimeConfig()
// jwt.verify(token, config.jwtSecret)
await requireAdmin(event)
await connectDB()
const events = await Event.find()
.sort({ startDate: 1 })
.lean()
return events
} catch (error) {
if (error.statusCode) throw error
throw createError({
statusCode: 500,
statusMessage: 'Failed to fetch events'
})
}
})
})

View file

@ -1,21 +1,10 @@
import Event from "../../models/event.js";
import { connectDB } from "../../utils/mongoose.js";
import jwt from "jsonwebtoken";
import { requireAdmin } from "../../utils/auth.js";
export default defineEventHandler(async (event) => {
try {
// TODO: Temporarily disabled auth for testing - enable when authentication is set up
// const token = getCookie(event, 'auth-token') || getHeader(event, 'authorization')?.replace('Bearer ', '')
// if (!token) {
// throw createError({
// statusCode: 401,
// statusMessage: 'Authentication required'
// })
// }
// const config = useRuntimeConfig()
// const decoded = jwt.verify(token, config.jwtSecret)
const admin = await requireAdmin(event);
const body = await readBody(event);
@ -31,7 +20,7 @@ export default defineEventHandler(async (event) => {
const eventData = {
...body,
createdBy: "admin@ghostguild.org", // TODO: Use actual authenticated user
createdBy: admin.email,
startDate: new Date(body.startDate),
endDate: new Date(body.endDate),
registrationDeadline: body.registrationDeadline
@ -67,6 +56,7 @@ export default defineEventHandler(async (event) => {
return savedEvent;
} catch (error) {
if (error.statusCode) throw error;
console.error("Error creating event:", error);
throw createError({
statusCode: 500,

View file

@ -1,26 +1,15 @@
import Event from '../../../models/event.js'
import { connectDB } from '../../../utils/mongoose.js'
import jwt from 'jsonwebtoken'
import { requireAdmin } from '../../../utils/auth.js'
export default defineEventHandler(async (event) => {
try {
// TODO: Temporarily disabled auth for testing - enable when authentication is set up
// const token = getCookie(event, 'auth-token') || getHeader(event, 'authorization')?.replace('Bearer ', '')
// if (!token) {
// throw createError({
// statusCode: 401,
// statusMessage: 'Authentication required'
// })
// }
// const config = useRuntimeConfig()
// const decoded = jwt.verify(token, config.jwtSecret)
await requireAdmin(event)
const eventId = getRouterParam(event, 'id')
await connectDB()
const deletedEvent = await Event.findByIdAndDelete(eventId)
if (!deletedEvent) {
@ -29,13 +18,14 @@ export default defineEventHandler(async (event) => {
statusMessage: 'Event not found'
})
}
return { success: true, message: 'Event deleted successfully' }
} catch (error) {
if (error.statusCode) throw error
console.error('Error deleting event:', error)
throw createError({
statusCode: 500,
statusMessage: error.message || 'Failed to delete event'
})
}
})
})

View file

@ -1,47 +1,31 @@
import Event from '../../../models/event.js'
import { connectDB } from '../../../utils/mongoose.js'
import jwt from 'jsonwebtoken'
import { requireAdmin } from '../../../utils/auth.js'
export default defineEventHandler(async (event) => {
try {
// TODO: Temporarily disabled auth for testing - enable when authentication is set up
// const token = getCookie(event, 'auth-token') || getHeader(event, 'authorization')?.replace('Bearer ', '')
// if (!token) {
// throw createError({
// statusCode: 401,
// statusMessage: 'Authentication required'
// })
// }
// const config = useRuntimeConfig()
// const decoded = jwt.verify(token, config.jwtSecret)
await requireAdmin(event)
const eventId = getRouterParam(event, 'id')
console.log('🔍 API: Get event by ID called')
console.log('🔍 API: Event ID param:', eventId)
await connectDB()
const eventData = await Event.findById(eventId)
console.log('🔍 API: Event data found:', eventData ? 'YES' : 'NO')
console.log('🔍 API: Event data preview:', eventData ? { id: eventData._id, title: eventData.title } : null)
if (!eventData) {
console.log('❌ API: Event not found in database')
throw createError({
statusCode: 404,
statusMessage: 'Event not found'
})
}
console.log('✅ API: Returning event data')
return { data: eventData }
} catch (error) {
console.error('❌ API: Error fetching event:', error)
if (error.statusCode) throw error
console.error('Error fetching event:', error)
throw createError({
statusCode: 500,
statusMessage: error.message || 'Failed to fetch event'
})
}
})
})

View file

@ -1,25 +1,14 @@
import Event from '../../../models/event.js'
import { connectDB } from '../../../utils/mongoose.js'
import jwt from 'jsonwebtoken'
import { requireAdmin } from '../../../utils/auth.js'
export default defineEventHandler(async (event) => {
try {
// TODO: Temporarily disabled auth for testing - enable when authentication is set up
// const token = getCookie(event, 'auth-token') || getHeader(event, 'authorization')?.replace('Bearer ', '')
// if (!token) {
// throw createError({
// statusCode: 401,
// statusMessage: 'Authentication required'
// })
// }
// const config = useRuntimeConfig()
// const decoded = jwt.verify(token, config.jwtSecret)
await requireAdmin(event)
const eventId = getRouterParam(event, 'id')
const body = await readBody(event)
// Validate required fields
if (!body.title || !body.description || !body.startDate || !body.endDate) {
throw createError({
@ -29,7 +18,7 @@ export default defineEventHandler(async (event) => {
}
await connectDB()
const updateData = {
...body,
startDate: new Date(body.startDate),
@ -37,7 +26,7 @@ export default defineEventHandler(async (event) => {
registrationDeadline: body.registrationDeadline ? new Date(body.registrationDeadline) : null,
updatedAt: new Date()
}
// Handle ticket data
if (body.tickets) {
updateData.tickets = {
@ -67,13 +56,14 @@ export default defineEventHandler(async (event) => {
statusMessage: 'Event not found'
})
}
return updatedEvent
} catch (error) {
if (error.statusCode) throw error
console.error('Error updating event:', error)
throw createError({
statusCode: 500,
statusMessage: error.message || 'Failed to update event'
})
}
})
})

View file

@ -1,34 +1,22 @@
import Member from '../../models/member.js'
import { connectDB } from '../../utils/mongoose.js'
import jwt from 'jsonwebtoken'
import { requireAdmin } from '../../utils/auth.js'
export default defineEventHandler(async (event) => {
try {
// TODO: Temporarily disabled auth for testing - enable when authentication is set up
// Basic auth check - you may want to implement proper admin role checking
// const token = getCookie(event, 'auth-token') || getHeader(event, 'authorization')?.replace('Bearer ', '')
// if (!token) {
// throw createError({
// statusCode: 401,
// statusMessage: 'Authentication required'
// })
// }
// const config = useRuntimeConfig()
// jwt.verify(token, config.jwtSecret)
await requireAdmin(event)
await connectDB()
const members = await Member.find()
.sort({ createdAt: -1 })
.lean()
return members
} catch (error) {
if (error.statusCode) throw error
throw createError({
statusCode: 500,
statusMessage: 'Failed to fetch members'
})
}
})
})

View file

@ -1,24 +1,13 @@
import Member from '../../models/member.js'
import { connectDB } from '../../utils/mongoose.js'
import jwt from 'jsonwebtoken'
import { requireAdmin } from '../../utils/auth.js'
export default defineEventHandler(async (event) => {
try {
// TODO: Temporarily disabled auth for testing - enable when authentication is set up
// const token = getCookie(event, 'auth-token') || getHeader(event, 'authorization')?.replace('Bearer ', '')
// if (!token) {
// throw createError({
// statusCode: 401,
// statusMessage: 'Authentication required'
// })
// }
// const config = useRuntimeConfig()
// jwt.verify(token, config.jwtSecret)
await requireAdmin(event)
const body = await readBody(event)
// Validate required fields
if (!body.name || !body.email || !body.circle || !body.contributionTier) {
throw createError({
@ -28,7 +17,7 @@ export default defineEventHandler(async (event) => {
}
await connectDB()
// Check if member already exists
const existingMember = await Member.findOne({ email: body.email })
if (existingMember) {
@ -47,14 +36,14 @@ export default defineEventHandler(async (event) => {
})
const savedMember = await newMember.save()
return savedMember
} catch (error) {
if (error.statusCode) throw error
throw createError({
statusCode: 500,
statusMessage: 'Failed to create member'
})
}
})
})

View file

@ -1,9 +1,11 @@
import Series from "../../models/series.js";
import Event from "../../models/event.js";
import { connectDB } from "../../utils/mongoose.js";
import { requireAdmin } from "../../utils/auth.js";
export default defineEventHandler(async (event) => {
try {
await requireAdmin(event);
await connectDB();
// Fetch all series

View file

@ -1,12 +1,14 @@
import Series from '../../models/series.js'
import { connectDB } from '../../utils/mongoose.js'
import { requireAdmin } from '../../utils/auth.js'
export default defineEventHandler(async (event) => {
try {
const admin = await requireAdmin(event)
await connectDB()
const body = await readBody(event)
// Validate required fields
if (!body.id || !body.title || !body.description) {
throw createError({
@ -14,7 +16,7 @@ export default defineEventHandler(async (event) => {
statusMessage: 'Series ID, title, and description are required'
})
}
// Create new series
const newSeries = new Series({
id: body.id,
@ -22,7 +24,7 @@ export default defineEventHandler(async (event) => {
description: body.description,
type: body.type || 'workshop_series',
totalEvents: body.totalEvents || null,
createdBy: 'admin' // TODO: Get from authentication
createdBy: admin.email
})
await newSeries.save()

View file

@ -1,9 +1,11 @@
import Series from '../../models/series.js'
import Event from '../../models/event.js'
import { connectDB } from '../../utils/mongoose.js'
import { requireAdmin } from '../../utils/auth.js'
export default defineEventHandler(async (event) => {
try {
await requireAdmin(event)
await connectDB()
const body = await readBody(event)

View file

@ -1,9 +1,11 @@
import Series from '../../../models/series.js'
import Event from '../../../models/event.js'
import { connectDB } from '../../../utils/mongoose.js'
import { requireAdmin } from '../../../utils/auth.js'
export default defineEventHandler(async (event) => {
try {
await requireAdmin(event)
await connectDB()
const id = getRouterParam(event, 'id')

View file

@ -1,9 +1,11 @@
import Series from '../../../models/series.js'
import Event from '../../../models/event.js'
import { connectDB } from '../../../utils/mongoose.js'
import { requireAdmin } from '../../../utils/auth.js'
export default defineEventHandler(async (event) => {
try {
await requireAdmin(event)
await connectDB()
const id = getRouterParam(event, 'id')

View file

@ -1,9 +1,11 @@
import Series from '../../../models/series.js'
import Event from '../../../models/event.js'
import { connectDB } from '../../../utils/mongoose.js'
import { requireAdmin } from '../../../utils/auth.js'
export default defineEventHandler(async (event) => {
try {
await requireAdmin(event)
await connectDB()
const body = await readBody(event)

View file

@ -19,19 +19,23 @@ export default defineEventHandler(async (event) => {
});
}
const GENERIC_MESSAGE = "If this email is registered, we've sent a login link.";
const member = await Member.findOne({ email });
if (!member) {
throw createError({
statusCode: 404,
statusMessage: "No account found with that email address",
});
// Return same response shape to prevent enumeration
return {
success: true,
message: GENERIC_MESSAGE,
};
}
// Generate magic link token
const token = jwt.sign(
{ memberId: member._id },
process.env.JWT_SECRET,
{ expiresIn: "15m" }, // Shorter expiry for security
{ expiresIn: "15m" },
);
// Get the base URL for the magic link
@ -65,7 +69,7 @@ export default defineEventHandler(async (event) => {
return {
success: true,
message: "Login link sent to your email",
message: GENERIC_MESSAGE,
};
} catch (error) {
console.error("Failed to send email:", error);

View file

@ -1,60 +1,30 @@
import jwt from "jsonwebtoken";
import Member from "../../models/member.js";
import { connectDB } from "../../utils/mongoose.js";
import { requireAuth } from "../../utils/auth.js";
export default defineEventHandler(async (event) => {
await connectDB();
const member = await requireAuth(event);
const token = getCookie(event, "auth-token");
console.log("Auth check - token found:", !!token);
if (!token) {
console.log("No auth token found in cookies");
throw createError({
statusCode: 401,
statusMessage: "Not authenticated",
});
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const member = await Member.findById(decoded.memberId).select("-__v");
if (!member) {
throw createError({
statusCode: 404,
statusMessage: "Member not found",
});
}
return {
_id: member._id,
id: member._id,
email: member.email,
name: member.name,
circle: member.circle,
contributionTier: member.contributionTier,
membershipLevel: `${member.circle}-${member.contributionTier}`,
// Profile fields
pronouns: member.pronouns,
timeZone: member.timeZone,
avatar: member.avatar,
studio: member.studio,
bio: member.bio,
location: member.location,
socialLinks: member.socialLinks,
offering: member.offering,
lookingFor: member.lookingFor,
showInDirectory: member.showInDirectory,
privacy: member.privacy,
// Peer support
peerSupport: member.peerSupport,
};
} catch (err) {
console.error("Token verification error:", err);
throw createError({
statusCode: 401,
statusMessage: "Invalid or expired token",
});
}
return {
_id: member._id,
id: member._id,
email: member.email,
name: member.name,
role: member.role || 'member',
circle: member.circle,
contributionTier: member.contributionTier,
membershipLevel: `${member.circle}-${member.contributionTier}`,
// Profile fields
pronouns: member.pronouns,
timeZone: member.timeZone,
avatar: member.avatar,
studio: member.studio,
bio: member.bio,
location: member.location,
socialLinks: member.socialLinks,
offering: member.offering,
lookingFor: member.lookingFor,
showInDirectory: member.showInDirectory,
privacy: member.privacy,
// Peer support
peerSupport: member.peerSupport,
};
});

View file

@ -0,0 +1,58 @@
import jwt from 'jsonwebtoken'
import Member from '../../models/member.js'
import { connectDB } from '../../utils/mongoose.js'
export default defineEventHandler(async (event) => {
await connectDB()
const token = getCookie(event, 'auth-token')
if (!token) {
throw createError({
statusCode: 401,
statusMessage: 'Not authenticated'
})
}
let decoded
try {
decoded = jwt.verify(token, useRuntimeConfig().jwtSecret)
} catch (err) {
throw createError({
statusCode: 401,
statusMessage: 'Invalid or expired token'
})
}
const member = await Member.findById(decoded.memberId)
if (!member) {
throw createError({
statusCode: 401,
statusMessage: 'Member not found'
})
}
if (member.status === 'suspended' || member.status === 'cancelled') {
throw createError({
statusCode: 403,
statusMessage: 'Account is ' + member.status
})
}
// Issue a fresh token
const newToken = jwt.sign(
{ memberId: member._id, email: member.email },
useRuntimeConfig().jwtSecret,
{ expiresIn: '7d' }
)
setCookie(event, 'auth-token', newToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 // 7 days
})
return { success: true }
})

View file

@ -6,8 +6,6 @@ export default defineEventHandler(async (event) => {
await connectDB()
const token = getCookie(event, 'auth-token')
console.log('🔍 Auth status check - token exists:', !!token)
if (!token) {
return { authenticated: false, member: null }
}
@ -17,11 +15,13 @@ export default defineEventHandler(async (event) => {
const member = await Member.findById(decoded.memberId).select('-__v')
if (!member) {
console.log('⚠️ Token valid but member not found')
return { authenticated: false, member: null }
}
console.log('✅ Auth status check - member found:', member.email)
if (member.status === 'suspended' || member.status === 'cancelled') {
return { authenticated: false, member: null, reason: 'account_' + member.status }
}
return {
authenticated: true,
member: {
@ -34,7 +34,6 @@ export default defineEventHandler(async (event) => {
}
}
} catch (err) {
console.error('❌ Auth status check - token verification failed:', err.message)
return { authenticated: false, member: null }
}
})

View file

@ -33,22 +33,21 @@ export default defineEventHandler(async (event) => {
const sessionToken = jwt.sign(
{ memberId: member._id, email: member.email },
process.env.JWT_SECRET,
{ expiresIn: '30d' }
{ expiresIn: '7d' }
)
// Set the session cookie
setCookie(event, 'auth-token', sessionToken, {
httpOnly: false, // Allow JavaScript access for debugging in development
secure: false, // Don't require HTTPS in development
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30 // 30 days
maxAge: 60 * 60 * 24 * 7 // 7 days
})
// Redirect to the members dashboard or home page
await sendRedirect(event, '/members', 302)
} catch (err) {
console.error('Token verification error:', err)
throw createError({
statusCode: 401,
statusMessage: 'Invalid or expired token'

View file

@ -38,8 +38,6 @@ export default defineEventHandler(async (event) => {
})
}
// Debug: Log token (first few chars only)
console.log('Using Helcim token:', helcimToken.substring(0, 10) + '...')
// Test the connection first with native fetch
try {
@ -55,8 +53,7 @@ export default defineEventHandler(async (event) => {
throw new Error(`HTTP ${testResponse.status}: ${testResponse.statusText}`)
}
const testData = await testResponse.json()
console.log('Connection test passed:', testData)
await testResponse.json()
} catch (testError) {
console.error('Connection test failed:', testError)
throw createError({
@ -113,18 +110,14 @@ export default defineEventHandler(async (event) => {
)
// Set the session cookie server-side
console.log('Setting auth-token cookie for member:', member.email)
console.log('NODE_ENV:', process.env.NODE_ENV)
setCookie(event, 'auth-token', token, {
httpOnly: true, // Server-only for security
secure: false, // Don't require HTTPS in development
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24, // 24 hours
path: '/',
domain: undefined // Let browser set domain automatically
})
console.log('Cookie set successfully')
return {
success: true,
customerId: customerData.id,

View file

@ -1,13 +1,14 @@
// Initialize HelcimPay.js session
import { requireAuth } from "../../utils/auth.js";
const HELCIM_API_BASE = "https://api.helcim.com/v2";
export default defineEventHandler(async (event) => {
try {
await requireAuth(event);
const config = useRuntimeConfig(event);
const body = await readBody(event);
// Debug log the request body
console.log("Initialize payment request body:", body);
const helcimToken =
config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN;
@ -43,8 +44,6 @@ export default defineEventHandler(async (event) => {
requestBody.orderNumber = `${body.metadata.eventId}`;
}
console.log("Helcim request body:", JSON.stringify(requestBody, null, 2));
// Initialize HelcimPay.js session
const response = await fetch(`${HELCIM_API_BASE}/helcim-pay/initialize`, {
method: "POST",

View file

@ -3,6 +3,7 @@ import { getHelcimPlanId, requiresPayment } from '../../config/contributions.js'
import Member from '../../models/member.js'
import { connectDB } from '../../utils/mongoose.js'
import { getSlackService } from '../../utils/slack.ts'
import { requireAuth } from '../../utils/auth.js'
const HELCIM_API_BASE = 'https://api.helcim.com/v2'
@ -72,6 +73,7 @@ async function inviteToSlack(member) {
export default defineEventHandler(async (event) => {
try {
await requireAuth(event)
await connectDB()
const config = useRuntimeConfig(event)
const body = await readBody(event)
@ -91,11 +93,8 @@ export default defineEventHandler(async (event) => {
})
}
console.log('Subscription request body:', body)
// Check if payment is required
if (!requiresPayment(body.contributionTier)) {
console.log('No payment required for tier:', body.contributionTier)
// For free tier, just update member status
const member = await Member.findOneAndUpdate(
{ helcimCustomerId: body.customerId },
@ -107,8 +106,6 @@ export default defineEventHandler(async (event) => {
{ new: true }
)
console.log('Updated member for free tier:', member)
// Send Slack invitation for free tier members
await inviteToSlack(member)
@ -119,11 +116,8 @@ export default defineEventHandler(async (event) => {
}
}
console.log('Payment required for tier:', body.contributionTier)
// Get the Helcim plan ID
const planId = getHelcimPlanId(body.contributionTier)
console.log('Plan ID for tier:', planId)
// Validate card token is provided
if (!body.cardToken) {
@ -135,8 +129,6 @@ export default defineEventHandler(async (event) => {
// Check if we have a configured plan for this tier
if (!planId) {
console.log('No Helcim plan configured for tier:', body.contributionTier)
const member = await Member.findOneAndUpdate(
{ helcimCustomerId: body.customerId },
{
@ -168,8 +160,6 @@ export default defineEventHandler(async (event) => {
// Try to create subscription in Helcim
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
console.log('Attempting to create Helcim subscription with plan ID:', planId)
// Generate a proper alphanumeric idempotency key (exactly 25 characters)
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
let idempotencyKey = ''
@ -197,10 +187,6 @@ export default defineEventHandler(async (event) => {
'idempotency-key': idempotencyKey
}
console.log('Subscription request body:', requestBody)
console.log('Request headers:', requestHeaders)
console.log('Request URL:', `${HELCIM_API_BASE}/subscriptions`)
try {
const subscriptionResponse = await fetch(`${HELCIM_API_BASE}/subscriptions`, {
method: 'POST',
@ -210,47 +196,11 @@ export default defineEventHandler(async (event) => {
if (!subscriptionResponse.ok) {
const errorText = await subscriptionResponse.text()
console.error('Subscription creation failed:')
console.error('Status:', subscriptionResponse.status)
console.error('Status Text:', subscriptionResponse.statusText)
console.error('Headers:', Object.fromEntries(subscriptionResponse.headers.entries()))
console.error('Response Body:', errorText)
console.error('Request was:', {
url: `${HELCIM_API_BASE}/subscriptions`,
method: 'POST',
body: requestBody,
headers: {
'accept': 'application/json',
'content-type': 'application/json',
'api-token': helcimToken ? 'present' : 'missing'
}
})
console.error('Subscription creation failed:', subscriptionResponse.status)
// If it's a validation error, let's try to get more info about available plans
if (subscriptionResponse.status === 400 || subscriptionResponse.status === 404) {
console.log('Plan might not exist. Trying to get list of available payment plans...')
// Try to fetch available payment plans
try {
const plansResponse = await fetch(`${HELCIM_API_BASE}/payment-plans`, {
method: 'GET',
headers: {
'accept': 'application/json',
'api-token': helcimToken
}
})
if (plansResponse.ok) {
const plansData = await plansResponse.json()
console.log('Available payment plans:', JSON.stringify(plansData, null, 2))
} else {
console.log('Could not fetch payment plans:', plansResponse.status, plansResponse.statusText)
}
} catch (planError) {
console.log('Error fetching payment plans:', planError.message)
}
// For now, just update member status and let user know we need to configure plans
// Plan might not exist -- update member status and proceed
const member = await Member.findOneAndUpdate(
{ helcimCustomerId: body.customerId },
{
@ -287,7 +237,6 @@ export default defineEventHandler(async (event) => {
}
const subscriptionData = await subscriptionResponse.json()
console.log('Subscription created successfully:', subscriptionData)
// Extract the first subscription from the response array
const subscription = subscriptionData.data?.[0]

View file

@ -1,8 +1,11 @@
// Update customer billing address
import { requireAuth } from '../../utils/auth.js'
const HELCIM_API_BASE = 'https://api.helcim.com/v2'
export default defineEventHandler(async (event) => {
try {
await requireAuth(event)
const config = useRuntimeConfig(event)
const body = await readBody(event)

View file

@ -1,11 +1,14 @@
// Verify payment token from HelcimPay.js
import { requireAuth } from '../../utils/auth.js'
const HELCIM_API_BASE = 'https://api.helcim.com/v2'
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({
@ -14,25 +17,57 @@ export default defineEventHandler(async (event) => {
})
}
console.log('Payment verification request:', {
customerId: body.customerId,
cardToken: body.cardToken ? 'present' : 'missing'
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
if (!helcimToken) {
throw createError({
statusCode: 500,
statusMessage: 'Helcim API token not configured'
})
}
// Verify the card token by fetching the customer's cards from Helcim
const response = await fetch(`${HELCIM_API_BASE}/customers/${body.customerId}/cards`, {
method: 'GET',
headers: {
'accept': 'application/json',
'api-token': helcimToken
}
})
// Since HelcimPay.js already verified the payment and we have the card token,
// we can just return success. The card is already associated with the customer.
console.log('Payment already verified through HelcimPay.js, returning success')
if (!response.ok) {
const errorText = await response.text()
console.error('Payment verification failed:', response.status, errorText)
throw createError({
statusCode: 502,
statusMessage: 'Payment verification failed with Helcim'
})
}
const cards = await response.json()
// Verify the card token exists for this customer
const cardExists = Array.isArray(cards) && cards.some(card =>
card.cardToken === body.cardToken || card.id
)
if (!cardExists && Array.isArray(cards) && cards.length === 0) {
throw createError({
statusCode: 400,
statusMessage: 'No payment method found for this customer'
})
}
return {
success: true,
cardToken: body.cardToken,
message: 'Payment verified successfully through HelcimPay.js'
message: 'Payment verified with Helcim'
}
} catch (error) {
console.error('Error verifying payment:', error)
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.message || 'Failed to verify payment'
statusMessage: error.statusMessage || 'Failed to verify payment'
})
}
})
})

View file

@ -46,9 +46,11 @@ export default defineEventHandler(async (event) => {
// Search by name or bio
if (search) {
// Escape special regex characters to prevent ReDoS
const escaped = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
dbQuery.$or = [
{ name: { $regex: search, $options: "i" } },
{ bio: { $regex: search, $options: "i" } },
{ name: { $regex: escaped, $options: "i" } },
{ bio: { $regex: escaped, $options: "i" } },
];
}
@ -60,11 +62,12 @@ export default defineEventHandler(async (event) => {
];
// If search is also present, combine with AND
if (search) {
const escaped = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
dbQuery.$and = [
{
$or: [
{ name: { $regex: search, $options: "i" } },
{ bio: { $regex: search, $options: "i" } },
{ name: { $regex: escaped, $options: "i" } },
{ bio: { $regex: escaped, $options: "i" } },
],
},
{

View file

@ -1,29 +1,9 @@
import jwt from "jsonwebtoken";
import Member from "../../models/member.js";
import { connectDB } from "../../utils/mongoose.js";
import { requireAuth } from "../../utils/auth.js";
export default defineEventHandler(async (event) => {
await connectDB();
const token = getCookie(event, "auth-token");
if (!token) {
throw createError({
statusCode: 401,
statusMessage: "Not authenticated",
});
}
let memberId;
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
memberId = decoded.memberId;
} catch (err) {
throw createError({
statusCode: 401,
statusMessage: "Invalid or expired token",
});
}
const authedMember = await requireAuth(event);
const memberId = authedMember._id;
const body = await readBody(event);
@ -37,7 +17,6 @@ export default defineEventHandler(async (event) => {
"location",
"socialLinks",
"showInDirectory",
"helcimCustomerId",
];
// Define privacy fields

View file

@ -1,4 +1,5 @@
import { v2 as cloudinary } from 'cloudinary'
import { requireAuth } from '../../utils/auth.js'
// Configure Cloudinary
cloudinary.config({
@ -9,6 +10,7 @@ cloudinary.config({
export default defineEventHandler(async (event) => {
try {
await requireAuth(event)
// Parse the multipart form data
const formData = await readMultipartFormData(event)