feat: Add history endpoints for resource versioning and implement rollback functionality in server, along with UI integration in ASNs, Domains, and IPRanges managers for enhanced data management
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 4m45s

This commit is contained in:
2025-08-11 17:29:41 +07:00
parent 460c06dfba
commit 6f45015bec
5 changed files with 189 additions and 3 deletions
+52 -3
View File
@@ -1,3 +1,39 @@
// History endpoints (require bucket versioning to be enabled). If versioning disabled, best effort.
app.get('/api/history/:resource', async (req, res) => {
const { resource } = req.params;
const key = resourceToKey(resource);
if (!key) return sendError(res, 400, 'Unknown resource', 'E_RESOURCE');
try {
if (!s3.listObjectVersions) {
return sendError(res, 501, 'S3 listObjectVersions not available', 'E_NOT_SUPPORTED');
}
const out = await s3.listObjectVersions({ Bucket: BUCKET_NAME, Prefix: key, MaxKeys: 20 }).promise();
const versions = (out.Versions || [])
.filter(v => v.Key === key)
.slice(0, 10)
.map(v => ({ versionId: v.VersionId, isLatest: v.IsLatest, lastModified: toIso(v.LastModified), size: v.Size, etag: v.ETag }));
return res.json({ items: versions });
} catch (e) {
console.error('history error', e);
return sendError(res, 500, 'Error reading history', 'E_S3', { error: String(e?.message || e) });
}
});
app.post('/api/history/:resource/rollback', async (req, res) => {
const { resource } = req.params;
const { versionId } = req.body || {};
const key = resourceToKey(resource);
if (!key || !versionId) return sendError(res, 400, 'Bad request', 'E_BAD_REQUEST');
try {
// Copy specific version over same key to rollback
await s3.copyObject({ Bucket: BUCKET_NAME, CopySource: `/${BUCKET_NAME}/${encodeURIComponent(key)}?versionId=${encodeURIComponent(versionId)}`, Key: key }).promise();
const meta = await headMeta(key);
return sendOk(res, meta);
} catch (e) {
console.error('rollback error', e);
return sendError(res, 500, 'Error rollback', 'E_S3', { error: String(e?.message || e) });
}
});
require('dotenv').config();
const express = require('express');
const AWS = require('aws-sdk');
@@ -51,6 +87,16 @@ function sendError(res, status, message, code, details) {
return res.status(status).json({ message, code, details });
}
// Map UI resource -> S3 key (for history endpoints)
function resourceToKey(resource) {
switch (resource) {
case 'domains-new': return 'bgp_data/domains_community.txt';
case 'ip-ranges': return 'bgp_data/ips.txt';
case 'asns': return 'bgp_data/asns.txt';
default: return null;
}
}
// Serve static files from the React app
app.use(express.static(path.join(__dirname, 'public')));
@@ -410,7 +456,8 @@ app.post('/api/asns', async (req, res) => {
let current = null;
try { current = await headS3ObjectEtag('bgp_data/asns.txt'); } catch {}
if (current && etag && current !== String(etag)) {
return sendError(res, 412, 'Precondition Failed: ETag mismatch', 'E_ETAG_MISMATCH', { currentEtag: current });
const meta = await headMeta('bgp_data/asns.txt');
return sendError(res, 412, 'Precondition Failed: ETag mismatch', 'E_ETAG_MISMATCH', { currentEtag: current, meta });
}
} catch {}
@@ -498,7 +545,8 @@ app.post('/api/domains-new', async (req, res) => {
let current = null;
try { current = await headS3ObjectEtag('bgp_data/domains_community.txt'); } catch {}
if (current && etag && current !== String(etag)) {
return sendError(res, 412, 'Precondition Failed: ETag mismatch', 'E_ETAG_MISMATCH', { currentEtag: current });
const meta = await headMeta('bgp_data/domains_community.txt');
return sendError(res, 412, 'Precondition Failed: ETag mismatch', 'E_ETAG_MISMATCH', { currentEtag: current, meta });
}
} catch {}
@@ -586,7 +634,8 @@ app.post('/api/ip-ranges', async (req, res) => {
let current = null;
try { current = await headS3ObjectEtag('bgp_data/ips.txt'); } catch {}
if (current && etag && current !== String(etag)) {
return sendError(res, 412, 'Precondition Failed: ETag mismatch', 'E_ETAG_MISMATCH', { currentEtag: current });
const meta = await headMeta('bgp_data/ips.txt');
return sendError(res, 412, 'Precondition Failed: ETag mismatch', 'E_ETAG_MISMATCH', { currentEtag: current, meta });
}
} catch {}