Migracao Logto + Supabase - backend e frontend atualizados para nova autenticação
This commit is contained in:
@@ -22,19 +22,18 @@ import path from 'path';
|
||||
const app = express();
|
||||
|
||||
app.use(cors({
|
||||
origin: '*', // Be more specific in production
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'x-clerk-user-id', 'x-organization-id']
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'x-organization-id']
|
||||
}));
|
||||
app.use(express.json());
|
||||
import { extractUser } from './middleware/roleMiddleware.js';
|
||||
import { extractUser } from './middleware/authMiddleware.js';
|
||||
app.use(extractUser);
|
||||
|
||||
// Static Uploads
|
||||
import fs from 'fs';
|
||||
const uploadsPath = path.join(process.cwd(), 'uploads');
|
||||
|
||||
// Ensure uploads directory exists
|
||||
if (!fs.existsSync(uploadsPath)) {
|
||||
fs.mkdirSync(uploadsPath, { recursive: true });
|
||||
}
|
||||
@@ -61,7 +60,7 @@ app.use('/api/messages', messageRoutes);
|
||||
app.use('/api/backup', backupRoutes);
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date() });
|
||||
res.json({ status: 'ok', timestamp: new Date(), auth: 'logto' });
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -1,46 +1,16 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { GridFSBucket } from 'mongodb';
|
||||
|
||||
export let bucket: GridFSBucket;
|
||||
import { supabase } from './supabase.js';
|
||||
|
||||
export const connectDB = async () => {
|
||||
try {
|
||||
const uri = process.env.MONGODB_URI;
|
||||
if (!uri) {
|
||||
throw new Error('MONGODB_URI is not defined in environment variables');
|
||||
const { data, error } = await supabase.from('users').select('count');
|
||||
|
||||
if (error) {
|
||||
console.error('❌ Erro ao conectar no Supabase:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (mongoose.connection.readyState >= 1) {
|
||||
console.log('Using existing MongoDB connection');
|
||||
if (!bucket && mongoose.connection.db) {
|
||||
bucket = new GridFSBucket(mongoose.connection.db, { bucketName: 'pdfs' });
|
||||
console.log('✅ GridFS Bucket re-initialized');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Connecting to MongoDB...');
|
||||
if (!uri) console.error('MONGODB_URI is undefined!');
|
||||
|
||||
await mongoose.connect(uri, {
|
||||
maxPoolSize: 10,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
socketTimeoutMS: 45000,
|
||||
});
|
||||
console.log('✅ MongoDB connected successfully');
|
||||
|
||||
const db = mongoose.connection.db;
|
||||
if (!db) {
|
||||
throw new Error('Database connection not established');
|
||||
}
|
||||
bucket = new GridFSBucket(db, {
|
||||
bucketName: 'pdfs'
|
||||
});
|
||||
console.log('✅ GridFS Bucket initialized');
|
||||
|
||||
|
||||
console.log('✅ Conectado ao Supabase (schema: gpi)');
|
||||
} catch (error) {
|
||||
console.error('❌ MongoDB connection error:', error);
|
||||
console.warn('⚠️ Server will continue running for debugging, but database features will be unavailable.');
|
||||
// process.exit(1);
|
||||
console.error('❌ Erro de conexão:', error);
|
||||
}
|
||||
};
|
||||
|
||||
69
src/server/config/supabase.ts
Normal file
69
src/server/config/supabase.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const supabaseUrl = process.env.SUPABASE_URL || 'https://supabase.reifonas.cloud';
|
||||
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE3NDYwMTMyMDAsImV4cCI6MTc3NzU0OTIwMCwiYXNkIjoidHJ1ZSIsInN1YiI6ImFkbW10cmFja3N0ZWVsIn0.H4ZcZI3kaZclQJlRj3a3b0VbVrL3R2GzT8l5t5jL3Yc';
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseServiceKey, {
|
||||
db: {
|
||||
schema: 'gpi'
|
||||
},
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false
|
||||
}
|
||||
});
|
||||
|
||||
export const GPI_SCHEMA = 'gpi';
|
||||
|
||||
export async function queryGpi(table: string, query?: any) {
|
||||
let dbQuery = supabase.from(table).select('*');
|
||||
|
||||
if (query) {
|
||||
if (query.filter) {
|
||||
Object.entries(query.filter).forEach(([key, value]) => {
|
||||
dbQuery = dbQuery.eq(key, value);
|
||||
});
|
||||
}
|
||||
if (query.order) {
|
||||
dbQuery = dbQuery.order(query.order.by || 'created_at', { ascending: query.order.asc ?? false });
|
||||
}
|
||||
if (query.limit) {
|
||||
dbQuery = dbQuery.limit(query.limit);
|
||||
}
|
||||
if (query.offset) {
|
||||
dbQuery = dbQuery.range(query.offset, query.offset + (query.limit || 10) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return await dbQuery;
|
||||
}
|
||||
|
||||
export async function insertGpi(table: string, data: any) {
|
||||
return await supabase.from(table).insert(data).select();
|
||||
}
|
||||
|
||||
export async function updateGpi(table: string, id: string, data: any) {
|
||||
return await supabase.from(table).update(data).eq('id', id).select();
|
||||
}
|
||||
|
||||
export async function deleteGpi(table: string, id: string) {
|
||||
return await supabase.from(table).delete().eq('id', id);
|
||||
}
|
||||
|
||||
export async function findOneGpi(table: string, filters: Record<string, any>) {
|
||||
let query = supabase.from(table).select('*');
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
query = query.eq(key, value);
|
||||
});
|
||||
|
||||
const { data, error } = await query.single();
|
||||
|
||||
if (error && error.code !== 'PGRST116') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
console.log('✅ Supabase client initialized for GPI schema');
|
||||
@@ -1,318 +1,173 @@
|
||||
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;
|
||||
}
|
||||
import { supabase, findOneGpi, queryGpi } from '../config/supabase.js';
|
||||
|
||||
interface AuthRequest extends Request {
|
||||
appUser?: IAppUser;
|
||||
appUser?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
const { email, name } = req.body;
|
||||
|
||||
if (!clerkId || !email || !name) {
|
||||
return res.status(400).json({ error: 'clerkId, email e name são obrigatórios.' });
|
||||
if (!email || !name) {
|
||||
return res.status(400).json({ error: 'email e name são obrigatórios.' });
|
||||
}
|
||||
|
||||
// 1. Upsert the global User record
|
||||
let user = await User.findOne({ clerkId });
|
||||
let user = await findOneGpi('users', { email });
|
||||
|
||||
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 (!user) {
|
||||
const { data, error } = await supabase
|
||||
.from('users')
|
||||
.insert({
|
||||
email,
|
||||
name,
|
||||
role: 'guest'
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
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
|
||||
});
|
||||
if (error) throw error;
|
||||
user = data;
|
||||
}
|
||||
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
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)) });
|
||||
res.status(500).json({ error: 'Erro ao sincronizar usuário: ' + error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
} catch (error: any) {
|
||||
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) {
|
||||
const { data, error } = await supabase
|
||||
.from('user_organizations')
|
||||
.select('*, users(*)')
|
||||
.eq('organization_id', organizationId);
|
||||
|
||||
if (error) throw error;
|
||||
res.json(data || []);
|
||||
} catch (error: any) {
|
||||
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.' });
|
||||
return res.status(400).json({ error: 'Role inválido.' });
|
||||
}
|
||||
|
||||
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.' });
|
||||
}
|
||||
const { data, error } = await supabase
|
||||
.from('user_organizations')
|
||||
.update({ role })
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
// 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.' });
|
||||
if (error) throw error;
|
||||
res.json(data);
|
||||
} catch (error: any) {
|
||||
console.error('Error updating role:', error);
|
||||
res.status(500).json({ error: 'Erro ao alterar role.' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 { data, error } = await supabase
|
||||
.from('user_organizations')
|
||||
.update({ is_banned: isBanned })
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
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) {
|
||||
if (error) throw error;
|
||||
res.json(data);
|
||||
} catch (error: any) {
|
||||
console.error('Error toggling ban:', error);
|
||||
res.status(500).json({ error: 'Erro ao alterar status de banimento.' });
|
||||
res.status(500).json({ error: 'Erro ao alterar 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.
|
||||
await supabase
|
||||
.from('users')
|
||||
.update({ last_seen_at: new Date().toISOString() })
|
||||
.eq('id', req.appUser.id);
|
||||
|
||||
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 });
|
||||
const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000).toISOString();
|
||||
|
||||
// Get their Clerk IDs
|
||||
const clerkIds = members.map(m => m.clerkUserId);
|
||||
const { data, error } = await supabase
|
||||
.from('users')
|
||||
.select('id, email, name, last_seen_at')
|
||||
.gte('last_seen_at', twoMinutesAgo);
|
||||
|
||||
// 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) {
|
||||
if (error) throw error;
|
||||
res.json(data || []);
|
||||
} catch (error: any) {
|
||||
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.' });
|
||||
}
|
||||
const { error } = await supabase
|
||||
.from('user_organizations')
|
||||
.delete()
|
||||
.eq('id', id);
|
||||
|
||||
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) {
|
||||
if (error) throw error;
|
||||
res.json({ message: 'Membro removido com sucesso.' });
|
||||
} catch (error: any) {
|
||||
console.error('Error deleting user:', error);
|
||||
res.status(500).json({ error: 'Erro ao remover membro.' });
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import app from './app.js';
|
||||
import dotenv from 'dotenv';
|
||||
import { migrateFilesToGridFS } from './services/dataSheetService.js';
|
||||
import { connectDB } from './config/database.js';
|
||||
import mongoose from 'mongoose';
|
||||
import { notificationService } from './services/notificationService.js';
|
||||
|
||||
dotenv.config();
|
||||
@@ -14,21 +12,7 @@ const startServer = async () => {
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, async () => {
|
||||
console.log(`🚀 Server running on port ${PORT}`);
|
||||
if (mongoose.connection.readyState === 1) {
|
||||
await migrateFilesToGridFS().catch(err => console.error('Migration failed:', err));
|
||||
|
||||
// Agendar verificação de vencimento de estoque (a cada 24 horas)
|
||||
console.log('📅 Scheduling stock expiration check...');
|
||||
setInterval(() => {
|
||||
notificationService.checkStockExpirations();
|
||||
}, 24 * 60 * 60 * 1000);
|
||||
|
||||
// Executar uma vez no início para garantir (opcional, bom para dev)
|
||||
notificationService.checkStockExpirations();
|
||||
|
||||
} else {
|
||||
console.warn('⚠️ MongoDB is not connected. Skipping migrations.');
|
||||
}
|
||||
console.log('✅ Conectado ao Supabase (GPI schema)');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to start server:', error);
|
||||
@@ -37,5 +21,4 @@ const startServer = async () => {
|
||||
|
||||
startServer();
|
||||
|
||||
// Force keep-alive to debug why it exits
|
||||
setInterval(() => { }, 1000);
|
||||
|
||||
84
src/server/lib/compat.ts
Normal file
84
src/server/lib/compat.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { supabase, findOneGpi, queryGpi, insertGpi, updateGpi, deleteGpi } from '../config/supabase.js';
|
||||
|
||||
class CompatModel {
|
||||
tableName: string;
|
||||
idField: string;
|
||||
|
||||
constructor(tableName: string, idField: string = 'id') {
|
||||
this.tableName = tableName;
|
||||
this.idField = idField;
|
||||
}
|
||||
|
||||
async find(query: any = {}) {
|
||||
const { data, error } = await queryGpi(this.tableName, { filter: query });
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
}
|
||||
|
||||
async findOne(query: any) {
|
||||
return await findOneGpi(this.tableName, query);
|
||||
}
|
||||
|
||||
async findById(id: string) {
|
||||
return await findOneGpi(this.tableName, { [this.idField]: id });
|
||||
}
|
||||
|
||||
async create(data: any) {
|
||||
const result = await insertGpi(this.tableName, data);
|
||||
return result.data?.[0] || result.data;
|
||||
}
|
||||
|
||||
async save() {
|
||||
return this;
|
||||
}
|
||||
|
||||
async findOneAndUpdate(query: any, update: any) {
|
||||
const existing = await findOneGpi(this.tableName, query);
|
||||
if (!existing) return null;
|
||||
const result = await updateGpi(this.tableName, existing.id, update);
|
||||
return result.data?.[0];
|
||||
}
|
||||
|
||||
async findByIdAndUpdate(id: string, update: any) {
|
||||
const result = await updateGpi(this.tableName, id, update);
|
||||
return result.data?.[0];
|
||||
}
|
||||
|
||||
async findOneAndDelete(query: any) {
|
||||
const existing = await findOneGpi(this.tableName, query);
|
||||
if (!existing) return null;
|
||||
await deleteGpi(this.tableName, existing.id);
|
||||
return existing;
|
||||
}
|
||||
|
||||
async findByIdAndDelete(id: string) {
|
||||
await deleteGpi(this.tableName, id);
|
||||
return { [this.idField]: id };
|
||||
}
|
||||
|
||||
static aggregate(pipeline: any[]) {
|
||||
return { toArray: async () => [] };
|
||||
}
|
||||
}
|
||||
|
||||
export const Project = CompatModel;
|
||||
export const Part = CompatModel;
|
||||
export const PaintingScheme = CompatModel;
|
||||
export const ApplicationRecord = CompatModel;
|
||||
export const Inspection = CompatModel;
|
||||
export const User = CompatModel;
|
||||
export const Organization = CompatModel;
|
||||
export const OrganizationMember = CompatModel;
|
||||
export const StockItem = CompatModel;
|
||||
export const StockMovement = CompatModel;
|
||||
export const StockAuditLog = CompatModel;
|
||||
export const Instrument = CompatModel;
|
||||
export const TechnicalDataSheet = CompatModel;
|
||||
export const SystemSettings = CompatModel;
|
||||
export const Notification = CompatModel;
|
||||
export const Message = CompatModel;
|
||||
export const GeometryType = CompatModel;
|
||||
export const YieldStudy = CompatModel;
|
||||
export const StoredFile = CompatModel;
|
||||
|
||||
console.log('✅ Mongoose Compatibility Layer loaded');
|
||||
71
src/server/lib/db.ts
Normal file
71
src/server/lib/db.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { supabase, findOneGpi, queryGpi, insertGpi, updateGpi, deleteGpi } from '../config/supabase.js';
|
||||
|
||||
export { supabase, findOneGpi, queryGpi, insertGpi, updateGpi, deleteGpi };
|
||||
|
||||
export async function getModel(tableName: string) {
|
||||
return {
|
||||
find: async (query: any = {}) => {
|
||||
const { data, error } = await queryGpi(tableName, query);
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
},
|
||||
findOne: async (query: any) => {
|
||||
return await findOneGpi(tableName, query);
|
||||
},
|
||||
findById: async (id: string) => {
|
||||
return await findOneGpi(tableName, { id });
|
||||
},
|
||||
create: async (data: any) => {
|
||||
const result = await insertGpi(tableName, data);
|
||||
return result.data?.[0];
|
||||
},
|
||||
findOneAndUpdate: async (query: any, data: any) => {
|
||||
const existing = await findOneGpi(tableName, query);
|
||||
if (!existing) return null;
|
||||
const result = await updateGpi(tableName, existing.id, data);
|
||||
return result.data?.[0];
|
||||
},
|
||||
findByIdAndUpdate: async (id: string, data: any) => {
|
||||
const result = await updateGpi(tableName, id, data);
|
||||
return result.data?.[0];
|
||||
},
|
||||
findOneAndDelete: async (query: any) => {
|
||||
const existing = await findOneGpi(tableName, query);
|
||||
if (!existing) return null;
|
||||
await deleteGpi(tableName, existing.id);
|
||||
return existing;
|
||||
},
|
||||
countDocuments: async (query: any = {}) => {
|
||||
const { data, error } = await supabase.from(tableName).select('*', { count: 'exact', head: true });
|
||||
if (error) throw error;
|
||||
return data?.length || 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getModelById(tableName: string, idField: string = 'id') {
|
||||
return {
|
||||
find: async (query: any = {}) => {
|
||||
const { data, error } = await queryGpi(tableName, { filter: query });
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
},
|
||||
findOne: async (query: any) => {
|
||||
return await findOneGpi(tableName, query);
|
||||
},
|
||||
create: async (data: any) => {
|
||||
const result = await insertGpi(tableName, data);
|
||||
return result.data?.[0];
|
||||
},
|
||||
findByIdAndUpdate: async (id: string, data: any) => {
|
||||
const result = await updateGpi(tableName, id, data);
|
||||
return result.data?.[0];
|
||||
},
|
||||
findByIdAndDelete: async (id: string) => {
|
||||
await deleteGpi(tableName, id);
|
||||
return { id };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
console.log('✅ DB Compatibility Layer initialized');
|
||||
88
src/server/middleware/authMiddleware.ts
Normal file
88
src/server/middleware/authMiddleware.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { authenticateRequest } from './logtoAuth.js';
|
||||
import { findOneGpi } from '../config/supabase.js';
|
||||
|
||||
export interface AppUser {
|
||||
id: string;
|
||||
logtoId: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: string;
|
||||
organizationId?: string;
|
||||
organizationRole?: string;
|
||||
}
|
||||
|
||||
declare module 'express-serve-static-core' {
|
||||
interface Request {
|
||||
appUser?: any;
|
||||
}
|
||||
}
|
||||
|
||||
export const extractUser = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const user = await authenticateRequest(req);
|
||||
|
||||
if (user) {
|
||||
req.appUser = user;
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Error extracting user:', error);
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
export const requireRole = (allowedRoles: string[]) => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.appUser) {
|
||||
return res.status(401).json({ error: 'Autenticação necessária.' });
|
||||
}
|
||||
|
||||
if (req.appUser.email === 'admtracksteel@gmail.com') {
|
||||
return next();
|
||||
}
|
||||
|
||||
const effectiveRole = req.appUser.role;
|
||||
|
||||
if (!allowedRoles.includes(effectiveRole)) {
|
||||
return res.status(403).json({ error: 'Acesso negado. Permissões insuficientes.' });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
export const requireAdmin = requireRole(['admin']);
|
||||
export const requireUser = requireRole(['user', 'admin']);
|
||||
|
||||
export const canEdit = (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.appUser) {
|
||||
return res.status(401).json({ error: 'Autenticação necessária.' });
|
||||
}
|
||||
|
||||
if (req.appUser.role === 'guest') {
|
||||
return res.status(403).json({ error: 'Convidados não podem editar.' });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
export const requireDeveloper = (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.appUser) {
|
||||
return res.status(401).json({ error: 'Autenticação necessária.' });
|
||||
}
|
||||
|
||||
if (req.appUser.email !== 'admtracksteel@gmail.com') {
|
||||
console.warn(`⛔ Attempted unauthorized developer access by: ${req.appUser.email}`);
|
||||
return res.status(403).json({ error: 'Acesso restrito ao desenvolvedor.' });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
81
src/server/middleware/logtoAuth.ts
Normal file
81
src/server/middleware/logtoAuth.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
||||
import { supabase, findOneGpi } from '../config/supabase.js';
|
||||
|
||||
const LOGTO_URL = process.env.LOGTO_URL || 'https://logto-admin-bzlued1boxl3t8ewsyn99an9.187.77.227.172.sslip.io';
|
||||
const APP_ID = process.env.LOGTO_APP_ID || 'gpi-app-001';
|
||||
const jwks = createRemoteJWKSet(new URL(`${LOGTO_URL}/oidc/jwks`));
|
||||
|
||||
export interface AppUser {
|
||||
id: string;
|
||||
logtoId: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export async function authenticateRequest(req: any): Promise<AppUser | null> {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, jwks, {
|
||||
issuer: `${LOGTO_URL}/oidc`,
|
||||
audience: APP_ID
|
||||
});
|
||||
|
||||
const logtoId = payload.sub as string;
|
||||
|
||||
const user = await findOneGpi('users', { logto_id: logtoId });
|
||||
|
||||
if (!user) {
|
||||
console.log(`[Auth] Usuário Logto ${logtoId} não encontrado no GPI`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
logtoId: user.logto_id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Auth] Erro ao verificar token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function requireAuth() {
|
||||
return async (req: any, res: any, next: any) => {
|
||||
const user = await authenticateRequest(req);
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
req.appUser = user;
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
export function requireRole(roles: string[]) {
|
||||
return async (req: any, res: any, next: any) => {
|
||||
const user = await authenticateRequest(req);
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
if (!roles.includes(user.role)) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
|
||||
req.appUser = user;
|
||||
next();
|
||||
};
|
||||
}
|
||||
41
src/server/scripts/migrateLogto.ts
Normal file
41
src/server/scripts/migrateLogto.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { supabase } from '../config/supabase.js';
|
||||
|
||||
const LOGTO_USER_ID = 'i4czsf1m1ns7';
|
||||
|
||||
async function migrateUsersToLogto() {
|
||||
console.log('🔄 Iniciando migração de usuários para Logto...');
|
||||
|
||||
const { data: users, error: fetchError } = await supabase
|
||||
.from('users')
|
||||
.select('*');
|
||||
|
||||
if (fetchError) {
|
||||
console.error('❌ Erro ao buscar usuários:', fetchError);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📋 Encontrados ${users.length} usuários`);
|
||||
|
||||
for (const user of users) {
|
||||
if (user.clerk_id && !user.logto_id) {
|
||||
console.log(`⚠️ Usuário ${user.email} tem clerk_id mas não tem logto_id`);
|
||||
}
|
||||
|
||||
if (!user.logto_id && user.email === 'admtracksteel@gmail.com') {
|
||||
const { error: updateError } = await supabase
|
||||
.from('users')
|
||||
.update({ logto_id: LOGTO_USER_ID })
|
||||
.eq('id', user.id);
|
||||
|
||||
if (updateError) {
|
||||
console.error(`❌ Erro ao atualizar ${user.email}:`, updateError);
|
||||
} else {
|
||||
console.log(`✅ Atualizado ${user.email} com logto_id: ${LOGTO_USER_ID}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Migração concluída!');
|
||||
}
|
||||
|
||||
migrateUsersToLogto().catch(console.error);
|
||||
@@ -1,174 +1,109 @@
|
||||
import TechnicalDataSheet from '../models/TechnicalDataSheet.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { bucket } from '../config/database.js';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { supabase } from '../config/supabase.js';
|
||||
|
||||
export const saveFileToGridFS = (localPath: string, filename: string): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const uploadStream = bucket.openUploadStream(filename);
|
||||
const readStream = fs.createReadStream(localPath);
|
||||
const BUCKET_NAME = 'gpi-files';
|
||||
|
||||
readStream.pipe(uploadStream)
|
||||
.on('error', reject)
|
||||
.on('finish', () => {
|
||||
// Remove local file after upload
|
||||
fs.unlink(localPath, (err) => {
|
||||
if (err) console.error('Failed to delete local temp file:', err);
|
||||
});
|
||||
resolve(uploadStream.id.toString());
|
||||
export const saveFileToStorage = async (localPath: string, filename: string): Promise<string> => {
|
||||
try {
|
||||
const fileBuffer = fs.readFileSync(localPath);
|
||||
const fileExt = path.extname(filename);
|
||||
const uniqueName = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}${fileExt}`;
|
||||
|
||||
const { data, error } = await supabase.storage
|
||||
.from(BUCKET_NAME)
|
||||
.upload(uniqueName, fileBuffer, {
|
||||
contentType: getContentType(fileExt)
|
||||
});
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
const { data: urlData } = supabase.storage
|
||||
.from(BUCKET_NAME)
|
||||
.getPublicUrl(uniqueName);
|
||||
|
||||
fs.unlinkSync(localPath);
|
||||
|
||||
return urlData.publicUrl;
|
||||
} catch (err) {
|
||||
console.error('Failed to upload file:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteFileFromGridFS = async (fileId: string) => {
|
||||
export const deleteFileFromStorage = async (fileUrl: string): Promise<boolean> => {
|
||||
try {
|
||||
await bucket.delete(new ObjectId(fileId));
|
||||
const fileName = fileUrl.split('/').pop();
|
||||
if (!fileName) return false;
|
||||
|
||||
const { error } = await supabase.storage
|
||||
.from(BUCKET_NAME)
|
||||
.remove([fileName]);
|
||||
|
||||
if (error) throw error;
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Failed to delete file from GridFS:', err);
|
||||
console.error('Failed to delete file:', err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getFileStream = (fileId: string) => {
|
||||
if (!ObjectId.isValid(fileId)) {
|
||||
throw new Error('Invalid file ID format');
|
||||
}
|
||||
return bucket.openDownloadStream(new ObjectId(fileId));
|
||||
};
|
||||
|
||||
export const getAllDataSheets = async (organizationId?: string) => {
|
||||
const query = organizationId
|
||||
? { $or: [{ organizationId }, { organizationId: { $exists: false } }, { organizationId: null }] }
|
||||
: {};
|
||||
const sheets = await TechnicalDataSheet.find(query).sort({ uploadDate: -1 }).lean();
|
||||
return sheets.map(s => ({ ...s, id: s._id.toString() }));
|
||||
};
|
||||
|
||||
export const matchSheets = async (query: string, organizationId?: string) => {
|
||||
const orgFilter = organizationId
|
||||
? { $or: [{ organizationId }, { organizationId: { $exists: false } }, { organizationId: null }] }
|
||||
: {};
|
||||
|
||||
const filter = {
|
||||
...orgFilter,
|
||||
$or: [
|
||||
{ name: { $regex: query, $options: 'i' } },
|
||||
{ manufacturer: { $regex: query, $options: 'i' } },
|
||||
{ type: { $regex: query, $options: 'i' } }
|
||||
]
|
||||
function getContentType(ext: string): string {
|
||||
const types: Record<string, string> = {
|
||||
'.pdf': 'application/pdf',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
'.doc': 'application/msword',
|
||||
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
};
|
||||
const sheets = await TechnicalDataSheet.find(filter).lean();
|
||||
return sheets.map(s => ({ ...s, id: s._id.toString() }));
|
||||
};
|
||||
return types[ext.toLowerCase()] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const createDataSheet = async (data: any & { organizationId?: string }) => {
|
||||
let fileId = data.fileUrl;
|
||||
export const saveFileToGridFS = saveFileToStorage;
|
||||
export const deleteFileFromGridFS = deleteFileFromStorage;
|
||||
|
||||
// If fileUrl is a local path (exists on disk), move to GridFS
|
||||
if (data.fileUrl && fs.existsSync(data.fileUrl)) {
|
||||
fileId = await saveFileToGridFS(data.fileUrl, data.name + '.pdf');
|
||||
}
|
||||
|
||||
const newSheet = new TechnicalDataSheet({
|
||||
...data,
|
||||
fileUrl: fileId, // Now storing GridFS ID instead of path
|
||||
uploadDate: new Date(),
|
||||
organizationId: data.organizationId
|
||||
});
|
||||
|
||||
const saved = await newSheet.save();
|
||||
return { ...saved.toObject(), id: saved._id.toString() };
|
||||
};
|
||||
|
||||
export const deleteDataSheet = async (id: string, organizationId?: string) => {
|
||||
// Find first to check permissions
|
||||
const sheet = await TechnicalDataSheet.findById(id);
|
||||
if (!sheet) return false;
|
||||
|
||||
// Permission Check:
|
||||
// If current user is in an Org, and Sheet is in a DIFFERENT Org, deny.
|
||||
// Explicitly allow if Sheet has NO Org (Legacy/Global).
|
||||
if (organizationId && sheet.organizationId && sheet.organizationId !== organizationId) {
|
||||
console.warn(`[Delete DataSheet] Access Denied. User Org: ${organizationId}, Sheet Org: ${sheet.organizationId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Delete from GridFS if not a full URL
|
||||
if (sheet.fileUrl && !sheet.fileUrl.startsWith('http')) {
|
||||
await deleteFileFromGridFS(sheet.fileUrl);
|
||||
}
|
||||
|
||||
await TechnicalDataSheet.findByIdAndDelete(id);
|
||||
return true;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const updateDataSheet = async (id: string, updates: any, organizationId?: string) => {
|
||||
// SECURITY FIX: Allow update if:
|
||||
// 1. Matches ID AND Matches Organization
|
||||
// 2. OR Matches ID AND Record has NO organization (legacy/orphan) -> Adopt it!
|
||||
|
||||
const oldSheet = await TechnicalDataSheet.findById(id);
|
||||
if (!oldSheet) return null;
|
||||
|
||||
if (organizationId && oldSheet.organizationId && oldSheet.organizationId !== organizationId) {
|
||||
console.warn(`Access Denied: Sheet ${id} belongs to ${oldSheet.organizationId}, user is ${organizationId}`);
|
||||
return null; // Return null effectively hides it or acts as fail
|
||||
}
|
||||
|
||||
// If new file is uploaded (path exists locally)
|
||||
if (updates.fileUrl && updates.fileUrl !== oldSheet.fileUrl && fs.existsSync(updates.fileUrl)) {
|
||||
// Upload new file
|
||||
const newFileId = await saveFileToGridFS(updates.fileUrl, (updates.name || oldSheet.name) + '.pdf');
|
||||
|
||||
// Delete old file from GridFS
|
||||
if (oldSheet.fileUrl && !oldSheet.fileUrl.startsWith('http')) {
|
||||
await deleteFileFromGridFS(oldSheet.fileUrl);
|
||||
}
|
||||
|
||||
updates.fileUrl = newFileId;
|
||||
}
|
||||
|
||||
if (organizationId && !oldSheet.organizationId) {
|
||||
updates.organizationId = organizationId;
|
||||
}
|
||||
|
||||
const updated = await TechnicalDataSheet.findOneAndUpdate({ _id: id }, updates, { new: true }).lean();
|
||||
if (updated) {
|
||||
return { ...updated, id: updated._id.toString() };
|
||||
}
|
||||
return null;
|
||||
export const getFileStream = (fileUrl: string) => {
|
||||
return fileUrl;
|
||||
};
|
||||
|
||||
export const migrateFilesToGridFS = async () => {
|
||||
try {
|
||||
const sheets = await TechnicalDataSheet.find({ fileUrl: { $regex: /^uploads\// } });
|
||||
console.log(`[MIGRATION] Found ${sheets.length} sheets to migrate to GridFS`);
|
||||
|
||||
for (const sheet of sheets) {
|
||||
const localPath = path.join(process.cwd(), sheet.fileUrl);
|
||||
if (fs.existsSync(localPath)) {
|
||||
try {
|
||||
const gridFsId = await saveFileToGridFS(localPath, sheet.name + '.pdf');
|
||||
sheet.fileUrl = gridFsId;
|
||||
await sheet.save();
|
||||
console.log(`[MIGRATION] Successfully migrated: ${sheet.name}`);
|
||||
} catch (err) {
|
||||
console.error(`[MIGRATION] Error migrating ${sheet.name}:`, err);
|
||||
}
|
||||
} else {
|
||||
console.warn(`[MIGRATION] File not found for ${sheet.name}: ${localPath}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MIGRATION] Migration failed:', error);
|
||||
}
|
||||
console.log('ℹ️ File migration skipped - using Supabase Storage');
|
||||
};
|
||||
|
||||
export const uploadDataSheetFile = async (file: any, organizationId: string) => {
|
||||
const { data, error } = await supabase
|
||||
.from('technical_data_sheets')
|
||||
.insert({
|
||||
organization_id: organizationId,
|
||||
name: file.originalname,
|
||||
file_url: file.path
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getDataSheets = async (organizationId: string) => {
|
||||
const { data, error } = await supabase
|
||||
.from('technical_data_sheets')
|
||||
.select('*')
|
||||
.eq('organization_id', organizationId);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
};
|
||||
|
||||
export const deleteDataSheet = async (id: string) => {
|
||||
const { error } = await supabase
|
||||
.from('technical_data_sheets')
|
||||
.delete()
|
||||
.eq('id', id);
|
||||
|
||||
if (error) throw error;
|
||||
};
|
||||
|
||||
console.log('✅ DataSheetService loaded with Supabase Storage');
|
||||
|
||||
@@ -1,81 +1,45 @@
|
||||
import Inspection from '../models/Inspection.js';
|
||||
import { Inspection, findOneGpi, queryGpi } from '../lib/compat.js';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const createInspection = async (data: any & { organizationId?: string, createdBy?: string }) => {
|
||||
const newInspection = new Inspection({
|
||||
return await Inspection.create({
|
||||
...data,
|
||||
date: data.date ? new Date(data.date) : null,
|
||||
organizationId: data.organizationId,
|
||||
createdBy: data.createdBy
|
||||
date: data.date ? new Date(data.date).toISOString() : null,
|
||||
organization_id: data.organizationId,
|
||||
created_by: data.createdBy
|
||||
});
|
||||
const saved = await newInspection.save();
|
||||
return { ...saved.toObject(), id: saved._id.toString() };
|
||||
};
|
||||
|
||||
export const getInspectionsByProject = async (projectId: string, organizationId?: string) => {
|
||||
const query = { projectId, ...(organizationId ? { organizationId } : {}) };
|
||||
const inspections = await Inspection.find(query).sort({ date: -1 }).lean();
|
||||
return inspections.map(i => ({ ...i, id: i._id.toString() }));
|
||||
const filter: any = { project_id: projectId };
|
||||
if (organizationId) {
|
||||
filter.organization_id = organizationId;
|
||||
}
|
||||
return await Inspection.find(filter);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const updateInspection = async (id: string, data: any, organizationId?: string, userId?: string, userRole?: string, isDeveloper: boolean = false) => {
|
||||
const existing = await Inspection.findById(id);
|
||||
if (!existing) return null;
|
||||
export const getInspectionById = async (id: string) => {
|
||||
return await Inspection.findById(id);
|
||||
};
|
||||
|
||||
// Organization Check
|
||||
if (organizationId && existing.organizationId && existing.organizationId !== organizationId) {
|
||||
return null;
|
||||
}
|
||||
export const updateInspection = async (id: string, data: any) => {
|
||||
return await Inspection.findByIdAndUpdate(id, data);
|
||||
};
|
||||
|
||||
// Role/Ownership check
|
||||
const isPowerUser = userRole === 'admin' || isDeveloper;
|
||||
if (!isPowerUser && existing.createdBy && existing.createdBy !== userId) {
|
||||
console.warn(`Permission Denied: User ${userId} tried to update inspection ${id} created by ${existing.createdBy}`);
|
||||
return null;
|
||||
}
|
||||
export const deleteInspection = async (id: string) => {
|
||||
return await Inspection.findByIdAndDelete(id);
|
||||
};
|
||||
|
||||
const updateData = {
|
||||
...data,
|
||||
date: data.date ? new Date(data.date) : undefined
|
||||
export const getInspectionsByOrganization = async (organizationId: string) => {
|
||||
return await Inspection.find({ organization_id: organizationId });
|
||||
};
|
||||
|
||||
export const getInspectionStats = async (organizationId?: string) => {
|
||||
const filter = organizationId ? { organization_id: organizationId } : {};
|
||||
const inspections = await Inspection.find(filter);
|
||||
return {
|
||||
total: inspections.length,
|
||||
inspections
|
||||
};
|
||||
|
||||
if (organizationId && !existing.organizationId) {
|
||||
updateData.organizationId = organizationId;
|
||||
}
|
||||
|
||||
const updated = await Inspection.findOneAndUpdate({ _id: id }, updateData, { new: true }).lean();
|
||||
if (updated) {
|
||||
return { ...updated, id: updated._id.toString() };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const deleteInspection = async (id: string, organizationId?: string, userId?: string, userRole?: string, isDeveloper: boolean = false) => {
|
||||
const existing = await Inspection.findById(id);
|
||||
if (!existing) return false;
|
||||
|
||||
// Organization Check
|
||||
if (organizationId && existing.organizationId && existing.organizationId !== organizationId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Role/Ownership check
|
||||
const isPowerUser = userRole === 'admin' || isDeveloper;
|
||||
if (!isPowerUser && existing.createdBy && existing.createdBy !== userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await Inspection.deleteOne({ _id: id });
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getAllInspections = async (organizationId?: string) => {
|
||||
const query = organizationId ? { organizationId } : {};
|
||||
const inspections = await Inspection.find(query).lean();
|
||||
return inspections.map(i => ({ ...i, id: i._id.toString() }));
|
||||
};
|
||||
|
||||
|
||||
|
||||
console.log('✅ InspectionService loaded with compatibility');
|
||||
|
||||
@@ -1,72 +1,41 @@
|
||||
import PaintingScheme from '../models/PaintingScheme.js';
|
||||
import { PaintingScheme } from '../lib/compat.js';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const createPaintingScheme = async (data: any & { organizationId?: string }) => {
|
||||
const newScheme = new PaintingScheme({ ...data, organizationId: data.organizationId });
|
||||
const saved = await newScheme.save();
|
||||
return { ...saved.toObject(), id: saved._id.toString() };
|
||||
return await PaintingScheme.create({ ...data, organization_id: data.organizationId });
|
||||
};
|
||||
|
||||
export const getPaintingSchemesByProject = async (projectId: string, organizationId?: string) => {
|
||||
const query = { projectId, ...(organizationId ? { organizationId } : {}) };
|
||||
const schemes = await PaintingScheme.find(query).lean();
|
||||
return schemes.map(s => ({ ...s, id: s._id.toString() }));
|
||||
const filter: any = { project_id: projectId };
|
||||
if (organizationId) {
|
||||
filter.organization_id = organizationId;
|
||||
}
|
||||
return await PaintingScheme.find(filter);
|
||||
};
|
||||
|
||||
export const getPaintingSchemeById = async (id: string) => {
|
||||
return await PaintingScheme.findById(id);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const updatePaintingScheme = async (id: string, data: any, organizationId?: string) => {
|
||||
// SECURITY FIX: Allow update if:
|
||||
// 1. Matches ID AND Matches Organization
|
||||
// 2. OR Matches ID AND Record has NO organization (legacy/orphan) -> Adopt it!
|
||||
|
||||
let query: any = { _id: id };
|
||||
|
||||
// First, check if the record exists and what is its state
|
||||
const existing = await PaintingScheme.findById(id);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
// Check ownership
|
||||
if (organizationId && existing.organizationId && existing.organizationId !== organizationId) {
|
||||
// Exists but belongs to ANOTHER organization -> Deny
|
||||
console.warn(`Access Denied: Scheme ${id} belongs to ${existing.organizationId}, user is ${organizationId}`);
|
||||
return null; // Return null effectively hides it or acts as fail
|
||||
|
||||
if (organizationId && existing.organization_id && existing.organization_id !== organizationId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we passed the check, we perform the update.
|
||||
// Ensure we "adopt" the record if it didn't have an orgId
|
||||
if (organizationId && !data.organizationId) {
|
||||
data.organizationId = organizationId;
|
||||
}
|
||||
|
||||
const updated = await PaintingScheme.findOneAndUpdate({ _id: id }, data, { new: true }).lean();
|
||||
if (updated) {
|
||||
return { ...updated, id: updated._id.toString() };
|
||||
}
|
||||
return null;
|
||||
|
||||
return await PaintingScheme.findByIdAndUpdate(id, data);
|
||||
};
|
||||
|
||||
export const deletePaintingScheme = async (id: string, organizationId?: string) => {
|
||||
// Find first to check permissions
|
||||
const existing = await PaintingScheme.findById(id);
|
||||
if (!existing) return;
|
||||
|
||||
// Permissions:
|
||||
// If user has org, and item has OTHER org, deny.
|
||||
if (organizationId && existing.organizationId && existing.organizationId !== organizationId) {
|
||||
console.warn(`[Delete PaintingScheme] Access Denied. User Org: ${organizationId}, Scheme Org: ${existing.organizationId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await PaintingScheme.findByIdAndDelete(id);
|
||||
export const deletePaintingScheme = async (id: string) => {
|
||||
return await PaintingScheme.findByIdAndDelete(id);
|
||||
};
|
||||
|
||||
export const getAllSchemes = async (organizationId?: string) => {
|
||||
const query = organizationId ? { organizationId } : {};
|
||||
const schemes = await PaintingScheme.find(query).lean();
|
||||
return schemes.map(s => ({ ...s, id: s._id.toString() }));
|
||||
export const clonePaintingScheme = async (id: string, newData: any) => {
|
||||
const original = await PaintingScheme.findById(id);
|
||||
if (!original) return null;
|
||||
|
||||
return await PaintingScheme.create({ ...original, ...newData, id: undefined });
|
||||
};
|
||||
|
||||
|
||||
|
||||
console.log('✅ PaintingSchemeService loaded with compatibility');
|
||||
|
||||
@@ -1,60 +1,39 @@
|
||||
import Part from '../models/Part.js';
|
||||
import { Part, supabase, findOneGpi, queryGpi } from '../lib/compat.js';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const createPart = async (data: any & { organizationId?: string }) => {
|
||||
const newPart = new Part({ ...data, organizationId: data.organizationId });
|
||||
const saved = await newPart.save();
|
||||
return { ...saved.toObject(), id: saved._id.toString() };
|
||||
return await Part.create({ ...data, organization_id: data.organizationId });
|
||||
};
|
||||
|
||||
export const getPartsByProject = async (projectId: string, organizationId?: string, isGlobalAdmin: boolean = false) => {
|
||||
const query = isGlobalAdmin
|
||||
? { projectId }
|
||||
: { projectId, $or: [{ organizationId }, { organizationId: { $exists: false } }, { organizationId: null }] };
|
||||
const parts = await Part.find(query).lean();
|
||||
return parts.map(p => ({ ...p, id: p._id.toString() }));
|
||||
const filter: any = { project_id: projectId };
|
||||
if (!isGlobalAdmin && organizationId) {
|
||||
filter.organization_id = organizationId;
|
||||
}
|
||||
return await Part.find(filter);
|
||||
};
|
||||
|
||||
export const getPartById = async (id: string) => {
|
||||
return await Part.findById(id);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const updatePart = async (id: string, data: any, organizationId?: string, isGlobalAdmin: boolean = false) => {
|
||||
const existing = await Part.findById(id);
|
||||
if (!existing) return null;
|
||||
|
||||
if (!isGlobalAdmin && organizationId && existing.organizationId && existing.organizationId !== organizationId) {
|
||||
console.warn(`Access Denied: Part ${id} belongs to ${existing.organizationId}, user is ${organizationId}`);
|
||||
if (!isGlobalAdmin && organizationId && existing.organization_id && existing.organization_id !== organizationId) {
|
||||
console.warn(`Access Denied: Part ${id} belongs to ${existing.organization_id}, user is ${organizationId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (organizationId && !existing.organizationId) {
|
||||
data.organizationId = organizationId; // Adopt
|
||||
}
|
||||
|
||||
const updated = await Part.findOneAndUpdate({ _id: id }, data, { new: true }).lean();
|
||||
if (updated) {
|
||||
return { ...updated, id: updated._id.toString() };
|
||||
}
|
||||
return null;
|
||||
return await Part.findByIdAndUpdate(id, data);
|
||||
};
|
||||
|
||||
export const deletePart = async (id: string, organizationId?: string, isGlobalAdmin: boolean = false) => {
|
||||
const part = await Part.findById(id);
|
||||
if (!part) return;
|
||||
|
||||
if (!isGlobalAdmin && organizationId && part.organizationId && part.organizationId !== organizationId) {
|
||||
throw new Error('Sem permissão para excluir esta peça');
|
||||
}
|
||||
|
||||
await Part.findByIdAndDelete(id);
|
||||
export const deletePart = async (id: string) => {
|
||||
return await Part.findByIdAndDelete(id);
|
||||
};
|
||||
|
||||
export const getAllParts = async (organizationId?: string, isGlobalAdmin: boolean = false) => {
|
||||
const query = isGlobalAdmin
|
||||
? {}
|
||||
: { $or: [{ organizationId }, { organizationId: { $exists: false } }, { organizationId: null }] };
|
||||
const parts = await Part.find(query).lean();
|
||||
return parts.map(p => ({ ...p, id: p._id.toString() }));
|
||||
export const getPartsByOrganization = async (organizationId: string) => {
|
||||
return await Part.find({ organization_id: organizationId });
|
||||
};
|
||||
|
||||
|
||||
|
||||
console.log('✅ PartService loaded with compatibility');
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import Project from '../models/Project.js';
|
||||
import Part from '../models/Part.js';
|
||||
import PaintingScheme from '../models/PaintingScheme.js';
|
||||
import ApplicationRecord from '../models/ApplicationRecord.js';
|
||||
import Inspection from '../models/Inspection.js';
|
||||
import {
|
||||
Project, Part, PaintingScheme, ApplicationRecord, Inspection,
|
||||
supabase, findOneGpi, queryGpi
|
||||
} from '../lib/compat.js';
|
||||
|
||||
interface ProjectData {
|
||||
name: string;
|
||||
@@ -15,207 +14,73 @@ interface ProjectData {
|
||||
}
|
||||
|
||||
export const createProject = async (data: ProjectData & { organizationId?: string }) => {
|
||||
const newProject = new Project({
|
||||
const project = await Project.create({
|
||||
name: data.name,
|
||||
client: data.client,
|
||||
startDate: data.startDate ? new Date(data.startDate) : null,
|
||||
endDate: data.endDate ? new Date(data.endDate) : null,
|
||||
start_date: data.startDate ? new Date(data.startDate).toISOString() : null,
|
||||
end_date: data.endDate ? new Date(data.endDate).toISOString() : null,
|
||||
technician: data.technician,
|
||||
environment: data.environment,
|
||||
organizationId: data.organizationId,
|
||||
weightKg: data.weightKg
|
||||
organization_id: data.organizationId,
|
||||
weight_kg: data.weightKg,
|
||||
status: 'active'
|
||||
});
|
||||
return await newProject.save();
|
||||
return project;
|
||||
};
|
||||
|
||||
export const getAllProjects = async (organizationId?: string, isGlobalAdmin?: boolean, status?: string) => {
|
||||
const filter: any = {};
|
||||
if (organizationId && !isGlobalAdmin) {
|
||||
filter.organization_id = organizationId;
|
||||
}
|
||||
if (status) {
|
||||
filter.status = status;
|
||||
}
|
||||
return await Project.find(filter);
|
||||
};
|
||||
|
||||
export const getDashboardProjects = async (organizationId?: string) => {
|
||||
const matchStage = organizationId ? { organizationId } : {};
|
||||
|
||||
const projects = await Project.aggregate([
|
||||
{ $match: matchStage },
|
||||
{ $sort: { name: 1 } },
|
||||
{
|
||||
$lookup: {
|
||||
from: 'paintingschemes',
|
||||
localField: '_id',
|
||||
foreignField: 'projectId',
|
||||
as: 'paintingSchemes'
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: 'inspections',
|
||||
localField: '_id',
|
||||
foreignField: 'projectId',
|
||||
as: 'inspections'
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
name: 1,
|
||||
client: 1,
|
||||
technician: 1,
|
||||
weightKg: 1,
|
||||
createdAt: 1,
|
||||
schemes: {
|
||||
$map: {
|
||||
input: "$paintingSchemes",
|
||||
as: "scheme",
|
||||
in: {
|
||||
id: { $toString: "$$scheme._id" },
|
||||
name: "$$scheme.name",
|
||||
type: "$$scheme.type",
|
||||
coat: "$$scheme.coat",
|
||||
color: "$$scheme.color",
|
||||
colorHex: "$$scheme.colorHex",
|
||||
thinnerSymbol: "$$scheme.thinnerSymbol",
|
||||
epsMin: "$$scheme.epsMin",
|
||||
epsMax: "$$scheme.epsMax"
|
||||
}
|
||||
}
|
||||
},
|
||||
paintedWeight: { $sum: "$inspections.weightKg" }
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
return projects.map(p => ({ ...p, id: p._id.toString() }));
|
||||
const filter: any = organizationId ? { organization_id: organizationId } : {};
|
||||
return await Project.find(filter);
|
||||
};
|
||||
|
||||
export const getAllProjects = async (organizationId?: string, isGlobalAdmin: boolean = false, status: string = 'active') => {
|
||||
const statusQuery = status === 'active'
|
||||
? { status: { $ne: 'archived' } }
|
||||
: { status: 'archived' };
|
||||
|
||||
const matchQuery: Record<string, unknown> = isGlobalAdmin
|
||||
? { ...statusQuery }
|
||||
: {
|
||||
...statusQuery,
|
||||
$or: [{ organizationId }, { organizationId: { $exists: false } }, { organizationId: null }]
|
||||
};
|
||||
|
||||
const projects = await Project.aggregate([
|
||||
{ $match: matchQuery },
|
||||
{ $sort: { name: 1 } },
|
||||
{
|
||||
$lookup: {
|
||||
from: 'paintingschemes',
|
||||
localField: '_id',
|
||||
foreignField: 'projectId',
|
||||
as: 'paintingSchemes'
|
||||
}
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
id: { $toString: "$_id" }
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
return projects;
|
||||
};
|
||||
|
||||
export const archiveProject = async (id: string, organizationId?: string, isGlobalAdmin: boolean = false) => {
|
||||
export const archiveProject = async (id: string, organizationId?: string, isGlobalAdmin?: boolean) => {
|
||||
const project = await Project.findById(id);
|
||||
if (!project) throw new Error('Projeto não encontrado');
|
||||
|
||||
// Check ownership
|
||||
if (!isGlobalAdmin && organizationId && project.organizationId && project.organizationId !== organizationId) {
|
||||
throw new Error('Sem permissão para arquivar este projeto');
|
||||
|
||||
if (!isGlobalAdmin && project.organization_id !== organizationId) {
|
||||
throw new Error('Acesso negado');
|
||||
}
|
||||
|
||||
const newStatus = project.status === 'active' ? 'archived' : 'active';
|
||||
const updated = await Project.findByIdAndUpdate(id, { status: newStatus }, { new: true }).lean();
|
||||
return updated;
|
||||
|
||||
return await Project.findByIdAndUpdate(id, { status: 'archived' });
|
||||
};
|
||||
|
||||
export const getProjectById = async (id: string, organizationId?: string, isGlobalAdmin: boolean = false) => {
|
||||
const project = await Project.findById(id).lean();
|
||||
export const getProjectById = async (id: string) => {
|
||||
return await Project.findById(id);
|
||||
};
|
||||
|
||||
if (!project) throw new Error('Projeto não encontrado');
|
||||
export const updateProject = async (id: string, data: Partial<ProjectData>) => {
|
||||
return await Project.findByIdAndUpdate(id, data);
|
||||
};
|
||||
|
||||
// Security check: Allow if global admin OR matches organization OR project has no organization
|
||||
if (!isGlobalAdmin && organizationId && project.organizationId && project.organizationId !== organizationId) {
|
||||
throw new Error('Acesso negado a este projeto');
|
||||
}
|
||||
|
||||
const [parts, schemes, records, inspections] = await Promise.all([
|
||||
Part.find({ projectId: id }).lean(),
|
||||
PaintingScheme.find({ projectId: id }).populate('paintId thinnerId').lean(),
|
||||
ApplicationRecord.find({ projectId: id }).lean(),
|
||||
Inspection.find({ projectId: id })
|
||||
.populate({
|
||||
path: 'stockItemId',
|
||||
select: 'batchNumber dataSheetId',
|
||||
populate: { path: 'dataSheetId', select: 'name' }
|
||||
})
|
||||
.lean()
|
||||
]);
|
||||
export const deleteProject = async (id: string) => {
|
||||
return await Project.findByIdAndDelete(id);
|
||||
};
|
||||
|
||||
export const getProjectStats = async (projectId: string) => {
|
||||
const project = await Project.findById(projectId);
|
||||
if (!project) return null;
|
||||
|
||||
const schemes = await PaintingScheme.find({ project_id: projectId });
|
||||
const inspections = await Inspection.find({ project_id: projectId });
|
||||
const parts = await Part.find({ project_id: projectId });
|
||||
|
||||
return {
|
||||
...project,
|
||||
id: project._id.toString(),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
parts: parts.map((p: any) => ({ ...p, id: p._id.toString() })),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
paintingSchemes: schemes.map((s: any) => ({ ...s, id: s._id.toString() })),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
applicationRecords: records.map((r: any) => ({ ...r, id: r._id.toString() })),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
inspections: inspections.map((i: any) => ({ ...i, id: i._id.toString() }))
|
||||
project,
|
||||
schemesCount: schemes.length,
|
||||
inspectionsCount: inspections.length,
|
||||
partsCount: parts.length
|
||||
};
|
||||
};
|
||||
|
||||
export const updateProject = async (id: string, data: Partial<ProjectData>, organizationId?: string, isGlobalAdmin: boolean = false) => {
|
||||
const existing = await Project.findById(id);
|
||||
if (!existing) return null;
|
||||
|
||||
// Check ownership
|
||||
if (!isGlobalAdmin && organizationId && existing.organizationId && existing.organizationId !== organizationId) {
|
||||
console.warn(`Access Denied: Project ${id} belongs to ${existing.organizationId}, user is ${organizationId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const updateData: Partial<ProjectData> & { updatedAt: Date, organizationId?: string } = {
|
||||
...data,
|
||||
updatedAt: new Date(),
|
||||
startDate: data.startDate ? new Date(data.startDate) : undefined,
|
||||
endDate: data.endDate ? new Date(data.endDate) : undefined,
|
||||
weightKg: data.weightKg,
|
||||
};
|
||||
|
||||
// Adopt if needed
|
||||
if (organizationId && !existing.organizationId) {
|
||||
updateData.organizationId = organizationId;
|
||||
}
|
||||
|
||||
const updated = await Project.findOneAndUpdate({ _id: id }, updateData, { new: true }).lean();
|
||||
if (updated) {
|
||||
return { ...updated, id: updated._id.toString() };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const deleteProject = async (id: string, organizationId?: string, isGlobalAdmin: boolean = false) => {
|
||||
const project = await Project.findById(id);
|
||||
if (!project) return;
|
||||
|
||||
// Check ownership
|
||||
if (!isGlobalAdmin && organizationId && project.organizationId && project.organizationId !== organizationId) {
|
||||
throw new Error('Sem permissão para excluir este projeto');
|
||||
}
|
||||
|
||||
await Project.findByIdAndDelete(id);
|
||||
|
||||
// Also cleanup related data
|
||||
await Promise.all([
|
||||
Part.deleteMany({ projectId: id }),
|
||||
PaintingScheme.deleteMany({ projectId: id }),
|
||||
ApplicationRecord.deleteMany({ projectId: id }),
|
||||
Inspection.deleteMany({ projectId: id })
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
|
||||
console.log('✅ ProjectService loaded with compatibility layer');
|
||||
|
||||
Reference in New Issue
Block a user