528 lines
18 KiB
TypeScript
528 lines
18 KiB
TypeScript
import type { Express, Request, Response } from "express";
|
|
import { db } from "../../db/index";
|
|
import {
|
|
communities, communityChannels, communityMembers, communityMessages, users
|
|
} from "@shared/schema";
|
|
import { eq, desc, and, sql as sqlQuery } from "drizzle-orm";
|
|
import { z } from "zod";
|
|
|
|
const communitySchema = z.object({
|
|
name: z.string().min(1).max(100),
|
|
description: z.string().optional(),
|
|
iconEmoji: z.string().max(10).optional(),
|
|
iconColor: z.string().max(20).optional(),
|
|
isPrivate: z.boolean().optional(),
|
|
});
|
|
|
|
const channelSchema = z.object({
|
|
name: z.string().min(1).max(100),
|
|
description: z.string().optional(),
|
|
type: z.enum(["text", "voice", "announcement"]).optional(),
|
|
isPrivate: z.boolean().optional(),
|
|
projectId: z.number().optional(),
|
|
});
|
|
|
|
const messageSchema = z.object({
|
|
content: z.string().min(1),
|
|
replyToId: z.number().optional(),
|
|
});
|
|
|
|
async function checkMembership(userId: string, communityId: number): Promise<{ isMember: boolean; role: string | null }> {
|
|
const [member] = await db.select({ role: communityMembers.role })
|
|
.from(communityMembers)
|
|
.where(and(
|
|
eq(communityMembers.communityId, communityId),
|
|
eq(communityMembers.userId, userId)
|
|
))
|
|
.limit(1);
|
|
return { isMember: !!member, role: member?.role || null };
|
|
}
|
|
|
|
function canModerate(role: string | null): boolean {
|
|
return role === "owner" || role === "admin" || role === "moderator";
|
|
}
|
|
|
|
export function registerCommunityRoutes(app: Express) {
|
|
// ========== COMMUNITIES ==========
|
|
|
|
// List communities for current user
|
|
app.get("/api/communities", async (req: Request, res: Response) => {
|
|
try {
|
|
if (!req.isAuthenticated()) {
|
|
return res.status(401).json({ error: "Not authenticated" });
|
|
}
|
|
const userId = req.user!.id;
|
|
|
|
// Get communities where user is a member
|
|
const memberCommunities = await db
|
|
.select({
|
|
id: communities.id,
|
|
name: communities.name,
|
|
description: communities.description,
|
|
iconEmoji: communities.iconEmoji,
|
|
iconColor: communities.iconColor,
|
|
isPrivate: communities.isPrivate,
|
|
createdAt: communities.createdAt,
|
|
role: communityMembers.role,
|
|
status: communityMembers.status,
|
|
})
|
|
.from(communities)
|
|
.innerJoin(communityMembers, eq(communities.id, communityMembers.communityId))
|
|
.where(eq(communityMembers.userId, userId))
|
|
.orderBy(desc(communities.createdAt));
|
|
|
|
res.json(memberCommunities);
|
|
} catch (error) {
|
|
console.error("List communities error:", error);
|
|
res.status(500).json({ error: "Failed to list communities" });
|
|
}
|
|
});
|
|
|
|
// Create community
|
|
app.post("/api/communities", async (req: Request, res: Response) => {
|
|
try {
|
|
if (!req.isAuthenticated()) {
|
|
return res.status(401).json({ error: "Not authenticated" });
|
|
}
|
|
const userId = req.user!.id;
|
|
const data = communitySchema.parse(req.body);
|
|
|
|
const [community] = await db.insert(communities).values({
|
|
...data,
|
|
createdBy: userId,
|
|
}).returning();
|
|
|
|
// Add creator as owner member
|
|
await db.insert(communityMembers).values({
|
|
communityId: community.id,
|
|
userId,
|
|
role: "owner",
|
|
status: "online",
|
|
});
|
|
|
|
// Create default general channel
|
|
await db.insert(communityChannels).values({
|
|
communityId: community.id,
|
|
name: "geral",
|
|
description: "Canal geral da comunidade",
|
|
type: "text",
|
|
});
|
|
|
|
res.status(201).json(community);
|
|
} catch (error: any) {
|
|
console.error("Create community error:", error);
|
|
res.status(400).json({ error: error.message || "Failed to create community" });
|
|
}
|
|
});
|
|
|
|
// Get community details with channels and members
|
|
app.get("/api/communities/:id", async (req: Request, res: Response) => {
|
|
try {
|
|
if (!req.isAuthenticated()) {
|
|
return res.status(401).json({ error: "Not authenticated" });
|
|
}
|
|
const communityId = parseInt(req.params.id);
|
|
const userId = req.user!.id;
|
|
|
|
// Check membership
|
|
const { isMember } = await checkMembership(userId, communityId);
|
|
if (!isMember) {
|
|
return res.status(403).json({ error: "Not a member of this community" });
|
|
}
|
|
|
|
const [community] = await db.select().from(communities).where(eq(communities.id, communityId)).limit(1);
|
|
if (!community) {
|
|
return res.status(404).json({ error: "Community not found" });
|
|
}
|
|
|
|
const channels = await db.select().from(communityChannels)
|
|
.where(eq(communityChannels.communityId, communityId))
|
|
.orderBy(communityChannels.orderIndex);
|
|
|
|
const members = await db
|
|
.select({
|
|
id: communityMembers.id,
|
|
userId: communityMembers.userId,
|
|
role: communityMembers.role,
|
|
nickname: communityMembers.nickname,
|
|
status: communityMembers.status,
|
|
statusMessage: communityMembers.statusMessage,
|
|
joinedAt: communityMembers.joinedAt,
|
|
lastActiveAt: communityMembers.lastActiveAt,
|
|
username: users.username,
|
|
})
|
|
.from(communityMembers)
|
|
.innerJoin(users, eq(communityMembers.userId, users.id))
|
|
.where(eq(communityMembers.communityId, communityId));
|
|
|
|
res.json({ ...community, channels, members });
|
|
} catch (error) {
|
|
console.error("Get community error:", error);
|
|
res.status(500).json({ error: "Failed to get community" });
|
|
}
|
|
});
|
|
|
|
// Delete community
|
|
app.delete("/api/communities/:id", async (req: Request, res: Response) => {
|
|
try {
|
|
if (!req.isAuthenticated()) {
|
|
return res.status(401).json({ error: "Not authenticated" });
|
|
}
|
|
const communityId = parseInt(req.params.id);
|
|
const userId = req.user!.id;
|
|
|
|
// Check if user is owner
|
|
const [member] = await db.select().from(communityMembers)
|
|
.where(and(
|
|
eq(communityMembers.communityId, communityId),
|
|
eq(communityMembers.userId, userId),
|
|
eq(communityMembers.role, "owner")
|
|
)).limit(1);
|
|
|
|
if (!member) {
|
|
return res.status(403).json({ error: "Only owners can delete communities" });
|
|
}
|
|
|
|
await db.delete(communities).where(eq(communities.id, communityId));
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error("Delete community error:", error);
|
|
res.status(500).json({ error: "Failed to delete community" });
|
|
}
|
|
});
|
|
|
|
// ========== CHANNELS ==========
|
|
|
|
// Create channel (admin+ only)
|
|
app.post("/api/communities/:id/channels", async (req: Request, res: Response) => {
|
|
try {
|
|
if (!req.isAuthenticated()) {
|
|
return res.status(401).json({ error: "Not authenticated" });
|
|
}
|
|
const communityId = parseInt(req.params.id);
|
|
const userId = req.user!.id;
|
|
|
|
// Check if user can moderate
|
|
const { isMember, role } = await checkMembership(userId, communityId);
|
|
if (!isMember || !canModerate(role)) {
|
|
return res.status(403).json({ error: "Permission denied" });
|
|
}
|
|
|
|
const data = channelSchema.parse(req.body);
|
|
|
|
const [channel] = await db.insert(communityChannels).values({
|
|
...data,
|
|
communityId,
|
|
}).returning();
|
|
|
|
res.status(201).json(channel);
|
|
} catch (error: any) {
|
|
console.error("Create channel error:", error);
|
|
res.status(400).json({ error: error.message || "Failed to create channel" });
|
|
}
|
|
});
|
|
|
|
// Delete channel (admin+ only)
|
|
app.delete("/api/communities/:id/channels/:channelId", async (req: Request, res: Response) => {
|
|
try {
|
|
if (!req.isAuthenticated()) {
|
|
return res.status(401).json({ error: "Not authenticated" });
|
|
}
|
|
const communityId = parseInt(req.params.id);
|
|
const channelId = parseInt(req.params.channelId);
|
|
const userId = req.user!.id;
|
|
|
|
// Check if user can moderate
|
|
const { isMember, role } = await checkMembership(userId, communityId);
|
|
if (!isMember || !canModerate(role)) {
|
|
return res.status(403).json({ error: "Permission denied" });
|
|
}
|
|
|
|
await db.delete(communityChannels).where(eq(communityChannels.id, channelId));
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error("Delete channel error:", error);
|
|
res.status(500).json({ error: "Failed to delete channel" });
|
|
}
|
|
});
|
|
|
|
// ========== MEMBERS ==========
|
|
|
|
// Add member to community (admin+ only)
|
|
app.post("/api/communities/:id/members", async (req: Request, res: Response) => {
|
|
try {
|
|
if (!req.isAuthenticated()) {
|
|
return res.status(401).json({ error: "Not authenticated" });
|
|
}
|
|
const communityId = parseInt(req.params.id);
|
|
const currentUserId = req.user!.id;
|
|
const { userId, role = "member" } = req.body;
|
|
|
|
// Check if current user can add members
|
|
const { isMember, role: userRole } = await checkMembership(currentUserId, communityId);
|
|
if (!isMember || !canModerate(userRole)) {
|
|
return res.status(403).json({ error: "Permission denied" });
|
|
}
|
|
|
|
const [member] = await db.insert(communityMembers).values({
|
|
communityId,
|
|
userId,
|
|
role,
|
|
status: "offline",
|
|
}).returning();
|
|
|
|
res.status(201).json(member);
|
|
} catch (error: any) {
|
|
console.error("Add member error:", error);
|
|
res.status(400).json({ error: error.message || "Failed to add member" });
|
|
}
|
|
});
|
|
|
|
// Update member status (online/offline)
|
|
app.patch("/api/communities/:id/members/status", async (req: Request, res: Response) => {
|
|
try {
|
|
if (!req.isAuthenticated()) {
|
|
return res.status(401).json({ error: "Not authenticated" });
|
|
}
|
|
const communityId = parseInt(req.params.id);
|
|
const userId = req.user!.id;
|
|
const { status, statusMessage } = req.body;
|
|
|
|
await db.update(communityMembers)
|
|
.set({
|
|
status,
|
|
statusMessage,
|
|
lastActiveAt: new Date(),
|
|
})
|
|
.where(and(
|
|
eq(communityMembers.communityId, communityId),
|
|
eq(communityMembers.userId, userId)
|
|
));
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error("Update status error:", error);
|
|
res.status(500).json({ error: "Failed to update status" });
|
|
}
|
|
});
|
|
|
|
// Remove member from community (admin+ or self-leave)
|
|
app.delete("/api/communities/:id/members/:memberId", async (req: Request, res: Response) => {
|
|
try {
|
|
if (!req.isAuthenticated()) {
|
|
return res.status(401).json({ error: "Not authenticated" });
|
|
}
|
|
const communityId = parseInt(req.params.id);
|
|
const memberId = parseInt(req.params.memberId);
|
|
const currentUserId = req.user!.id;
|
|
|
|
// Get the member being removed
|
|
const [targetMember] = await db.select()
|
|
.from(communityMembers)
|
|
.where(and(
|
|
eq(communityMembers.id, memberId),
|
|
eq(communityMembers.communityId, communityId)
|
|
))
|
|
.limit(1);
|
|
|
|
if (!targetMember) {
|
|
return res.status(404).json({ error: "Member not found" });
|
|
}
|
|
|
|
// Check if current user is the target (self-leave)
|
|
const isSelfLeave = targetMember.userId === currentUserId;
|
|
|
|
if (!isSelfLeave) {
|
|
// Check if current user can remove members
|
|
const { isMember, role } = await checkMembership(currentUserId, communityId);
|
|
if (!isMember || !canModerate(role)) {
|
|
return res.status(403).json({ error: "Permission denied" });
|
|
}
|
|
|
|
// Cannot remove owner
|
|
if (targetMember.role === "owner") {
|
|
return res.status(403).json({ error: "Cannot remove community owner" });
|
|
}
|
|
}
|
|
|
|
await db.delete(communityMembers).where(eq(communityMembers.id, memberId));
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error("Remove member error:", error);
|
|
res.status(500).json({ error: "Failed to remove member" });
|
|
}
|
|
});
|
|
|
|
// ========== MESSAGES ==========
|
|
|
|
// Helper: verify channel belongs to community
|
|
async function verifyChannelInCommunity(channelId: number, communityId: number): Promise<boolean> {
|
|
const [channel] = await db.select()
|
|
.from(communityChannels)
|
|
.where(and(
|
|
eq(communityChannels.id, channelId),
|
|
eq(communityChannels.communityId, communityId)
|
|
))
|
|
.limit(1);
|
|
return !!channel;
|
|
}
|
|
|
|
// Get messages from a channel
|
|
app.get("/api/communities/:id/channels/:channelId/messages", async (req: Request, res: Response) => {
|
|
try {
|
|
if (!req.isAuthenticated()) {
|
|
return res.status(401).json({ error: "Not authenticated" });
|
|
}
|
|
const communityId = parseInt(req.params.id);
|
|
const channelId = parseInt(req.params.channelId);
|
|
const userId = req.user!.id;
|
|
const limit = parseInt(req.query.limit as string) || 50;
|
|
|
|
// Verify membership
|
|
const { isMember } = await checkMembership(userId, communityId);
|
|
if (!isMember) {
|
|
return res.status(403).json({ error: "Not a member of this community" });
|
|
}
|
|
|
|
// Verify channel belongs to community
|
|
const validChannel = await verifyChannelInCommunity(channelId, communityId);
|
|
if (!validChannel) {
|
|
return res.status(404).json({ error: "Channel not found in this community" });
|
|
}
|
|
|
|
const messages = await db
|
|
.select({
|
|
id: communityMessages.id,
|
|
content: communityMessages.content,
|
|
userId: communityMessages.userId,
|
|
channelId: communityMessages.channelId,
|
|
replyToId: communityMessages.replyToId,
|
|
isPinned: communityMessages.isPinned,
|
|
createdAt: communityMessages.createdAt,
|
|
username: users.username,
|
|
})
|
|
.from(communityMessages)
|
|
.innerJoin(users, eq(communityMessages.userId, users.id))
|
|
.where(eq(communityMessages.channelId, channelId))
|
|
.orderBy(desc(communityMessages.createdAt))
|
|
.limit(limit);
|
|
|
|
res.json(messages.reverse());
|
|
} catch (error) {
|
|
console.error("Get messages error:", error);
|
|
res.status(500).json({ error: "Failed to get messages" });
|
|
}
|
|
});
|
|
|
|
// Send message to a channel
|
|
app.post("/api/communities/:id/channels/:channelId/messages", async (req: Request, res: Response) => {
|
|
try {
|
|
if (!req.isAuthenticated()) {
|
|
return res.status(401).json({ error: "Not authenticated" });
|
|
}
|
|
const communityId = parseInt(req.params.id);
|
|
const channelId = parseInt(req.params.channelId);
|
|
const userId = req.user!.id;
|
|
const data = messageSchema.parse(req.body);
|
|
|
|
// Verify membership
|
|
const { isMember } = await checkMembership(userId, communityId);
|
|
if (!isMember) {
|
|
return res.status(403).json({ error: "Not a member of this community" });
|
|
}
|
|
|
|
// Verify channel belongs to community
|
|
const validChannel = await verifyChannelInCommunity(channelId, communityId);
|
|
if (!validChannel) {
|
|
return res.status(404).json({ error: "Channel not found in this community" });
|
|
}
|
|
|
|
const [message] = await db.insert(communityMessages).values({
|
|
channelId,
|
|
userId,
|
|
content: data.content,
|
|
replyToId: data.replyToId,
|
|
}).returning();
|
|
|
|
res.status(201).json(message);
|
|
} catch (error: any) {
|
|
console.error("Send message error:", error);
|
|
res.status(400).json({ error: error.message || "Failed to send message" });
|
|
}
|
|
});
|
|
|
|
// Delete message (owner only)
|
|
app.delete("/api/communities/:id/channels/:channelId/messages/:messageId", async (req: Request, res: Response) => {
|
|
try {
|
|
if (!req.isAuthenticated()) {
|
|
return res.status(401).json({ error: "Not authenticated" });
|
|
}
|
|
const communityId = parseInt(req.params.id);
|
|
const channelId = parseInt(req.params.channelId);
|
|
const messageId = parseInt(req.params.messageId);
|
|
const userId = req.user!.id;
|
|
|
|
// Verify membership
|
|
const { isMember, role } = await checkMembership(userId, communityId);
|
|
if (!isMember) {
|
|
return res.status(403).json({ error: "Not a member of this community" });
|
|
}
|
|
|
|
// Verify channel belongs to community
|
|
const validChannel = await verifyChannelInCommunity(channelId, communityId);
|
|
if (!validChannel) {
|
|
return res.status(404).json({ error: "Channel not found in this community" });
|
|
}
|
|
|
|
// Verify message exists and user can delete it
|
|
const [message] = await db.select()
|
|
.from(communityMessages)
|
|
.where(eq(communityMessages.id, messageId))
|
|
.limit(1);
|
|
|
|
if (!message) {
|
|
return res.status(404).json({ error: "Message not found" });
|
|
}
|
|
|
|
// Only message owner or moderators can delete
|
|
if (message.userId !== userId && !canModerate(role)) {
|
|
return res.status(403).json({ error: "Cannot delete this message" });
|
|
}
|
|
|
|
await db.delete(communityMessages).where(eq(communityMessages.id, messageId));
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error("Delete message error:", error);
|
|
res.status(500).json({ error: "Failed to delete message" });
|
|
}
|
|
});
|
|
|
|
// Get all users (for adding members)
|
|
app.get("/api/communities/users/search", async (req: Request, res: Response) => {
|
|
try {
|
|
if (!req.isAuthenticated()) {
|
|
return res.status(401).json({ error: "Not authenticated" });
|
|
}
|
|
const query = (req.query.q as string) || "";
|
|
|
|
const allUsers = await db.select({
|
|
id: users.id,
|
|
username: users.username,
|
|
email: users.email,
|
|
}).from(users).limit(50);
|
|
|
|
const filtered = query
|
|
? allUsers.filter(u =>
|
|
u.username?.toLowerCase().includes(query.toLowerCase()) ||
|
|
u.email?.toLowerCase().includes(query.toLowerCase())
|
|
)
|
|
: allUsers;
|
|
|
|
res.json(filtered);
|
|
} catch (error) {
|
|
console.error("Search users error:", error);
|
|
res.status(500).json({ error: "Failed to search users" });
|
|
}
|
|
});
|
|
}
|