Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Launchpad/backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "shuttle-platform-backend",
"version": "4.0.3",
"version": "4.1.0",
"private": true,
"scripts": {
"start": "node src/index.js",
Expand Down
8 changes: 8 additions & 0 deletions Launchpad/backend/src/checkout.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
const express = require('express');
const fs = require('fs');
const path = require('path');
const { checkShopPermission } = require('./users');
const router = express.Router();

const VALID_SLUG = /^[a-zA-Z0-9_-]+$/;

const SHOPS_DIR = path.join(__dirname, '..', 'shops');

function getSchemaPath(slug) {
Expand Down Expand Up @@ -66,6 +69,7 @@ const DEFAULT_SCHEMA = {
// GET /api/shops/:slug/checkout/schema
router.get('/shops/:slug/checkout/schema', (req, res) => {
const { slug } = req.params;
if (!VALID_SLUG.test(slug)) return res.status(400).json({ error: 'Invalid slug' });
const schemaPath = getSchemaPath(slug);
try {
if (fs.existsSync(schemaPath)) {
Expand All @@ -82,6 +86,10 @@ router.get('/shops/:slug/checkout/schema', (req, res) => {
// PUT /api/shops/:slug/checkout/schema
router.put('/shops/:slug/checkout/schema', (req, res) => {
const { slug } = req.params;
if (!VALID_SLUG.test(slug)) return res.status(400).json({ error: 'Invalid slug' });
if (!checkShopPermission(req, 'can_edit_ui')) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
const schema = req.body;
if (!schema || !Array.isArray(schema.sections)) {
return res.status(400).json({ error: 'Invalid schema: must have sections array' });
Expand Down
40 changes: 40 additions & 0 deletions Launchpad/backend/src/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,46 @@ router.delete('/:slug/files', (req, res) => {
res.json({ message: 'Deleted', path: relPath });
});

// POST /api/shops/:slug/files/rename body: { path, newPath }
// Renames or moves a file/directory within the shop. Covers:
// renaming collections, moving items between collections, renaming photos.
router.post('/:slug/files/rename', (req, res) => {
if (!checkShopPermission(req, 'can_edit_ui')) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
const { slug } = req.params;
const { path: relPath, newPath } = req.body || {};
if (!relPath || !newPath) {
return res.status(400).json({ error: 'path and newPath are required' });
}
if (relPath === '.' || newPath === '.') {
return res.status(400).json({ error: 'Cannot rename root directory' });
}

const resolvedSrc = safeShopPath(slug, relPath);
const resolvedDest = safeShopPath(slug, newPath);
if (!resolvedSrc || !resolvedDest) return res.status(400).json({ error: 'Invalid path' });
if (resolvedSrc === resolvedDest) return res.status(400).json({ error: 'Source and destination are the same' });

if (!fs.existsSync(resolvedSrc)) return res.status(404).json({ error: 'Source not found' });
if (fs.existsSync(resolvedDest)) return res.status(409).json({ error: 'Destination already exists' });

// Prevent moving a directory into itself
if (fs.statSync(resolvedSrc).isDirectory() && (resolvedDest + path.sep).startsWith(resolvedSrc + path.sep)) {
return res.status(400).json({ error: 'Cannot move a directory into itself' });
}

try {
fs.mkdirSync(path.dirname(resolvedDest), { recursive: true });
fs.renameSync(resolvedSrc, resolvedDest);
} catch (err) {
return res.status(500).json({ error: `Rename failed: ${err.message}` });
}

req.app.locals.auditLog?.('file_renamed', { req, details: { slug, path: relPath, newPath } });
res.json({ message: 'Renamed', path: relPath, newPath });
});

// POST /api/shops/:slug/files/upload-zip?path=DATABASE
const uploadZip = multer({
storage: multer.diskStorage({
Expand Down
Loading