arcadia-suite-sv/server/communities/routes.ts

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" });
}
});
}