Enhance application structure: Add runtime configuration for environment variables, integrate new dependencies for Cloudinary and UI components, and refactor member management features including improved forms and member dashboard. Update styles and layout for better user experience.
This commit is contained in:
parent
6e7e27ac4e
commit
e4a0a9ab0f
61 changed files with 7902 additions and 950 deletions
70
server/api/admin/dashboard.get.js
Normal file
70
server/api/admin/dashboard.get.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import Member from '../../models/member.js'
|
||||
import Event from '../../models/event.js'
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
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 connectDB()
|
||||
|
||||
// Get stats
|
||||
const totalMembers = await Member.countDocuments()
|
||||
const now = new Date()
|
||||
const activeEvents = await Event.countDocuments({
|
||||
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 }
|
||||
})
|
||||
.sort({ startDate: 1 })
|
||||
.limit(5)
|
||||
.lean()
|
||||
|
||||
return {
|
||||
stats: {
|
||||
totalMembers,
|
||||
activeEvents,
|
||||
monthlyRevenue,
|
||||
pendingSlackInvites
|
||||
},
|
||||
recentMembers,
|
||||
upcomingEvents
|
||||
}
|
||||
} catch (error) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to fetch dashboard data'
|
||||
})
|
||||
}
|
||||
})
|
||||
34
server/api/admin/events.get.js
Normal file
34
server/api/admin/events.get.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import Event from '../../models/event.js'
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
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 connectDB()
|
||||
|
||||
const events = await Event.find()
|
||||
.sort({ startDate: 1 })
|
||||
.lean()
|
||||
|
||||
return events
|
||||
} catch (error) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to fetch events'
|
||||
})
|
||||
}
|
||||
})
|
||||
50
server/api/admin/events.post.js
Normal file
50
server/api/admin/events.post.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import Event from '../../models/event.js'
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
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 body = await readBody(event)
|
||||
|
||||
// Validate required fields
|
||||
if (!body.title || !body.description || !body.startDate || !body.endDate) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Missing required fields'
|
||||
})
|
||||
}
|
||||
|
||||
await connectDB()
|
||||
|
||||
const newEvent = new Event({
|
||||
...body,
|
||||
createdBy: 'admin@ghostguild.org', // TODO: Use actual authenticated user
|
||||
startDate: new Date(body.startDate),
|
||||
endDate: new Date(body.endDate),
|
||||
registrationDeadline: body.registrationDeadline ? new Date(body.registrationDeadline) : null
|
||||
})
|
||||
|
||||
const savedEvent = await newEvent.save()
|
||||
|
||||
return savedEvent
|
||||
} catch (error) {
|
||||
console.error('Error creating event:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: error.message || 'Failed to create event'
|
||||
})
|
||||
}
|
||||
})
|
||||
41
server/api/admin/events/[id].delete.js
Normal file
41
server/api/admin/events/[id].delete.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import Event from '../../../models/event.js'
|
||||
import { connectDB } from '../../../utils/mongoose.js'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
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 eventId = getRouterParam(event, 'id')
|
||||
|
||||
await connectDB()
|
||||
|
||||
const deletedEvent = await Event.findByIdAndDelete(eventId)
|
||||
|
||||
if (!deletedEvent) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Event not found'
|
||||
})
|
||||
}
|
||||
|
||||
return { success: true, message: 'Event deleted successfully' }
|
||||
} catch (error) {
|
||||
console.error('Error deleting event:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: error.message || 'Failed to delete event'
|
||||
})
|
||||
}
|
||||
})
|
||||
47
server/api/admin/events/[id].get.js
Normal file
47
server/api/admin/events/[id].get.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import Event from '../../../models/event.js'
|
||||
import { connectDB } from '../../../utils/mongoose.js'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
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 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)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: error.message || 'Failed to fetch event'
|
||||
})
|
||||
}
|
||||
})
|
||||
62
server/api/admin/events/[id].put.js
Normal file
62
server/api/admin/events/[id].put.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import Event from '../../../models/event.js'
|
||||
import { connectDB } from '../../../utils/mongoose.js'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
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 eventId = getRouterParam(event, 'id')
|
||||
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'
|
||||
})
|
||||
}
|
||||
|
||||
await connectDB()
|
||||
|
||||
const updateData = {
|
||||
...body,
|
||||
startDate: new Date(body.startDate),
|
||||
endDate: new Date(body.endDate),
|
||||
registrationDeadline: body.registrationDeadline ? new Date(body.registrationDeadline) : null,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
|
||||
const updatedEvent = await Event.findByIdAndUpdate(
|
||||
eventId,
|
||||
updateData,
|
||||
{ new: true, runValidators: true }
|
||||
)
|
||||
|
||||
if (!updatedEvent) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Event not found'
|
||||
})
|
||||
}
|
||||
|
||||
return updatedEvent
|
||||
} catch (error) {
|
||||
console.error('Error updating event:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: error.message || 'Failed to update event'
|
||||
})
|
||||
}
|
||||
})
|
||||
34
server/api/admin/members.get.js
Normal file
34
server/api/admin/members.get.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import Member from '../../models/member.js'
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
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 connectDB()
|
||||
|
||||
const members = await Member.find()
|
||||
.sort({ createdAt: -1 })
|
||||
.lean()
|
||||
|
||||
return members
|
||||
} catch (error) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to fetch members'
|
||||
})
|
||||
}
|
||||
})
|
||||
60
server/api/admin/members.post.js
Normal file
60
server/api/admin/members.post.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import Member from '../../models/member.js'
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
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)
|
||||
|
||||
const body = await readBody(event)
|
||||
|
||||
// Validate required fields
|
||||
if (!body.name || !body.email || !body.circle || !body.contributionTier) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Missing required fields'
|
||||
})
|
||||
}
|
||||
|
||||
await connectDB()
|
||||
|
||||
// Check if member already exists
|
||||
const existingMember = await Member.findOne({ email: body.email })
|
||||
if (existingMember) {
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
statusMessage: 'Member with this email already exists'
|
||||
})
|
||||
}
|
||||
|
||||
const newMember = new Member({
|
||||
name: body.name,
|
||||
email: body.email,
|
||||
circle: body.circle,
|
||||
contributionTier: body.contributionTier,
|
||||
slackInvited: false
|
||||
})
|
||||
|
||||
const savedMember = await newMember.save()
|
||||
|
||||
return savedMember
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to create member'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -1,32 +1,76 @@
|
|||
// server/api/auth/login.post.js
|
||||
import jwt from 'jsonwebtoken'
|
||||
import Member from '../../models/member'
|
||||
import { Resend } from 'resend'
|
||||
import Member from '../../models/member.js'
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY)
|
||||
|
||||
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 member = await Member.findOne({ email })
|
||||
if (!member) {
|
||||
throw createError({ statusCode: 404 })
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'No account found with that email address'
|
||||
})
|
||||
}
|
||||
|
||||
// Send magic link via Resend
|
||||
// Generate magic link token
|
||||
const token = jwt.sign(
|
||||
{ memberId: member._id },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '7d' }
|
||||
{ expiresIn: '15m' } // Shorter expiry for security
|
||||
)
|
||||
|
||||
await resend.emails.send({
|
||||
from: 'Ghost Guild <noreply@ghostguild.org>',
|
||||
to: email,
|
||||
subject: 'Your Ghost Guild login link',
|
||||
html: `
|
||||
<a href="https://ghostguild.org/auth/verify?token=${token}">
|
||||
Click here to log in
|
||||
</a>
|
||||
`
|
||||
})
|
||||
// Get the base URL for the magic link
|
||||
const headers = getHeaders(event)
|
||||
const baseUrl = process.env.BASE_URL || `${headers.host?.includes('localhost') ? 'http' : 'https'}://${headers.host}`
|
||||
|
||||
return { success: true }
|
||||
// Send magic link via Resend
|
||||
try {
|
||||
await resend.emails.send({
|
||||
from: 'Ghost Guild <noreply@ghostguild.org>',
|
||||
to: email,
|
||||
subject: 'Your Ghost Guild login link',
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #2563eb;">Welcome back to Ghost Guild!</h2>
|
||||
<p>Click the button below to sign in to your account:</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${baseUrl}/api/auth/verify?token=${token}"
|
||||
style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
|
||||
Sign In to Ghost Guild
|
||||
</a>
|
||||
</div>
|
||||
<p style="color: #666; font-size: 14px;">
|
||||
This link will expire in 15 minutes for security. If you didn't request this login link, you can safely ignore this email.
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Login link sent to your email'
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to send email:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to send login email. Please try again.'
|
||||
})
|
||||
}
|
||||
})
|
||||
11
server/api/auth/logout.post.js
Normal file
11
server/api/auth/logout.post.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export default defineEventHandler(async (event) => {
|
||||
// Clear the auth token cookie
|
||||
setCookie(event, 'auth-token', '', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: 0 // Expire immediately
|
||||
})
|
||||
|
||||
return { message: 'Logged out successfully' }
|
||||
})
|
||||
57
server/api/auth/verify.get.js
Normal file
57
server/api/auth/verify.get.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// server/api/auth/verify.get.js
|
||||
import jwt from 'jsonwebtoken'
|
||||
import Member from '../../models/member.js'
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// Connect to database
|
||||
await connectDB()
|
||||
|
||||
const query = getQuery(event)
|
||||
const { token } = query
|
||||
|
||||
if (!token) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Token is required'
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify the JWT token
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET)
|
||||
const member = await Member.findById(decoded.memberId)
|
||||
|
||||
if (!member) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Member not found'
|
||||
})
|
||||
}
|
||||
|
||||
// Create a new session token for the authenticated user
|
||||
const sessionToken = jwt.sign(
|
||||
{ memberId: member._id, email: member.email },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '30d' }
|
||||
)
|
||||
|
||||
// Set the session cookie
|
||||
setCookie(event, 'auth-token', sessionToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 60 * 24 * 30 // 30 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'
|
||||
})
|
||||
}
|
||||
})
|
||||
65
server/api/events/[id].get.js
Normal file
65
server/api/events/[id].get.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import Event from '../../models/event.js'
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
import mongoose from 'mongoose'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// Ensure database connection
|
||||
await connectDB()
|
||||
const identifier = getRouterParam(event, 'id')
|
||||
|
||||
if (!identifier) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Event identifier is required'
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch event from database - try by slug first, then by ID
|
||||
let eventData
|
||||
|
||||
// Check if identifier is a valid MongoDB ObjectId
|
||||
if (mongoose.Types.ObjectId.isValid(identifier)) {
|
||||
eventData = await Event.findById(identifier)
|
||||
.select('-registrations.email') // Hide emails for privacy
|
||||
.lean()
|
||||
}
|
||||
|
||||
// If not found by ID or not a valid ObjectId, try by slug
|
||||
if (!eventData) {
|
||||
eventData = await Event.findOne({ slug: identifier })
|
||||
.select('-registrations.email') // Hide emails for privacy
|
||||
.lean()
|
||||
}
|
||||
|
||||
if (!eventData) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Event not found'
|
||||
})
|
||||
}
|
||||
|
||||
// Add computed fields
|
||||
const eventWithMeta = {
|
||||
...eventData,
|
||||
id: eventData._id.toString(),
|
||||
registeredCount: eventData.registrations?.length || 0,
|
||||
isFull: eventData.maxAttendees ?
|
||||
(eventData.registrations?.length || 0) >= eventData.maxAttendees :
|
||||
false
|
||||
}
|
||||
|
||||
return eventWithMeta
|
||||
} catch (error) {
|
||||
console.error('Error fetching event:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to fetch event'
|
||||
})
|
||||
}
|
||||
})
|
||||
116
server/api/events/[id]/register.post.js
Normal file
116
server/api/events/[id]/register.post.js
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import Event from '../../../models/event.js'
|
||||
import Member from '../../../models/member.js'
|
||||
import { connectDB } from '../../../utils/mongoose.js'
|
||||
import mongoose from 'mongoose'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// Ensure database connection
|
||||
await connectDB()
|
||||
const identifier = getRouterParam(event, 'id')
|
||||
const body = await readBody(event)
|
||||
|
||||
if (!identifier) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Event identifier is required'
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Check if identifier is a valid MongoDB ObjectId
|
||||
if (mongoose.Types.ObjectId.isValid(identifier)) {
|
||||
eventData = await Event.findById(identifier)
|
||||
}
|
||||
|
||||
// If not found by ID or not a valid ObjectId, try by slug
|
||||
if (!eventData) {
|
||||
eventData = await Event.findOne({ slug: identifier })
|
||||
}
|
||||
|
||||
if (!eventData) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Event not found'
|
||||
})
|
||||
}
|
||||
|
||||
// Check if event is full
|
||||
if (eventData.maxAttendees && eventData.registrations.length >= eventData.maxAttendees) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Event is full'
|
||||
})
|
||||
}
|
||||
|
||||
// Check if already registered
|
||||
const alreadyRegistered = eventData.registrations.some(
|
||||
reg => reg.email.toLowerCase() === body.email.toLowerCase()
|
||||
)
|
||||
|
||||
if (alreadyRegistered) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'You are already registered for this event'
|
||||
})
|
||||
}
|
||||
|
||||
// Check member status if event is members-only
|
||||
if (eventData.membersOnly && body.membershipLevel === 'non-member') {
|
||||
// Check if email belongs to a member
|
||||
const member = await Member.findOne({ email: body.email.toLowerCase() })
|
||||
|
||||
if (!member) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'This event is for members only. Please become a member to register.'
|
||||
})
|
||||
}
|
||||
|
||||
// Update membership level from database
|
||||
body.membershipLevel = `${member.circle}-${member.contributionTier}`
|
||||
}
|
||||
|
||||
// Add registration
|
||||
eventData.registrations.push({
|
||||
name: body.name,
|
||||
email: body.email.toLowerCase(),
|
||||
membershipLevel: body.membershipLevel || 'non-member',
|
||||
dietary: body.dietary || false,
|
||||
registeredAt: new Date()
|
||||
})
|
||||
|
||||
// Save the updated event
|
||||
await eventData.save()
|
||||
|
||||
// TODO: Send confirmation email using Resend
|
||||
// await sendEventRegistrationEmail(body.email, eventData)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Successfully registered for the event',
|
||||
registrationId: eventData.registrations[eventData.registrations.length - 1]._id
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error registering for event:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to register for event'
|
||||
})
|
||||
}
|
||||
})
|
||||
53
server/api/events/index.get.js
Normal file
53
server/api/events/index.get.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import Event from '../../models/event.js'
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// Ensure database connection
|
||||
await connectDB()
|
||||
// Get query parameters for filtering
|
||||
const query = getQuery(event)
|
||||
const filter = {}
|
||||
|
||||
// Only show visible events on public calendar (unless specifically requested)
|
||||
if (query.includeHidden !== 'true') {
|
||||
filter.isVisible = true
|
||||
}
|
||||
|
||||
// Filter for upcoming events only if requested
|
||||
if (query.upcoming === 'true') {
|
||||
filter.startDate = { $gte: new Date() }
|
||||
}
|
||||
|
||||
// Filter by event type if provided
|
||||
if (query.eventType) {
|
||||
filter.eventType = query.eventType
|
||||
}
|
||||
|
||||
// Filter for members-only events
|
||||
if (query.membersOnly !== undefined) {
|
||||
filter.membersOnly = query.membersOnly === 'true'
|
||||
}
|
||||
|
||||
// Fetch events from database
|
||||
const events = await Event.find(filter)
|
||||
.sort({ startDate: 1 })
|
||||
.select('-registrations') // Don't expose registration details in list view
|
||||
.lean()
|
||||
|
||||
// Add computed fields
|
||||
const eventsWithMeta = events.map(event => ({
|
||||
...event,
|
||||
id: event._id.toString(),
|
||||
registeredCount: event.registrations?.length || 0
|
||||
}))
|
||||
|
||||
return eventsWithMeta
|
||||
} catch (error) {
|
||||
console.error('Error fetching events:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to fetch events'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -1,14 +1,42 @@
|
|||
// server/api/members/create.post.js
|
||||
import Member from '../../models/member'
|
||||
import Member from '../../models/member.js'
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
// Simple payment check function to avoid import issues
|
||||
const requiresPayment = (contributionValue) => contributionValue !== '0'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// Ensure database is connected
|
||||
await connectDB()
|
||||
|
||||
const body = await readBody(event)
|
||||
|
||||
try {
|
||||
// Check if member already exists
|
||||
const existingMember = await Member.findOne({ email: body.email })
|
||||
if (existingMember) {
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
statusMessage: 'A member with this email already exists'
|
||||
})
|
||||
}
|
||||
|
||||
const member = new Member(body)
|
||||
await member.save()
|
||||
|
||||
// TODO: Process payment with Helcim if not free tier
|
||||
if (requiresPayment(body.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) {
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: error.message
|
||||
|
|
|
|||
72
server/api/upload/image.post.js
Normal file
72
server/api/upload/image.post.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { v2 as cloudinary } from 'cloudinary'
|
||||
|
||||
// Configure Cloudinary
|
||||
cloudinary.config({
|
||||
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
|
||||
api_key: process.env.CLOUDINARY_API_KEY,
|
||||
api_secret: process.env.CLOUDINARY_API_SECRET
|
||||
})
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// Parse the multipart form data
|
||||
const formData = await readMultipartFormData(event)
|
||||
|
||||
if (!formData || formData.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'No file provided'
|
||||
})
|
||||
}
|
||||
|
||||
// Find the file in the form data
|
||||
const fileData = formData.find(item => item.name === 'file')
|
||||
|
||||
if (!fileData) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'No file found in upload'
|
||||
})
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
if (!fileData.type?.startsWith('image/')) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Invalid file type. Only images are allowed.'
|
||||
})
|
||||
}
|
||||
|
||||
// Convert buffer to base64 for Cloudinary upload
|
||||
const base64File = `data:${fileData.type};base64,${fileData.data.toString('base64')}`
|
||||
|
||||
// Upload to Cloudinary
|
||||
const result = await cloudinary.uploader.upload(base64File, {
|
||||
folder: 'ghost-guild/events',
|
||||
transformation: [
|
||||
{ quality: 'auto', fetch_format: 'auto' },
|
||||
{ width: 1200, height: 630, crop: 'fill' } // Standard social media dimensions
|
||||
],
|
||||
allowed_formats: ['jpg', 'png', 'webp', 'gif'],
|
||||
resource_type: 'image'
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
secure_url: result.secure_url,
|
||||
public_id: result.public_id,
|
||||
width: result.width,
|
||||
height: result.height,
|
||||
format: result.format,
|
||||
bytes: result.bytes
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Image upload error:', error)
|
||||
|
||||
throw createError({
|
||||
statusCode: error.statusCode || 500,
|
||||
statusMessage: error.statusMessage || 'Image upload failed'
|
||||
})
|
||||
}
|
||||
})
|
||||
99
server/models/event.js
Normal file
99
server/models/event.js
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import mongoose from 'mongoose'
|
||||
|
||||
const eventSchema = new mongoose.Schema({
|
||||
title: { type: String, required: true },
|
||||
slug: { type: String, required: true, unique: true },
|
||||
tagline: String,
|
||||
description: { type: String, required: true },
|
||||
content: String,
|
||||
featureImage: {
|
||||
url: String, // Cloudinary URL
|
||||
publicId: String, // Cloudinary public ID for transformations
|
||||
alt: String // Alt text for accessibility
|
||||
},
|
||||
startDate: { type: Date, required: true },
|
||||
endDate: { type: Date, required: true },
|
||||
eventType: {
|
||||
type: String,
|
||||
enum: ['community', 'workshop', 'social', 'showcase'],
|
||||
default: 'community'
|
||||
},
|
||||
// Online-first location handling
|
||||
location: {
|
||||
type: String,
|
||||
required: true,
|
||||
// This will typically be a Slack channel or video conference link
|
||||
validate: {
|
||||
validator: function(v) {
|
||||
// Must be either a valid URL or a Slack channel reference
|
||||
const urlPattern = /^https?:\/\/.+/;
|
||||
const slackPattern = /^#[a-zA-Z0-9-_]+$/;
|
||||
return urlPattern.test(v) || slackPattern.test(v);
|
||||
},
|
||||
message: 'Location must be a valid URL (video conference link) or Slack channel (starting with #)'
|
||||
}
|
||||
},
|
||||
isOnline: { type: Boolean, default: true }, // Default to online-first
|
||||
// Visibility and status controls
|
||||
isVisible: { type: Boolean, default: true }, // Hide from public calendar when false
|
||||
isCancelled: { type: Boolean, default: false },
|
||||
cancellationMessage: String, // Custom message for cancelled events
|
||||
membersOnly: { type: Boolean, default: false },
|
||||
// Circle targeting
|
||||
targetCircles: [{
|
||||
type: String,
|
||||
enum: ['community', 'founder', 'practitioner'],
|
||||
required: false
|
||||
}],
|
||||
maxAttendees: Number,
|
||||
registrationRequired: { type: Boolean, default: false },
|
||||
registrationDeadline: Date,
|
||||
agenda: [String],
|
||||
speakers: [{
|
||||
name: String,
|
||||
role: String,
|
||||
bio: String
|
||||
}],
|
||||
registrations: [{
|
||||
name: String,
|
||||
email: String,
|
||||
membershipLevel: String,
|
||||
registeredAt: { type: Date, default: Date.now }
|
||||
}],
|
||||
createdBy: { type: String, required: true },
|
||||
createdAt: { type: Date, default: Date.now },
|
||||
updatedAt: { type: Date, default: Date.now }
|
||||
})
|
||||
|
||||
// Generate slug from title
|
||||
function generateSlug(title) {
|
||||
return title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
}
|
||||
|
||||
// Pre-save hook to generate slug
|
||||
eventSchema.pre('save', async function(next) {
|
||||
try {
|
||||
if (this.isNew || this.isModified('title')) {
|
||||
let baseSlug = generateSlug(this.title)
|
||||
let slug = baseSlug
|
||||
let counter = 1
|
||||
|
||||
// Ensure slug is unique
|
||||
while (await this.constructor.findOne({ slug, _id: { $ne: this._id } })) {
|
||||
slug = `${baseSlug}-${counter}`
|
||||
counter++
|
||||
}
|
||||
|
||||
this.slug = slug
|
||||
}
|
||||
next()
|
||||
} catch (error) {
|
||||
console.error('Error in pre-save hook:', error)
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
export default mongoose.models.Event || mongoose.model('Event', eventSchema)
|
||||
|
|
@ -1,17 +1,25 @@
|
|||
// server/models/member.js
|
||||
import mongoose from 'mongoose'
|
||||
import { resolve } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
||||
|
||||
// Import configs using dynamic imports to avoid build issues
|
||||
const getValidCircleValues = () => ['community', 'founder', 'practitioner']
|
||||
const getValidContributionValues = () => ['0', '5', '15', '30', '50']
|
||||
|
||||
const memberSchema = new mongoose.Schema({
|
||||
email: { type: String, required: true, unique: true },
|
||||
name: { type: String, required: true },
|
||||
circle: {
|
||||
type: String,
|
||||
enum: ['community', 'founder', 'practitioner'],
|
||||
enum: getValidCircleValues(),
|
||||
required: true
|
||||
},
|
||||
contributionTier: {
|
||||
type: String,
|
||||
enum: ['0', '5', '15', '30', '50'],
|
||||
enum: getValidContributionValues(),
|
||||
required: true
|
||||
},
|
||||
helcimCustomerId: String,
|
||||
|
|
|
|||
140
server/utils/helcim.js
Normal file
140
server/utils/helcim.js
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// Helcim Payment Integration Utilities
|
||||
|
||||
export const processHelcimPayment = async (paymentData) => {
|
||||
const { amount, paymentToken, customerData } = paymentData;
|
||||
|
||||
// Check if Helcim is configured
|
||||
const helcimAccountId = process.env.HELCIM_ACCOUNT_ID;
|
||||
const helcimApiToken = process.env.HELCIM_API_TOKEN;
|
||||
|
||||
if (!helcimAccountId || !helcimApiToken) {
|
||||
console.warn('Helcim not configured - skipping payment processing');
|
||||
return {
|
||||
success: false,
|
||||
message: 'Payment processing not configured',
|
||||
testMode: true
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// In production, you would make API calls to Helcim here
|
||||
// Example structure:
|
||||
const response = await fetch('https://api.helcim.com/v2/payment/purchase', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'api-token': helcimApiToken,
|
||||
'account-id': helcimAccountId
|
||||
},
|
||||
body: JSON.stringify({
|
||||
amount,
|
||||
currency: 'CAD',
|
||||
paymentToken,
|
||||
customerCode: customerData.email,
|
||||
contactName: customerData.name,
|
||||
billingAddress: {
|
||||
contactName: customerData.name,
|
||||
email: customerData.email
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
return {
|
||||
success: result.success || false,
|
||||
transactionId: result.transactionId,
|
||||
customerId: result.customerCode,
|
||||
message: result.message
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Helcim payment error:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || 'Payment processing failed'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const createHelcimSubscription = async (subscriptionData) => {
|
||||
const { customerId, planId, amount } = subscriptionData;
|
||||
|
||||
const helcimAccountId = process.env.HELCIM_ACCOUNT_ID;
|
||||
const helcimApiToken = process.env.HELCIM_API_TOKEN;
|
||||
|
||||
if (!helcimAccountId || !helcimApiToken) {
|
||||
console.warn('Helcim not configured - skipping subscription creation');
|
||||
return {
|
||||
success: false,
|
||||
message: 'Subscription processing not configured',
|
||||
testMode: true
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Create recurring payment plan
|
||||
const response = await fetch('https://api.helcim.com/v2/payment/plan', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'api-token': helcimApiToken,
|
||||
'account-id': helcimAccountId
|
||||
},
|
||||
body: JSON.stringify({
|
||||
customerCode: customerId,
|
||||
planName: `Ghost Guild ${planId}`,
|
||||
amount,
|
||||
currency: 'CAD',
|
||||
frequency: 'MONTHLY',
|
||||
startDate: new Date().toISOString().split('T')[0]
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
return {
|
||||
success: result.success || false,
|
||||
subscriptionId: result.planId,
|
||||
message: result.message
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Helcim subscription error:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || 'Subscription creation failed'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const cancelHelcimSubscription = async (subscriptionId) => {
|
||||
const helcimApiToken = process.env.HELCIM_API_TOKEN;
|
||||
|
||||
if (!helcimApiToken) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Subscription management not configured'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.helcim.com/v2/payment/plan/${subscriptionId}/cancel`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'api-token': helcimApiToken
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
return {
|
||||
success: result.success || false,
|
||||
message: result.message
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Helcim cancellation error:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || 'Subscription cancellation failed'
|
||||
};
|
||||
}
|
||||
};
|
||||
24
server/utils/mongoose.js
Normal file
24
server/utils/mongoose.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import mongoose from 'mongoose';
|
||||
|
||||
let isConnected = false;
|
||||
|
||||
export const connectDB = async () => {
|
||||
if (isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const MONGODB_URI = process.env.NUXT_MONGODB_URI || process.env.MONGODB_URI || 'mongodb://localhost:27017/ghostguild';
|
||||
|
||||
try {
|
||||
await mongoose.connect(MONGODB_URI, {
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
});
|
||||
isConnected = true;
|
||||
console.log('MongoDB connected successfully');
|
||||
} catch (error) {
|
||||
console.error('MongoDB connection error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default connectDB;
|
||||
Loading…
Add table
Add a link
Reference in a new issue