Add peer support functionality and UI
This commit is contained in:
parent
2b55ca4104
commit
1b8dacf92a
11 changed files with 1159 additions and 35 deletions
|
|
@ -48,6 +48,8 @@ export default defineEventHandler(async (event) => {
|
|||
lookingFor: member.lookingFor,
|
||||
showInDirectory: member.showInDirectory,
|
||||
privacy: member.privacy,
|
||||
// Peer support
|
||||
peerSupport: member.peerSupport,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Token verification error:", err);
|
||||
|
|
|
|||
119
server/api/members/me/peer-support.patch.js
Normal file
119
server/api/members/me/peer-support.patch.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import jwt from "jsonwebtoken";
|
||||
import Member from "../../../models/member.js";
|
||||
import { connectDB } from "../../../utils/mongoose.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await connectDB();
|
||||
|
||||
const token = getCookie(event, "auth-token");
|
||||
|
||||
if (!token) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Not authenticated",
|
||||
});
|
||||
}
|
||||
|
||||
let memberId;
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
memberId = decoded.memberId;
|
||||
} catch (err) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Invalid or expired token",
|
||||
});
|
||||
}
|
||||
|
||||
const body = await readBody(event);
|
||||
|
||||
// Build update object for peer support settings
|
||||
const updateData = {
|
||||
"peerSupport.enabled": body.enabled || false,
|
||||
"peerSupport.topics": body.topics || [],
|
||||
"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();
|
||||
|
||||
if (slackService) {
|
||||
console.log(
|
||||
"[Peer Support] Slack service initialized, looking up user...",
|
||||
);
|
||||
const slackUserId = await slackService.findUserIdByUsername(
|
||||
body.slackUsername,
|
||||
);
|
||||
|
||||
if (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);
|
||||
|
||||
if (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");
|
||||
}
|
||||
} 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",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
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,
|
||||
{ $set: updateData },
|
||||
{ new: true, runValidators: true },
|
||||
);
|
||||
|
||||
if (!member) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Member not found",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
peerSupport: member.peerSupport,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Peer support update error:", error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to update peer support settings",
|
||||
});
|
||||
}
|
||||
});
|
||||
63
server/api/peer-support.get.js
Normal file
63
server/api/peer-support.get.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import jwt from "jsonwebtoken";
|
||||
import Member from "../models/member.js";
|
||||
import { connectDB } from "../utils/mongoose.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await connectDB();
|
||||
|
||||
// Check if user is authenticated (optional for this endpoint)
|
||||
const token = getCookie(event, "auth-token");
|
||||
let isAuthenticated = false;
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
jwt.verify(token, process.env.JWT_SECRET);
|
||||
isAuthenticated = true;
|
||||
} catch (err) {
|
||||
isAuthenticated = false;
|
||||
}
|
||||
}
|
||||
|
||||
const query = getQuery(event);
|
||||
const topic = query.topic;
|
||||
|
||||
// Build query for peer supporters
|
||||
const dbQuery = {
|
||||
"peerSupport.enabled": true,
|
||||
status: "active",
|
||||
};
|
||||
|
||||
// Filter by topic if specified
|
||||
if (topic) {
|
||||
dbQuery["peerSupport.topics"] = topic;
|
||||
}
|
||||
|
||||
try {
|
||||
const supporters = await Member.find(dbQuery)
|
||||
.select(
|
||||
"name avatar circle peerSupport slackUserId createdAt"
|
||||
)
|
||||
.sort({ createdAt: -1 })
|
||||
.lean();
|
||||
|
||||
// Get unique topics for filter options
|
||||
const allTopics = supporters
|
||||
.flatMap((supporter) => supporter.peerSupport?.topics || [])
|
||||
.filter((topic, index, self) => self.indexOf(topic) === index)
|
||||
.sort();
|
||||
|
||||
return {
|
||||
supporters,
|
||||
totalCount: supporters.length,
|
||||
filters: {
|
||||
availableTopics: allTopics,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Peer support fetch error:", error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to fetch peer supporters",
|
||||
});
|
||||
}
|
||||
});
|
||||
28
server/api/test/peer-support-debug.get.js
Normal file
28
server/api/test/peer-support-debug.get.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import jwt from "jsonwebtoken";
|
||||
import Member from "../../models/member.js";
|
||||
import { connectDB } from "../../utils/mongoose.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await connectDB();
|
||||
|
||||
const token = getCookie(event, "auth-token");
|
||||
if (!token) {
|
||||
throw createError({ statusCode: 401, statusMessage: "Not authenticated" });
|
||||
}
|
||||
|
||||
let memberId;
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
memberId = decoded.memberId;
|
||||
} catch (err) {
|
||||
throw createError({ statusCode: 401, statusMessage: "Invalid token" });
|
||||
}
|
||||
|
||||
const member = await Member.findById(memberId).select("name peerSupport slackUserId");
|
||||
|
||||
return {
|
||||
name: member.name,
|
||||
peerSupport: member.peerSupport,
|
||||
slackUserId: member.slackUserId,
|
||||
};
|
||||
});
|
||||
|
|
@ -63,6 +63,16 @@ const memberSchema = new mongoose.Schema({
|
|||
lookingFor: String,
|
||||
showInDirectory: { type: Boolean, default: true },
|
||||
|
||||
// Peer support settings
|
||||
peerSupport: {
|
||||
enabled: { type: Boolean, default: false },
|
||||
topics: [String],
|
||||
availability: String,
|
||||
personalMessage: String,
|
||||
slackUsername: String,
|
||||
slackDMChannelId: String, // DM channel ID for direct messaging
|
||||
},
|
||||
|
||||
// Privacy settings for profile fields
|
||||
privacy: {
|
||||
pronouns: {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export class SlackService {
|
|||
*/
|
||||
async inviteUserToSlack(
|
||||
email: string,
|
||||
realName: string
|
||||
realName: string,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
userId?: string;
|
||||
|
|
@ -34,7 +34,7 @@ export class SlackService {
|
|||
});
|
||||
|
||||
console.log(
|
||||
`Successfully invited existing user ${email} to vetting channel`
|
||||
`Successfully invited existing user ${email} to vetting channel`,
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
|
|
@ -65,7 +65,7 @@ export class SlackService {
|
|||
|
||||
if (inviteResponse.ok && inviteResponse.user) {
|
||||
console.log(
|
||||
`Successfully invited ${email} to workspace as single-channel guest`
|
||||
`Successfully invited ${email} to workspace as single-channel guest`,
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
|
|
@ -79,7 +79,7 @@ export class SlackService {
|
|||
console.log(
|
||||
`Admin API not available or failed: ${
|
||||
adminError.data?.error || adminError.message
|
||||
}`
|
||||
}`,
|
||||
);
|
||||
|
||||
// Fall back to manual process
|
||||
|
|
@ -113,6 +113,67 @@ export class SlackService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find user ID by username (display name or real name)
|
||||
*/
|
||||
async findUserIdByUsername(username: string): Promise<string | null> {
|
||||
try {
|
||||
const cleanUsername = username.replace("@", "").toLowerCase();
|
||||
|
||||
// List all users and search for matching username
|
||||
const response = await this.client.users.list();
|
||||
|
||||
if (!response.members) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Search for user by name or display_name
|
||||
const user = response.members.find((member: any) => {
|
||||
const name = member.name?.toLowerCase() || "";
|
||||
const realName = member.real_name?.toLowerCase() || "";
|
||||
const displayName = member.profile?.display_name?.toLowerCase() || "";
|
||||
|
||||
return (
|
||||
name === cleanUsername ||
|
||||
displayName === cleanUsername ||
|
||||
realName.includes(cleanUsername)
|
||||
);
|
||||
});
|
||||
|
||||
return user?.id || null;
|
||||
} catch (error) {
|
||||
console.error("Error looking up Slack user by username:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open/get a DM channel with a user and return the channel ID
|
||||
* This creates or opens a DM conversation and returns the channel ID (starts with D)
|
||||
*/
|
||||
async openDMChannel(userId: string): Promise<string | null> {
|
||||
try {
|
||||
const response = await this.client.conversations.open({
|
||||
users: userId,
|
||||
});
|
||||
|
||||
if (response.ok && response.channel?.id) {
|
||||
console.log(
|
||||
`Opened DM channel for user ${userId}: ${response.channel.id}`,
|
||||
);
|
||||
return response.channel.id;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
"Error opening DM channel:",
|
||||
error.data?.error || error.message,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification to the vetting channel about a new member
|
||||
*/
|
||||
|
|
@ -121,7 +182,7 @@ export class SlackService {
|
|||
memberEmail: string,
|
||||
circle: string,
|
||||
contributionTier: string,
|
||||
invitationStatus: string = "manual_invitation_required"
|
||||
invitationStatus: string = "manual_invitation_required",
|
||||
): Promise<void> {
|
||||
try {
|
||||
let statusMessage = "";
|
||||
|
|
@ -224,7 +285,7 @@ export function getSlackService(): SlackService | null {
|
|||
|
||||
if (!config.slackBotToken || !config.slackVettingChannelId) {
|
||||
console.warn(
|
||||
"Slack integration not configured - missing bot token or channel ID"
|
||||
"Slack integration not configured - missing bot token or channel ID",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue