fix: use private helcimApiToken for all server-side Helcim API calls

This commit is contained in:
Jennie Robinson Faber 2026-04-04 13:37:34 +01:00
parent ccd1d0783a
commit d31b5b4dac
53 changed files with 1755 additions and 572 deletions

View file

@ -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

View file

@ -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;

View file

@ -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',
})
}
});
})

View file

@ -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')
}

View file

@ -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',
})
}
});
})

View file

@ -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;

View 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',
})
}
})

View file

@ -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,