fix: use private helcimApiToken for all server-side Helcim API calls
This commit is contained in:
parent
ccd1d0783a
commit
d31b5b4dac
53 changed files with 1755 additions and 572 deletions
42
server/api/admin/members/[id].put.js
Normal file
42
server/api/admin/members/[id].put.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import Member from '../../../models/member.js'
|
||||
import { connectDB } from '../../../utils/mongoose.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAdmin(event)
|
||||
|
||||
const body = await validateBody(event, adminMemberUpdateSchema)
|
||||
const memberId = getRouterParam(event, 'id')
|
||||
|
||||
await connectDB()
|
||||
|
||||
// If email changed, check for duplicates
|
||||
const existing = await Member.findById(memberId)
|
||||
if (!existing) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Member not found' })
|
||||
}
|
||||
|
||||
if (body.email !== existing.email) {
|
||||
const emailTaken = await Member.findOne({ email: body.email })
|
||||
if (emailTaken) {
|
||||
throw createError({ statusCode: 409, statusMessage: 'Email already in use by another member' })
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await Member.findByIdAndUpdate(memberId, {
|
||||
name: body.name,
|
||||
email: body.email,
|
||||
circle: body.circle,
|
||||
contributionTier: body.contributionTier,
|
||||
status: body.status,
|
||||
}, { new: true })
|
||||
|
||||
return {
|
||||
_id: updated._id,
|
||||
name: updated.name,
|
||||
email: updated.email,
|
||||
circle: updated.circle,
|
||||
contributionTier: updated.contributionTier,
|
||||
status: updated.status,
|
||||
role: updated.role,
|
||||
}
|
||||
})
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import jwt from 'jsonwebtoken'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { Resend } from 'resend'
|
||||
import Member from '../../../models/member.js'
|
||||
import { connectDB } from '../../../utils/mongoose.js'
|
||||
|
|
@ -10,12 +11,12 @@ export default defineEventHandler(async (event) => {
|
|||
const { memberIds, emailTemplate } = await validateBody(event, memberInviteSchema)
|
||||
await connectDB()
|
||||
|
||||
const config = useRuntimeConfig(event)
|
||||
const headers = getHeaders(event)
|
||||
const baseUrl =
|
||||
process.env.BASE_URL ||
|
||||
`${headers.host?.includes('localhost') ? 'http' : 'https'}://${headers.host}`
|
||||
const baseUrl = process.env.BASE_URL
|
||||
if (!baseUrl) {
|
||||
throw createError({ statusCode: 500, statusMessage: 'BASE_URL environment variable is not set' })
|
||||
}
|
||||
|
||||
const config = useRuntimeConfig(event)
|
||||
const members = await Member.find({ _id: { $in: memberIds } })
|
||||
|
||||
if (members.length === 0) {
|
||||
|
|
@ -28,15 +29,32 @@ export default defineEventHandler(async (event) => {
|
|||
const results = []
|
||||
|
||||
for (const member of members) {
|
||||
// Skip suspended/cancelled — do not reactivate silently
|
||||
if (member.status === 'suspended' || member.status === 'cancelled') {
|
||||
results.push({
|
||||
memberId: member._id,
|
||||
email: member.email,
|
||||
success: false,
|
||||
error: `Skipped: account is ${member.status}`,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate 48-hour magic login token (same format as login.post.js)
|
||||
// Generate single-use invite token (48h), same jti pattern as login.post.js
|
||||
const jti = randomUUID()
|
||||
const token = jwt.sign(
|
||||
{ memberId: member._id },
|
||||
{ memberId: member._id, jti },
|
||||
config.jwtSecret,
|
||||
{ expiresIn: '48h' }
|
||||
{ expiresIn: '48h' },
|
||||
)
|
||||
|
||||
const loginLink = `${baseUrl}/api/auth/verify?token=${token}`
|
||||
// Store jti for single-use enforcement in verify.post.js
|
||||
member.magicLinkJti = jti
|
||||
member.magicLinkJtiUsed = false
|
||||
|
||||
// Token in fragment — never hits server logs
|
||||
const loginLink = `${baseUrl}/verify#${token}`
|
||||
|
||||
// Interpolate template variables
|
||||
const emailText = emailTemplate
|
||||
|
|
@ -59,9 +77,9 @@ export default defineEventHandler(async (event) => {
|
|||
const { error: emailError } = await resend.emails.send({
|
||||
from: 'Ghost Guild <welcome@babyghosts.org>',
|
||||
to: [member.email],
|
||||
subject: 'You\'re invited to Ghost Guild',
|
||||
subject: "You're invited to Ghost Guild",
|
||||
text: emailText,
|
||||
html: emailHtml
|
||||
html: emailHtml,
|
||||
})
|
||||
|
||||
if (emailError) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export default defineEventHandler(async (event) => {
|
|||
id: member._id,
|
||||
email: member.email,
|
||||
name: member.name,
|
||||
status: member.status,
|
||||
role: member.role || 'member',
|
||||
circle: member.circle,
|
||||
contributionTier: member.contributionTier,
|
||||
|
|
@ -23,8 +24,11 @@ export default defineEventHandler(async (event) => {
|
|||
offering: member.offering,
|
||||
lookingFor: member.lookingFor,
|
||||
showInDirectory: member.showInDirectory,
|
||||
notifications: member.notifications,
|
||||
privacy: member.privacy,
|
||||
// Peer support
|
||||
peerSupport: member.peerSupport,
|
||||
// Timestamps
|
||||
createdAt: member.createdAt,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,9 +40,16 @@ export default defineEventHandler(async (event) => {
|
|||
})
|
||||
}
|
||||
|
||||
// Issue a fresh token
|
||||
if (decoded.tv !== member.tokenVersion) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Session has been revoked'
|
||||
})
|
||||
}
|
||||
|
||||
// Issue a fresh token with current tokenVersion
|
||||
const newToken = jwt.sign(
|
||||
{ memberId: member._id, email: member.email },
|
||||
{ memberId: member._id, email: member.email, tv: member.tokenVersion },
|
||||
useRuntimeConfig().jwtSecret,
|
||||
{ expiresIn: '7d' }
|
||||
)
|
||||
|
|
@ -51,7 +58,8 @@ export default defineEventHandler(async (event) => {
|
|||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 60 * 24 * 7 // 7 days
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
if (member.status === 'suspended' || member.status === 'cancelled') {
|
||||
return { authenticated: false, member: null, reason: 'account_' + member.status }
|
||||
return { authenticated: false, member: null }
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -29,6 +29,7 @@ export default defineEventHandler(async (event) => {
|
|||
email: member.email,
|
||||
name: member.name,
|
||||
circle: member.circle,
|
||||
status: member.status,
|
||||
contributionTier: member.contributionTier,
|
||||
membershipLevel: `${member.circle}-${member.contributionTier}`
|
||||
}
|
||||
|
|
|
|||
41
server/api/dev/member-login.get.js
Normal file
41
server/api/dev/member-login.get.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import jwt from 'jsonwebtoken'
|
||||
import Member from '../../models/member.js'
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// Only allow in development
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Not found' })
|
||||
}
|
||||
|
||||
const query = getQuery(event)
|
||||
const email = query.email
|
||||
|
||||
if (!email) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'email query param required' })
|
||||
}
|
||||
|
||||
await connectDB()
|
||||
|
||||
const member = await Member.findOne({ email: email.toLowerCase() })
|
||||
|
||||
if (!member) {
|
||||
throw createError({ statusCode: 404, statusMessage: `No member found with email: ${email}` })
|
||||
}
|
||||
|
||||
const config = useRuntimeConfig(event)
|
||||
const token = jwt.sign(
|
||||
{ memberId: member._id, email: member.email },
|
||||
config.jwtSecret,
|
||||
{ expiresIn: '7d' }
|
||||
)
|
||||
|
||||
setCookie(event, 'auth-token', token, {
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
})
|
||||
|
||||
await sendRedirect(event, '/member/account', 302)
|
||||
})
|
||||
|
|
@ -3,12 +3,15 @@ import {
|
|||
sendEventCancellationEmail,
|
||||
sendWaitlistNotificationEmail,
|
||||
} from "../../../utils/resend.js";
|
||||
import { connectDB } from "../../../utils/mongoose.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, "id");
|
||||
const body = await validateBody(event, cancelRegistrationSchema);
|
||||
const { email } = body;
|
||||
|
||||
await connectDB();
|
||||
|
||||
try {
|
||||
// Check if id is a valid ObjectId or treat as slug
|
||||
const isObjectId = /^[0-9a-fA-F]{24}$/.test(id);
|
||||
|
|
@ -46,13 +49,15 @@ export default defineEventHandler(async (event) => {
|
|||
eventDoc.registrations[registrationIndex].membershipLevel,
|
||||
};
|
||||
|
||||
// Remove the registration
|
||||
eventDoc.registrations.splice(registrationIndex, 1);
|
||||
|
||||
// Update registered count
|
||||
eventDoc.registeredCount = eventDoc.registrations.length;
|
||||
|
||||
await eventDoc.save();
|
||||
// Use $pull to avoid re-validating the whole document (e.g. legacy location formats)
|
||||
await Event.findByIdAndUpdate(
|
||||
eventDoc._id,
|
||||
{
|
||||
$pull: { registrations: { email: registration.email } },
|
||||
$inc: { registeredCount: -1 },
|
||||
},
|
||||
{ runValidators: false }
|
||||
);
|
||||
|
||||
// Send cancellation confirmation email
|
||||
try {
|
||||
|
|
@ -90,9 +95,13 @@ export default defineEventHandler(async (event) => {
|
|||
if (waitlistEntry) {
|
||||
await sendWaitlistNotificationEmail(waitlistEntry, eventData);
|
||||
|
||||
// Mark as notified
|
||||
waitlistEntry.notified = true;
|
||||
await eventDoc.save();
|
||||
// Mark as notified using findByIdAndUpdate to avoid re-validating the document
|
||||
const entryIndex = eventDoc.tickets.waitlist.entries.indexOf(waitlistEntry);
|
||||
await Event.findByIdAndUpdate(
|
||||
eventDoc._id,
|
||||
{ $set: { [`tickets.waitlist.entries.${entryIndex}.notified`]: true } },
|
||||
{ runValidators: false }
|
||||
);
|
||||
}
|
||||
} catch (waitlistError) {
|
||||
// Log error but don't fail the cancellation
|
||||
|
|
|
|||
|
|
@ -76,8 +76,8 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
// If event requires payment and user is not a member, redirect to payment flow
|
||||
if (
|
||||
eventData.pricing.paymentRequired &&
|
||||
!eventData.pricing.isFree &&
|
||||
eventData.pricing?.paymentRequired &&
|
||||
!eventData.pricing?.isFree &&
|
||||
!member
|
||||
) {
|
||||
throw createError({
|
||||
|
|
@ -109,10 +109,13 @@ export default defineEventHandler(async (event) => {
|
|||
registeredAt: new Date(),
|
||||
};
|
||||
|
||||
eventData.registrations.push(registration);
|
||||
|
||||
// Save the updated event
|
||||
await eventData.save();
|
||||
// Use $push to avoid re-validating the whole document (e.g. legacy location formats)
|
||||
const result = await Event.findByIdAndUpdate(
|
||||
eventData._id,
|
||||
{ $push: { registrations: registration } },
|
||||
{ new: true, runValidators: false }
|
||||
);
|
||||
const newRegistration = result.registrations[result.registrations.length - 1];
|
||||
|
||||
// Send confirmation email using Resend
|
||||
try {
|
||||
|
|
@ -125,8 +128,7 @@ export default defineEventHandler(async (event) => {
|
|||
return {
|
||||
success: true,
|
||||
message: "Successfully registered for the event",
|
||||
registrationId:
|
||||
eventData.registrations[eventData.registrations.length - 1]._id,
|
||||
registrationId: newRegistration._id,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error registering for event:", error);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default defineEventHandler(async (event) => {
|
|||
const config = useRuntimeConfig(event)
|
||||
const body = await validateBody(event, helcimCreatePlanSchema)
|
||||
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
const helcimToken = config.helcimApiToken
|
||||
|
||||
|
||||
const response = await fetch(`${HELCIM_API_BASE}/payment-plans`, {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export default defineEventHandler(async (event) => {
|
|||
})
|
||||
}
|
||||
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
const helcimToken = config.helcimApiToken
|
||||
|
||||
const response = await fetch(
|
||||
`${HELCIM_API_BASE}/customers/${member.helcimCustomerId}`,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
// Get token directly from environment if not in config
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
const helcimToken = config.helcimApiToken
|
||||
|
||||
if (!helcimToken) {
|
||||
throw createError({
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export default defineEventHandler(async (event) => {
|
|||
})
|
||||
}
|
||||
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
const helcimToken = config.helcimApiToken
|
||||
|
||||
// First, search for existing customer
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -5,16 +5,16 @@ 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 validateBody(event, helcimInitializePaymentSchema);
|
||||
|
||||
|
||||
const helcimToken =
|
||||
config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN;
|
||||
|
||||
// Determine payment type based on whether this is for a subscription or one-time payment
|
||||
// Event ticket purchases can be made without authentication
|
||||
const isEventTicket = body.metadata?.type === "event_ticket";
|
||||
if (!isEventTicket) {
|
||||
await requireAuth(event);
|
||||
}
|
||||
|
||||
const helcimToken = config.helcimApiToken;
|
||||
const amount = body.amount || 0;
|
||||
|
||||
// For event tickets with amount > 0, we do a purchase
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export default defineEventHandler(async (event) => {
|
|||
try {
|
||||
await requireAdmin(event)
|
||||
const config = useRuntimeConfig(event)
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
const helcimToken = config.helcimApiToken
|
||||
|
||||
const response = await fetch(`${HELCIM_API_BASE}/payment-plans`, {
|
||||
method: 'GET',
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
// Try to create subscription in Helcim
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
const helcimToken = config.helcimApiToken
|
||||
|
||||
// Generate a proper alphanumeric idempotency key (exactly 25 characters)
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export default defineEventHandler(async (event) => {
|
|||
try {
|
||||
await requireAdmin(event)
|
||||
const config = useRuntimeConfig(event)
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
const helcimToken = config.helcimApiToken
|
||||
|
||||
const response = await fetch(`${HELCIM_API_BASE}/subscriptions`, {
|
||||
method: 'GET',
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
const { billingAddress } = body
|
||||
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
const helcimToken = config.helcimApiToken
|
||||
|
||||
// Update customer billing address in Helcim
|
||||
const response = await fetch(`${HELCIM_API_BASE}/customers/${body.customerId}`, {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default defineEventHandler(async (event) => {
|
|||
const config = useRuntimeConfig(event)
|
||||
const body = await validateBody(event, paymentVerifySchema)
|
||||
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
const helcimToken = config.helcimApiToken
|
||||
|
||||
if (!helcimToken) {
|
||||
throw createError({
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ export default defineEventHandler(async (event) => {
|
|||
};
|
||||
}
|
||||
|
||||
const helcimToken =
|
||||
config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN;
|
||||
const helcimToken = config.helcimApiToken;
|
||||
|
||||
try {
|
||||
// Cancel Helcim subscription
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export default defineEventHandler(async (event) => {
|
|||
try {
|
||||
const members = await Member.find(dbQuery)
|
||||
.select(
|
||||
"name pronouns timeZone avatar studio bio location socialLinks offering lookingFor privacy circle peerSupport slackUserId createdAt",
|
||||
"name pronouns timeZone avatar studio bio location socialLinks offering lookingFor privacy circle peerSupport createdAt",
|
||||
)
|
||||
.sort({ createdAt: -1 })
|
||||
.lean();
|
||||
|
|
@ -124,10 +124,15 @@ export default defineEventHandler(async (event) => {
|
|||
if (isVisible("offering")) filtered.offering = member.offering;
|
||||
if (isVisible("lookingFor")) filtered.lookingFor = member.lookingFor;
|
||||
|
||||
// Always show peer support if enabled (it's opt-in, so public by nature)
|
||||
// Peer support: expose only fields needed for matching/contact UX
|
||||
// slackUserId, slackDMChannelId, slackUsername, personalMessage are internal
|
||||
if (member.peerSupport?.enabled) {
|
||||
filtered.peerSupport = member.peerSupport;
|
||||
filtered.slackUserId = member.slackUserId;
|
||||
filtered.peerSupport = {
|
||||
enabled: true,
|
||||
skillTopics: member.peerSupport.skillTopics,
|
||||
supportTopics: member.peerSupport.supportTopics,
|
||||
availability: member.peerSupport.availability,
|
||||
};
|
||||
}
|
||||
|
||||
return filtered;
|
||||
|
|
|
|||
|
|
@ -1,120 +1,89 @@
|
|||
import jwt from "jsonwebtoken";
|
||||
import Member from "../../../models/member.js";
|
||||
import { connectDB } from "../../../utils/mongoose.js";
|
||||
import Member from '../../../models/member.js'
|
||||
import { connectDB } from '../../../utils/mongoose.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await connectDB();
|
||||
await connectDB()
|
||||
const member = await requireAuth(event)
|
||||
|
||||
const token = getCookie(event, "auth-token");
|
||||
|
||||
if (!token) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Not authenticated",
|
||||
});
|
||||
}
|
||||
|
||||
let memberId;
|
||||
try {
|
||||
const decoded = jwt.verify(token, useRuntimeConfig().jwtSecret);
|
||||
memberId = decoded.memberId;
|
||||
} catch (err) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Invalid or expired token",
|
||||
});
|
||||
}
|
||||
|
||||
const body = await validateBody(event, peerSupportUpdateSchema);
|
||||
const body = await validateBody(event, peerSupportUpdateSchema)
|
||||
|
||||
// Build update object for peer support settings
|
||||
const updateData = {
|
||||
"peerSupport.enabled": body.enabled || false,
|
||||
"peerSupport.skillTopics": body.skillTopics || [],
|
||||
"peerSupport.supportTopics": body.supportTopics || [],
|
||||
"peerSupport.availability": body.availability || "",
|
||||
"peerSupport.personalMessage": body.personalMessage || "",
|
||||
"peerSupport.slackUsername": body.slackUsername || "",
|
||||
};
|
||||
'peerSupport.enabled': body.enabled || false,
|
||||
'peerSupport.skillTopics': body.skillTopics || [],
|
||||
'peerSupport.supportTopics': body.supportTopics || [],
|
||||
'peerSupport.availability': body.availability || '',
|
||||
'peerSupport.personalMessage': body.personalMessage || '',
|
||||
'peerSupport.slackUsername': body.slackUsername || '',
|
||||
}
|
||||
|
||||
// If Slack username provided and peer support enabled, try to fetch Slack user ID
|
||||
if (body.enabled && body.slackUsername) {
|
||||
try {
|
||||
console.log(
|
||||
`[Peer Support] Attempting to fetch Slack user ID for: ${body.slackUsername}`,
|
||||
);
|
||||
)
|
||||
|
||||
// Dynamically import the Slack service
|
||||
const { getSlackService } = await import("../../../utils/slack.ts");
|
||||
const slackService = getSlackService();
|
||||
const { getSlackService } = await import('../../../utils/slack.ts')
|
||||
const slackService = getSlackService()
|
||||
|
||||
if (slackService) {
|
||||
console.log(
|
||||
"[Peer Support] Slack service initialized, looking up user...",
|
||||
);
|
||||
const slackUserId = await slackService.findUserIdByUsername(
|
||||
body.slackUsername,
|
||||
);
|
||||
console.log('[Peer Support] Slack service initialized, looking up user...')
|
||||
const slackUserId = await slackService.findUserIdByUsername(body.slackUsername)
|
||||
|
||||
if (slackUserId) {
|
||||
updateData["slackUserId"] = slackUserId;
|
||||
updateData['slackUserId'] = slackUserId
|
||||
console.log(
|
||||
`[Peer Support] ✓ Found Slack user ID for ${body.slackUsername}: ${slackUserId}`,
|
||||
);
|
||||
)
|
||||
|
||||
// Now get/create the DM channel
|
||||
console.log("[Peer Support] Opening DM channel...");
|
||||
const dmChannelId = await slackService.openDMChannel(slackUserId);
|
||||
console.log('[Peer Support] Opening DM channel...')
|
||||
const dmChannelId = await slackService.openDMChannel(slackUserId)
|
||||
|
||||
if (dmChannelId) {
|
||||
updateData["peerSupport.slackDMChannelId"] = dmChannelId;
|
||||
console.log(`[Peer Support] ✓ Got DM channel ID: ${dmChannelId}`);
|
||||
updateData['peerSupport.slackDMChannelId'] = dmChannelId
|
||||
console.log(`[Peer Support] ✓ Got DM channel ID: ${dmChannelId}`)
|
||||
} else {
|
||||
console.warn("[Peer Support] Could not get DM channel ID");
|
||||
console.warn('[Peer Support] Could not get DM channel ID')
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`[Peer Support] Could not find Slack user ID for username: ${body.slackUsername}`,
|
||||
);
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
"[Peer Support] Slack service not configured, skipping user ID lookup",
|
||||
);
|
||||
console.log('[Peer Support] Slack service not configured, skipping user ID lookup')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[Peer Support] Error fetching Slack user ID:",
|
||||
error.message,
|
||||
);
|
||||
console.error("[Peer Support] Stack trace:", error.stack);
|
||||
console.error('[Peer Support] Error fetching Slack user ID:', error.message)
|
||||
console.error('[Peer Support] Stack trace:', error.stack)
|
||||
// Continue anyway - we'll still save the username
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const member = await Member.findByIdAndUpdate(
|
||||
memberId,
|
||||
const updated = await Member.findByIdAndUpdate(
|
||||
member._id,
|
||||
{ $set: updateData },
|
||||
{ new: true, runValidators: true },
|
||||
);
|
||||
)
|
||||
|
||||
if (!member) {
|
||||
if (!updated) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Member not found",
|
||||
});
|
||||
statusMessage: 'Member not found',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
peerSupport: member.peerSupport,
|
||||
};
|
||||
peerSupport: updated.peerSupport,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Peer support update error:", error);
|
||||
console.error('Peer support update error:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to update peer support settings",
|
||||
});
|
||||
statusMessage: 'Failed to update peer support settings',
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,114 +1,94 @@
|
|||
import Event from "../../models/event";
|
||||
import Member from "../../models/member";
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
import Event from '../../models/event'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event);
|
||||
const { memberId } = query;
|
||||
|
||||
if (!memberId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Member ID is required",
|
||||
});
|
||||
}
|
||||
await connectDB()
|
||||
const member = await requireAuth(event)
|
||||
|
||||
try {
|
||||
// Verify member exists
|
||||
const member = await Member.findById(memberId);
|
||||
if (!member) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Member not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Find all events where the user is registered
|
||||
const events = await Event.find({
|
||||
"registrations.memberId": memberId,
|
||||
'registrations.memberId': member._id,
|
||||
isCancelled: { $ne: true },
|
||||
})
|
||||
.select("title slug description startDate endDate location")
|
||||
.sort({ startDate: 1 });
|
||||
.select('title slug description startDate endDate location')
|
||||
.sort({ startDate: 1 })
|
||||
|
||||
// Generate iCal format
|
||||
const ical = generateICalendar(events, member);
|
||||
const ical = generateICalendar(events, member)
|
||||
|
||||
// Set headers for calendar subscription (not download)
|
||||
setHeader(event, "Content-Type", "text/calendar; charset=utf-8");
|
||||
setHeader(event, "Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
setHeader(event, "Pragma", "no-cache");
|
||||
setHeader(event, "Expires", "0");
|
||||
setHeader(event, 'Content-Type', 'text/calendar; charset=utf-8')
|
||||
setHeader(event, 'Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
setHeader(event, 'Pragma', 'no-cache')
|
||||
setHeader(event, 'Expires', '0')
|
||||
|
||||
return ical;
|
||||
return ical
|
||||
} catch (error) {
|
||||
console.error("Error generating calendar:", error);
|
||||
console.error('Error generating calendar:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to generate calendar",
|
||||
});
|
||||
statusMessage: 'Failed to generate calendar',
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
function generateICalendar(events, member) {
|
||||
const now = new Date();
|
||||
const now = new Date()
|
||||
const timestamp = now
|
||||
.toISOString()
|
||||
.replace(/[-:]/g, "")
|
||||
.replace(/\.\d{3}/, "");
|
||||
.replace(/[-:]/g, '')
|
||||
.replace(/\.\d{3}/, '')
|
||||
|
||||
let ical = [
|
||||
"BEGIN:VCALENDAR",
|
||||
"VERSION:2.0",
|
||||
"PRODID:-//Ghost Guild//Events Calendar//EN",
|
||||
"CALSCALE:GREGORIAN",
|
||||
"METHOD:PUBLISH",
|
||||
"X-WR-CALNAME:Ghost Guild - My Events",
|
||||
"X-WR-TIMEZONE:UTC",
|
||||
"X-WR-CALDESC:Your registered Ghost Guild events",
|
||||
"REFRESH-INTERVAL;VALUE=DURATION:PT1H",
|
||||
"X-PUBLISHED-TTL:PT1H",
|
||||
];
|
||||
'BEGIN:VCALENDAR',
|
||||
'VERSION:2.0',
|
||||
'PRODID:-//Ghost Guild//Events Calendar//EN',
|
||||
'CALSCALE:GREGORIAN',
|
||||
'METHOD:PUBLISH',
|
||||
'X-WR-CALNAME:Ghost Guild - My Events',
|
||||
'X-WR-TIMEZONE:UTC',
|
||||
'X-WR-CALDESC:Your registered Ghost Guild events',
|
||||
'REFRESH-INTERVAL;VALUE=DURATION:PT1H',
|
||||
'X-PUBLISHED-TTL:PT1H',
|
||||
]
|
||||
|
||||
events.forEach((evt) => {
|
||||
const eventStart = new Date(evt.startDate);
|
||||
const eventEnd = new Date(evt.endDate);
|
||||
const eventStart = new Date(evt.startDate)
|
||||
const eventEnd = new Date(evt.endDate)
|
||||
|
||||
const dtstart = eventStart
|
||||
.toISOString()
|
||||
.replace(/[-:]/g, "")
|
||||
.replace(/\.\d{3}/, "");
|
||||
.replace(/[-:]/g, '')
|
||||
.replace(/\.\d{3}/, '')
|
||||
const dtend = eventEnd
|
||||
.toISOString()
|
||||
.replace(/[-:]/g, "")
|
||||
.replace(/\.\d{3}/, "");
|
||||
const dtstamp = timestamp;
|
||||
.replace(/[-:]/g, '')
|
||||
.replace(/\.\d{3}/, '')
|
||||
const dtstamp = timestamp
|
||||
|
||||
// Clean description for iCal format
|
||||
const description = (evt.description || "")
|
||||
.replace(/\n/g, "\\n")
|
||||
.replace(/,/g, "\\,");
|
||||
const description = (evt.description || '')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/,/g, '\\,')
|
||||
|
||||
const eventUrl = `https://ghostguild.org/events/${evt.slug || evt._id}`;
|
||||
const eventUrl = `https://ghostguild.org/events/${evt.slug || evt._id}`
|
||||
|
||||
ical.push("BEGIN:VEVENT");
|
||||
ical.push(`UID:${evt._id}@ghostguild.org`);
|
||||
ical.push(`DTSTAMP:${dtstamp}`);
|
||||
ical.push(`DTSTART:${dtstart}`);
|
||||
ical.push(`DTEND:${dtend}`);
|
||||
ical.push(`SUMMARY:${evt.title}`);
|
||||
ical.push(`DESCRIPTION:${description}\\n\\nView event: ${eventUrl}`);
|
||||
ical.push(`LOCATION:${evt.location || "Online"}`);
|
||||
ical.push(`URL:${eventUrl}`);
|
||||
ical.push(`STATUS:CONFIRMED`);
|
||||
ical.push("END:VEVENT");
|
||||
});
|
||||
ical.push('BEGIN:VEVENT')
|
||||
ical.push(`UID:${evt._id}@ghostguild.org`)
|
||||
ical.push(`DTSTAMP:${dtstamp}`)
|
||||
ical.push(`DTSTART:${dtstart}`)
|
||||
ical.push(`DTEND:${dtend}`)
|
||||
ical.push(`SUMMARY:${evt.title}`)
|
||||
ical.push(`DESCRIPTION:${description}\\n\\nView event: ${eventUrl}`)
|
||||
ical.push(`LOCATION:${evt.location || 'Online'}`)
|
||||
ical.push(`URL:${eventUrl}`)
|
||||
ical.push('STATUS:CONFIRMED')
|
||||
ical.push('END:VEVENT')
|
||||
})
|
||||
|
||||
ical.push("END:VCALENDAR");
|
||||
ical.push('END:VCALENDAR')
|
||||
|
||||
return ical.join("\r\n");
|
||||
return ical.join('\r\n')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,60 +1,36 @@
|
|||
import Event from "../../models/event";
|
||||
import Member from "../../models/member";
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
import Event from '../../models/event'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event);
|
||||
const { memberId } = query;
|
||||
|
||||
if (!memberId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Member ID is required",
|
||||
});
|
||||
}
|
||||
await connectDB()
|
||||
const member = await requireAuth(event)
|
||||
|
||||
try {
|
||||
// Verify member exists
|
||||
const member = await Member.findById(memberId);
|
||||
if (!member) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Member not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Find all events where the user is registered
|
||||
// Filter out cancelled events and only show future events
|
||||
const now = new Date();
|
||||
const now = new Date()
|
||||
|
||||
const events = await Event.find({
|
||||
"registrations.memberId": memberId,
|
||||
'registrations.memberId': member._id,
|
||||
isCancelled: { $ne: true },
|
||||
startDate: { $gte: now },
|
||||
})
|
||||
.select(
|
||||
"title slug description startDate endDate location featureImage maxAttendees registeredCount",
|
||||
)
|
||||
.select('title slug description startDate endDate location featureImage maxAttendees registeredCount')
|
||||
.sort({ startDate: 1 })
|
||||
.limit(10);
|
||||
|
||||
console.log(
|
||||
`Found ${events.length} registered events for member ${memberId}`,
|
||||
);
|
||||
.limit(10)
|
||||
|
||||
return {
|
||||
events,
|
||||
count: events.length,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching member events:", error);
|
||||
console.error('Error fetching member events:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to fetch registered events",
|
||||
});
|
||||
statusMessage: 'Failed to fetch registered events',
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export default defineEventHandler(async (event) => {
|
|||
"location",
|
||||
"socialLinks",
|
||||
"showInDirectory",
|
||||
"notifications",
|
||||
];
|
||||
|
||||
// Privacy fields from validated body
|
||||
|
|
@ -96,6 +97,7 @@ export default defineEventHandler(async (event) => {
|
|||
offering: member.offering,
|
||||
lookingFor: member.lookingFor,
|
||||
showInDirectory: member.showInDirectory,
|
||||
notifications: member.notifications,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error;
|
||||
|
|
|
|||
34
server/api/members/update-circle.post.js
Normal file
34
server/api/members/update-circle.post.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Update member's circle
|
||||
import Member from '../../models/member.js'
|
||||
import { connectDB } from '../../utils/mongoose.js'
|
||||
import { requireAuth } from '../../utils/auth.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const member = await requireAuth(event)
|
||||
await connectDB()
|
||||
const body = await validateBody(event, updateCircleSchema)
|
||||
|
||||
if (member.circle === body.circle) {
|
||||
return { success: true, message: 'Already in this circle' }
|
||||
}
|
||||
|
||||
await Member.findByIdAndUpdate(
|
||||
member._id,
|
||||
{ $set: { circle: body.circle } },
|
||||
{ runValidators: false }
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Circle updated to ${body.circle}`,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error
|
||||
console.error('Error updating circle:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'An unexpected error occurred',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -1,48 +1,19 @@
|
|||
// Update member's contribution tier
|
||||
import jwt from "jsonwebtoken";
|
||||
import {
|
||||
getHelcimPlanId,
|
||||
requiresPayment,
|
||||
isValidContributionValue,
|
||||
} from "../../config/contributions.js";
|
||||
import Member from "../../models/member.js";
|
||||
import { connectDB } from "../../utils/mongoose.js";
|
||||
import Member from "../../models/member.js";
|
||||
|
||||
const HELCIM_API_BASE = "https://api.helcim.com/v2";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const member = await requireAuth(event);
|
||||
await connectDB();
|
||||
const config = useRuntimeConfig(event);
|
||||
const body = await validateBody(event, updateContributionSchema);
|
||||
const token = getCookie(event, "auth-token");
|
||||
|
||||
if (!token) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Not authenticated",
|
||||
});
|
||||
}
|
||||
|
||||
// Decode JWT token
|
||||
let decoded;
|
||||
try {
|
||||
decoded = jwt.verify(token, useRuntimeConfig().jwtSecret);
|
||||
} catch (err) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Invalid or expired token",
|
||||
});
|
||||
}
|
||||
|
||||
// Get member
|
||||
const member = await Member.findById(decoded.memberId);
|
||||
if (!member) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Member not found",
|
||||
});
|
||||
}
|
||||
|
||||
const oldTier = member.contributionTier;
|
||||
const newTier = body.contributionTier;
|
||||
|
|
@ -55,8 +26,7 @@ export default defineEventHandler(async (event) => {
|
|||
};
|
||||
}
|
||||
|
||||
const helcimToken =
|
||||
config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN;
|
||||
const helcimToken = config.helcimApiToken;
|
||||
const oldRequiresPayment = requiresPayment(oldTier);
|
||||
const newRequiresPayment = requiresPayment(newTier);
|
||||
|
||||
|
|
@ -73,8 +43,7 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
// Try to fetch customer info from Helcim to check for saved cards
|
||||
const helcimToken =
|
||||
config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN;
|
||||
const helcimToken = config.helcimApiToken;
|
||||
|
||||
try {
|
||||
const customerResponse = await fetch(
|
||||
|
|
@ -185,11 +154,11 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
// Update member record
|
||||
member.contributionTier = newTier;
|
||||
member.helcimSubscriptionId = subscription.id;
|
||||
member.paymentMethod = "card";
|
||||
member.status = "active";
|
||||
await member.save();
|
||||
await Member.findByIdAndUpdate(
|
||||
member._id,
|
||||
{ $set: { contributionTier: newTier, helcimSubscriptionId: subscription.id, paymentMethod: "card", status: "active" } },
|
||||
{ runValidators: false }
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
@ -241,10 +210,11 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
// Update member to free tier
|
||||
member.contributionTier = newTier;
|
||||
member.helcimSubscriptionId = null;
|
||||
member.paymentMethod = "none";
|
||||
await member.save();
|
||||
await Member.findByIdAndUpdate(
|
||||
member._id,
|
||||
{ $set: { contributionTier: newTier, helcimSubscriptionId: null, paymentMethod: "none" } },
|
||||
{ runValidators: false }
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
@ -303,8 +273,11 @@ export default defineEventHandler(async (event) => {
|
|||
const subscriptionData = await response.json();
|
||||
|
||||
// Update member record
|
||||
member.contributionTier = newTier;
|
||||
await member.save();
|
||||
await Member.findByIdAndUpdate(
|
||||
member._id,
|
||||
{ $set: { contributionTier: newTier } },
|
||||
{ runValidators: false }
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
@ -321,8 +294,11 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
// Case 4: Moving between free tiers (shouldn't happen but handle it)
|
||||
member.contributionTier = newTier;
|
||||
await member.save();
|
||||
await Member.findByIdAndUpdate(
|
||||
member._id,
|
||||
{ $set: { contributionTier: newTier } },
|
||||
{ runValidators: false }
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue