import jwt from "jsonwebtoken"; import Member from "../../models/member.js"; import Tag from "../../models/tag.js"; import { connectDB } from "../../utils/mongoose.js"; export default defineEventHandler(async (event) => { await connectDB(); // Check if user is authenticated const token = getCookie(event, "auth-token"); let isAuthenticated = false; let currentMemberId = null; if (token) { try { const decoded = jwt.verify(token, useRuntimeConfig().jwtSecret); currentMemberId = decoded.memberId; isAuthenticated = true; } catch (err) { // Invalid token, treat as public isAuthenticated = false; } } const query = getQuery(event); const search = query.search || ""; const circle = query.circle || ""; const tags = query.tags ? query.tags.split(",") : []; const peerSupport = query.peerSupport || ""; const topics = query.topics ? query.topics.split(",") : []; const craftTag = query.craftTag || ""; const connectionTag = query.connectionTag || ""; // Build query const dbQuery = { showInDirectory: true, status: "active", }; // Filter by circle if specified if (circle) { dbQuery.circle = circle; } // Collect $and conditions for combining multiple filters const andConditions = []; // Filter by peer support availability (check both old and new fields) if (peerSupport === "true") { andConditions.push({ $or: [ { "peerSupport.enabled": true }, { "communityConnections.offerPeerSupport": true }, ], }); } // Search by name or bio if (search) { // Escape special regex characters to prevent ReDoS const escaped = search.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); andConditions.push({ $or: [ { name: { $regex: escaped, $options: "i" } }, { bio: { $regex: escaped, $options: "i" } }, ], }); } // Filter by tags (search in offering.tags or lookingFor.tags) if (tags.length > 0) { andConditions.push({ $or: [ { "offering.tags": { $in: tags } }, { "lookingFor.tags": { $in: tags } }, ], }); } // Filter by peer support topics if (topics.length > 0) { dbQuery["peerSupport.topics"] = { $in: topics }; } // Filter by craft tag if (craftTag) { dbQuery.craftTags = craftTag; } // Filter by connection tag if (connectionTag) { dbQuery["communityConnections.topics.tagSlug"] = connectionTag; } // Apply combined $and conditions if (andConditions.length > 0) { dbQuery.$and = andConditions; } try { const members = await Member.find(dbQuery) .select( "name pronouns timeZone avatar studio bio location socialLinks offering lookingFor privacy circle peerSupport craftTags communityConnections createdAt", ) .sort({ createdAt: -1 }) .lean(); // Filter fields based on privacy settings const filteredMembers = members.map((member) => { const privacy = member.privacy || {}; const filtered = { _id: member._id, name: member.name, circle: member.circle, createdAt: member.createdAt, }; // Helper function to check if field should be visible const isVisible = (field) => { const privacySetting = privacy[field] || "members"; if (privacySetting === "public") return true; if (privacySetting === "members" && isAuthenticated) return true; if (privacySetting === "private") return false; return false; }; // Add fields based on privacy settings if (isVisible("avatar")) filtered.avatar = member.avatar; if (isVisible("pronouns")) filtered.pronouns = member.pronouns; if (isVisible("timeZone")) filtered.timeZone = member.timeZone; if (isVisible("studio")) filtered.studio = member.studio; if (isVisible("bio")) filtered.bio = member.bio; if (isVisible("location")) filtered.location = member.location; if (isVisible("socialLinks")) filtered.socialLinks = member.socialLinks; if (isVisible("offering")) filtered.offering = member.offering; if (isVisible("lookingFor")) filtered.lookingFor = member.lookingFor; // Craft tags (with fallback to offering.tags for backward compat) if (isVisible("craftTags")) { filtered.craftTags = member.craftTags; } // Community connections (expose only public-safe fields) if (isVisible("communityConnections")) { filtered.communityConnections = { topics: member.communityConnections?.topics, offerPeerSupport: member.communityConnections?.offerPeerSupport, availability: member.communityConnections?.availability, }; } // Peer support: expose only fields needed for matching/contact UX // slackUserId, slackDMChannelId, slackUsername, personalMessage are internal if (member.peerSupport?.enabled) { filtered.peerSupport = { enabled: true, skillTopics: member.peerSupport.skillTopics, supportTopics: member.peerSupport.supportTopics, availability: member.peerSupport.availability, }; } return filtered; }); // Get unique tags for filter options (from both offering and lookingFor) — backward compat const allTags = members .flatMap((m) => [ ...(m.offering?.tags || []), ...(m.lookingFor?.tags || []), ]) .filter((tag, index, self) => self.indexOf(tag) === index) .sort(); // Get unique peer support topics const allTopics = members .filter((m) => m.peerSupport?.enabled) .flatMap((m) => m.peerSupport?.topics || []) .filter((topic, index, self) => self.indexOf(topic) === index) .sort(); // Fetch predefined tags from Tag model for filter bars const [craftTags, cooperativeTags] = await Promise.all([ Tag.find({ pool: "craft", active: true }).sort({ label: 1 }).lean(), Tag.find({ pool: "cooperative", active: true }).sort({ label: 1 }).lean(), ]); return { members: filteredMembers, totalCount: filteredMembers.length, filters: { availableSkills: allTags, availableTopics: allTopics, craftTags: craftTags.map((t) => ({ slug: t.slug, label: t.label })), cooperativeTags: cooperativeTags.map((t) => ({ slug: t.slug, label: t.label, })), }, }; } catch (error) { console.error("Directory fetch error:", error); throw createError({ statusCode: 500, message: "Failed to fetch member directory", }); } });