Chybova hlaseni konektoru rikaji, co se stalo, a kam to slo
Duvod od sluzby jde primo do hlasky: z tela odpovedi se vytahne detail, error_description, message, title, error i seznam missingHeaders. Retezec, ktery vypada jako JSON, se rozbaluje dal - sluzba iDoklad presne takhle predava telo od iDokladu samotneho. Kdyz sluzba nenapsala nic, rekne se to. 401 a 403 uz nejsou jedna hlaska. 401 = udaje sluzba dostala a neuznala je. 403 = tvar udaju je v poradku, zakazuje se samo volani. V kazde hlasce je cela adresa vcetne serveru (ScriptRequestInfo.url), bez query - v query muze byt tajemstvi. Zaklad adresy je z konfigurace a konektor ho smi prepsat, takze se neda odvodit z toho, kde je nasazeny portal. Adresa je videt i na karte konektoru a v odpovedi na test, i kdyz overeni projde. Tlacitko Logy na karte konektoru a historie poslednich peti overeni. Odpoved sluzby dosud existovala jen v odpovedi na test, tedy do prekresleni stranky, a v logu containeru. Do logu containeru se nikdo divat nechodi. Zaznam se uklada i pri uspechu, jinak by neslo poznat, jestli konektor nesel nikdy, nebo prestal jit ve chvili, kdy nekdo sahnul na udaje. Migrace 003_connector_checks.sql, endpoint GET /connectors/:id/checks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6297bbf480
commit
5a124a53d8
+89
-18
@@ -19,6 +19,7 @@ import { Router, type Request, type Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { accessFor } from '../data/access.js';
|
||||
import {
|
||||
CHECK_HISTORY,
|
||||
connectorCountsByService,
|
||||
createConnector,
|
||||
deleteConnector,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
toPublicConnector,
|
||||
updateConnector,
|
||||
validateConnectorValues,
|
||||
type ConnectorCheck,
|
||||
} from '../data/connectorStore.js';
|
||||
import {
|
||||
canSeeService,
|
||||
@@ -255,6 +257,11 @@ connectorsRouter.delete('/:id', async (req, res) => {
|
||||
* Vola `verifyPath` sluzby, coz je zamerne **cteci** volani, ktere vyzaduje
|
||||
* autorizaci. Kdyz ho sluzba nema, overi se jen `/health` - a odpoved to rekne
|
||||
* nahlas, aby si nikdo nemyslel, ze jsou overene i pristupove udaje.
|
||||
*
|
||||
* Ve vysledku je vzdy **cela adresa vcetne serveru**, i kdyz overeni projde.
|
||||
* Zaklad adresy je z konfigurace (`SERVICES_BASE_URL`) a konektor ho smi
|
||||
* prepsat, takze "vratilo 403" bez serveru nerika, jestli se to vubec trefilo
|
||||
* na spravny stroj. Hadat to podle toho, kde je nasazeny portal, nejde.
|
||||
*/
|
||||
connectorsRouter.post('/:id/test', async (req, res) => {
|
||||
const tenantId = tenantOrDeny(req, res);
|
||||
@@ -269,16 +276,29 @@ connectorsRouter.post('/:id/test', async (req, res) => {
|
||||
|
||||
const target = resolveTarget(service.id, connector);
|
||||
if (!target.ready) {
|
||||
await setConnectorStatus(connector.id, 'error', target.missing.join(', '), [tenantId]);
|
||||
return res.json({
|
||||
ok: false,
|
||||
checked: 'nic',
|
||||
message: `Napojení není hotové: ${target.missing.join(', ')}.`,
|
||||
});
|
||||
const message = `Napojení není hotové: ${target.missing.join(', ')}.`;
|
||||
await setConnectorStatus(
|
||||
connector.id,
|
||||
'error',
|
||||
message,
|
||||
{
|
||||
at: new Date().toISOString(),
|
||||
ok: false,
|
||||
checked: 'nic',
|
||||
status: null,
|
||||
message,
|
||||
detail: null,
|
||||
request: null,
|
||||
},
|
||||
[tenantId],
|
||||
);
|
||||
return res.json({ ok: false, checked: 'nic', message, baseUrl: target.baseUrl });
|
||||
}
|
||||
|
||||
const path = service.verifyPath ?? '/health';
|
||||
const checked = service.verifyPath ? 'přístupové údaje' : 'jen dostupnost služby';
|
||||
// Zaloha pro pripad, ze se k volani vubec nedoslo a chyba tedy `request` nema.
|
||||
const verifyUrl = `${target.baseUrl.replace(/\/+$/, '')}${path}`;
|
||||
const redact = createRedactor(Object.values(target.headers));
|
||||
|
||||
const controller = new AbortController();
|
||||
@@ -295,43 +315,94 @@ connectorsRouter.post('/:id/test', async (req, res) => {
|
||||
});
|
||||
|
||||
const response = await http.get(path);
|
||||
await setConnectorStatus(connector.id, 'ok', null, [tenantId]);
|
||||
const message = service.verifyPath
|
||||
? 'Napojení funguje, přístupové údaje jsou platné.'
|
||||
: 'Služba odpovídá. Přístupové údaje se tímhle neověřily, služba na to nemá čtecí volání.';
|
||||
const request = { method: 'GET', path, url: verifyUrl };
|
||||
|
||||
// Do historie patri i uspech. Bez nej se neda poznat, jestli konektor
|
||||
// nesel nikdy, nebo prestal jit ve chvili, kdy se sahlo na udaje.
|
||||
await setConnectorStatus(
|
||||
connector.id,
|
||||
'ok',
|
||||
null,
|
||||
{
|
||||
at: new Date().toISOString(),
|
||||
ok: true,
|
||||
checked,
|
||||
status: response.status,
|
||||
message,
|
||||
detail: null,
|
||||
request,
|
||||
},
|
||||
[tenantId],
|
||||
);
|
||||
|
||||
return res.json({
|
||||
ok: true,
|
||||
checked,
|
||||
status: response.status,
|
||||
request: { method: 'GET', path },
|
||||
message: service.verifyPath
|
||||
? 'Napojení funguje, přístupové údaje jsou platné.'
|
||||
: 'Služba odpovídá. Přístupové údaje se tímhle neověřily, služba na to nemá čtecí volání.',
|
||||
request,
|
||||
message,
|
||||
baseUrl: target.baseUrl,
|
||||
});
|
||||
} catch (err) {
|
||||
// Cela odpoved sluzby, ne jen "HTTP 401". Duvod je napsany prave v ni.
|
||||
const isScriptError = err instanceof ScriptError;
|
||||
const message = isScriptError ? redact(err.message) : redact(describe(err, 400));
|
||||
const status = isScriptError ? err.status : undefined;
|
||||
const detail = isScriptError
|
||||
? err.detail
|
||||
? redact(err.detail)
|
||||
: undefined
|
||||
: redact(truncate(String(err instanceof Error ? err.stack ?? err.message : err), config.errorDetailBytes));
|
||||
|
||||
await setConnectorStatus(connector.id, 'error', message, [tenantId]);
|
||||
console.warn(
|
||||
`[connectors] test ${connector.id} selhal: ${message}` + (detail ? `
|
||||
${detail}` : ''),
|
||||
);
|
||||
const request =
|
||||
isScriptError && err.request ? err.request : { method: 'GET', path, url: verifyUrl };
|
||||
const check: ConnectorCheck = {
|
||||
at: new Date().toISOString(),
|
||||
ok: false,
|
||||
checked,
|
||||
status: status ?? null,
|
||||
message,
|
||||
detail: detail ?? null,
|
||||
request,
|
||||
};
|
||||
|
||||
await setConnectorStatus(connector.id, 'error', message, check, [tenantId]);
|
||||
console.warn(`[connectors] test ${connector.id} selhal: ${message}`);
|
||||
|
||||
// Neuspesne overeni neni chyba API, je to vysledek. Proto 200.
|
||||
return res.json({
|
||||
ok: false,
|
||||
checked,
|
||||
message,
|
||||
...(isScriptError && err.status !== undefined ? { status: err.status } : {}),
|
||||
request: isScriptError && err.request ? err.request : { method: 'GET', path },
|
||||
...(status !== undefined ? { status } : {}),
|
||||
request,
|
||||
baseUrl: target.baseUrl,
|
||||
...(detail ? { detail } : {}),
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Historie overeni konektoru.
|
||||
*
|
||||
* Zvlast, ne v seznamu konektoru: kazdy zaznam nese cele telo odpovedi sluzby
|
||||
* a u peti konektoru by to byly desitky kilobajtu, ktere nikdo necte.
|
||||
* Nacte se az kdyz o to nekdo v portalu opravdu stoji.
|
||||
*/
|
||||
connectorsRouter.get('/:id/checks', async (req, res) => {
|
||||
const tenantId = tenantOrDeny(req, res);
|
||||
if (!tenantId) return;
|
||||
|
||||
const connector = await getConnector(req.params.id, [tenantId]);
|
||||
if (!connector) {
|
||||
return res.status(404).json({ error: 'not_found', message: 'Konektor neexistuje.' });
|
||||
}
|
||||
if (!serviceOrDeny(req, res, connector.serviceId, tenantId)) return;
|
||||
|
||||
return res.json({ items: connector.checks, limit: CHECK_HISTORY });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user