682 lines
24 KiB
JavaScript
682 lines
24 KiB
JavaScript
require("dotenv").config();
|
|
|
|
const express = require("express");
|
|
const multer = require("multer");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const csv = require("csv-parser");
|
|
const fastCsv = require("fast-csv");
|
|
const axios = require("axios");
|
|
const cors = require("cors");
|
|
const session = require("express-session"); // adiciona session
|
|
const { geocodeWithGoogle, addressWithGoogle } = require("./service/geocodeService");
|
|
const { fetchJson } = require("./service/fetchService");
|
|
const { BASE_BACKOFF_MS, MAX_RETRIES, REQUEST_DELAY_MS, sleep } = require("./service/retryService");
|
|
const { API_URL, HEADERS } = require("./config/apiConfig");
|
|
const { normalizePartnerSigla } = require("./service/normalizeService");
|
|
const authRoutes = require("./routes/authRoutes.js");
|
|
|
|
function createApp() {
|
|
const upload = multer({ dest: "uploads/" });
|
|
const app = express();
|
|
|
|
// se estiver atrás de um reverse proxy (nginx/traefik) em produção, habilite:
|
|
app.set("trust proxy", 1);
|
|
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
|
|
// session TEM que vir antes das rotas que usam req.session
|
|
app.use(
|
|
session({
|
|
secret: process.env.SESSION_SECRET || "change-me",
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: {
|
|
maxAge: 24 * 60 * 60 * 1000,
|
|
secure: process.env.NODE_ENV === "production",
|
|
sameSite: "lax",
|
|
},
|
|
})
|
|
);
|
|
|
|
// -- Desenvolvimento: pular autenticação se configurado
|
|
// Para ativar: defina NODE_ENV=development e DEV_SKIP_AUTH=true no .env
|
|
if (process.env.NODE_ENV === 'development' && process.env.DEV_SKIP_AUTH === 'true') {
|
|
app.use((req, res, next) => {
|
|
// garante que exista uma sessão autenticada para facilitar testes locais
|
|
if (req.session && (!req.session.user || !req.session.user.authenticated)) {
|
|
req.session.user = { authenticated: true, dev: true };
|
|
}
|
|
next();
|
|
});
|
|
}
|
|
|
|
// middleware que protege rotas que exigem login
|
|
function requireAuth(req, res, next) {
|
|
if (req.session?.user?.authenticated) {
|
|
return next();
|
|
}
|
|
|
|
if (req.xhr || req.headers.accept?.includes("application/json")) {
|
|
return res.status(401).json({ error: "not_authenticated" });
|
|
}
|
|
|
|
return res.redirect("/login");
|
|
}
|
|
|
|
|
|
// proteger demais rotas (ex.: /upload, /consulta)
|
|
app.use((req, res, next) => {
|
|
// permissão liberada para rotas de auth já tratadas; proteger o resto
|
|
if (req.path.startsWith("/auth") || req.path === "/login") return next();
|
|
return requireAuth(req, res, next);
|
|
});
|
|
|
|
// redirect raiz
|
|
app.get("/", (req, res) => {
|
|
// em dev com bypass, sirva a página diretamente (sem redirect)
|
|
if (process.env.NODE_ENV === 'development' && process.env.DEV_SKIP_AUTH === 'true') {
|
|
return res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
}
|
|
|
|
if (req.session?.user?.authenticated) {
|
|
return res.redirect("/public/index.html");
|
|
}
|
|
return res.redirect("/login");
|
|
});
|
|
|
|
/////////////////////////////////////////////////////
|
|
|
|
async function getMinDistance(lat, lon) {
|
|
// tenta várias vezes com backoff exponencial; trata 429 usando Retry-After se disponível
|
|
let attempt = 0;
|
|
while (attempt < MAX_RETRIES) {
|
|
try {
|
|
// envia também o raio (em metros) - API espera esse parâmetro em várias rotas
|
|
const resp = await axios.get(API_URL, {
|
|
headers: HEADERS,
|
|
params: {
|
|
raio: 5000,
|
|
latitude: lat,
|
|
longitude: lon,
|
|
"itens[]": ["caixa"],
|
|
consultarPasta: "S",
|
|
},
|
|
timeout: 10000,
|
|
});
|
|
const data = resp.data;
|
|
const registros = data && data.registros ? data.registros : [];
|
|
// find registros that have a numeric distancia and keep original object for robust extraction
|
|
const candidates = registros
|
|
.map((r) => ({ raw: r, distanciaRaw: r && r.distancia }))
|
|
.map((o) => ({
|
|
raw: o.raw,
|
|
num:
|
|
o.distanciaRaw !== undefined &&
|
|
o.distanciaRaw !== null &&
|
|
o.distanciaRaw !== ""
|
|
? Number(o.distanciaRaw)
|
|
: null,
|
|
}))
|
|
.filter((x) => x.num !== null && !Number.isNaN(x.num));
|
|
if (candidates.length) {
|
|
candidates.sort((a, b) => a.num - b.num);
|
|
const best = candidates[0];
|
|
const r = best.raw || {};
|
|
// robust extraction of pasta sigla with fallbacks
|
|
let pastaSigla = null;
|
|
try {
|
|
if (r.pasta) {
|
|
if (typeof r.pasta === "string" && r.pasta.trim())
|
|
pastaSigla = r.pasta.trim();
|
|
else if (r.pasta.sigla && String(r.pasta.sigla).trim())
|
|
pastaSigla = String(r.pasta.sigla).trim();
|
|
else if (
|
|
r.pasta.cidade &&
|
|
r.pasta.cidade.sigla &&
|
|
String(r.pasta.cidade.sigla).trim()
|
|
)
|
|
pastaSigla = String(r.pasta.cidade.sigla).trim();
|
|
}
|
|
} catch (e) {
|
|
pastaSigla = null;
|
|
}
|
|
// if closest has no pastaSigla, try find any candidate with non-empty sigla
|
|
if (!pastaSigla) {
|
|
for (let j = 0; j < candidates.length; j++) {
|
|
const rr = candidates[j].raw || {};
|
|
try {
|
|
if (rr.pasta) {
|
|
if (typeof rr.pasta === "string" && rr.pasta.trim()) {
|
|
pastaSigla = rr.pasta.trim();
|
|
break;
|
|
}
|
|
if (rr.pasta.sigla && String(rr.pasta.sigla).trim()) {
|
|
pastaSigla = String(rr.pasta.sigla).trim();
|
|
break;
|
|
}
|
|
if (
|
|
rr.pasta.cidade &&
|
|
rr.pasta.cidade.sigla &&
|
|
String(rr.pasta.cidade.sigla).trim()
|
|
) {
|
|
pastaSigla = String(rr.pasta.cidade.sigla).trim();
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// continue
|
|
}
|
|
}
|
|
}
|
|
if (!pastaSigla)
|
|
console.warn(
|
|
`[WARN] Nenhuma pasta.sigla encontrada para coordenadas ${lat},${lon} (closest dist ${best.num})`
|
|
);
|
|
pastaSigla = normalizePartnerSigla(pastaSigla);
|
|
return { dist: best.num, pastaSigla };
|
|
}
|
|
// sem distancias válidas
|
|
return null;
|
|
} catch (err) {
|
|
attempt += 1;
|
|
// se for 429, tente respeitar Retry-After quando disponível
|
|
if (err.response && err.response.status === 429) {
|
|
const ra =
|
|
err.response.headers &&
|
|
(err.response.headers["retry-after"] ||
|
|
err.response.headers["Retry-After"]);
|
|
let waitMs = BASE_BACKOFF_MS * Math.pow(2, attempt - 1);
|
|
if (ra) {
|
|
const raSec = parseInt(ra, 10);
|
|
if (!isNaN(raSec)) waitMs = raSec * 1000;
|
|
}
|
|
console.warn(
|
|
`[WARN] 429 recebido para ${lat},${lon} - aguardando ${waitMs}ms e tentando novamente (attempt ${attempt}/${MAX_RETRIES})`
|
|
);
|
|
await sleep(waitMs);
|
|
continue;
|
|
}
|
|
|
|
// para outros erros de rede/timeout, aguarda backoff exponencial e tenta de novo
|
|
const waitMs = BASE_BACKOFF_MS * Math.pow(2, attempt - 1);
|
|
console.warn(
|
|
`[WARN] Erro ao consultar API para ${lat},${lon}: ${err.message} - backoff ${waitMs}ms (attempt ${attempt}/${MAX_RETRIES})`
|
|
);
|
|
await sleep(waitMs);
|
|
}
|
|
}
|
|
// exauriu tentativas
|
|
console.error(`[ERROR] Exauriu retries para ${lat},${lon}`);
|
|
return null;
|
|
}
|
|
|
|
// upload CSV endpoint
|
|
const jobs = {}; // jobId -> { status, total, processed, download, error }
|
|
|
|
app.post("/upload", upload.single("csvfile"), (req, res) => {
|
|
if (!req.file)
|
|
return res.status(400).json({ error: "Nenhum arquivo enviado" });
|
|
const filePath = req.file.path;
|
|
const jobId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
jobs[jobId] = {
|
|
status: "queued",
|
|
total: 0,
|
|
processed: 0,
|
|
download: null,
|
|
error: null,
|
|
};
|
|
|
|
(async () => {
|
|
jobs[jobId].status = "processing";
|
|
try {
|
|
const rows = [];
|
|
await new Promise((resolve, reject) => {
|
|
fs.createReadStream(filePath)
|
|
.pipe(csv({ separator: ";" }))
|
|
.on("data", (data) => {
|
|
// ignora linhas totalmente vazias (todos os campos nulos/undefined/strings vazias)
|
|
const values = Object.values(data);
|
|
const allEmpty =
|
|
values.length === 0 ||
|
|
values.every(
|
|
(v) => v === null || v === undefined || String(v).trim() === ""
|
|
);
|
|
if (!allEmpty) rows.push(data);
|
|
})
|
|
.on("end", resolve)
|
|
.on("error", reject);
|
|
});
|
|
|
|
jobs[jobId].total = rows.length;
|
|
|
|
const coordCache = new Map();
|
|
|
|
const outRows = [];
|
|
for (let i = 0; i < rows.length; i++) {
|
|
const row = rows[i];
|
|
// normalize keys to avoid duplicates caused by different headers
|
|
const norm = {};
|
|
Object.keys(row).forEach((k) => {
|
|
// normalize header: lowercase, remove diacritics and non-alphanumeric
|
|
const kn = k
|
|
.trim()
|
|
.toLowerCase()
|
|
.normalize("NFKD")
|
|
.replace(/[\u0300-\u036f]/g, "")
|
|
.replace(/[^a-z0-9]/g, "");
|
|
norm[kn] = row[k];
|
|
});
|
|
|
|
// Input columns (from normalized map)
|
|
const rawCep = norm["cep"]
|
|
? String(norm["cep"]).replace(/\D/g, "")
|
|
: "";
|
|
const rawNumero = norm["numero"] ? String(norm["numero"]).trim() : "";
|
|
// prefer lat/lon from normalized input if available
|
|
const rawLat = norm["latitude"] || norm["lat"] || null;
|
|
const rawLon =
|
|
norm["longitude"] || norm["lon"] || norm["long"] || null;
|
|
|
|
// Prefer existing lat/lon if provided from normalized fields
|
|
let lat = null,
|
|
lon = null;
|
|
if (rawLat && rawLon) {
|
|
lat = Number(String(rawLat).replace(",", "."));
|
|
lon = Number(String(rawLon).replace(",", "."));
|
|
}
|
|
|
|
let builtAddress = "";
|
|
|
|
const googleGeocodeAddress = await addressWithGoogle(lat, lon);
|
|
if (googleGeocodeAddress) {
|
|
builtAddress = googleGeocodeAddress;
|
|
} else {
|
|
console.warn(
|
|
`Google Reverse Geocoding não retornou resultado para coords ${lat},${lon} (row ${i + 1})`
|
|
);
|
|
}
|
|
|
|
console.log(`Row ${i + 1}: CEP='${rawCep}' Número='${rawNumero}' Lat='${lat}' Lon='${lon}' BuiltAddress='${builtAddress}'`);
|
|
|
|
// If no coords, try ViaCEP -> Google
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
|
|
if (rawCep) {
|
|
const cep8 = rawCep.padStart(8, "0");
|
|
const cepRestData = await fetch(
|
|
'https://api.cep.rest/', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ cep: cep8 })
|
|
}
|
|
).then(r => r.json());
|
|
if (cepRestData && !cepRestData.erro) {
|
|
const logradouro = cepRestData.data.logradouro || "";
|
|
const bairro = cepRestData.data.bairro || "";
|
|
const cidade = cepRestData.data.localidade || "";
|
|
const uf = cepRestData.data.uf || "";
|
|
if (logradouro) {
|
|
builtAddress =
|
|
`${logradouro}, ${rawNumero}, ${bairro}, ${cidade} - ${uf}`
|
|
.replace(/, ,/g, ",")
|
|
.replace(/^,\s*/, "");
|
|
} else {
|
|
// fallback: use neighborhood/city
|
|
builtAddress = `${bairro || ""} ${
|
|
cidade ? ", " + cidade : ""
|
|
} ${uf ? "- " + uf : ""}`.trim();
|
|
}
|
|
|
|
// build addressToUse (builtAddress already assembled above)
|
|
if (!process.env.GOOGLE_API_KEY) {
|
|
console.error(
|
|
"[ERROR] GOOGLE_API_KEY não definida. Não será possível geocodificar. Defina a chave no .env ou em process.env"
|
|
);
|
|
} else {
|
|
const addressToUse =
|
|
builtAddress || `${cidade} ${uf} ${cep8}`;
|
|
const geo = await geocodeWithGoogle(addressToUse);
|
|
if (geo) {
|
|
lat = geo.lat;
|
|
lon = geo.lon;
|
|
} else
|
|
console.warn(
|
|
`Google Geocoding não retornou resultado para '${addressToUse}' (CEP ${cep8}, row ${
|
|
i + 1
|
|
})`
|
|
);
|
|
}
|
|
} else {
|
|
console.warn(`ViaCEP erro for CEP ${rawCep} (row ${i + 1})`);
|
|
}
|
|
} else {
|
|
console.log(`Row ${i + 1}: missing/invalid CEP -> '${rawCep}'`);
|
|
}
|
|
}
|
|
|
|
// Prepare explicit output row to avoid extra columns
|
|
const out = {
|
|
CEP: rawCep || "",
|
|
Número: rawNumero || "",
|
|
Endereço: builtAddress || "",
|
|
// write lat/lon as strings with dot decimal and fixed precision to avoid locale swaps
|
|
Latitude: Number.isFinite(lat) ? Number(lat).toFixed(6) : "",
|
|
Longitude: Number.isFinite(lon) ? Number(lon).toFixed(6) : "",
|
|
"Não dedicado": "",
|
|
Dedicado: "",
|
|
Distancia: "",
|
|
"Parceiro/Sothis": "",
|
|
};
|
|
|
|
if (Number.isFinite(lat) && Number.isFinite(lon)) {
|
|
const coordKey = `${lat.toFixed(6)},${lon.toFixed(6)}`;
|
|
if (coordCache.has(coordKey)) {
|
|
const cached = coordCache.get(coordKey); // cached is either null or { dist, pastaSigla }
|
|
if (cached !== null) {
|
|
const d = cached.dist;
|
|
const di = Math.round(Number(d));
|
|
out["Não dedicado"] = di <= 500 ? "viável" : "Não viável";
|
|
out["Dedicado"] = di <= 1000 ? "viável" : "Não viável";
|
|
out["Distancia"] = `${di}M`;
|
|
out["Parceiro/Sothis"] =
|
|
normalizePartnerSigla(cached.pastaSigla) || "";
|
|
} else {
|
|
out["Não dedicado"] = "Não viável";
|
|
out["Dedicado"] = "Não viável";
|
|
out["Distancia"] = "5km +";
|
|
out["Parceiro/Sothis"] = "";
|
|
}
|
|
} else {
|
|
const minResult = await getMinDistance(lat, lon); // { dist, pastaSigla } or null
|
|
coordCache.set(coordKey, minResult);
|
|
if (minResult !== null) {
|
|
const di = Math.round(Number(minResult.dist));
|
|
out["Não dedicado"] = di <= 500 ? "viável" : "Não viável";
|
|
out["Dedicado"] = di <= 1000 ? "viável" : "Não viável";
|
|
out["Distancia"] = `${di}M`;
|
|
out["Parceiro/Sothis"] =
|
|
normalizePartnerSigla(minResult.pastaSigla) || "";
|
|
} else {
|
|
out["Não dedicado"] = "Não viável";
|
|
out["Dedicado"] = "Não viável";
|
|
out["Distancia"] = "5km +";
|
|
out["Parceiro/Sothis"] = "";
|
|
}
|
|
await sleep(REQUEST_DELAY_MS);
|
|
}
|
|
} else {
|
|
// no coords available -> keep defaults
|
|
}
|
|
|
|
outRows.push(out);
|
|
jobs[jobId].processed = i + 1;
|
|
}
|
|
|
|
// write output csv - use explicit outRows and fixed header order
|
|
const outPath = path.join(__dirname, "outputs");
|
|
if (!fs.existsSync(outPath)) fs.mkdirSync(outPath);
|
|
const originalName =
|
|
req.file && req.file.originalname
|
|
? req.file.originalname
|
|
: `upload_${Date.now()}.csv`;
|
|
const parsed = path.parse(originalName);
|
|
let outBase = `${parsed.name}_output`;
|
|
let outFile = path.join(outPath, `${outBase}.csv`);
|
|
if (fs.existsSync(outFile)) {
|
|
outFile = path.join(outPath, `${outBase}_${Date.now()}.csv`);
|
|
}
|
|
const headers = [
|
|
"CEP",
|
|
"Número",
|
|
"Endereço",
|
|
"Latitude",
|
|
"Longitude",
|
|
"Não dedicado",
|
|
"Dedicado",
|
|
"Distancia",
|
|
"Parceiro/Sothis",
|
|
];
|
|
await new Promise((resolve, reject) => {
|
|
const ws = fs.createWriteStream(outFile);
|
|
ws.write("\uFEFF");
|
|
fastCsv
|
|
.write(outRows, { headers: headers, delimiter: ";" })
|
|
.pipe(ws)
|
|
.on("finish", resolve)
|
|
.on("error", reject);
|
|
});
|
|
|
|
try {
|
|
fs.unlinkSync(filePath);
|
|
} catch (e) {}
|
|
|
|
jobs[jobId].status = "done";
|
|
jobs[jobId].download = `/download/${path.basename(outFile)}`;
|
|
} catch (err) {
|
|
console.error(err);
|
|
jobs[jobId].status = "error";
|
|
jobs[jobId].error = String(err.message || err);
|
|
}
|
|
})();
|
|
|
|
return res.json({ jobId });
|
|
});
|
|
|
|
// download model endpoint
|
|
app.get("/download-model/:name", (req, res) => {
|
|
const name = req.params.name;
|
|
const safeName = path.basename(name); // evita ../
|
|
const filePath = path.join(__dirname, "models", safeName);
|
|
if (!fs.existsSync(filePath)) return res.status(404).send("Arquivo não encontrado");
|
|
return res.download(filePath, safeName);
|
|
});
|
|
|
|
// lista os arquivos disponíveis em /models
|
|
app.get("/download-models/list", (req, res) => {
|
|
const modelDir = path.join(__dirname, "models");
|
|
if (!fs.existsSync(modelDir)) return res.status(404).json({ error: "Pasta de modelos não encontrada" });
|
|
const files = fs.readdirSync(modelDir).filter((f) => fs.statSync(path.join(modelDir, f)).isFile());
|
|
return res.json({ files });
|
|
});
|
|
|
|
|
|
|
|
// download result endpoint
|
|
app.get("/download/:name", (req, res) => {
|
|
const name = req.params.name;
|
|
const p = path.join(__dirname, "outputs", name);
|
|
if (!fs.existsSync(p))
|
|
return res.status(404).send("Arquivo não encontrado");
|
|
res.download(p);
|
|
});
|
|
|
|
// job status endpoint
|
|
app.get("/status/:jobId", (req, res) => {
|
|
const job = jobs[req.params.jobId];
|
|
if (!job) return res.status(404).json({ error: "job não encontrado" });
|
|
return res.json(job);
|
|
});
|
|
|
|
// manual query endpoint
|
|
// /consulta now accepts either latitude+longitude OR cep+numero. If cep is provided we resolve ViaCEP -> Google -> Geogrid
|
|
app.get("/consulta", async (req, res) => {
|
|
const {
|
|
latitude: rawLat,
|
|
longitude: rawLon,
|
|
cep: rawCep,
|
|
numero: rawNumero,
|
|
} = req.query;
|
|
|
|
// If cep provided, use ViaCEP -> Google geocoding -> Geogrid
|
|
if (rawCep) {
|
|
const cep = String(rawCep).replace(/\D/g, "");
|
|
const numero = rawNumero ? String(rawNumero).trim() : "";
|
|
try {
|
|
const viaCepData = await fetch(
|
|
'https://api.cep.rest/', { method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ cep })
|
|
}
|
|
).then(r => r.json());
|
|
if (!viaCepData || viaCepData.erro)
|
|
return res.status(404).json({ error: "CEP não encontrado" });
|
|
const logradouro = viaCepData.data.logradouro || "";
|
|
const bairro = viaCepData.data.bairro || "";
|
|
const cidade = viaCepData.data.localidade || "";
|
|
const uf = viaCepData.data.uf || "";
|
|
const endereco =
|
|
`${logradouro}, ${numero}, ${bairro}, ${cidade} - ${uf}`
|
|
.replace(/, ,/g, ",")
|
|
.replace(/^,\s*/, "");
|
|
|
|
if (!process.env.GOOGLE_API_KEY)
|
|
return res
|
|
.status(500)
|
|
.json({ error: "GOOGLE_API_KEY não definida no servidor" });
|
|
const geo = await geocodeWithGoogle(
|
|
endereco || `${cidade} ${uf} ${cep}`
|
|
);
|
|
if (!geo)
|
|
return res
|
|
.status(404)
|
|
.json({ error: "geocode não encontrado (Google)" });
|
|
const lat = Number(geo.lat);
|
|
const lon = Number(geo.lon);
|
|
const result = await getMinDistance(lat, lon);
|
|
if (result && result.dist !== undefined) {
|
|
return res.json({
|
|
endereco,
|
|
latitude: lat,
|
|
longitude: lon,
|
|
distancia: result.dist,
|
|
parceiro: result.pastaSigla || "",
|
|
});
|
|
}
|
|
return res.json({
|
|
endereco,
|
|
latitude: lat,
|
|
longitude: lon,
|
|
distancia: "5km +",
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
return res.status(500).json({ error: "Erro na consulta" });
|
|
}
|
|
}
|
|
|
|
// Otherwise require latitude+longitude
|
|
if (!rawLat || !rawLon)
|
|
return res.status(400).json({
|
|
error: "latitude e longitude são obrigatórios (ou forneça cep)",
|
|
});
|
|
const latitude = Number(String(rawLat).replace(",", "."));
|
|
const longitude = Number(String(rawLon).replace(",", "."));
|
|
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
|
|
console.warn(
|
|
`Consulta manual com parâmetros inválidos: lat='${rawLat}' lon='${rawLon}'`
|
|
);
|
|
return res.status(400).json({ error: "latitude ou longitude inválidos" });
|
|
}
|
|
|
|
try {
|
|
console.log(`Consulta manual: lat=${latitude} lon=${longitude}`);
|
|
const result = await getMinDistance(latitude, longitude);
|
|
console.log(`Resultado consulta manual: ${JSON.stringify(result)}`);
|
|
if (result && result.dist !== undefined) {
|
|
return res.json({
|
|
distancia: result.dist,
|
|
parceiro: result.pastaSigla || "",
|
|
});
|
|
}
|
|
return res.json({ distancia: "5km +" });
|
|
} catch (err) {
|
|
console.error(err);
|
|
return res.status(500).json({ error: "Erro na consulta" });
|
|
}
|
|
});
|
|
|
|
// manual CEP+Numero query: resolves ViaCEP -> Nominatim -> Geogrid
|
|
app.get("/consulta-cep", async (req, res) => {
|
|
const { cep: rawCep, numero: rawNumero } = req.query;
|
|
if (!rawCep) return res.status(400).json({ error: "cep é obrigatório" });
|
|
const cep = String(rawCep).replace(/\D/g, "");
|
|
const numero = rawNumero ? String(rawNumero).trim() : "";
|
|
|
|
try {
|
|
const cepRestData = await fetch(
|
|
'https://api.cep.rest/', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ cep })
|
|
}
|
|
).then(r => r.json());
|
|
if (!cepRestData || cepRestData.erro)
|
|
return res.status(404).json({ error: "CEP não encontrado" });
|
|
const logradouro = cepRestData.data.logradouro || "";
|
|
const bairro = cepRestData.data.bairro || "";
|
|
const cidade = cepRestData.data.localidade || "";
|
|
const uf = cepRestData.data.uf || "";
|
|
const endereco = `${logradouro}, ${numero}, ${bairro}, ${cidade} - ${uf}`
|
|
.replace(/, ,/g, ",")
|
|
.replace(/^,\s*/, "");
|
|
|
|
if (!process.env.GOOGLE_API_KEY)
|
|
return res
|
|
.status(500)
|
|
.json({ error: "GOOGLE_API_KEY não definida no servidor" });
|
|
const geo = await geocodeWithGoogle(endereco || `${cidade} ${uf} ${cep}`);
|
|
if (!geo)
|
|
return res
|
|
.status(404)
|
|
.json({ error: "geocode não encontrado (Google)" });
|
|
const lat = Number(geo.lat);
|
|
const lon = Number(geo.lon);
|
|
const result = await getMinDistance(lat, lon);
|
|
if (result && result.dist !== undefined) {
|
|
return res.json({
|
|
endereco,
|
|
latitude: lat,
|
|
longitude: lon,
|
|
distancia: result.dist,
|
|
parceiro: result.pastaSigla || "",
|
|
});
|
|
}
|
|
return res.json({
|
|
endereco,
|
|
latitude: lat,
|
|
longitude: lon,
|
|
distancia: "5km +",
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
return res.status(500).json({ error: "erro na consulta" });
|
|
}
|
|
});
|
|
|
|
////////////////////////////////////////////////////
|
|
|
|
// Servir arquivos estáticos (index.html)
|
|
// app.use("/public", express.static(path.join(__dirname, "public")));
|
|
|
|
// Usa as rotas de autenticação
|
|
app.use("/", authRoutes);
|
|
|
|
// servir arquivos estáticos da pasta public (rotas protegidas já são tratadas pelo middleware global)
|
|
app.use(express.static(path.join(__dirname, "public")));
|
|
|
|
// rota protegida que serve o index.html
|
|
app.get("/app", requireAuth, (req, res) => {
|
|
res.sendFile(path.join(__dirname, "public", "index.html"));
|
|
});
|
|
|
|
|
|
/////////////////////////////////////////////////////
|
|
|
|
return app;
|
|
}
|
|
|
|
module.exports = {
|
|
createApp,
|
|
};
|