Actualizar index.js
This commit is contained in:
parent
879d1a711f
commit
2a68ddf1bb
332
index.js
332
index.js
|
|
@ -1,253 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const { chromium } = require('playwright');
|
||||
const admin = require('firebase-admin');
|
||||
|
||||
const PORT = parseInt(process.env.PORT || process.env.CAPROVER_PORT || '80', 10);
|
||||
const REQUIRE_AUTH = String(process.env.REQUIRE_AUTH || '0') === '1';
|
||||
const HS_CRED_DOC_PATH = process.env.HS_CRED_DOC_PATH || 'secrets/homeserve';
|
||||
|
||||
const DEFAULT_SEL = {
|
||||
user: process.env.SEL_USER || 'input[type="text"]',
|
||||
pass: process.env.SEL_PASS || 'input[type="password"]',
|
||||
submit: process.env.SEL_SUBMIT || 'button[type="submit"]',
|
||||
|
||||
searchBox: process.env.SEL_SEARCH_BOX || 'input[placeholder*="Buscar"], input[type="search"]',
|
||||
searchBtn: process.env.SEL_SEARCH_BTN || 'button:has-text("Buscar"), button:has-text("Search")',
|
||||
openRow: process.env.SEL_OPEN_ROW || 'table tbody tr:first-child',
|
||||
|
||||
statusDropdown:
|
||||
process.env.SEL_STATUS_DROPDOWN ||
|
||||
'select[name*="estado"], select[id*="estado"], select:has(option)',
|
||||
|
||||
noteTextarea:
|
||||
process.env.SEL_NOTE_TEXTAREA ||
|
||||
'textarea[name*="nota"], textarea[id*="nota"], textarea',
|
||||
|
||||
saveBtn:
|
||||
process.env.SEL_SAVE_BTN ||
|
||||
'button:has-text("Guardar"), button:has-text("Save"), button:has-text("Actualizar")',
|
||||
};
|
||||
|
||||
function log(...args) {
|
||||
console.log('[estados-hs]', ...args);
|
||||
}
|
||||
|
||||
function safeStr(v) {
|
||||
return (v == null) ? '' : String(v);
|
||||
}
|
||||
|
||||
function stripDigits(v) {
|
||||
return safeStr(v).replace(/\D+/g, '');
|
||||
}
|
||||
|
||||
// ---------------- Firebase init ----------------
|
||||
|
||||
let firestore = null;
|
||||
|
||||
function initFirebaseOnce() {
|
||||
if (firestore) return firestore;
|
||||
|
||||
try {
|
||||
if (admin.apps && admin.apps.length) {
|
||||
firestore = admin.firestore();
|
||||
return firestore;
|
||||
}
|
||||
|
||||
if (process.env.FIREBASE_PRIVATE_KEY) {
|
||||
if (!process.env.FIREBASE_PROJECT_ID || !process.env.FIREBASE_CLIENT_EMAIL) {
|
||||
throw new Error('Missing env: FIREBASE_PROJECT_ID or FIREBASE_CLIENT_EMAIL');
|
||||
}
|
||||
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert({
|
||||
projectId: process.env.FIREBASE_PROJECT_ID,
|
||||
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
||||
privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
|
||||
}),
|
||||
});
|
||||
|
||||
firestore = admin.firestore();
|
||||
return firestore;
|
||||
}
|
||||
|
||||
admin.initializeApp({ credential: admin.credential.applicationDefault() });
|
||||
firestore = admin.firestore();
|
||||
return firestore;
|
||||
|
||||
} catch (e) {
|
||||
log('Firebase init error:', e?.message || e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyFirebaseIdToken(req) {
|
||||
const authHeader = req.headers.authorization || '';
|
||||
const m = authHeader.match(/^Bearer\s+(.+)$/i);
|
||||
if (!m) throw new Error('Missing Authorization Bearer token');
|
||||
|
||||
const idToken = m[1];
|
||||
initFirebaseOnce();
|
||||
const decoded = await admin.auth().verifyIdToken(idToken);
|
||||
return decoded;
|
||||
}
|
||||
|
||||
// ---------------- HomeServe creds cache ----------------
|
||||
|
||||
let cachedCreds = null;
|
||||
let cachedCredsAt = 0;
|
||||
const CREDS_TTL_MS = 60 * 1000;
|
||||
|
||||
async function loadHomeServeCreds() {
|
||||
const now = Date.now();
|
||||
if (cachedCreds && (now - cachedCredsAt) < CREDS_TTL_MS) return cachedCreds;
|
||||
|
||||
const db = initFirebaseOnce();
|
||||
|
||||
const envUser = process.env.HOMESERVE_USER;
|
||||
const envPass = process.env.HOMESERVE_PASS;
|
||||
const envBaseUrl = process.env.HOMESERVE_BASE_URL;
|
||||
|
||||
let fromFirestore = null;
|
||||
try {
|
||||
const ref = db.doc(HS_CRED_DOC_PATH);
|
||||
const snap = await ref.get();
|
||||
if (snap.exists) fromFirestore = snap.data() || {};
|
||||
} catch (e) {
|
||||
log('Warning: cannot read HS creds from Firestore:', e?.message || e);
|
||||
}
|
||||
|
||||
const baseUrl =
|
||||
safeStr(envBaseUrl || fromFirestore?.baseUrl || 'https://gestor.homeserve.es/').trim();
|
||||
|
||||
const user =
|
||||
safeStr(envUser || fromFirestore?.user || fromFirestore?.username || '').trim();
|
||||
|
||||
const pass =
|
||||
safeStr(envPass || fromFirestore?.pass || fromFirestore?.password || '').trim();
|
||||
|
||||
const selectors = { ...DEFAULT_SEL, ...(fromFirestore?.selectors || {}) };
|
||||
|
||||
if (!user || !pass) {
|
||||
throw new Error(
|
||||
`HomeServe credentials missing. Put user/pass in Firestore doc "${HS_CRED_DOC_PATH}" or set HOMESERVE_USER/HOMESERVE_PASS envs.`
|
||||
);
|
||||
}
|
||||
|
||||
cachedCreds = { baseUrl, user, pass, selectors };
|
||||
cachedCredsAt = now;
|
||||
return cachedCreds;
|
||||
}
|
||||
|
||||
// ---------------- Playwright helpers ----------------
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
async function withBrowser(fn) {
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
return await fn(page);
|
||||
} finally {
|
||||
await browser.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async function loginHomeServe(page, creds) {
|
||||
const { baseUrl, user, pass, selectors: SEL } = creds;
|
||||
|
||||
await page.goto(baseUrl, { waitUntil: 'domcontentloaded', timeout: 120000 });
|
||||
|
||||
await page.waitForSelector(SEL.user, { timeout: 60000 });
|
||||
await page.fill(SEL.user, user);
|
||||
|
||||
await page.waitForSelector(SEL.pass, { timeout: 60000 });
|
||||
await page.fill(SEL.pass, pass);
|
||||
|
||||
const btn = await page.$(SEL.submit);
|
||||
if (btn) await btn.click();
|
||||
else await page.keyboard.press('Enter');
|
||||
|
||||
await page.waitForLoadState('networkidle', { timeout: 120000 });
|
||||
}
|
||||
|
||||
async function openParte(page, parteId, SEL) {
|
||||
const hasSearch = await page.$(SEL.searchBox);
|
||||
if (hasSearch) {
|
||||
await page.fill(SEL.searchBox, String(parteId));
|
||||
const btn = await page.$(SEL.searchBtn);
|
||||
if (btn) await btn.click();
|
||||
else await page.keyboard.press('Enter');
|
||||
|
||||
await page.waitForLoadState('networkidle', { timeout: 120000 });
|
||||
await sleep(1200);
|
||||
}
|
||||
|
||||
await page.waitForSelector(SEL.openRow, { timeout: 60000 });
|
||||
await page.click(SEL.openRow);
|
||||
|
||||
await page.waitForLoadState('networkidle', { timeout: 120000 });
|
||||
await sleep(1200);
|
||||
}
|
||||
|
||||
async function selectStatusByCode(page, SEL, code) {
|
||||
const statusCode = safeStr(code).trim();
|
||||
if (!statusCode) throw new Error('Missing status code');
|
||||
|
||||
await page.waitForSelector(SEL.statusDropdown, { timeout: 60000 });
|
||||
|
||||
try {
|
||||
await page.selectOption(SEL.statusDropdown, { value: statusCode });
|
||||
return;
|
||||
} catch (_) {}
|
||||
|
||||
const r = await page.evaluate(({ sel, code }) => {
|
||||
const s = document.querySelector(sel);
|
||||
if (!s) return { ok: false, why: 'select not found' };
|
||||
|
||||
const opts = Array.from(s.querySelectorAll('option'));
|
||||
const hit =
|
||||
opts.find(o => (o.value || '').trim() === code) ||
|
||||
opts.find(o => ((o.textContent || '').replace(/\s+/g, ' ')).includes(code));
|
||||
|
||||
if (!hit) return { ok: false, why: 'no matching option' };
|
||||
|
||||
s.value = hit.value;
|
||||
s.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return { ok: true, value: hit.value, text: (hit.textContent || '').trim() };
|
||||
}, { sel: SEL.statusDropdown, code: statusCode });
|
||||
|
||||
if (!r || !r.ok) throw new Error(`No matching status option for code "${statusCode}"`);
|
||||
}
|
||||
|
||||
async function setEstado(page, SEL, code, noteMaybe) {
|
||||
await selectStatusByCode(page, SEL, code);
|
||||
|
||||
if (noteMaybe) {
|
||||
const ta = await page.$(SEL.noteTextarea);
|
||||
if (ta) await page.fill(SEL.noteTextarea, String(noteMaybe));
|
||||
}
|
||||
|
||||
const save = await page.$(SEL.saveBtn);
|
||||
if (!save) throw new Error('Save button not found');
|
||||
|
||||
await save.click();
|
||||
await page.waitForLoadState('networkidle', { timeout: 120000 });
|
||||
await sleep(1200);
|
||||
}
|
||||
|
||||
// ---------------- Express app ----------------
|
||||
|
||||
const app = express();
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.status(200).send('ok');
|
||||
});
|
||||
|
|
@ -256,85 +6,9 @@ app.get('/health', (req, res) => {
|
|||
res.status(200).json({
|
||||
ok: true,
|
||||
service: 'estados-hs',
|
||||
port: PORT,
|
||||
requireAuth: REQUIRE_AUTH,
|
||||
hsCredDocPath: HS_CRED_DOC_PATH,
|
||||
port: Number(process.env.PORT || 0),
|
||||
requireAuth: String(process.env.REQUIRE_AUTH || ''),
|
||||
hsCredDocPath: String(process.env.HS_CRED_DOC_PATH || ''),
|
||||
ts: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/homeserve/change-status', async (req, res) => {
|
||||
const startedAt = new Date().toISOString();
|
||||
|
||||
try {
|
||||
let decoded = null;
|
||||
if (REQUIRE_AUTH) {
|
||||
decoded = await verifyFirebaseIdToken(req);
|
||||
}
|
||||
|
||||
const serviceNumberRaw = req.body?.serviceNumber ?? req.body?.parteId ?? req.body?.parte ?? req.body?.codigo;
|
||||
const codeRaw = req.body?.code ?? req.body?.statusCode ?? req.body?.newStatusValue ?? req.body?.selectedCode;
|
||||
const dateString = safeStr(req.body?.dateString ?? '').trim();
|
||||
const observation = safeStr(req.body?.observation ?? req.body?.note ?? '').trim();
|
||||
|
||||
const serviceNumber = stripDigits(serviceNumberRaw);
|
||||
const code = stripDigits(codeRaw) || safeStr(codeRaw).trim();
|
||||
|
||||
if (!serviceNumber || serviceNumber.length < 5) {
|
||||
return res.status(400).json({ ok: false, error: 'Invalid serviceNumber' });
|
||||
}
|
||||
if (!code) {
|
||||
return res.status(400).json({ ok: false, error: 'Missing status code' });
|
||||
}
|
||||
|
||||
const creds = await loadHomeServeCreds();
|
||||
const SEL = creds.selectors;
|
||||
|
||||
const finalNote = [observation, dateString ? `Fecha: ${dateString}` : ''].filter(Boolean).join(' | ');
|
||||
|
||||
await withBrowser(async (page) => {
|
||||
await loginHomeServe(page, creds);
|
||||
await openParte(page, serviceNumber, SEL);
|
||||
await setEstado(page, SEL, code, finalNote || null);
|
||||
});
|
||||
|
||||
return res.json({
|
||||
ok: true,
|
||||
startedAtISO: startedAt,
|
||||
finishedAtISO: new Date().toISOString(),
|
||||
serviceNumber,
|
||||
code,
|
||||
uid: decoded?.uid || null,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
const msg = safeStr(e?.message || e);
|
||||
log('ERROR change-status:', msg);
|
||||
return res.status(500).json({
|
||||
ok: false,
|
||||
error: msg,
|
||||
startedAtISO: startedAt,
|
||||
finishedAtISO: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ ok: false, error: 'Not found' });
|
||||
});
|
||||
|
||||
const server = app.listen(PORT, () => {
|
||||
log(`listening on :${PORT}`);
|
||||
log(`HS_CRED_DOC_PATH=${HS_CRED_DOC_PATH}`);
|
||||
log(`REQUIRE_AUTH=${REQUIRE_AUTH ? '1' : '0'}`);
|
||||
log(`ENV PORT=${process.env.PORT || '(unset)'} CAPROVER_PORT=${process.env.CAPROVER_PORT || '(unset)'}`);
|
||||
});
|
||||
|
||||
function shutdown(signal) {
|
||||
log(`received ${signal}, shutting down...`);
|
||||
server.close(() => process.exit(0));
|
||||
setTimeout(() => process.exit(0), 3000).unref();
|
||||
}
|
||||
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
Loading…
Reference in New Issue