From 087e0eb491ed6b93e5b16f737bf1b963a931e3d3 Mon Sep 17 00:00:00 2001 From: Denis Shatskiy Date: Mon, 11 Aug 2025 17:41:04 +0700 Subject: [PATCH] refactor: Reintroduce history endpoints for resource versioning and rollback functionality in server, ensuring proper initialization and organization of code --- backend/server.js | 90 ++++++++++++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 36 deletions(-) diff --git a/backend/server.js b/backend/server.js index 41d27a9..ddf0241 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,39 +1,4 @@ -// 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) }); - } -}); +// History endpoints are registered below after app is initialized require('dotenv').config(); const express = require('express'); const AWS = require('aws-sdk'); @@ -42,6 +7,7 @@ const path = require('path'); const compression = require('compression'); const Ajv = require('ajv'); const net = require('net'); +const crypto = require('crypto'); const app = express(); const port = 3001; @@ -97,6 +63,21 @@ function resourceToKey(resource) { } } +// Compute sha256 of string +function sha256OfString(s) { + return crypto.createHash('sha256').update(Buffer.from(String(s), 'utf-8')).digest('hex'); +} + +function mapAjvErrors(errors) { + if (!Array.isArray(errors)) return []; + return errors.map((e) => ({ + message: e.message, + instancePath: e.instancePath, + keyword: e.keyword, + params: e.params, + })); +} + // Serve static files from the React app app.use(express.static(path.join(__dirname, 'public'))); @@ -1020,6 +1001,43 @@ app.delete('/api/locks/:resource', (req, res) => { res.json({ released: true }); }); +// 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) }); + } +}); + // Generate MikroTik configuration from filters app.get('/api/filters/generate-config', async (req, res) => { const params = {