COMMIT 16MAR
This commit is contained in:
@@ -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' });
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.' });
|
||||
|
||||
Reference in New Issue
Block a user