COMMIT 16MAR

This commit is contained in:
2026-03-17 07:34:09 -03:00
parent e88d145df7
commit c3563b9513
40 changed files with 903 additions and 2156 deletions

View File

@@ -1,8 +1,7 @@
import { Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import User, { IUser } from '../models/User.js';
import { IAppUser } from '../middleware/roleMiddleware.js';
import { query } from '../config/database.js';
import { v4 as uuidv4 } from 'uuid';
const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key_change_in_prod';
@@ -16,8 +15,8 @@ export const register = async (req: Request, res: Response): Promise<void> => {
return;
}
const existingUser = await User.findOne({ email });
if (existingUser) {
const existingRes = await query('SELECT id FROM gpi.users WHERE email = $1', [email]);
if (existingRes.rows.length > 0) {
res.status(400).json({ error: 'Email já cadastrado' });
return;
}
@@ -25,22 +24,26 @@ export const register = async (req: Request, res: Response): Promise<void> => {
const salt = await bcrypt.genSalt(10);
const passwordHash = await bcrypt.hash(password, salt);
// Gere um externalId falso apenas para manter retrocompatibilidade no banco
// Gere um fakeAuthId para manter compatibilidade com sistemas que esperam um externalId
const fakeAuthId = `user_${uuidv4().replace(/-/g, '')}`;
const newUser = new User({
name,
email,
passwordHash,
externalId: fakeAuthId,
role: 'member',
isBanned: false
});
const insertRes = await query(
`INSERT INTO gpi.users (name, email, clerk_id, role, is_banned, updated_at)
VALUES ($1, $2, $3, 'user', false, NOW()) RETURNING *`,
[name, email, fakeAuthId]
);
const newUser = insertRes.rows[0];
await newUser.save();
// Se houver uma coluna de password_hash no banco (precisamos garantir que exista)
try {
await query('UPDATE gpi.users SET password_hash = $1 WHERE id = $2', [passwordHash, newUser.id]);
} catch (e) {
console.warn('Could not update password_hash, check if column exists', e);
}
const token = jwt.sign(
{ userId: newUser._id.toString(), externalId: newUser.externalId, role: newUser.role, organizationId: newUser.organizationId },
{ userId: newUser.id, externalId: newUser.clerk_id, role: newUser.role },
JWT_SECRET,
{ expiresIn: '7d' }
);
@@ -48,7 +51,7 @@ export const register = async (req: Request, res: Response): Promise<void> => {
res.status(201).json({
message: 'Usuário criado com sucesso',
token,
user: { id: newUser._id, name: newUser.name, email: newUser.email, role: newUser.role, externalId: newUser.externalId }
user: { id: newUser.id, name: newUser.name, email: newUser.email, role: newUser.role, externalId: newUser.clerk_id }
});
} catch (error) {
console.error('Register Error:', error);
@@ -65,25 +68,29 @@ export const login = async (req: Request, res: Response): Promise<void> => {
return;
}
const user = await User.findOne({ email });
const userRes = await query('SELECT * FROM gpi.users WHERE email = $1', [email]);
const user = userRes.rows[0];
if (!user) {
res.status(400).json({ error: 'Usuário não encontrado' });
return;
}
if (!user.passwordHash) {
res.status(400).json({ error: 'Usuário do sistema antigo. Por favor, solicite a redefinição de senha ou recrie sua conta se possível.' });
// Recuperar password_hash de algum lugar se não estiver no select principal (alguns schemas escondem)
// No nosso caso a query SELECT * deve trazer se existir.
if (!user.password_hash) {
res.status(400).json({ error: 'Usuário sem senha definida ou método de login externo.' });
return;
}
const isMatch = await bcrypt.compare(password, user.passwordHash);
const isMatch = await bcrypt.compare(password, user.password_hash);
if (!isMatch) {
res.status(400).json({ error: 'Credenciais inválidas' });
return;
}
const token = jwt.sign(
{ userId: user._id.toString(), externalId: user.externalId, role: user.role, organizationId: user.organizationId },
{ userId: user.id, externalId: user.clerk_id || user.logto_id, role: user.role },
JWT_SECRET,
{ expiresIn: '7d' }
);
@@ -92,12 +99,11 @@ export const login = async (req: Request, res: Response): Promise<void> => {
message: 'Login realizado com sucesso',
token,
user: {
id: user._id,
id: user.id,
name: user.name,
email: user.email,
role: user.role,
externalId: user.externalId,
organizationId: user.organizationId
externalId: user.clerk_id || user.logto_id
}
});
} catch (error) {
@@ -108,20 +114,12 @@ export const login = async (req: Request, res: Response): Promise<void> => {
export const getMe = async (req: Request, res: Response): Promise<void> => {
try {
// O usuário é extraído pelo middleware extractUser e colocado em req.appUser
if (!req.appUser) {
res.status(401).json({ error: 'Não autorizado' });
return;
}
res.status(200).json({
id: req.appUser._id,
name: req.appUser.name,
email: req.appUser.email,
role: req.appUser.role,
externalId: req.appUser.externalId,
organizationId: req.appUser.organizationId
});
res.status(200).json(req.appUser);
} catch (error) {
console.error('GetMe Error:', error);
res.status(500).json({ error: 'Erro no servidor' });

View File

@@ -1,10 +1,5 @@
import { Request, Response } from 'express';
import GeometryType from '../models/GeometryType.js';
import { IAppUser } from '../middleware/roleMiddleware.js';
interface AuthRequest extends Request {
appUser?: IAppUser;
}
import { query } from '../config/database.js';
// Default geometry types to seed if none exist
const DEFAULT_TYPES = [
@@ -22,139 +17,110 @@ const DEFAULT_TYPES = [
{ name: 'Peças diversas (outras)', efficiencyLoss: 20 }
];
export const getAllnames = async (req: AuthRequest, res: Response) => {
export const getAllnames = async (req: Request, res: Response) => {
try {
const organizationId = req.appUser?.organizationId;
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
console.log(`[GeometryType] Fetching for org: ${organizationId}, globalAdmin: ${isGlobalAdmin}`);
if (!organizationId && !isGlobalAdmin) {
return res.status(400).json({ error: 'Organization ID missing' });
}
// Search for org-specific types OR orphan types (legacy)
const query = isGlobalAdmin
? {}
: { $or: [{ organizationId }, { organizationId: { $exists: false } }, { organizationId: null }] };
let sql = 'SELECT * FROM gpi.geometry_types';
const params: any[] = [];
let types = await GeometryType.find(query).sort({ name: 1 });
if (!isGlobalAdmin) {
sql += ' WHERE organization_id = $1';
params.push(organizationId);
}
// Auto-seed if empty AND we HAVE an organization (don't seed for global view)
if (types.length === 0 && organizationId) {
console.log(`[GeometryType] No types found. Seeding defaults...`);
try {
const seedData = DEFAULT_TYPES.map(t => ({ ...t, organizationId }));
types = await GeometryType.insertMany(seedData) as any;
console.log(`[GeometryType] Seeded ${types.length} types successfully.`);
} catch (seedError) {
console.error('[GeometryType] Seeding failed:', seedError);
return res.json([]);
sql += ' ORDER BY name ASC';
const result = await query(sql, params);
let types = result.rows;
// Auto-seed if empty for org
if (types.length === 0 && organizationId && !isGlobalAdmin) {
for (const t of DEFAULT_TYPES) {
await query(
'INSERT INTO gpi.geometry_types (organization_id, name, efficiency_loss) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING',
[organizationId, t.name, t.efficiencyLoss]
);
}
const reFetch = await query('SELECT * FROM gpi.geometry_types WHERE organization_id = $1 ORDER BY name ASC', [organizationId]);
types = reFetch.rows;
}
res.json(types);
} catch (error: unknown) {
console.error('[GeometryType] Error in getAllnames:', error);
const message = error instanceof Error ? error.message : 'Unknown error';
res.status(500).json({ error: message });
} catch (error: any) {
console.error('[GeometryType] Error:', error);
res.status(500).json({ error: error.message });
}
};
export const restoreDefaults = async (req: AuthRequest, res: Response) => {
export const restoreDefaults = async (req: Request, res: Response) => {
try {
const organizationId = req.appUser?.organizationId;
if (!organizationId) {
return res.status(400).json({ error: 'Organization ID missing' });
if (!organizationId) return res.status(400).json({ error: 'Organization ID missing' });
await query('DELETE FROM gpi.geometry_types WHERE organization_id = $1', [organizationId]);
for (const t of DEFAULT_TYPES) {
await query(
'INSERT INTO gpi.geometry_types (organization_id, name, efficiency_loss) VALUES ($1, $2, $3)',
[organizationId, t.name, t.efficiencyLoss]
);
}
// Delete all existing types for this org
await GeometryType.deleteMany({ organizationId });
// Insert defaults
const seedData = DEFAULT_TYPES.map(t => ({ ...t, organizationId }));
const types = await GeometryType.insertMany(seedData);
res.json(types);
} catch (error: unknown) {
console.error('[GeometryType] Error in restoreDefaults:', error);
const message = error instanceof Error ? error.message : 'Unknown error';
res.status(500).json({ error: message });
const result = await query('SELECT * FROM gpi.geometry_types WHERE organization_id = $1 ORDER BY name ASC', [organizationId]);
res.json(result.rows);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
};
export const createType = async (req: AuthRequest, res: Response) => {
export const createType = async (req: Request, res: Response) => {
try {
const organizationId = req.appUser?.organizationId;
const { name, efficiencyLoss } = req.body;
if (!name) {
return res.status(400).json({ error: 'Name is required' });
}
const newType = new GeometryType({
name,
efficiencyLoss: Number(efficiencyLoss) || 0,
organizationId
});
const saved = await newType.save();
res.status(201).json(saved);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
if (message.includes('E11000')) {
return res.status(409).json({ error: 'A geometry type with this name already exists' });
}
res.status(500).json({ error: message });
const result = await query(
'INSERT INTO gpi.geometry_types (organization_id, name, efficiency_loss) VALUES ($1, $2, $3) RETURNING *',
[organizationId, name, Number(efficiencyLoss) || 0]
);
res.status(201).json(result.rows[0]);
} catch (error: any) {
if (error.code === '23505') return res.status(409).json({ error: 'Already exists' });
res.status(500).json({ error: error.message });
}
};
export const updateType = async (req: AuthRequest, res: Response) => {
export const updateType = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const organizationId = req.appUser?.organizationId;
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
const { name, efficiencyLoss } = req.body;
const query = isGlobalAdmin
? { _id: id }
: { _id: id, organizationId };
const updated = await GeometryType.findOneAndUpdate(
query,
{ name, efficiencyLoss: Number(efficiencyLoss) },
{ new: true }
const result = await query(
'UPDATE gpi.geometry_types SET name = $1, efficiency_loss = $2, updated_at = NOW() WHERE id = $3 AND organization_id = $4 RETURNING *',
[name, Number(efficiencyLoss), id, organizationId]
);
if (!updated) {
return res.status(404).json({ error: 'Record not found' });
}
res.json(updated);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
res.status(500).json({ error: message });
if (result.rows.length === 0) return res.status(404).json({ error: 'Not found' });
res.json(result.rows[0]);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
};
export const deleteType = async (req: AuthRequest, res: Response) => {
export const deleteType = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const organizationId = req.appUser?.organizationId;
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
const query = isGlobalAdmin
? { _id: id }
: { _id: id, organizationId };
const deleted = await GeometryType.findOneAndDelete(query);
if (!deleted) {
return res.status(404).json({ error: 'Record not found' });
}
const result = await query('DELETE FROM gpi.geometry_types WHERE id = $1 AND organization_id = $2', [id, organizationId]);
if (result.rowCount === 0) return res.status(404).json({ error: 'Not found' });
res.status(204).send();
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
res.status(500).json({ error: message });
} catch (error: any) {
res.status(500).json({ error: error.message });
}
};

View File

@@ -1,6 +1,5 @@
import { Request, Response } from 'express';
import Message from '../models/Message.js';
import OrganizationMember from '../models/OrganizationMember.js';
import { query } from '../config/database.js';
// Send a message
export const sendMessage = async (req: Request, res: Response) => {
@@ -21,36 +20,27 @@ export const sendMessage = async (req: Request, res: Response) => {
return res.status(400).json({ error: 'Destinatário e mensagem são obrigatórios.' });
}
if (message.length > 255) {
return res.status(400).json({ error: 'Mensagem muito longa (máximo 255 caracteres).' });
}
// Check if there's already a pending (unread) message from this user to that user
const existingMessage = await Message.findOne({
organizationId,
fromUserId,
toUserId,
isRead: false,
});
const existingRes = await query(
'SELECT * FROM gpi.messages WHERE organization_id = $1 AND from_user_id = $2 AND to_user_id = $3 AND is_read = false',
[organizationId, fromUserId, toUserId]
);
if (existingMessage) {
// Update existing message instead of creating a new one
existingMessage.message = message;
existingMessage.updatedAt = new Date();
await existingMessage.save();
return res.json(existingMessage);
if (existingRes.rows.length > 0) {
const updatedRes = await query(
'UPDATE gpi.messages SET message = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
[message, existingRes.rows[0].id]
);
return res.json(updatedRes.rows[0]);
}
// Create new message
const newMessage = new Message({
organizationId,
fromUserId,
toUserId,
message,
});
const insertRes = await query(
`INSERT INTO gpi.messages (organization_id, from_user_id, to_user_id, message)
VALUES ($1, $2, $3, $4) RETURNING *`,
[organizationId, fromUserId, toUserId, message]
);
await newMessage.save();
res.status(201).json(newMessage);
res.status(201).json(insertRes.rows[0]);
} catch (error) {
console.error('Error sending message:', error);
res.status(500).json({ error: 'Erro ao enviar mensagem.' });
@@ -63,34 +53,25 @@ export const getUnreadMessages = async (req: Request, res: Response) => {
const toUserId = req.appUser?.externalId;
const organizationId = req.headers['x-organization-id'] as string;
if (!organizationId) {
return res.status(400).json({ error: 'Organização não selecionada.' });
if (!organizationId || !toUserId) {
return res.status(400).json({ error: 'Contexto incompleto.' });
}
if (!toUserId) {
return res.status(401).json({ error: 'Usuário não autenticado.' });
}
const sql = `
SELECT m.*, u.name as "fromUserName", u.email as "fromUserEmail"
FROM gpi.messages m
LEFT JOIN gpi.users u ON m.from_user_id = u.clerk_id OR m.from_user_id = u.logto_id
WHERE m.organization_id = $1 AND m.to_user_id = $2 AND m.is_read = false AND m.is_archived = false AND m.is_deleted_by_recipient = false
ORDER BY m.created_at DESC
`;
const result = await query(sql, [organizationId, toUserId]);
const messages = result.rows.map(m => ({
...m,
fromUser: { name: m.fromUserName, email: m.fromUserEmail }
}));
const messages = await Message.find({
organizationId,
toUserId,
isRead: false,
isArchived: false,
isDeletedByRecipient: false,
}).sort({ createdAt: -1 });
// Populate sender info
const messagesWithSender = await Promise.all(
messages.map(async (msg) => {
const sender = await OrganizationMember.findOne({ userId: msg.fromUserId });
return {
...msg.toObject(),
fromUser: sender ? { name: sender.name, email: sender.email } : null,
};
})
);
res.json(messagesWithSender);
res.json(messages);
} catch (error) {
console.error('Error getting unread messages:', error);
res.status(500).json({ error: 'Erro ao buscar mensagens.' });
@@ -104,29 +85,16 @@ export const markMessageAsRead = async (req: Request, res: Response) => {
const userId = req.appUser?.externalId;
const organizationId = req.headers['x-organization-id'] as string;
if (!organizationId) {
return res.status(400).json({ error: 'Organização não selecionada.' });
}
const result = await query(
'UPDATE gpi.messages SET is_read = true, read_at = NOW() WHERE id = $1 AND organization_id = $2 AND to_user_id = $3 RETURNING *',
[id, organizationId, userId]
);
if (!userId) {
return res.status(401).json({ error: 'Usuário não autenticado.' });
}
const message = await Message.findOne({
_id: id,
organizationId,
toUserId: userId,
});
if (!message) {
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Mensagem não encontrada.' });
}
message.isRead = true;
message.readAt = new Date();
await message.save();
res.json(message);
res.json(result.rows[0]);
} catch (error) {
console.error('Error marking message as read:', error);
res.status(500).json({ error: 'Erro ao marcar mensagem como lida.' });
@@ -139,32 +107,25 @@ export const getMyPendingMessages = async (req: Request, res: Response) => {
const fromUserId = req.appUser?.externalId;
const organizationId = req.headers['x-organization-id'] as string;
if (!organizationId) {
return res.status(400).json({ error: 'Organização não selecionada.' });
if (!organizationId || !fromUserId) {
return res.status(400).json({ error: 'Contexto incompleto.' });
}
if (!fromUserId) {
return res.status(401).json({ error: 'Usuário não autenticado.' });
}
const sql = `
SELECT m.*, u.name as "toUserName", u.email as "toUserEmail"
FROM gpi.messages m
LEFT JOIN gpi.users u ON m.to_user_id = u.clerk_id OR m.to_user_id = u.logto_id
WHERE m.organization_id = $1 AND m.from_user_id = $2 AND m.is_read = false
ORDER BY m.created_at DESC
`;
const result = await query(sql, [organizationId, fromUserId]);
const messages = result.rows.map(m => ({
...m,
toUser: { name: m.toUserName, email: m.toUserEmail }
}));
const messages = await Message.find({
organizationId,
fromUserId,
isRead: false,
}).sort({ createdAt: -1 });
// Populate recipient info
const messagesWithRecipient = await Promise.all(
messages.map(async (msg) => {
const recipient = await OrganizationMember.findOne({ userId: msg.toUserId });
return {
...msg.toObject(),
toUser: recipient ? { name: recipient.name, email: recipient.email } : null,
};
})
);
res.json(messagesWithRecipient);
res.json(messages);
} catch (error) {
console.error('Error getting pending messages:', error);
res.status(500).json({ error: 'Erro ao buscar mensagens pendentes.' });
@@ -178,26 +139,15 @@ export const deleteMessage = async (req: Request, res: Response) => {
const userId = req.appUser?.externalId;
const organizationId = req.headers['x-organization-id'] as string;
if (!organizationId) {
return res.status(400).json({ error: 'Organização não selecionada.' });
const result = await query(
'DELETE FROM gpi.messages WHERE id = $1 AND from_user_id = $2 AND organization_id = $3 AND is_read = false',
[id, userId, organizationId]
);
if (result.rowCount === 0) {
return res.status(404).json({ error: 'Mensagem não encontrada ou já lida.' });
}
if (!userId) {
return res.status(401).json({ error: 'Usuário não autenticado.' });
}
const message = await Message.findOne({
_id: id,
organizationId,
fromUserId: userId,
isRead: false, // Can only delete unread messages
});
if (!message) {
return res.status(404).json({ error: 'Mensagem não encontrada ou já foi lida.' });
}
await message.deleteOne();
res.json({ message: 'Mensagem deletada com sucesso.' });
} catch (error) {
console.error('Error deleting message:', error);
@@ -205,20 +155,21 @@ export const deleteMessage = async (req: Request, res: Response) => {
}
};
// Recipient deletes/archives a message
// Recipient archives a message
export const archiveMessage = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const userId = req.appUser?.externalId;
const organizationId = req.headers['x-organization-id'] as string;
const message = await Message.findOne({ _id: id, toUserId: userId, organizationId });
if (!message) return res.status(404).json({ error: 'Mensagem não encontrada.' });
const result = await query(
'UPDATE gpi.messages SET is_archived = true, is_read = true WHERE id = $1 AND to_user_id = $2 AND organization_id = $3 RETURNING *',
[id, userId, organizationId]
);
message.isArchived = true;
message.isRead = true; // Arquivar implica ler
await message.save();
res.json(message);
if (result.rows.length === 0) return res.status(404).json({ error: 'Mensagem não encontrada.' });
res.json(result.rows[0]);
} catch (error) {
console.error('Error archiving message:', error);
res.status(500).json({ error: 'Erro ao arquivar mensagem.' });
@@ -231,11 +182,13 @@ export const recipientDeleteMessage = async (req: Request, res: Response) => {
const userId = req.appUser?.externalId;
const organizationId = req.headers['x-organization-id'] as string;
const message = await Message.findOne({ _id: id, toUserId: userId, organizationId });
if (!message) return res.status(404).json({ error: 'Mensagem não encontrada.' });
const result = await query(
'UPDATE gpi.messages SET is_deleted_by_recipient = true WHERE id = $1 AND to_user_id = $2 AND organization_id = $3 RETURNING *',
[id, userId, organizationId]
);
if (result.rows.length === 0) return res.status(404).json({ error: 'Mensagem não encontrada.' });
message.isDeletedByRecipient = true;
await message.save();
res.json({ message: 'Mensagem excluída com sucesso.' });
} catch (error) {
console.error('Error deleting message:', error);

View File

@@ -1,23 +1,20 @@
import { Request, Response } from 'express';
import SystemSettings from '../models/SystemSettings.js';
import User from '../models/User.js';
import OrganizationMember from '../models/OrganizationMember.js';
import Organization from '../models/Organization.js';
import { query } from '../config/database.js';
import path from 'path';
import fs from 'fs';
import os from 'os';
export const getSettings = async (req: Request, res: Response) => {
try {
let settings = await SystemSettings.findOne({ settingsId: 'global' });
const resSettings = await query('SELECT * FROM gpi.system_settings WHERE settings_id = $1', ['global']);
let settings = resSettings.rows[0];
if (!settings) {
// Create default if not exists
settings = await SystemSettings.create({
settingsId: 'global',
appName: 'GPI',
appSubtitle: 'Gestão de Pintura Industrial'
});
const insertRes = await query(
'INSERT INTO gpi.system_settings (settings_id, app_name, app_subtitle) VALUES ($1, $2, $3) RETURNING *',
['global', 'GPI', 'Gestão de Pintura Industrial']
);
settings = insertRes.rows[0];
}
res.json(settings);
@@ -31,41 +28,34 @@ export const updateSettings = async (req: Request, res: Response) => {
try {
const { appName, appSubtitle, appLogoUrl } = req.body;
const settings = await SystemSettings.findOneAndUpdate(
{ settingsId: 'global' },
{
appName,
appSubtitle,
appLogoUrl,
updatedBy: req.appUser?.email
},
{ new: true, upsert: true } // Create if not exists
const result = await query(
`INSERT INTO gpi.system_settings (settings_id, app_name, app_subtitle, app_logo_url, updated_by, updated_at)
VALUES ('global', $1, $2, $3, $4, NOW())
ON CONFLICT (settings_id) DO UPDATE SET
app_name = EXCLUDED.app_name,
app_subtitle = EXCLUDED.app_subtitle,
app_logo_url = EXCLUDED.app_logo_url,
updated_by = EXCLUDED.updated_by,
updated_at = NOW()
RETURNING *`,
[appName, appSubtitle, appLogoUrl, req.appUser?.email]
);
console.log(`⚙️ System Settings updated by ${req.appUser?.email}`);
res.json(settings);
res.json(result.rows[0]);
} catch (error) {
console.error('Error updating system settings:', error);
res.status(500).json({ error: 'Erro ao atualizar configurações do sistema' });
}
};
export const serveLogo = async (req: Request, res: Response) => {
try {
const { filename } = req.params as { filename: string };
// Check tmp dir first (Serverless/Netlify uploads)
const tmpPath = path.join(os.tmpdir(), 'uploads', filename);
// Check local dir (Development)
const localPath = path.join(process.cwd(), 'uploads', filename);
if (fs.existsSync(tmpPath)) {
res.sendFile(tmpPath);
} else if (fs.existsSync(localPath)) {
if (fs.existsSync(localPath)) {
res.sendFile(localPath);
} else {
console.error(`Logo file not found in tmp or local: ${filename}`);
res.status(404).json({ error: 'Imagem não encontrada' });
}
} catch (error) {
@@ -79,11 +69,7 @@ export const uploadLogo = async (req: Request, res: Response) => {
if (!req.file) {
return res.status(400).json({ error: 'Nenhum arquivo enviado.' });
}
// Return the API URL instead of static path
// This ensures requests go through /api proxy and we control serving
const fileUrl = `/api/system-settings/logo-image/${req.file.filename}`;
res.json({ url: fileUrl });
} catch (error) {
console.error('Error uploading logo:', error);
@@ -91,11 +77,10 @@ export const uploadLogo = async (req: Request, res: Response) => {
}
};
// Global Admin Functions
export const getGlobalUsers = async (req: Request, res: Response) => {
try {
const users = await User.find({}).sort({ createdAt: -1 });
res.json(users);
const resUsers = await query('SELECT * FROM gpi.users ORDER BY created_at DESC');
res.json(resUsers.rows);
} catch (error) {
console.error('Error getting global users:', error);
res.status(500).json({ error: 'Erro ao buscar usuários globais.' });
@@ -104,51 +89,20 @@ export const getGlobalUsers = async (req: Request, res: Response) => {
export const getGlobalOrganizations = async (req: Request, res: Response) => {
try {
// Aggregate members to group by org and get full member lists
const organizations = await OrganizationMember.aggregate([
{
$group: {
_id: '$organizationId',
members: {
$push: {
name: '$name',
email: '$email',
role: '$role',
userId: '$userId',
isBanned: '$isBanned'
}
},
lastActive: { $max: '$updatedAt' }
}
},
{
$lookup: {
from: 'organizations', // Ensure this matches the collection name of Organization model
localField: '_id',
foreignField: 'externalId',
as: 'orgDetails'
}
},
{
$unwind: {
path: '$orgDetails',
preserveNullAndEmptyArrays: true
}
},
{
$project: {
_id: 1,
lastActive: 1,
members: 1,
memberCount: { $size: '$members' },
isBanned: { $ifNull: ['$orgDetails.isBanned', false] },
name: { $ifNull: ['$orgDetails.name', ''] }
}
},
{ $sort: { memberCount: -1 } }
]);
res.json(organizations);
const sql = `
SELECT
o.id as _id,
o.name,
o.is_banned as "isBanned",
COUNT(uo.user_id) as "memberCount",
MAX(uo.updated_at) as "lastActive"
FROM gpi.organizations o
LEFT JOIN gpi.user_organizations uo ON o.id = uo.organization_id
GROUP BY o.id, o.name, o.is_banned
ORDER BY "memberCount" DESC
`;
const resOrgs = await query(sql);
res.json(resOrgs.rows);
} catch (error) {
console.error('Error getting global organizations:', error);
res.status(500).json({ error: 'Erro ao buscar organizações globais.' });
@@ -163,15 +117,12 @@ export const toggleOrganizationBan = async (req: Request, res: Response) => {
return res.status(400).json({ error: 'ID da organização é obrigatório.' });
}
// Upsert the Organization record
const org = await Organization.findOneAndUpdate(
{ externalId: organizationId },
{ isBanned: isBanned },
{ new: true, upsert: true }
const resOrg = await query(
'UPDATE gpi.organizations SET is_banned = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
[isBanned, organizationId]
);
console.log(`Organization ${organizationId} ban status set to ${isBanned} by ${req.appUser?.email}`);
res.json(org);
res.json(resOrg.rows[0]);
} catch (error) {
console.error('Error toggling organization ban:', error);
res.status(500).json({ error: 'Erro ao atualizar status da organização.' });