import { Request, Response } from 'express'; import User, { IUser } from '../models/User.js'; import OrganizationMember, { OrgRole } from '../models/OrganizationMember.js'; // Define locally to avoid import cycle risks interface IAppUser extends IUser { organizationId?: string; organizationRole?: OrgRole; organizationBanned?: boolean; } interface AuthRequest extends Request { appUser?: IAppUser; } /** * Sync user from Clerk to MongoDB * Creates user if doesn't exist, updates if exists * Also creates/updates OrganizationMember for the current organization */ export const syncUser = async (req: Request, res: Response) => { console.log('--- syncUser called ---', req.body); try { const { clerkId, email, name, organizationId, clerkRole } = req.body; if (!clerkId || !email || !name) { return res.status(400).json({ error: 'clerkId, email e name são obrigatórios.' }); } // 1. Upsert the global User record let user = await User.findOne({ clerkId }); if (user) { user.email = email; user.name = name; await user.save(); } else { user = await User.create({ clerkId, email, name, role: 'guest', // Default global role isBanned: false }); } if (organizationId) { // Map Clerk role to our app role let appRole: OrgRole = 'guest'; if (clerkRole === 'org:admin') { appRole = 'admin'; } else if (clerkRole === 'org:member') { appRole = 'user'; } // Use findOneAndUpdate with upsert to handle race conditions atomically // This avoids the need for try/catch on create and handles existing members too const member = await OrganizationMember.findOneAndUpdate( { clerkUserId: clerkId, organizationId }, { $set: { name, email, // Only update role if it's the first time (creation) // Or we can optionally update it if needed. // For now, let's NOT overwrite role on update to preserve local changes, // UNLESS we want to force sync with Clerk. // Let's use $setOnInsert for fields we only want to set on creation. }, $setOnInsert: { role: appRole, isBanned: false } }, { upsert: true, new: true, setDefaultsOnInsert: true } ); // Return combined info return res.json({ ...user.toObject(), organizationRole: member.role, organizationBanned: member.isBanned }); } res.json(user); } catch (error) { console.error('Error syncing user:', error); // Retornar 200 mesmo com erro para não travar o frontend se for algo não crítico, // mas aqui é crítico. Vamos logar melhor. res.status(500).json({ error: 'Erro ao sincronizar usuário: ' + (error instanceof Error ? error.message : String(error)) }); } }; /** * Get current user data with organization context */ export const getCurrentUser = async (req: AuthRequest, res: Response) => { try { if (!req.appUser) { return res.status(404).json({ error: 'Usuário não encontrado.' }); } const organizationId = req.headers['x-organization-id'] as string; if (organizationId) { const member = await OrganizationMember.findOne({ clerkUserId: req.appUser.clerkId, organizationId }); if (member) { return res.json({ ...req.appUser.toObject(), role: member.role, isBanned: member.isBanned, organizationId }); } } res.json(req.appUser); } catch (error) { console.error('Error getting current user:', error); res.status(500).json({ error: 'Erro ao buscar usuário.' }); } }; /** * Get all users for the current organization (admin only) */ export const getAllUsers = async (req: Request, res: Response) => { try { const organizationId = req.headers['x-organization-id'] as string; console.log('getAllUsers called with organizationId:', organizationId); if (!organizationId) { return res.status(400).json({ error: 'Organização não selecionada.' }); } const members = await OrganizationMember.find({ organizationId }).sort({ createdAt: -1 }); console.log(`Found ${members.length} members for org ${organizationId}:`, members.map(m => ({ name: m.name, email: m.email, clerkId: m.clerkUserId }))); res.json(members); } catch (error) { console.error('Error getting users:', error); res.status(500).json({ error: 'Erro ao buscar usuários.' }); } }; /** * Update user role within organization (admin only) */ export const updateUserRole = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const { role } = req.body; const organizationId = req.headers['x-organization-id'] as string; if (!organizationId) { return res.status(400).json({ error: 'Organização não selecionada.' }); } if (!['guest', 'user', 'admin'].includes(role)) { return res.status(400).json({ error: 'Role inválido. Use: guest, user ou admin.' }); } const member = await OrganizationMember.findById(id); if (!member || member.organizationId !== organizationId) { return res.status(404).json({ error: 'Usuário não encontrado nesta organização.' }); } // Prevent removing the last admin if (member.role === 'admin' && role !== 'admin') { const adminCount = await OrganizationMember.countDocuments({ organizationId, role: 'admin' }); if (adminCount <= 1) { return res.status(400).json({ error: 'Não é possível remover o último administrador.' }); } } member.role = role as OrgRole; await member.save(); res.json(member); } catch (error) { console.error('Error toggling ban:', error); res.status(500).json({ error: 'Erro ao alterar status de banimento.' }); } }; /** * Ban or unban user within organization (admin only) */ export const toggleBanUser = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const { isBanned } = req.body; const organizationId = req.headers['x-organization-id'] as string; if (!organizationId) { return res.status(400).json({ error: 'Organização não selecionada.' }); } const member = await OrganizationMember.findById(id); if (!member || member.organizationId !== organizationId) { return res.status(404).json({ error: 'Usuário não encontrado nesta organização.' }); } // Prevent banning yourself if (req.appUser && member.clerkUserId === req.appUser.clerkId) { return res.status(400).json({ error: 'Você não pode banir a si mesmo.' }); } // Prevent banning another admin if (member.role === 'admin') { return res.status(400).json({ error: 'Não é possível banir um administrador.' }); } member.isBanned = isBanned; await member.save(); res.json(member); } catch (error) { console.error('Error toggling ban:', error); res.status(500).json({ error: 'Erro ao alterar status de banimento.' }); } }; /** * Update current user's lastSeenAt timestamp */ export const heartbeat = async (req: AuthRequest, res: Response) => { try { if (!req.appUser) { return res.status(401).json({ error: 'Não autenticado.' }); } // Update User model await User.findByIdAndUpdate(req.appUser._id, { lastSeenAt: new Date() }); // Also update Organization Member for tighter query // But for now User model is enough if we join correctly, or just use User model for presence. // Actually, since we want to show users per organization, we should filter by Org. // Our 'User.ts' has organizationId, but it might be just the 'default' one. // Let's rely on OrganizationMember for the list, but we need to update lastSeenAt there too? // Strategy: Update User (global), and when querying active users, join or filter. // Better: Update OrganizationMember too if we want org-specific presence? // Simpler: Just update User. When fetching active users, we fetch OrganizationMembers and populate User details, filtering by User.lastSeenAt. res.status(200).send(); } catch (error) { // Silent fail for heartbeat console.error('Heartbeat error:', error); res.status(500).send(); } }; /** * Get active users in the same organization (seen in last 2 mins) */ export const getActiveUsers = async (req: AuthRequest, res: Response) => { try { const organizationId = req.headers['x-organization-id'] as string; const currentUserId = req.appUser?._id; if (!organizationId) { return res.status(400).json([]); } // Find members of this org const members = await OrganizationMember.find({ organizationId }); // Get their Clerk IDs const clerkIds = members.map(m => m.clerkUserId); // Find Users who were seen recently (2 minutes) const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000); const activeUsers = await User.find({ clerkId: { $in: clerkIds }, lastSeenAt: { $gte: twoMinutesAgo }, _id: { $ne: currentUserId } // Optional: exclude self }).select('name email lastSeenAt clerkId'); // Only needed fields res.json(activeUsers); } catch (error) { console.error('Error getting active users:', error); res.status(500).json([]); } }; // Delete organization member export const deleteUser = async (req: Request, res: Response) => { try { const { id } = req.params; const organizationId = req.headers['x-organization-id'] as string; if (!organizationId) { return res.status(400).json({ error: 'Organização não selecionada.' }); } console.log(`Deleting member ${id} from organization ${organizationId}`); // Delete from OrganizationMember collection const result = await OrganizationMember.findByIdAndDelete(id); if (!result) { return res.status(404).json({ error: 'Membro não encontrado.' }); } console.log(`Member ${result.name} deleted successfully`); res.json({ message: 'Membro removido com sucesso.', deletedMember: result }); } catch (error) { console.error('Error deleting user:', error); res.status(500).json({ error: 'Erro ao remover membro.' }); } };