Upload source code

This commit is contained in:
2026-03-12 19:36:34 +00:00
parent 783b6cb7e8
commit c7fb0c8561
158 changed files with 22553 additions and 0 deletions

View File

@@ -0,0 +1,319 @@
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.' });
}
};