🚀 Auto-deploy: GPI atualizado em 10/08/2026 00:57:32

This commit is contained in:
2026-08-10 00:57:32 +00:00
parent d3ed824d81
commit 0a8bd86218
28 changed files with 1605 additions and 818 deletions
-55
View File
@@ -1,55 +0,0 @@
import express from 'express';
import cors from 'cors';
import projectRoutes from '../src/server/routes/projectRoutes.js';
import partRoutes from '../src/server/routes/partRoutes.js';
import paintingSchemeRoutes from '../src/server/routes/paintingSchemeRoutes.js';
import applicationRecordRoutes from '../src/server/routes/applicationRecordRoutes.js';
import inspectionRoutes from '../src/server/routes/inspectionRoutes.js';
import analysisRoutes from '../src/server/routes/analysisRoutes.js';
import dataSheetRoutes from '../src/server/routes/dataSheetRoutes.js';
import yieldStudyRoutes from '../src/server/routes/yieldStudyRoutes.js';
import userRoutes from '../src/server/routes/userRoutes.js';
import systemSettingsRoutes from '../src/server/routes/systemSettingsRoutes.js';
import geometryTypeRoutes from '../src/server/routes/geometryTypeRoutes.js';
import stockRoutes from '../src/server/routes/stockRoutes.js';
import notificationRoutes from '../src/server/routes/notificationRoutes.js';
import instrumentRoutes from '../src/server/routes/instrumentRoutes.js';
import { extractUser } from '../src/server/middleware/authMiddleware.js';
import path from 'path';
const app = express();
app.use(cors({
origin: '*', // Be more specific in production
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-organization-id', 'x-organization-name']
}));
app.use(express.json());
// Global Middleware
app.use(extractUser);
// Static Uploads
app.use('/uploads', express.static(path.join(process.cwd(), 'uploads')));
// Routes
app.use('/api/users', userRoutes);
app.use('/api/projects', projectRoutes);
app.use('/api/parts', partRoutes);
app.use('/api/painting-schemes', paintingSchemeRoutes);
app.use('/api/application-records', applicationRecordRoutes);
app.use('/api/inspections', inspectionRoutes);
app.use('/api', analysisRoutes);
app.use('/api/datasheets', dataSheetRoutes);
app.use('/api/yield-studies', yieldStudyRoutes);
app.use('/api/system-settings', systemSettingsRoutes);
app.use('/api/geometry-types', geometryTypeRoutes);
app.use('/api/stock', stockRoutes);
app.use('/api/notifications', notificationRoutes);
app.use('/api/instruments', instrumentRoutes);
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date() });
});
export default app;
+1 -1
View File
@@ -2,7 +2,7 @@ import dotenv from 'dotenv';
dotenv.config();
import type { VercelRequest, VercelResponse } from '@vercel/node';
import app from './app.js';
import app from '../src/server/app.js';
import { connectDB } from '../src/server/config/database.js';
export default async function handler(req: VercelRequest, res: VercelResponse) {
+173
View File
@@ -0,0 +1,173 @@
# GPI — Gestor de Pintura Industrial
## Descritivo Publicitário para Landing Page
---
## Título de Alto Impacto
**Controle total da pintura industrial: de projeto a inspeção final, em um único sistema.**
---
## 3 Benefícios Principais
### 1. Gestão Completa do Projeto de Pintura
Cadastre projetos com classificação ambiental ISO 12944 (C1 a C5, CX), controle de peso por evolução, e acompanhamento em tempo real. Cada projeto integra peças, esquemas de pintura, controle de aplicação e inspeções — tudo conectado e auditável.
### 2. Ferramentas Técnicas Integradas
Calculadora de ponto de orvalho, conversor μm/mils, cálculos de EPS/EPU, volume morto, consumo por área e por peso, e seletor de bicos airless — todas as ferramentas que o pintor e o engenheiro precisam, sem sair do sistema.
### 3. Análise de Conformidade e Rastreabilidade Total
Comparativo automático entre valores teóricos e reais (rendimento, diluição, espessura DFT). Relatório por peça/lote com status: Aprovado, Atenção ou Crítico. Inspections registram EPS, aderência, rugosidade e condições ambientais — com证据 visual.
---
## Análise Resumida de Todos os Recursos
### Gestão de Projetos
- Cadastro de projetos com cliente, ingeniero responsável, datas e ambiente (ISO 12944)
- Acompanhamento de evolução por peso (kg pintados vs total)
- Arquivo/restore de projetos
- Relatórios gerais e analíticos por projeto
### Cadastro de Peças e Geometrias
- Peças com tipo de geometria (viga, chapas, tubulares, etc.)
- Perda de eficiência por tipo de geometria (tabela configurável)
- Cálculo automático de área superficial e peso
- Vinculação a esquemas de pintura
### Esquemas de Pintura
- Cadastro de esquemas com camadas (Primer, Intermediate, Finish)
- EPS mínima e máxima requerida (μm)
- Sólidos por volume (%)
- Cor e código de cor com bolinha visual
- Diluente/reductor vinculado
- Clone de esquemas entre projetos
- Troca/importação de esquemas entre projetos
### Controle de Aplicação (Registros Diários)
- Registro de demãos por data, etapa e peça
- Espessura úmida aplicada (EPU)
- Espessura seca calculada (EPS)
- Operador, método de aplicação, diluente utilizado
- Volume de tinta e diluente consumidos
### Inspeções de Qualidade
- **Tratamento Superficial**: Tipo de jateamento (seco, hidrojato, mecânico, manual), grau de limpeza (Sa 1 a Sa 3, St2/St3, WJ-1 a WJ-4), flash rust, 5 leituras de rugosidade com média automática
- **Pintura**: Mínimo 10 pontos de EPS, teste de aderência, aparência (aprovado/reprovado/ressalvas), lote de tinta utilizado
- **Condições Ambientais**: Temperatura, umidade relativa, temperatura da peça, período (manhã/tarde/noite)
- Evolução por peso com gráfico visual
### Biblioteca Técnica de Produtos (Fichas)
- Upload de PDF com extração automática de dados via OCR (pdf-parse)
- Cadastro de nome, fabricante, tipo de tinta
- Sólidos por volume, rendimento teórico, fator de rendimento (m².μm/L)
- Intervalos WFT e DFT
- Diluição recomendada
- Relação de mistura (peso e volume)
- Download do PDF técnico
### Estudos de Rendimento (Yield Studies)
- Criação de estudos por tinta (ficha técnica)
- Categorias de peças (Estrutura Primária, Secundária, etc.)
- Dual calculation: por peso (L/t) e por área (m²)
- DFT alvo configurável
- EPU calculado automaticamente
- Projeção de consumo com variação DFT (±10%, ±20%, ±30%)
- Gráfico de barras por categoria
- Relatório PDF para impressão
### Estoque de Tintas e Diluentes
- Cadastro de lotes com RR, número de lote, quantidade, unidade
- Validade/expiration date com alerta
- Estoque mínimo com alerta visual de baixa quantidade
- Baixa de estoque por registro de aplicação
- Histórico completo de movimentações por lote
- Relatório de inventário para impressão
-分开 tintas (PAINT) e diluentes (THINNER)
### Análise de Conformidade
- Comparativo teórico vs real por peça/lote
- Rendimento (m²/L): real vs meta
- Diluição (%): real vs máximo permitido
- Espessura DFT: real vs range do esquema
- Status global: Aprovado / Atenção / Crítico
- Baseado na NBR 12103
### Calculadora Técnica
- **Conversões**: Microns (μm) ↔ Mils
- **Espessura**: Cálculo de EPS (seca) a partir de EPU, SV e diluição; Cálculo de EPU (úmida) a partir de EPS, SV e diluição; Volume morto (rugosidade + área)
- **Ambiente**: Ponto de orvalho com status (risco/seguro)
- **Consumo e Custo**: Por área (m²), por peso (L/t), estimativa de custos com preço por litro
- **Bicos Airless**: Código -> Ângulo do leque, vazão, descrição do uso (fino/médio/pesado)
### Ferramentas de Administração
- Gerenciamento de usuários (admin, user, guest)
- Configuração de tipos de geometria com perda de eficiência
- Backup e restore do banco de dados
- Diagnóstico de sistema (developer dashboard)
---
## Diferenciais do Mercado Concorrente
| Concorrentes Tradicionais | GPI |
|---------------------------|-----|
| Planilhas Excel isoladas | **Sistema centralizado** com projetos, peças, esquemas e inspeções integrados |
| Cálculos manuais ou calculadora separada | **Ferramentas técnicas integradas** — ponto de orvalho, EPS/EPU, bicos airless |
| Fichas técnicas em papel ou arquivo | **Biblioteca digital com OCR** — extração automática de PDF |
| Análise de conformidade em planilha | **Comparativo automático** teórico vs real com status visual |
| Estoque em planilha ou ERP genérico | **Gestão específica de tintas** com lotes, validade e alertas |
| Estudos de rendimento em papel | **Dual calculation** peso/área com projeção DFT e gráfico |
| Relatórios fragmentados | **Relatórios PDF** de inspeção, inventário e estudos |
| Sem norma técnica | **Conformidade NBR 12103** para análise de rendimento |
---
## Inovações Tecnológicas
1. **PDF Data Extraction via OCR**: Upload de fichas técnicas em PDF com extração automática de sólidos volume, rendimento, DFT — usando pdf-parse e processamento inteligente.
2. **Dual Yield Calculation**: Cálculo de consumo de tinta tanto por peso (L/t) quanto por área (m²) — com fator de eficiência dinâmica por tipo de geometria.
3. **Calculadora de Bicos Airless**: Decodificação de códigos de bicos (ex: 517) em ângulo de leque, vazão e recomendação de uso — ferramenta essencial para pintores.
4. **Análise de Conformidade Automática**: Comparativo em tempo real entre valores teóricos (esquema) e reais (aplicação/inspeção) — com thresholds de 20% (atenção) e 30% (crítico).
5. **Sistema de Estocagem com Lotes e Validade**: Gestão de estoque específica para pintura industrial com追踪 de lotes, datas de validade e alertas de estoque mínimo.
6. **Surface Treatment com 5 Leituras de Rugosidade**: Formulário completo de jateamento com avg/min/max de rugosidade, flash rust e condições ambientais — tudo integrado à inspeção.
7. **Yield Study com Projeção DFT**: Estudos de rendimento com projeção de consumo para variações de espessura (±10%, ±20%, ±30%) e gráfico visual por categoria.
8. **Inspection com Evolução Visual**: Gráfico de pizza mostrando evolução do projeto por peso da inspeção vs peso total — integrado ao relatório de inspeção.
---
## Benefícios Incontestáveis
- **Fim da dispersão**: Tudo — projeto, peça, esquema, controle, inspeção — em um único sistema. Sem planilhas espalhadas, sem planilhas perdidas.
- **Decisões baseadas em dados**: Análise de conformidade automática elimina o "achismo" — o sistema diz se está aprovado, em atenção ou crítico.
- **Precisão nos cálculos**: EPS, EPU, volume morto, ponto de orvalho — todas as fórmulas da norma NBR 12103 implementadas e validadas.
- **Rastreabilidade de lotes**: Cada inspeção registra qual lote de tinta foi usado — informação crítica para recalls e certificações de qualidade.
- **Eliminação de erros de transcrição**: Upload de PDF com extração automática dispensa digitação manual de dados técnicos de fichas.
- **Produtividade no canteiro**: Ferramentas técnicas integradas (calculadora, bicos, conversão) eliminam idas e vindas ao escritório para cálculos.
- **Entregas auditáveis**: Relatórios PDF de inspeção e estudos de rendimento servem como documento de entrega ao cliente — com evidência visual.
- **Configuração por tipo de geometria**: Cada tipo de peça tem sua própria perda de eficiência — estruturas diferentes, cálculos diferentes, sem simplificações.
---
## Parágrafo Final — A Dor da Gestão
Gerenciar pintura industrial é lidar com planilhas desatualizadas, cálculos manuais prone a erros, fichas técnicas em papel espalhadas pelo canteiro, e relatórios fragmentados que não se talking between si. O gestor precisa saber: quanto de tinta foi consumido, se a espessura está dentro da faixa, se há lotes prestes a vencer, e se o rendimento real está dentro do esperado. O **GPI** centraliza projetos, peças, esquemas de pintura, controle de aplicação, inspeções e estoque — com ferramentas técnicas integradas e análise de conformidade automática. Menos planilhas, mais controle, entregas dentro da norma. **Seu próximo projeto de pintura merece um sistema que trabalha tão hard quanto você — do primeiro projeto ao relatório final.**
+1 -3
View File
@@ -2,7 +2,6 @@ console.log('Loading Netlify Function...');
import serverless from 'serverless-http';
import express from 'express';
import cors from 'cors';
import mongoose from 'mongoose';
// Import local database connection that sets up GridFS/Bucket
import { connectDB } from '../../src/server/config/database.js';
@@ -50,8 +49,7 @@ app.use('/uploads', uploadsRoutes);
app.get('/api/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date(),
mongoState: mongoose.connection.readyState
timestamp: new Date()
});
});
+1191 -593
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -31,6 +31,8 @@
"dotenv": "^17.2.3",
"enhanced-resolve": "^5.18.4",
"express": "^5.2.1",
"express-rate-limit": "^8.6.2",
"helmet": "^8.3.0",
"jose": "^5.2.0",
"lucide-react": "^0.562.0",
"mongodb": "^7.1.1",
+17 -24
View File
@@ -2,26 +2,18 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react';
import type { AppUser } from '../types';
import { AuthContext } from './AuthContextType';
import { setApiOrganizationId } from '../services/api';
import api from '../services/api';
interface AuthProviderProps {
children: React.ReactNode;
}
const defaultUser: AppUser = {
id: '00000000-0000-0000-0000-000000000000',
email: 'guest@gpi.app',
name: 'Guest User',
role: 'user',
isBanned: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
const DEFAULT_ORGANIZATION_ID = 'e47e6210-4879-4e5b-bf21-9285d2713123';
const DEFAULT_ORGANIZATION_NAME = 'Organização Padrão';
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [appUser, setAppUser] = useState<AppUser | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const storedUser = localStorage.getItem('gpi_user');
@@ -34,25 +26,26 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
}
setApiOrganizationId(DEFAULT_ORGANIZATION_ID, DEFAULT_ORGANIZATION_NAME);
setIsLoading(false);
}, []);
const signInWithPassword = async (password: string): Promise<boolean> => {
if (password === '@@Gi05Br;;') {
const adminUser: AppUser = {
...defaultUser,
id: 'admin-001',
email: 'admtracksteel@gmail.com',
name: 'Administrator / DEV',
role: 'admin'
};
setAppUser(adminUser);
localStorage.setItem('gpi_user', JSON.stringify(adminUser));
return true;
try {
const response = await api.post('/users/login', { password });
if (response.data.success) {
const adminUser = response.data.user as AppUser;
setAppUser(adminUser);
localStorage.setItem('gpi_user', JSON.stringify(adminUser));
localStorage.setItem('gpi_token', response.data.token);
return true;
}
} catch (error) {
console.error("Login failed:", error);
}
return false;
};
const isDeveloper = useCallback(() => appUser?.email === 'admtracksteel@gmail.com', [appUser]);
const isDeveloper = useCallback(() => appUser?.email === 'admtracksteel@gmail.com' || appUser?.id === 'admin-user', [appUser]);
const isAdmin = useCallback(() => appUser?.role === 'admin' || appUser?.email === 'admtracksteel@gmail.com', [appUser]);
const isUser = useCallback(() => !!appUser, [appUser]);
const isGuest = useCallback(() => !appUser, [appUser]);
@@ -61,7 +54,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const value = useMemo(() => ({
appUser,
isLoading: false,
isLoading,
isSignedIn: !!appUser,
error: null,
isAdmin,
@@ -71,7 +64,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
canEdit,
refetchUser,
signInWithPassword
}), [appUser, isAdmin, isUser, isGuest, isDeveloper, canEdit, refetchUser, signInWithPassword]);
}), [appUser, isLoading, isAdmin, isUser, isGuest, isDeveloper, canEdit, refetchUser]);
return (
<AuthContext.Provider value={value}>
+5 -8
View File
@@ -3,20 +3,17 @@ import './index.css'
import App from './App.tsx'
export function getToken() {
return 'guest-token';
return localStorage.getItem('gpi_token');
}
export function getUser() {
return {
id: 'guest-user',
email: 'guest@gpi.app',
name: 'Guest User',
role: 'user'
};
const user = localStorage.getItem('gpi_user');
return user ? JSON.parse(user) : null;
}
export function setUser(token: string, user: any) {
console.log('User set (no auth):', user);
localStorage.setItem('gpi_token', token);
localStorage.setItem('gpi_user', JSON.stringify(user));
}
createRoot(document.getElementById('root')!).render(<App />)
+1 -2
View File
@@ -1,7 +1,6 @@
// API service configuration v2.0 - Logto Auth
import axios from 'axios';
import { triggerGuestWarning } from '../utils/toastHandler';
import { getToken } from '../main';
export const getBaseUrl = () => {
if (import.meta.env.VITE_API_URL) {
@@ -29,7 +28,7 @@ export const setApiOrganizationId = setApiOrgData;
api.interceptors.request.use(
(config) => {
const token = getToken();
const token = localStorage.getItem('gpi_token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
+67 -32
View File
@@ -1,5 +1,12 @@
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import path from 'path';
import fs from 'fs';
import { extractUser } from './middleware/authMiddleware.js';
import projectRoutes from './routes/projectRoutes.js';
import partRoutes from './routes/partRoutes.js';
import paintingSchemeRoutes from './routes/paintingSchemeRoutes.js';
@@ -11,76 +18,104 @@ import yieldStudyRoutes from './routes/yieldStudyRoutes.js';
import userRoutes from './routes/userRoutes.js';
import systemSettingsRoutes from './routes/systemSettingsRoutes.js';
import geometryTypeRoutes from './routes/geometryTypeRoutes.js';
import stockRoutes from './routes/stockRoutes.js';
import notificationRoutes from './routes/notificationRoutes.js';
import instrumentRoutes from './routes/instrumentRoutes.js';
import messageRoutes from './routes/messageRoutes.js';
import backupRoutes from './routes/backupRoutes.js';
import path from 'path';
const app = express();
// Security Headers
app.use(helmet({
contentSecurityPolicy: false, // Pode interferir com o React em dev se não configurado direito
crossOriginEmbedderPolicy: false
}));
// Rate Limiter
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 1000, // limit each IP to 1000 requests per windowMs
message: 'Too many requests from this IP, please try again later.'
});
// Apply rate limiter only to API routes
app.use('/api', apiLimiter);
// CORS
const allowedOrigins = [
'https://gpi.reifonas.cloud',
'http://localhost:5173',
'http://localhost:3000'
];
app.use(cors({
origin: '*',
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-organization-id']
}));
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
import { extractUser } from './middleware/authMiddleware.js';
app.use(extractUser);
app.use(express.json({ limit: '5mb' }));
app.use(express.urlencoded({ limit: '5mb', extended: true }));
// Static Uploads
import fs from 'fs';
const uploadsPath = path.join(process.cwd(), 'uploads');
if (!fs.existsSync(uploadsPath)) {
fs.mkdirSync(uploadsPath, { recursive: true });
}
app.use('/uploads', express.static(uploadsPath));
// Serve frontend static files
const distPath = path.join(process.cwd(), 'dist');
app.use(express.static(distPath));
// Routes
app.use('/api/users', userRoutes);
app.use('/api/projects', projectRoutes);
app.use('/api/parts', partRoutes);
app.use('/api/painting-schemes', paintingSchemeRoutes);
app.use('/api/application-records', applicationRecordRoutes);
app.use('/api/inspections', inspectionRoutes);
app.use('/api', analysisRoutes);
app.use('/api/datasheets', dataSheetRoutes);
app.use('/api/datasheets', dataSheetRoutes);
app.use('/api/yield-studies', yieldStudyRoutes);
app.use('/api/system-settings', systemSettingsRoutes);
app.use('/api/geometry-types', geometryTypeRoutes);
app.use('/api/stock', stockRoutes);
app.use('/api/notifications', notificationRoutes);
app.use('/api/instruments', instrumentRoutes);
app.use('/api/messages', messageRoutes);
app.use('/api/backup', backupRoutes);
// API Routes (all API routes pass through extractUser here, avoid duplication in route files)
const apiRouter = express.Router();
apiRouter.use(extractUser);
apiRouter.use('/users', userRoutes);
apiRouter.use('/projects', projectRoutes);
apiRouter.use('/parts', partRoutes);
apiRouter.use('/painting-schemes', paintingSchemeRoutes);
apiRouter.use('/application-records', applicationRecordRoutes);
apiRouter.use('/inspections', inspectionRoutes);
apiRouter.use('/', analysisRoutes);
apiRouter.use('/datasheets', dataSheetRoutes);
apiRouter.use('/yield-studies', yieldStudyRoutes);
apiRouter.use('/system-settings', systemSettingsRoutes);
apiRouter.use('/geometry-types', geometryTypeRoutes);
apiRouter.use('/stock', stockRoutes);
apiRouter.use('/notifications', notificationRoutes);
apiRouter.use('/instruments', instrumentRoutes);
apiRouter.use('/messages', messageRoutes);
apiRouter.use('/backup', backupRoutes);
app.use('/api', apiRouter);
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date(), auth: 'logto' });
});
app.get('/api/test', (req, res) => {
res.json({ status: 'ok', message: 'Test endpoint working' });
});
// SPA fallback - must be last
app.use((req, res, next) => {
if (req.path.startsWith('/api')) {
return next(); // Do not return index.html for API 404s
}
res.sendFile(path.join(distPath, 'index.html'));
});
// Error handler
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error('Express Error:', err);
if (err.message === 'Not allowed by CORS') {
return res.status(403).json({ error: 'CORS error' });
}
res.status(500).json({ error: 'Internal server error' });
});
+3 -5
View File
@@ -2,11 +2,11 @@ import { createClient } from '@supabase/supabase-js';
import dotenv from 'dotenv';
dotenv.config();
const supabaseUrl = process.env.SUPABASE_URL || 'https://supabase.reifonas.cloud';
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!supabaseServiceKey) {
throw new Error('❌ SUPABASE_SERVICE_ROLE_KEY is missing in environment variables');
if (!supabaseUrl || !supabaseServiceKey) {
throw new Error('❌ SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY must be provided in environment variables');
}
export const GPI_SCHEMA = 'public';
@@ -68,5 +68,3 @@ export async function findOneGpi(table: string, filters: Record<string, any>) {
return data;
}
console.log('✅ Supabase client initialized');
@@ -1,19 +1,7 @@
import { Request, Response } from 'express';
import { supabase } from '../config/supabase.js';
const TABLE_NOT_FOUND_CODES = ['42P01', 'PGRST116'];
const safeSupabaseQuery = async (table: string, query: any) => {
try {
return await query;
} catch (error: any) {
if (error.code && TABLE_NOT_FOUND_CODES.includes(error.code)) {
console.log(`Table ${table} not found, returning empty result`);
return { data: [], error: null };
}
throw error;
}
};
// Send a message
export const sendMessage = async (req: Request, res: Response) => {
+27
View File
@@ -191,4 +191,31 @@ export const deleteUser = async (req: Request, res: Response) => {
console.error('Error deleting user:', error);
res.json({ message: 'Membro removido com sucesso.' });
}
};
export const login = async (req: Request, res: Response) => {
try {
const { password } = req.body;
// In a real app, this should check against a hashed DB value or Logto.
// For now, moving the hardcoded password to the backend is safer than frontend.
const adminPassword = process.env.ADMIN_PASSWORD || '@@Gi05Br;;';
if (password === adminPassword) {
// Success - return a token
return res.json({
success: true,
token: 'gpi-admin-token',
user: {
id: 'admin-user',
role: 'admin',
name: 'Administrador'
}
});
}
return res.status(401).json({ success: false, error: 'Senha incorreta.' });
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ success: false, error: 'Erro interno.' });
}
};
-2
View File
@@ -155,5 +155,3 @@ export const YieldStudy = createModel('yield_studies');
export const StoredFile = createModel('stored_files');
export { queryGpi, findOneGpi, insertGpi, updateGpi, deleteGpi };
console.log('✅ Mongoose Compatibility Layer load complete (Extended Mode)');
+10 -4
View File
@@ -36,9 +36,17 @@ export async function getModel(tableName: string) {
return existing;
},
countDocuments: async (query: any = {}) => {
const { data, error } = await supabase.from(tableName).select('*', { count: 'exact', head: true });
let q = supabase.from(tableName).select('*', { count: 'exact', head: true });
if (query) {
Object.keys(query).forEach(key => {
q = q.eq(key, query[key]);
});
}
const { count, error } = await q;
if (error) throw error;
return data?.length || 0;
return count || 0;
}
};
}
@@ -67,5 +75,3 @@ export function getModelById(tableName: string, idField: string = 'id') {
}
};
}
console.log('✅ DB Compatibility Layer initialized');
+42 -10
View File
@@ -1,4 +1,5 @@
import { Request, Response, NextFunction } from 'express';
import { authenticateRequest } from './logtoAuth.js';
export interface IAppUser {
id: string;
@@ -16,22 +17,47 @@ declare module 'express-serve-static-core' {
}
}
const DEFAULT_ORG_ID = 'e47e6210-4879-4e5b-bf21-9285d2713123';
const GUEST_USER: IAppUser = {
id: '00000000-0000-0000-0000-000000000000',
logtoId: 'guest',
email: 'guest@gpi.app',
name: 'Guest User',
role: 'guest',
organizationId: DEFAULT_ORG_ID,
organizationRole: 'guest'
};
export const extractUser = async (req: Request, res: Response, next: NextFunction) => {
req.appUser = {
id: '00000000-0000-0000-0000-000000000000',
logtoId: 'guest',
email: 'guest@gpi.app',
name: 'Guest User',
role: 'user',
organizationId: req.headers['x-organization-id'] as string || 'e47e6210-4879-4e5b-bf21-9285d2713123',
organizationRole: 'user'
};
try {
const authenticatedUser = await authenticateRequest(req);
if (authenticatedUser) {
req.appUser = {
...authenticatedUser,
organizationId: req.headers['x-organization-id'] as string || DEFAULT_ORG_ID,
organizationRole: authenticatedUser.role
};
} else {
req.appUser = {
...GUEST_USER,
organizationId: req.headers['x-organization-id'] as string || DEFAULT_ORG_ID,
};
}
} catch {
req.appUser = {
...GUEST_USER,
organizationId: req.headers['x-organization-id'] as string || DEFAULT_ORG_ID,
};
}
next();
};
export const requireRole = (allowedRoles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
// No authentication required - allow all requests
if (!req.appUser || !allowedRoles.includes(req.appUser.role)) {
return res.status(403).json({ error: 'Acesso negado. Permissão insuficiente.' });
}
next();
};
};
@@ -40,9 +66,15 @@ export const requireAdmin = requireRole(['admin']);
export const requireUser = requireRole(['user', 'admin']);
export const canEdit = (req: Request, res: Response, next: NextFunction) => {
if (!req.appUser || req.appUser.role === 'guest') {
return res.status(403).json({ error: 'Convidados não podem editar. Faça login para ter acesso completo.' });
}
next();
};
export const requireDeveloper = (req: Request, res: Response, next: NextFunction) => {
if (!req.appUser || req.appUser.email !== 'admtracksteel@gmail.com') {
return res.status(403).json({ error: 'Acesso restrito ao desenvolvedor.' });
}
next();
};
+5 -5
View File
@@ -1,15 +1,15 @@
import { Router } from 'express';
import * as appRecordController from '../controllers/applicationRecordController.js';
import { extractUser, requireUser } from '../middleware/authMiddleware.js';
import { requireUser } from '../middleware/authMiddleware.js';
const router = Router();
// Public routes (read-only)
router.get('/project/:projectId', extractUser, appRecordController.getApplicationRecordsByProject);
router.get('/project/:projectId', appRecordController.getApplicationRecordsByProject);
// Protected routes (require user permission)
router.post('/', extractUser, requireUser, appRecordController.createApplicationRecord);
router.put('/:id', extractUser, requireUser, appRecordController.updateApplicationRecord);
router.delete('/:id', extractUser, requireUser, appRecordController.deleteApplicationRecord);
router.post('/', requireUser, appRecordController.createApplicationRecord);
router.put('/:id', requireUser, appRecordController.updateApplicationRecord);
router.delete('/:id', requireUser, appRecordController.deleteApplicationRecord);
export default router;
+7 -7
View File
@@ -1,6 +1,6 @@
import { Router } from 'express';
import * as inspectionController from '../controllers/inspectionController.js';
import { extractUser, requireUser } from '../middleware/authMiddleware.js';
import { requireUser } from '../middleware/authMiddleware.js';
import multer from 'multer';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
@@ -42,15 +42,15 @@ const upload = multer({
});
// Upload route
router.post('/upload', extractUser, requireUser, upload.single('photo'), inspectionController.uploadPhoto);
router.post('/upload', requireUser, upload.single('photo'), inspectionController.uploadPhoto);
// Public routes (read-only)
router.get('/', extractUser, inspectionController.getAllInspections);
router.get('/project/:projectId', extractUser, inspectionController.getInspectionsByProject);
router.get('/', inspectionController.getAllInspections);
router.get('/project/:projectId', inspectionController.getInspectionsByProject);
// Protected routes (require user permission)
router.post('/', extractUser, requireUser, inspectionController.createInspection);
router.put('/:id', extractUser, requireUser, inspectionController.updateInspection);
router.delete('/:id', extractUser, requireUser, inspectionController.deleteInspection);
router.post('/', requireUser, inspectionController.createInspection);
router.put('/:id', requireUser, inspectionController.updateInspection);
router.delete('/:id', requireUser, inspectionController.deleteInspection);
export default router;
+8 -8
View File
@@ -1,16 +1,16 @@
import express from 'express';
import { sendMessage, getUnreadMessages, markMessageAsRead, getMyPendingMessages, deleteMessage, archiveMessage, recipientDeleteMessage } from '../controllers/messageController.js';
import { extractUser } from '../middleware/authMiddleware.js';
import { } from '../middleware/authMiddleware.js';
const router = express.Router();
// All routes require authentication
router.post('/', extractUser, sendMessage);
router.get('/unread', extractUser, getUnreadMessages);
router.get('/pending', extractUser, getMyPendingMessages);
router.patch('/:id/read', extractUser, markMessageAsRead);
router.patch('/:id/archive', extractUser, archiveMessage);
router.delete('/:id', extractUser, deleteMessage); // Sender side (if unread)
router.delete('/:id/recipient', extractUser, recipientDeleteMessage); // Recipient side
router.post('/', sendMessage);
router.get('/unread', getUnreadMessages);
router.get('/pending', getMyPendingMessages);
router.patch('/:id/read', markMessageAsRead);
router.patch('/:id/archive', archiveMessage);
router.delete('/:id', deleteMessage); // Sender side (if unread)
router.delete('/:id/recipient', recipientDeleteMessage); // Recipient side
export default router;
+6 -6
View File
@@ -1,16 +1,16 @@
import { Router } from 'express';
import * as partController from '../controllers/partController.js';
import { extractUser, requireAdmin } from '../middleware/authMiddleware.js';
import { requireAdmin } from '../middleware/authMiddleware.js';
const router = Router();
// Public routes (read-only)
router.get('/', extractUser, partController.getAllParts);
router.get('/project/:projectId', extractUser, partController.getPartsByProject);
router.get('/', partController.getAllParts);
router.get('/project/:projectId', partController.getPartsByProject);
// Protected routes (require admin permission)
router.post('/', extractUser, requireAdmin, partController.createPart);
router.put('/:id', extractUser, requireAdmin, partController.updatePart);
router.delete('/:id', extractUser, requireAdmin, partController.deletePart);
router.post('/', requireAdmin, partController.createPart);
router.put('/:id', requireAdmin, partController.updatePart);
router.delete('/:id', requireAdmin, partController.deletePart);
export default router;
+8 -8
View File
@@ -1,18 +1,18 @@
import { Router } from 'express';
import * as projectController from '../controllers/projectController.js';
import { extractUser, requireAdmin } from '../middleware/authMiddleware.js';
import { requireAdmin } from '../middleware/authMiddleware.js';
const router = Router();
// Rotas com contexto de usuário (leitura)
router.get('/', extractUser, projectController.getAllProjects);
router.get('/dashboard', extractUser, projectController.getDashboardProjects);
router.get('/:id', extractUser, projectController.getProjectById);
router.get('/', projectController.getAllProjects);
router.get('/dashboard', projectController.getDashboardProjects);
router.get('/:id', projectController.getProjectById);
// Protected routes (require admin permission)
router.post('/', extractUser, requireAdmin, projectController.createProject);
router.put('/:id', extractUser, requireAdmin, projectController.updateProject);
router.delete('/:id', extractUser, requireAdmin, projectController.deleteProject);
router.patch('/:id/archive', extractUser, requireAdmin, projectController.archiveProject);
router.post('/', requireAdmin, projectController.createProject);
router.put('/:id', requireAdmin, projectController.updateProject);
router.delete('/:id', requireAdmin, projectController.deleteProject);
router.patch('/:id/archive', requireAdmin, projectController.archiveProject);
export default router;
+6 -6
View File
@@ -1,6 +1,6 @@
import express from 'express';
import { getSettings, updateSettings, uploadLogo, serveLogo } from '../controllers/systemSettingsController.js';
import { extractUser, requireDeveloper } from '../middleware/authMiddleware.js';
import { requireDeveloper } from '../middleware/authMiddleware.js';
import { uploadLogoDetails } from '../middleware/uploadMiddleware.js';
const router = express.Router();
@@ -9,10 +9,10 @@ const router = express.Router();
router.get('/', getSettings);
// Protected update
router.put('/', extractUser, requireDeveloper, updateSettings);
router.put('/', requireDeveloper, updateSettings);
// Protected upload
router.post('/logo', extractUser, requireDeveloper, uploadLogoDetails.single('logo'), uploadLogo);
router.post('/logo', requireDeveloper, uploadLogoDetails.single('logo'), uploadLogo);
// Public access to view logo (noauth needed for login screen etc)
router.get('/logo-image/:filename', (req, res) => {
@@ -23,8 +23,8 @@ router.get('/logo-image/:filename', (req, res) => {
// Global Admin Routes
import { getGlobalUsers, getGlobalOrganizations, toggleOrganizationBan } from '../controllers/systemSettingsController.js';
router.get('/users', extractUser, requireDeveloper, getGlobalUsers);
router.get('/organizations', extractUser, requireDeveloper, getGlobalOrganizations);
router.post('/organizations/ban', extractUser, requireDeveloper, toggleOrganizationBan);
router.get('/users', requireDeveloper, getGlobalUsers);
router.get('/organizations', requireDeveloper, getGlobalOrganizations);
router.post('/organizations/ban', requireDeveloper, toggleOrganizationBan);
export default router;
+12 -10
View File
@@ -1,20 +1,22 @@
import express from 'express';
import { syncUser, getCurrentUser, getAllUsers, updateUserRole, toggleBanUser, heartbeat, getActiveUsers, deleteUser } from '../controllers/userController.js';
import { extractUser } from '../middleware/authMiddleware.js';
import { syncUser, getCurrentUser, getAllUsers, updateUserRole, toggleBanUser, heartbeat, getActiveUsers, deleteUser, login } from '../controllers/userController.js';
import { requireAdmin, requireUser } from '../middleware/authMiddleware.js';
const router = express.Router();
// Public routes (no auth required)
router.get('/', getAllUsers);
router.get('/me', extractUser, getCurrentUser);
// Public routes (no auth required originally, but now all API has extractUser)
// Only users should see all users
router.get('/', requireUser, getAllUsers);
router.get('/me', getCurrentUser);
router.post('/login', login);
// Heartbeat & Presence
router.post('/heartbeat', extractUser, heartbeat);
router.get('/active', extractUser, getActiveUsers);
router.post('/heartbeat', heartbeat);
router.get('/active', getActiveUsers);
// Admin routes
router.patch('/:id/role', extractUser, updateUserRole);
router.patch('/:id/ban', extractUser, toggleBanUser);
router.delete('/:id', extractUser, deleteUser);
router.patch('/:id/role', requireAdmin, updateUserRole);
router.patch('/:id/ban', requireAdmin, toggleBanUser);
router.delete('/:id', requireAdmin, deleteUser);
export default router;
-8
View File
@@ -60,17 +60,10 @@ function getContentType(ext: string): string {
return types[ext.toLowerCase()] || 'application/octet-stream';
}
export const saveFileToGridFS = saveFileToStorage;
export const deleteFileFromGridFS = deleteFileFromStorage;
export const getFileStream = (fileUrl: string) => {
return fileUrl;
};
export const migrateFilesToGridFS = async () => {
console.log('️ File migration skipped - using Supabase Storage');
};
export const getAllDataSheets = async (organizationId?: string) => {
try {
let query = supabase.from('technical_data_sheets').select('*');
@@ -138,4 +131,3 @@ export const deleteDataSheet = async (id: string, organizationId?: string) => {
return true;
};
console.log('✅ DataSheetService loaded with Supabase Storage');
-1
View File
@@ -93,4 +93,3 @@ export const getInspectionStats = async (organizationId?: string) => {
};
};
console.log('✅ InspectionService loaded with Supabase');
@@ -61,4 +61,3 @@ export const getAllSchemes = async (organizationId?: string) => {
return data || [];
};
console.log('✅ PaintingSchemeService loaded with Supabase');
-1
View File
@@ -36,4 +36,3 @@ export const getPartsByOrganization = async (organizationId: string) => {
return await Part.find({ organization_id: organizationId });
};
console.log('✅ PartService loaded with compatibility');
+13 -6
View File
@@ -108,9 +108,18 @@ export const updateProject = async (id: string, data: Partial<ProjectData> & { o
const updateData: any = { ...data };
delete updateData.organizationId;
if (data.startDate) updateData.start_date = new Date(data.startDate).toISOString();
if (data.endDate) updateData.end_date = new Date(data.endDate).toISOString();
if (data.weightKg) updateData.weight_kg = data.weightKg;
if (data.startDate !== undefined) {
updateData.start_date = data.startDate ? new Date(data.startDate).toISOString() : null;
delete updateData.startDate;
}
if (data.endDate !== undefined) {
updateData.end_date = data.endDate ? new Date(data.endDate).toISOString() : null;
delete updateData.endDate;
}
if (data.weightKg !== undefined) {
updateData.weight_kg = data.weightKg;
delete updateData.weightKg;
}
const { data: updated, error } = await supabase
.from('projects')
@@ -153,6 +162,4 @@ export const getProjectStats = async (projectId: string) => {
inspections: inspectionsRes.data || [],
parts: partsRes.data || []
};
};
console.log('✅ ProjectService loaded with Supabase');
};