174 lines
11 KiB
JavaScript
174 lines
11 KiB
JavaScript
const { buildPagination } = require('#src/web/lib/pagination');
|
|
const renderApiSourcesPage = require('#src/web/routes/data-sources/api-sources/list');
|
|
const renderApiSourceAddPage = require('#src/web/routes/data-sources/api-sources/add');
|
|
const renderApiSourceEditPage = require('#src/web/routes/data-sources/api-sources/edit');
|
|
const { buildDuplicateApiSourceName, buildDuplicateApiSource } = require('#src/web/routes/data-sources/api-sources/duplicate');
|
|
const { fetchAppSettings } = require('#src/data/app-settings');
|
|
const { buildApiSourcePayload, testApiSourceConnection, testApiSourceAuthentication } = require('#src/data/api-sources');
|
|
const { buildApiGroupPayload, fetchApiGroupById, fetchApiGroupsPage, getApiRequestUsage, saveApiGroup } = require('#src/data/api-source-groups');
|
|
const { hasPermission } = require('#src/rbac');
|
|
|
|
module.exports = function registerApiSourceRoutes(app, deps) {
|
|
const { pool, common, backgroundTaskQueue, dataSourceTasks, getAuditUserId, redirectAfterSave, requirePermission } = deps;
|
|
const formatDate = deps.formatDashboardDate || String;
|
|
const base = '/data-sources/api-sources';
|
|
|
|
function syncSchedules(group, removed, actorId) {
|
|
(removed || []).forEach(function (id) { dataSourceTasks.removeRecurringRefresh('api-source', id); });
|
|
group.requests.forEach(function (request) {
|
|
if (group.enabled === 0 || group.enabled === false || !request.enabled) return dataSourceTasks.removeRecurringRefresh('api-source', request.id);
|
|
dataSourceTasks.registerRecurringRefresh('api-source', request.id, group.name + ' / ' + request.name, request.update_interval_value || request.updateIntervalValue, request.update_interval_unit || request.updateIntervalUnit, function () {
|
|
return dataSourceTasks.refreshApiSourceInBackground(request.id, actorId);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function enqueue(request, group, actorId) {
|
|
return backgroundTaskQueue.enqueueTask({
|
|
key: 'api-source-refresh:' + request.id, title: 'API request refresh', category: 'data-source', taskType: 'data-source-refresh',
|
|
payload: { sourceType: 'api-source', sourceId: request.id, sourceName: group.name + ' / ' + request.name, actorId: actorId },
|
|
metadata: { sourceType: 'api-source', sourceId: request.id, sourceName: group.name + ' / ' + request.name }
|
|
});
|
|
}
|
|
|
|
async function testConnection(req, res) {
|
|
res.set('Cache-Control', 'no-store');
|
|
try {
|
|
const body = Object.assign({}, req.body, { name: 'Connection test' });
|
|
const authOnly = String(req.path || '').endsWith('/auth-test');
|
|
if (authOnly) body.api_url = 'https://connection.test';
|
|
const payload = buildApiSourcePayload({ body: body });
|
|
res.json(await (authOnly ? testApiSourceAuthentication(payload) : testApiSourceConnection(payload)));
|
|
} catch (error) {
|
|
res.status(error.statusCode === 400 ? 400 : 500).json({ ok: false, error: error.statusCode === 400 ? error.message : 'Unable to test API connection.', steps: [] });
|
|
}
|
|
}
|
|
app.post(base + '/test', requirePermission('api-sources.create'), testConnection);
|
|
app.post(base + '/auth-test', requirePermission('api-sources.create'), testConnection);
|
|
app.post(base + '/:id/test', requirePermission('api-sources.update'), testConnection);
|
|
app.post(base + '/:id/auth-test', requirePermission('api-sources.update'), testConnection);
|
|
|
|
app.get(base, requirePermission('api-sources.read'), async function (req, res, next) {
|
|
try {
|
|
const search = common.getSearchQuery(req);
|
|
const sort = common.getSortQuery(req);
|
|
const direction = common.getSortDirectionQuery(req);
|
|
const data = await fetchApiGroupsPage(pool, Math.max(1, Number(req.query.page) || 1), 25, search, sort, direction);
|
|
res.send(renderApiSourcesPage({ apiSources: data.apiSources.map(function (group) {
|
|
return Object.assign({}, group, { lastPullLabel: group.last_pulled_at ? formatDate(group.last_pulled_at) : 'Never', lastPulledAtValue: group.last_pulled_at ? new Date(group.last_pulled_at).toISOString() : '' });
|
|
}), pagination: buildPagination(data.totalItems, data.currentPage, 'page', { search, sort, direction }, 25, 'API sources', 'API source pages') }, req.query.message || '', req.currentUser, formatDate));
|
|
} catch (error) { next(error); }
|
|
});
|
|
|
|
app.get(base + '/new', requirePermission('api-sources.create'), async function (req, res, next) {
|
|
try {
|
|
const settings = await fetchAppSettings(pool);
|
|
res.send(renderApiSourceAddPage({ updateIntervalValue: settings['data-sources.api_default_interval_value'], updateIntervalUnit: settings['data-sources.api_default_interval_unit'] }, req.query.message || '', req.currentUser));
|
|
} catch (error) { next(error); }
|
|
});
|
|
|
|
app.get(base + '/:id/edit', requirePermission('api-sources.update'), async function (req, res, next) {
|
|
try {
|
|
const group = await fetchApiGroupById(pool, Number(req.params.id));
|
|
if (!group) return res.status(404).send('API source not found');
|
|
const usage = await getApiRequestUsage(pool);
|
|
group.requests = group.requests.map(function (request) { return Object.assign(request, { inUse: usage.has(Number(request.id)) }); });
|
|
group.inUse = group.requests.some(function (request) { return request.inUse; });
|
|
res.send(renderApiSourceEditPage(group, {}, req.query.message || '', req.currentUser));
|
|
} catch (error) { next(error); }
|
|
});
|
|
|
|
app.get(base + '/:id/duplicate', requirePermission('api-sources.read'), requirePermission('api-sources.create'), async function (req, res, next) {
|
|
try {
|
|
const group = await fetchApiGroupById(pool, Number(req.params.id));
|
|
if (!group) return res.status(404).send('API source not found');
|
|
const initialName = buildDuplicateApiSourceName(group.name);
|
|
let name = initialName;
|
|
let suffix = 2;
|
|
while (await common.fetchDuplicateName(pool, 'i_api_source_groups', name)) name = initialName + ' (' + suffix++ + ')';
|
|
const duplicate = buildDuplicateApiSource(group, name);
|
|
res.send(renderApiSourceAddPage(duplicate, 'Review the copied values and save when ready.', req.currentUser, { messageVariant: 'info' }));
|
|
} catch (error) { next(error); }
|
|
});
|
|
|
|
async function save(req, res, next) {
|
|
try {
|
|
const id = req.params.id ? Number(req.params.id) : null;
|
|
if (req.body.data_source_action === 'toggle') {
|
|
const group = await fetchApiGroupById(pool, id);
|
|
if (!group) return res.status(404).send('API source not found');
|
|
const enabled = group.enabled === 0 || group.enabled === false ? 1 : 0;
|
|
await pool.query('UPDATE i_api_source_groups SET enabled = ?, modified_by = ? WHERE id = ?', [enabled, getAuditUserId(req), id]);
|
|
group.enabled = enabled;
|
|
syncSchedules(group, [], getAuditUserId(req));
|
|
return res.redirect(base + '/' + id + '/edit?message=' + encodeURIComponent(enabled ? 'API source enabled.' : 'API source disabled.'));
|
|
}
|
|
const payload = buildApiGroupPayload(req.body);
|
|
const result = await saveApiGroup(pool, id, payload, getAuditUserId(req), hasPermission(req.currentUser, 'api-sources.delete'));
|
|
syncSchedules({ name: payload.name, enabled: result.enabled, requests: result.requests }, result.removed, getAuditUserId(req));
|
|
if (!id) {
|
|
for (const request of result.requests) { if (request.enabled) await enqueue(request, payload, getAuditUserId(req)); }
|
|
}
|
|
redirectAfterSave(req, res, base + '/' + result.id + '/edit', { closeUrl: base, newUrl: base + '/new', message: id ? 'API source updated.' : 'API source created. Initial requests queued.' });
|
|
} catch (error) { next(error); }
|
|
}
|
|
app.post(base, requirePermission('api-sources.create'), save);
|
|
app.post(base + '/:id', requirePermission('api-sources.update'), save);
|
|
|
|
async function refresh(req, res, next) {
|
|
try {
|
|
const group = await fetchApiGroupById(pool, Number(req.params.id));
|
|
if (!group) return res.status(404).send('API source not found');
|
|
if (group.enabled === 0 || group.enabled === false) return res.status(409).json({ error: 'API source is disabled.' });
|
|
const requests = req.params.requestId ? group.requests.filter(function (request) { return Number(request.id) === Number(req.params.requestId); }) : group.requests;
|
|
if (!requests.length) return res.status(404).send('API request not found');
|
|
const tasks = [];
|
|
for (const request of requests) { if (request.enabled) tasks.push(await enqueue(request, group, getAuditUserId(req))); }
|
|
if (req.params.requestId && req.get('Accept') === 'application/json') {
|
|
if (!tasks.length) return res.status(409).json({ error: 'API request is disabled.' });
|
|
return res.status(202).json({
|
|
taskId: tasks[0].id,
|
|
stateUrl: base + '/' + group.id + '/requests/' + Number(req.params.requestId) + '/state',
|
|
message: 'Refreshing saved API request. Unsaved changes are kept.'
|
|
});
|
|
}
|
|
res.redirect(base + '/' + group.id + '/edit?message=' + encodeURIComponent('Enabled API request refreshes queued.'));
|
|
} catch (error) { next(error); }
|
|
}
|
|
app.post(base + '/:id/refresh', requirePermission('api-sources.allow'), refresh);
|
|
app.post(base + '/:id/requests/:requestId/refresh', requirePermission('api-sources.allow'), refresh);
|
|
|
|
app.get(base + '/:id/requests/:requestId/state', requirePermission('api-sources.allow'), async function (req, res, next) {
|
|
res.set('Cache-Control', 'no-store');
|
|
try {
|
|
const taskId = Number(req.query.refresh_task_id);
|
|
if (!Number.isSafeInteger(taskId) || taskId <= 0) return res.status(400).json({ error: 'Missing refresh task id.' });
|
|
const [requests] = await pool.query('SELECT id FROM i_api_sources WHERE id = ? AND auth_source_id = ?', [Number(req.params.requestId), Number(req.params.id)]);
|
|
if (!requests.length) return res.status(404).json({ error: 'API request not found.' });
|
|
const task = await dataSourceTasks.getTaskStatusById(taskId);
|
|
if (!task || task.key !== 'api-source-refresh:' + Number(req.params.requestId)) return res.status(404).json({ error: 'Refresh task not found.' });
|
|
res.json(task);
|
|
} catch (error) { next(error); }
|
|
});
|
|
|
|
app.post(base + '/:id/delete', requirePermission('api-sources.delete'), async function (req, res, next) {
|
|
const connection = await pool.getConnection();
|
|
try {
|
|
await connection.beginTransaction();
|
|
const [groups] = await connection.query('SELECT id FROM i_api_source_groups WHERE id = ? FOR UPDATE', [Number(req.params.id)]);
|
|
if (!groups.length) { await connection.rollback(); return res.status(404).send('API source not found'); }
|
|
const [requests] = await connection.query('SELECT id FROM i_api_sources WHERE auth_source_id = ? FOR UPDATE', [Number(req.params.id)]);
|
|
const usage = await getApiRequestUsage(connection);
|
|
if (requests.some(function (request) { return usage.has(Number(request.id)); })) {
|
|
await connection.rollback();
|
|
return res.redirect(base + '?message=' + encodeURIComponent('This API source has requests used by slides or template defaults.'));
|
|
}
|
|
await connection.query('DELETE FROM i_api_sources WHERE auth_source_id = ?', [Number(req.params.id)]);
|
|
await connection.query('DELETE FROM i_api_source_groups WHERE id = ?', [Number(req.params.id)]);
|
|
await connection.commit();
|
|
requests.forEach(function (request) { dataSourceTasks.removeRecurringRefresh('api-source', request.id); });
|
|
res.redirect(base + '?message=' + encodeURIComponent('API source deleted.'));
|
|
} catch (error) { await connection.rollback(); next(error); }
|
|
finally { connection.release(); }
|
|
});
|
|
}; |