Tests, UX improvements.
This commit is contained in:
parent
4e6f5d36b8
commit
0ae18f495e
63 changed files with 1384 additions and 2330 deletions
|
|
@ -2,7 +2,7 @@ import Member from '../../../models/member.js'
|
|||
import { connectDB } from '../../../utils/mongoose.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAdmin(event)
|
||||
const admin = await requireAdmin(event)
|
||||
|
||||
const body = await validateBody(event, adminMemberUpdateSchema)
|
||||
const memberId = getRouterParam(event, 'id')
|
||||
|
|
@ -30,6 +30,29 @@ export default defineEventHandler(async (event) => {
|
|||
status: body.status,
|
||||
}, { new: true })
|
||||
|
||||
// Log admin profile update
|
||||
const changedFields = []
|
||||
if (existing.name !== body.name) changedFields.push('name')
|
||||
if (existing.email !== body.email) changedFields.push('email')
|
||||
if (existing.circle !== body.circle) changedFields.push('circle')
|
||||
if (existing.contributionTier !== body.contributionTier) changedFields.push('contributionTier')
|
||||
if (existing.status !== body.status) changedFields.push('status')
|
||||
|
||||
if (changedFields.length) {
|
||||
logActivity(memberId, 'admin_profile_update', {
|
||||
fields: changedFields,
|
||||
changedBy: admin.name
|
||||
}, { performedBy: admin._id })
|
||||
}
|
||||
|
||||
// Log status change separately for admin-only visibility
|
||||
if (existing.status !== body.status) {
|
||||
logActivity(memberId, 'status_changed', {
|
||||
from: existing.status,
|
||||
to: body.status
|
||||
}, { performedBy: admin._id })
|
||||
}
|
||||
|
||||
return {
|
||||
_id: updated._id,
|
||||
name: updated.name,
|
||||
|
|
|
|||
28
server/api/admin/members/[id]/activity.get.js
Normal file
28
server/api/admin/members/[id]/activity.get.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import ActivityLog from '../../../../models/activityLog.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAdmin(event)
|
||||
|
||||
const id = getRouterParam(event, 'id')
|
||||
|
||||
const query = getQuery(event)
|
||||
const limit = Math.min(parseInt(query.limit) || 20, 50)
|
||||
const before = query.before ? new Date(query.before) : null
|
||||
|
||||
const filter = { member: id }
|
||||
if (before) filter.timestamp = { $lt: before }
|
||||
|
||||
const entries = await ActivityLog.find(filter)
|
||||
.sort({ timestamp: -1 })
|
||||
.limit(limit + 1)
|
||||
.lean()
|
||||
|
||||
const hasMore = entries.length > limit
|
||||
if (hasMore) entries.pop()
|
||||
|
||||
const nextCursor = hasMore && entries.length
|
||||
? entries[entries.length - 1].timestamp.toISOString()
|
||||
: null
|
||||
|
||||
return { entries, hasMore, nextCursor }
|
||||
})
|
||||
|
|
@ -18,18 +18,25 @@ export default defineEventHandler(async (event) => {
|
|||
})
|
||||
}
|
||||
|
||||
const member = await Member.findByIdAndUpdate(
|
||||
memberId,
|
||||
{ role },
|
||||
{ new: true }
|
||||
)
|
||||
|
||||
if (!member) {
|
||||
const existing = await Member.findById(memberId)
|
||||
if (!existing) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Member not found.'
|
||||
})
|
||||
}
|
||||
|
||||
const oldRole = existing.role
|
||||
const member = await Member.findByIdAndUpdate(
|
||||
memberId,
|
||||
{ role },
|
||||
{ new: true }
|
||||
)
|
||||
|
||||
logActivity(memberId, 'role_changed', {
|
||||
from: oldRole,
|
||||
to: role
|
||||
}, { performedBy: admin._id })
|
||||
|
||||
return { success: true, member }
|
||||
})
|
||||
|
|
|
|||
|
|
@ -100,6 +100,12 @@ export default defineEventHandler(async (event) => {
|
|||
{ runValidators: false }
|
||||
)
|
||||
|
||||
logActivity(member._id, 'email_sent', {
|
||||
emailType: 'invite',
|
||||
subject: "You're invited to Ghost Guild",
|
||||
body: emailText
|
||||
})
|
||||
|
||||
results.push({ memberId: member._id, email: member.email, success: true })
|
||||
} catch (err) {
|
||||
results.push({ memberId: member._id, email: member.email, success: false, error: err.message })
|
||||
|
|
|
|||
|
|
@ -52,19 +52,23 @@ export default defineEventHandler(async (event) => {
|
|||
// Token goes in the fragment — never sent to server, never logged
|
||||
const magicLink = `${baseUrl}/verify#${token}`;
|
||||
|
||||
const emailSubject = "Your Ghost Guild login link";
|
||||
const emailBody = `Hi,\n\nSign in to Ghost Guild:\n${magicLink}\n\nThis link expires in 15 minutes. If you didn't request it, ignore this email.`;
|
||||
|
||||
try {
|
||||
await resend.emails.send({
|
||||
from: "Ghost Guild <ghostguild@babyghosts.org>",
|
||||
to: email,
|
||||
subject: "Your Ghost Guild login link",
|
||||
text: `Hi,
|
||||
|
||||
Sign in to Ghost Guild:
|
||||
${magicLink}
|
||||
|
||||
This link expires in 15 minutes. If you didn't request it, ignore this email.`,
|
||||
subject: emailSubject,
|
||||
text: emailBody,
|
||||
});
|
||||
|
||||
logActivity(member._id, 'email_sent', {
|
||||
emailType: 'magic_link',
|
||||
subject: emailSubject,
|
||||
body: emailBody
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: GENERIC_MESSAGE,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Event from "../../../models/event";
|
||||
import Member from "../../../models/member";
|
||||
import {
|
||||
sendEventCancellationEmail,
|
||||
sendWaitlistNotificationEmail,
|
||||
|
|
@ -56,19 +57,40 @@ export default defineEventHandler(async (event) => {
|
|||
$pull: { registrations: { email: registration.email } },
|
||||
$inc: { registeredCount: -1 },
|
||||
},
|
||||
{ runValidators: false }
|
||||
{ runValidators: false },
|
||||
);
|
||||
|
||||
// Send cancellation confirmation email
|
||||
// Log activity + send cancellation confirmation email
|
||||
const cancellingMember = await Member.findOne({
|
||||
email: registration.email,
|
||||
}).lean();
|
||||
|
||||
if (cancellingMember) {
|
||||
logActivity(cancellingMember._id, 'event_cancelled', {
|
||||
eventId: eventDoc._id,
|
||||
eventTitle: eventDoc.title,
|
||||
eventSlug: eventDoc.slug
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const eventData = {
|
||||
title: eventDoc.title,
|
||||
slug: eventDoc.slug,
|
||||
_id: eventDoc._id,
|
||||
};
|
||||
await sendEventCancellationEmail(registration, eventData);
|
||||
const shouldSendCancellation =
|
||||
!cancellingMember || cancellingMember.notifications?.events !== false;
|
||||
if (shouldSendCancellation) {
|
||||
const eventData = {
|
||||
title: eventDoc.title,
|
||||
slug: eventDoc.slug,
|
||||
_id: eventDoc._id,
|
||||
};
|
||||
await sendEventCancellationEmail(registration, eventData);
|
||||
if (cancellingMember) {
|
||||
logActivity(cancellingMember._id, 'email_sent', {
|
||||
emailType: 'event_cancellation',
|
||||
subject: `Registration cancelled for ${eventDoc.title}`
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (emailError) {
|
||||
// Log error but don't fail the cancellation
|
||||
console.error("Failed to send cancellation email:", emailError);
|
||||
}
|
||||
|
||||
|
|
@ -89,18 +111,30 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
// Notify the first person on the waitlist who hasn't been notified yet
|
||||
const waitlistEntry = eventDoc.tickets.waitlist.entries.find(
|
||||
(entry) => !entry.notified
|
||||
(entry) => !entry.notified,
|
||||
);
|
||||
|
||||
if (waitlistEntry) {
|
||||
await sendWaitlistNotificationEmail(waitlistEntry, eventData);
|
||||
|
||||
// Mark as notified using findByIdAndUpdate to avoid re-validating the document
|
||||
const entryIndex = eventDoc.tickets.waitlist.entries.indexOf(waitlistEntry);
|
||||
const waitlistedMember = await Member.findOne({
|
||||
email: waitlistEntry.email,
|
||||
}).lean();
|
||||
const shouldNotifyWaitlist =
|
||||
!waitlistedMember ||
|
||||
waitlistedMember.notifications?.events !== false;
|
||||
if (shouldNotifyWaitlist) {
|
||||
await sendWaitlistNotificationEmail(waitlistEntry, eventData);
|
||||
}
|
||||
// Always mark as notified so we move on regardless
|
||||
const entryIndex =
|
||||
eventDoc.tickets.waitlist.entries.indexOf(waitlistEntry);
|
||||
await Event.findByIdAndUpdate(
|
||||
eventDoc._id,
|
||||
{ $set: { [`tickets.waitlist.entries.${entryIndex}.notified`]: true } },
|
||||
{ runValidators: false }
|
||||
{
|
||||
$set: {
|
||||
[`tickets.waitlist.entries.${entryIndex}.notified`]: true,
|
||||
},
|
||||
},
|
||||
{ runValidators: false },
|
||||
);
|
||||
}
|
||||
} catch (waitlistError) {
|
||||
|
|
|
|||
|
|
@ -113,16 +113,35 @@ export default defineEventHandler(async (event) => {
|
|||
const result = await Event.findByIdAndUpdate(
|
||||
eventData._id,
|
||||
{ $push: { registrations: registration } },
|
||||
{ new: true, runValidators: false }
|
||||
{ new: true, runValidators: false },
|
||||
);
|
||||
const newRegistration = result.registrations[result.registrations.length - 1];
|
||||
const newRegistration =
|
||||
result.registrations[result.registrations.length - 1];
|
||||
|
||||
// Send confirmation email using Resend
|
||||
try {
|
||||
await sendEventRegistrationEmail(registration, eventData);
|
||||
} catch (emailError) {
|
||||
// Log error but don't fail the registration
|
||||
console.error("Failed to send confirmation email:", emailError);
|
||||
// Log activity
|
||||
if (member) {
|
||||
logActivity(member._id, 'event_registered', {
|
||||
eventId: eventData._id,
|
||||
eventTitle: eventData.title,
|
||||
eventSlug: eventData.slug
|
||||
})
|
||||
}
|
||||
|
||||
// Send confirmation email — respect member notification preferences
|
||||
const shouldSendEventEmail =
|
||||
!member || member.notifications?.events !== false;
|
||||
if (shouldSendEventEmail) {
|
||||
try {
|
||||
await sendEventRegistrationEmail(registration, eventData);
|
||||
if (member) {
|
||||
logActivity(member._id, 'email_sent', {
|
||||
emailType: 'event_registration',
|
||||
subject: `You're registered for ${eventData.title}`
|
||||
})
|
||||
}
|
||||
} catch (emailError) {
|
||||
console.error("Failed to send confirmation email:", emailError);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -91,9 +91,11 @@ export default defineEventHandler(async (event) => {
|
|||
{ new: true }
|
||||
)
|
||||
|
||||
logActivity(member._id, 'subscription_created', { tier: body.contributionTier })
|
||||
|
||||
// Send Slack invitation for free tier members
|
||||
await inviteToSlack(member)
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
subscription: null,
|
||||
|
|
@ -262,6 +264,8 @@ export default defineEventHandler(async (event) => {
|
|||
{ new: true }
|
||||
)
|
||||
|
||||
logActivity(member._id, 'subscription_created', { tier: body.contributionTier })
|
||||
|
||||
// Send Slack invitation for paid tier members
|
||||
await inviteToSlack(member)
|
||||
|
||||
|
|
|
|||
43
server/api/members/[id]/activity.get.js
Normal file
43
server/api/members/[id]/activity.get.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import Member from '../../../models/member.js'
|
||||
import ActivityLog from '../../../models/activityLog.js'
|
||||
import { connectDB } from '../../../utils/mongoose.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await connectDB()
|
||||
|
||||
const id = getRouterParam(event, 'id')
|
||||
|
||||
const member = await Member.findOne({
|
||||
_id: id,
|
||||
showInDirectory: true,
|
||||
status: 'active'
|
||||
}).lean()
|
||||
|
||||
if (!member) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Member not found' })
|
||||
}
|
||||
|
||||
const query = getQuery(event)
|
||||
const limit = Math.min(parseInt(query.limit) || 5, 20)
|
||||
const before = query.before ? new Date(query.before) : null
|
||||
|
||||
const filter = {
|
||||
member: member._id,
|
||||
visibility: 'public'
|
||||
}
|
||||
if (before) filter.timestamp = { $lt: before }
|
||||
|
||||
const entries = await ActivityLog.find(filter)
|
||||
.sort({ timestamp: -1 })
|
||||
.limit(limit + 1)
|
||||
.lean()
|
||||
|
||||
const hasMore = entries.length > limit
|
||||
if (hasMore) entries.pop()
|
||||
|
||||
const nextCursor = hasMore && entries.length
|
||||
? entries[entries.length - 1].timestamp.toISOString()
|
||||
: null
|
||||
|
||||
return { entries, hasMore, nextCursor }
|
||||
})
|
||||
|
|
@ -62,6 +62,10 @@ export default defineEventHandler(async (event) => {
|
|||
{ runValidators: false }
|
||||
);
|
||||
|
||||
logActivity(member._id, 'subscription_cancelled', {
|
||||
effectiveDate: new Date().toISOString()
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Subscription cancelled successfully",
|
||||
|
|
|
|||
|
|
@ -100,10 +100,15 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
const member = new Member(validatedData)
|
||||
await member.save()
|
||||
|
||||
|
||||
// Log member joined
|
||||
logActivity(member._id, 'member_joined', {
|
||||
circle: member.circle
|
||||
}, { timestamp: member.createdAt })
|
||||
|
||||
// Send Slack invitation for new members
|
||||
await inviteToSlack(member)
|
||||
|
||||
|
||||
// TODO: Process payment with Helcim if not free tier
|
||||
if (requiresPayment(validatedData.contributionTier)) {
|
||||
// Payment processing will be added here
|
||||
|
|
@ -112,6 +117,10 @@ export default defineEventHandler(async (event) => {
|
|||
// Send welcome email (non-blocking)
|
||||
try {
|
||||
await sendWelcomeEmail(member)
|
||||
logActivity(member._id, 'email_sent', {
|
||||
emailType: 'welcome',
|
||||
subject: 'Welcome to Ghost Guild'
|
||||
})
|
||||
} catch (emailError) {
|
||||
console.error('Failed to send welcome email:', emailError)
|
||||
}
|
||||
|
|
|
|||
29
server/api/members/me/activity.get.js
Normal file
29
server/api/members/me/activity.get.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import ActivityLog from '../../../models/activityLog.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const member = await requireAuth(event)
|
||||
|
||||
const query = getQuery(event)
|
||||
const limit = Math.min(parseInt(query.limit) || 20, 50)
|
||||
const before = query.before ? new Date(query.before) : null
|
||||
|
||||
const filter = {
|
||||
member: member._id,
|
||||
visibility: { $in: ['member', 'public'] }
|
||||
}
|
||||
if (before) filter.timestamp = { $lt: before }
|
||||
|
||||
const entries = await ActivityLog.find(filter)
|
||||
.sort({ timestamp: -1 })
|
||||
.limit(limit + 1)
|
||||
.lean()
|
||||
|
||||
const hasMore = entries.length > limit
|
||||
if (hasMore) entries.pop()
|
||||
|
||||
const nextCursor = hasMore && entries.length
|
||||
? entries[entries.length - 1].timestamp.toISOString()
|
||||
: null
|
||||
|
||||
return { entries, hasMore, nextCursor }
|
||||
})
|
||||
|
|
@ -75,6 +75,14 @@ export default defineEventHandler(async (event) => {
|
|||
})
|
||||
}
|
||||
|
||||
if (body.enabled) {
|
||||
logActivity(member._id, 'peer_support_enabled', {
|
||||
topics: [...(body.skillTopics || []), ...(body.supportTopics || [])]
|
||||
})
|
||||
} else {
|
||||
logActivity(member._id, 'peer_support_disabled', {})
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
peerSupport: updated.peerSupport,
|
||||
|
|
|
|||
|
|
@ -80,6 +80,12 @@ export default defineEventHandler(async (event) => {
|
|||
});
|
||||
}
|
||||
|
||||
// Log which fields were updated
|
||||
const changedFields = Object.keys(body).filter(k => body[k] !== undefined && !k.endsWith('Privacy'))
|
||||
if (changedFields.length) {
|
||||
logActivity(memberId, 'profile_updated', { fields: changedFields })
|
||||
}
|
||||
|
||||
// Return sanitized member data
|
||||
return {
|
||||
id: member._id,
|
||||
|
|
|
|||
|
|
@ -13,12 +13,19 @@ export default defineEventHandler(async (event) => {
|
|||
return { success: true, message: 'Already in this circle' }
|
||||
}
|
||||
|
||||
const oldCircle = member.circle
|
||||
|
||||
await Member.findByIdAndUpdate(
|
||||
member._id,
|
||||
{ $set: { circle: body.circle } },
|
||||
{ runValidators: false }
|
||||
)
|
||||
|
||||
logActivity(member._id, 'circle_changed', {
|
||||
from: oldCircle,
|
||||
to: body.circle
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Circle updated to ${body.circle}`,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ export default defineEventHandler(async (event) => {
|
|||
};
|
||||
}
|
||||
|
||||
// Log contribution change (fire-and-forget, at the top so it logs regardless of which case path executes)
|
||||
const logContributionChange = () => {
|
||||
logActivity(member._id, 'contribution_changed', { from: oldTier, to: newTier })
|
||||
}
|
||||
|
||||
const helcimToken = config.helcimApiToken;
|
||||
const oldRequiresPayment = requiresPayment(oldTier);
|
||||
const newRequiresPayment = requiresPayment(newTier);
|
||||
|
|
@ -160,6 +165,8 @@ export default defineEventHandler(async (event) => {
|
|||
{ runValidators: false }
|
||||
);
|
||||
|
||||
logContributionChange()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Successfully upgraded to paid tier",
|
||||
|
|
@ -216,6 +223,8 @@ export default defineEventHandler(async (event) => {
|
|||
{ runValidators: false }
|
||||
);
|
||||
|
||||
logContributionChange()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Successfully downgraded to free tier",
|
||||
|
|
@ -279,6 +288,8 @@ export default defineEventHandler(async (event) => {
|
|||
{ runValidators: false }
|
||||
);
|
||||
|
||||
logContributionChange()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Successfully updated contribution level",
|
||||
|
|
@ -300,6 +311,8 @@ export default defineEventHandler(async (event) => {
|
|||
{ runValidators: false }
|
||||
);
|
||||
|
||||
logContributionChange()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Successfully updated contribution level",
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ export default defineEventHandler(async (event) => {
|
|||
{ runValidators: false }
|
||||
)
|
||||
|
||||
logActivity(member._id, 'email_changed', { previousEmail: oldEmail })
|
||||
|
||||
return {
|
||||
success: true,
|
||||
email: newEmail,
|
||||
|
|
|
|||
|
|
@ -139,6 +139,12 @@ export default defineEventHandler(async (event) => {
|
|||
})),
|
||||
paymentId,
|
||||
});
|
||||
if (member) {
|
||||
logActivity(member._id, 'email_sent', {
|
||||
emailType: 'series_pass',
|
||||
subject: `Series pass: ${series.title}`
|
||||
})
|
||||
}
|
||||
} catch (emailError) {
|
||||
console.error(
|
||||
"Failed to send series pass confirmation email:",
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
import Update from "../../models/update.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const member = await requireAuth(event);
|
||||
const memberId = member._id.toString();
|
||||
|
||||
const id = getRouterParam(event, "id");
|
||||
|
||||
try {
|
||||
const update = await Update.findById(id);
|
||||
|
||||
if (!update) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Update not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user is the author
|
||||
if (update.author.toString() !== memberId) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "You can only delete your own updates",
|
||||
});
|
||||
}
|
||||
|
||||
await Update.findByIdAndDelete(id);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error;
|
||||
console.error("Delete update error:", error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to delete update",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import Update from "../../models/update.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, "id");
|
||||
let memberId = null
|
||||
try {
|
||||
const member = await requireAuth(event)
|
||||
memberId = member._id.toString()
|
||||
} catch {
|
||||
// Not authenticated — continue with public-only access
|
||||
}
|
||||
|
||||
try {
|
||||
const update = await Update.findById(id).populate("author", "name avatar");
|
||||
|
||||
if (!update) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Update not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Check privacy permissions
|
||||
if (update.privacy === "private") {
|
||||
// Only author can view private updates
|
||||
if (!memberId || update.author._id.toString() !== memberId) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "You don't have permission to view this update",
|
||||
});
|
||||
}
|
||||
} else if (update.privacy === "members") {
|
||||
// Must be authenticated to view members-only updates
|
||||
if (!memberId) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "You must be a member to view this update",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return update;
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error;
|
||||
console.error("Get update error:", error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to fetch update",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import Update from "../../models/update.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const member = await requireAuth(event);
|
||||
const memberId = member._id.toString();
|
||||
|
||||
const id = getRouterParam(event, "id");
|
||||
const body = await validateBody(event, updatePatchSchema);
|
||||
|
||||
try {
|
||||
const update = await Update.findById(id);
|
||||
|
||||
if (!update) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Update not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user is the author
|
||||
if (update.author.toString() !== memberId) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "You can only edit your own updates",
|
||||
});
|
||||
}
|
||||
|
||||
// Update allowed fields
|
||||
if (body.content !== undefined) update.content = body.content;
|
||||
if (body.images !== undefined) update.images = body.images;
|
||||
if (body.privacy !== undefined) update.privacy = body.privacy;
|
||||
if (body.commentsEnabled !== undefined)
|
||||
update.commentsEnabled = body.commentsEnabled;
|
||||
|
||||
await update.save();
|
||||
await update.populate("author", "name avatar");
|
||||
|
||||
return update;
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error;
|
||||
console.error("Update edit error:", error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to update",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import Update from "../../models/update.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
let memberId = null
|
||||
try {
|
||||
const member = await requireAuth(event)
|
||||
memberId = member._id.toString()
|
||||
} catch {
|
||||
// Not authenticated — continue with public-only access
|
||||
}
|
||||
|
||||
const query = getQuery(event);
|
||||
const limit = parseInt(query.limit) || 20;
|
||||
const skip = parseInt(query.skip) || 0;
|
||||
|
||||
try {
|
||||
// Build privacy filter
|
||||
let privacyFilter;
|
||||
if (!memberId) {
|
||||
// Not authenticated - only show public updates
|
||||
privacyFilter = { privacy: "public" };
|
||||
} else {
|
||||
// Authenticated member - show public and members-only updates
|
||||
privacyFilter = { privacy: { $in: ["public", "members"] } };
|
||||
}
|
||||
|
||||
const updates = await Update.find(privacyFilter)
|
||||
.populate("author", "name avatar")
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(limit)
|
||||
.skip(skip);
|
||||
|
||||
const total = await Update.countDocuments(privacyFilter);
|
||||
|
||||
return {
|
||||
updates,
|
||||
total,
|
||||
hasMore: skip + limit < total,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Get updates error:", error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to fetch updates",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import Update from "../../models/update.js";
|
||||
import { validateBody } from "../../utils/validateBody.js";
|
||||
import { updateCreateSchema } from "../../utils/schemas.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const member = await requireAuth(event);
|
||||
const memberId = member._id.toString();
|
||||
|
||||
const body = await validateBody(event, updateCreateSchema);
|
||||
|
||||
try {
|
||||
const update = await Update.create({
|
||||
author: memberId,
|
||||
content: body.content,
|
||||
images: body.images || [],
|
||||
privacy: body.privacy || "members",
|
||||
commentsEnabled: body.commentsEnabled ?? true,
|
||||
});
|
||||
|
||||
// Populate author details
|
||||
await update.populate("author", "name avatar");
|
||||
|
||||
return update;
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error;
|
||||
console.error("Create update error:", error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to create update",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import Update from "../../models/update.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const member = await requireAuth(event);
|
||||
const memberId = member._id.toString();
|
||||
|
||||
const query = getQuery(event);
|
||||
const limit = parseInt(query.limit) || 20;
|
||||
const skip = parseInt(query.skip) || 0;
|
||||
|
||||
try {
|
||||
const updates = await Update.find({ author: memberId })
|
||||
.populate("author", "name avatar")
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(limit)
|
||||
.skip(skip);
|
||||
|
||||
const total = await Update.countDocuments({ author: memberId });
|
||||
|
||||
return {
|
||||
updates,
|
||||
total,
|
||||
hasMore: skip + limit < total,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Get my updates error:", error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to fetch updates",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
import Update from "../../../models/update.js";
|
||||
import Member from "../../../models/member.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const userId = getRouterParam(event, "id");
|
||||
let currentMemberId = null
|
||||
try {
|
||||
const member = await requireAuth(event)
|
||||
currentMemberId = member._id.toString()
|
||||
} catch {
|
||||
// Not authenticated — continue with public-only access
|
||||
}
|
||||
|
||||
const query = getQuery(event);
|
||||
const limit = parseInt(query.limit) || 20;
|
||||
const skip = parseInt(query.skip) || 0;
|
||||
|
||||
try {
|
||||
// Verify the user exists
|
||||
const user = await Member.findById(userId);
|
||||
if (!user) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Build privacy filter
|
||||
let privacyFilter;
|
||||
if (!currentMemberId) {
|
||||
// Not authenticated - only show public updates
|
||||
privacyFilter = { author: userId, privacy: "public" };
|
||||
} else if (currentMemberId === userId) {
|
||||
// Viewing own updates - show all
|
||||
privacyFilter = { author: userId };
|
||||
} else {
|
||||
// Authenticated member viewing another's updates - show public and members-only
|
||||
privacyFilter = { author: userId, privacy: { $in: ["public", "members"] } };
|
||||
}
|
||||
|
||||
const updates = await Update.find(privacyFilter)
|
||||
.populate("author", "name avatar")
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(limit)
|
||||
.skip(skip);
|
||||
|
||||
const total = await Update.countDocuments(privacyFilter);
|
||||
|
||||
return {
|
||||
updates,
|
||||
total,
|
||||
hasMore: skip + limit < total,
|
||||
user: {
|
||||
_id: user._id,
|
||||
name: user.name,
|
||||
avatar: user.avatar,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error;
|
||||
console.error("Get user updates error:", error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to fetch user updates",
|
||||
});
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue