Repository navigation
Expand file tree
/
Copy pathserver.js
More file actions
114 lines (103 loc) · 4.08 KB
/
Copy pathserver.js
File metadata and controls
114 lines (103 loc) · 4.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/* ═══════════════════════════════════════════════════════════════════
EXPERT WORK — Servidor (Node puro, sem dependências)
Mesma cartilha do IndustriAlly: um único processo serve as páginas como
texto e expõe a API sob /api/*. Duas pontas sobre a mesma base:
/ portal do cliente (empresa) · só leitura
/admin painel do admin geral (Celiware) · cadastra tudo
═══════════════════════════════════════════════════════════════════ */
const http = require('http');
const fs = require('fs/promises');
const path = require('path');
const { handlePortalApi, fotoAutorizada } = require('./portal');
const { handleAdminApi } = require('./admin');
const { sendJson, ROOT } = require('./store');
const PORT = Number(process.env.PORT || 3000);
const ROOT_DIR = ROOT;
const contentTypes = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.webp': 'image/webp',
};
// Arquivos que NUNCA podem ser servidos como estáticos (dados + código).
const BLOQUEADOS = new Set([
'/tenants.json', '/config.json',
'/server.js', '/portal.js', '/admin.js', '/store.js', '/credito.js',
'/seed.js', '/migrar.js', '/package.json',
]);
async function handleApi(req, res, pathname) {
if (pathname.startsWith('/api/admin')) {
await handleAdminApi(req, res, pathname);
return;
}
if (pathname === '/api/health' && req.method === 'GET') {
sendJson(res, 200, { ok: true });
return;
}
// /api/login, /api/logout, /api/portal, /api/portal/os/:id
await handlePortalApi(req, res, pathname);
}
async function serveStatic(req, res, pathname) {
const routes = {
'/': '/index.html',
'/portal': '/index.html',
'/admin': '/admin.html',
};
let requested = routes[pathname] || pathname;
const decoded = decodeURIComponent(requested);
if (BLOQUEADOS.has(decoded) || decoded.startsWith('/dados/')) {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Arquivo não encontrado.');
return;
}
// Fotos de OS: só para a empresa dona (checa cookie de sessão + ledger).
if (decoded.startsWith('/fotos/')) {
if (!(await fotoAutorizada(req, decoded))) {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Arquivo não encontrado.');
return;
}
}
const filePath = path.normalize(path.join(ROOT_DIR, decoded));
if (!filePath.startsWith(ROOT_DIR)) {
res.writeHead(403);
res.end('Acesso negado.');
return;
}
try {
const file = await fs.readFile(filePath);
const ext = path.extname(filePath).toLowerCase();
const headers = { 'Content-Type': contentTypes[ext] || 'application/octet-stream' };
// HTML nunca é cacheado: garante que correções (admin/portal) cheguem ao
// navegador sem depender de F5 forçado do usuário.
if (ext === '.html') headers['Cache-Control'] = 'no-store, must-revalidate';
res.writeHead(200, headers);
res.end(file);
} catch {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Arquivo não encontrado.');
}
}
const server = http.createServer(async (req, res) => {
try {
const url = new URL(req.url, `http://${req.headers.host}`);
if (url.pathname.startsWith('/api/')) {
await handleApi(req, res, url.pathname);
return;
}
await serveStatic(req, res, url.pathname);
} catch (error) {
sendJson(res, 500, { erro: error.message || 'Erro interno.' });
}
});
server.listen(PORT, () => {
console.log(`Expert Work rodando em http://localhost:${PORT}`);
console.log(` Cliente: http://localhost:${PORT}/`);
console.log(` Admin: http://localhost:${PORT}/admin`);
});