refactor(routes): Phase 3.4 - standardize browse.js

This commit is contained in:
Krystie
2026-03-29 20:13:25 -07:00
parent e6e788fdce
commit 8b1492142f
2 changed files with 25 additions and 10 deletions

View File

@@ -4,8 +4,18 @@ const fsp = require('fs').promises;
const path = require('path');
const { exists, isAccessible } = require('../fs-helpers');
const { paginate, parsePaginationParams } = require('../pagination');
const { ValidationError } = require('../errors');
module.exports = function(ctx) {
/**
* Browse route factory
* @param {Object} deps - Explicit dependencies
* @param {Function} deps.asyncHandler - Async route handler wrapper
* @param {Function} deps.validateSecurePath - Path traversal validator
* @param {Object} deps.auditLogger - Audit logger
* @param {Object} deps.docker - Docker client
* @returns {express.Router}
*/
module.exports = function({ asyncHandler, validateSecurePath, auditLogger, docker }) {
const router = express.Router();
// Parse browse roots from environment
@@ -20,7 +30,7 @@ module.exports = function(ctx) {
});
// Get available browse roots
router.get('/browse/roots', ctx.asyncHandler(async (req, res) => {
router.get('/browse/roots', asyncHandler(async (req, res) => {
const allRoots = BROWSE_ROOTS.map(r => ({
name: r.hostPath,
path: r.hostPath,
@@ -38,7 +48,7 @@ module.exports = function(ctx) {
}, 'browse-roots'));
// Browse directory contents
router.get('/browse/directories', ctx.asyncHandler(async (req, res) => {
router.get('/browse/directories', asyncHandler(async (req, res) => {
const requestedPath = req.query.path || '';
if (!requestedPath) {
@@ -62,7 +72,7 @@ module.exports = function(ctx) {
);
if (!matchingRoot) {
return ctx.errorResponse(res, 400, 'Path not in browseable roots', {
throw new ValidationError('Path not in browseable roots', {
availableRoots: BROWSE_ROOTS.map(r => r.hostPath)
});
}
@@ -73,10 +83,10 @@ module.exports = function(ctx) {
const allowedRoots = BROWSE_ROOTS.map(r => r.containerPath);
let resolvedPath;
try {
resolvedPath = await ctx.validateSecurePath(containerFullPath, allowedRoots, ctx.auditLogger);
resolvedPath = await validateSecurePath(containerFullPath, allowedRoots, auditLogger);
} catch (error) {
if (error.constructor.name === 'ValidationError') {
ctx.auditLogger.logSecurityEvent('path_traversal_attempt', {
auditLogger.logSecurityEvent('path_traversal_attempt', {
requestedPath, containerFullPath, allowedRoots,
error: error.message,
ip: req.ip,
@@ -124,7 +134,7 @@ module.exports = function(ctx) {
}, 'browse-dir'));
// Detect media mounts from existing media server containers
router.get('/media/detected-mounts', ctx.asyncHandler(async (req, res) => {
router.get('/media/detected-mounts', asyncHandler(async (req, res) => {
const mediaServerPatterns = [
'plex', 'jellyfin', 'emby', 'kodi', 'navidrome', 'airsonic',
'subsonic', 'funkwhale', 'beets', 'lidarr', 'sonarr', 'radarr',
@@ -136,7 +146,7 @@ module.exports = function(ctx) {
'/tmp', '/var', '/etc', '/opt', '/root', '/home', '/.', '/caddyfile'
];
const containers = await ctx.docker.client.listContainers({ all: false });
const containers = await docker.client.listContainers({ all: false });
const detectedMounts = [];
const seenPaths = new Set();
@@ -145,7 +155,7 @@ module.exports = function(ctx) {
const isMediaServer = mediaServerPatterns.some(p => imageName.includes(p));
if (!isMediaServer) continue;
const container = ctx.docker.client.getContainer(containerInfo.Id);
const container = docker.client.getContainer(containerInfo.Id);
const details = await container.inspect();
const binds = details.HostConfig?.Binds || [];

View File

@@ -371,7 +371,12 @@ async function createApp() {
asyncHandler: ctx.asyncHandler
}));
apiRouter.use('/ca', caRoutes(ctx));
apiRouter.use(browseRoutes(ctx));
apiRouter.use(browseRoutes({
asyncHandler: ctx.asyncHandler,
validateSecurePath: ctx.validateSecurePath,
auditLogger: ctx.auditLogger,
docker: ctx.docker
}));
apiRouter.use(errorLogsRoutes({
ERROR_LOG_FILE: ctx.ERROR_LOG_FILE,
auditLogger: ctx.auditLogger,