From ac2cb5b88316549f6915c75b04a2973d848d4a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80?= <78488229+viktor138irk@users.noreply.github.com> Date: Fri, 8 May 2026 15:23:18 +0900 Subject: [PATCH] Add FastPanel safe frontend deploy script --- deploy/deploy-agent/src/deploy-frontend.js | 60 ++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 deploy/deploy-agent/src/deploy-frontend.js diff --git a/deploy/deploy-agent/src/deploy-frontend.js b/deploy/deploy-agent/src/deploy-frontend.js new file mode 100644 index 0000000..80409be --- /dev/null +++ b/deploy/deploy-agent/src/deploy-frontend.js @@ -0,0 +1,60 @@ +import dotenv from 'dotenv'; +import { execFileSync } from 'node:child_process'; +import { existsSync, realpathSync, statSync } from 'node:fs'; +import path from 'node:path'; + +dotenv.config(); + +const sourcePath = process.env.FRONTEND_DEPLOY_SOURCE_PATH || '/opt/raspi-chat/source'; +const adminWebroot = process.env.FRONTEND_DEPLOY_ADMIN_WEBROOT; +const widgetWebroot = process.env.FRONTEND_DEPLOY_WIDGET_WEBROOT; +const branch = process.env.FRONTEND_DEPLOY_BRANCH || 'main'; +const safeMode = process.env.FASTPANEL_SAFE_MODE !== 'false'; + +function run(command, args, cwd = sourcePath) { + console.log(`$ ${command} ${args.join(' ')}`); + execFileSync(command, args, { cwd, stdio: 'inherit' }); +} + +function assertDirectory(label, target) { + if (!target) throw new Error(`${label} is required`); + if (!existsSync(target)) throw new Error(`${label} does not exist: ${target}`); + if (!statSync(target).isDirectory()) throw new Error(`${label} is not a directory: ${target}`); +} + +function assertFastPanelSafe(label, target) { + if (!safeMode) return; + const real = realpathSync(target); + const normalized = real.replaceAll('\\', '/'); + const valid = normalized.startsWith('/var/www/') && normalized.includes('/data/www/'); + if (!valid) { + throw new Error(`${label} must be inside FastPanel webroot /var/www//data/www/. Got: ${real}`); + } + if (['/var/www', '/var/www/'].includes(normalized)) { + throw new Error(`${label} points to a dangerous parent directory: ${real}`); + } +} + +assertDirectory('Source path', sourcePath); +assertDirectory('Admin webroot', adminWebroot); +assertDirectory('Widget webroot', widgetWebroot); +assertFastPanelSafe('Admin webroot', adminWebroot); +assertFastPanelSafe('Widget webroot', widgetWebroot); + +run('git', ['fetch', 'origin', branch]); +run('git', ['checkout', branch]); +run('git', ['pull', '--ff-only', 'origin', branch]); +run('npm', ['ci']); +run('npm', ['run', 'build:admin']); +run('npm', ['run', 'build:widget']); + +const adminDist = path.join(sourcePath, 'admin-panel', 'dist') + '/'; +const widgetDist = path.join(sourcePath, 'widget', 'dist') + '/'; + +assertDirectory('Admin dist', adminDist); +assertDirectory('Widget dist', widgetDist); + +run('rsync', ['-a', '--delete', adminDist, `${adminWebroot}/`], sourcePath); +run('rsync', ['-a', '--delete', widgetDist, `${widgetWebroot}/`], sourcePath); + +console.log('Frontend deployment finished safely. FastPanel configs were not touched.');