"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.docsRouter = void 0; const express_1 = require("express"); const auth_middleware_1 = require("../../middleware/auth.middleware"); const rbac_middleware_1 = require("../../middleware/rbac.middleware"); const env_1 = require("../../config/env"); const logger_1 = require("../../utils/logger"); const health_check_1 = require("../../utils/health-check"); const metrics_1 = require("../../utils/metrics"); const docs_files_service_1 = require("./docs-files.service"); const mkdocs_config_service_1 = require("./mkdocs-config.service"); const router = (0, express_1.Router)(); router.use(auth_middleware_1.authenticate); router.use(rbac_middleware_1.requireNonTemp); // Removed duplicated isServiceOnline - now using shared utility from utils/health-check.ts // GET /api/docs/status — check MkDocs and Code Server availability router.get('/status', async (_req, res, next) => { try { const [mkdocsOnline, codeServerOnline, siteServerOnline] = await Promise.all([ (0, health_check_1.isServiceOnline)(env_1.env.MKDOCS_PREVIEW_URL), (0, health_check_1.isServiceOnline)(env_1.env.CODE_SERVER_URL), (0, health_check_1.isServiceOnline)(env_1.env.MKDOCS_SITE_SERVER_URL), ]); res.json({ mkdocs: { online: mkdocsOnline, url: env_1.env.MKDOCS_PREVIEW_URL }, codeServer: { online: codeServerOnline, url: env_1.env.CODE_SERVER_URL }, siteServer: { online: siteServerOnline, url: env_1.env.MKDOCS_SITE_SERVER_URL }, }); } catch (err) { logger_1.logger.error('Failed to check docs status', err); next(err); } }); // GET /api/docs/config — return public-facing port numbers for iframe URLs router.get('/config', async (_req, res, _next) => { res.json({ codeServerPort: env_1.env.CODE_SERVER_PORT, mkdocsPort: env_1.env.MKDOCS_PORT, mkdocsSitePort: env_1.env.MKDOCS_SITE_SERVER_PORT, }); }); // --- MkDocs Config Endpoints --- // GET /api/docs/mkdocs-config — read raw mkdocs.yml content router.get('/mkdocs-config', async (_req, res, next) => { try { const content = await mkdocs_config_service_1.mkdocsConfigService.readConfig(); res.json({ content }); } catch (err) { logger_1.logger.error('Failed to read mkdocs config', err); next(err); } }); // PUT /api/docs/mkdocs-config — validate + write mkdocs.yml (SUPER_ADMIN only) router.put('/mkdocs-config', (0, rbac_middleware_1.requireRole)('SUPER_ADMIN'), async (req, res, next) => { try { const { content } = req.body; if (typeof content !== 'string') { res.status(400).json({ error: { message: 'Content string required', code: 'VALIDATION_ERROR' } }); return; } await mkdocs_config_service_1.mkdocsConfigService.writeConfig(content); res.json({ success: true }); } catch (err) { if (err.message?.startsWith('Invalid YAML')) { res.status(400).json({ error: { message: err.message, code: 'VALIDATION_ERROR' } }); return; } logger_1.logger.error('Failed to write mkdocs config', err); next(err); } }); // POST /api/docs/build — trigger mkdocs build in container (SUPER_ADMIN only) router.post('/build', (0, rbac_middleware_1.requireRole)('SUPER_ADMIN'), async (_req, res, next) => { try { const result = await mkdocs_config_service_1.mkdocsConfigService.triggerBuild(); res.json(result); } catch (err) { logger_1.logger.error('MkDocs build failed', err); next(err); } }); // --- File Management Endpoints --- // GET /api/docs/files — list file tree router.get('/files', async (_req, res, next) => { try { metrics_1.cm_docs_operations.inc({ operation: 'list' }); const tree = await docs_files_service_1.docsFilesService.listTree(); res.json(tree); } catch (err) { logger_1.logger.error('Failed to list docs files', err); next(err); } }); // POST /api/docs/files/rename — rename/move file router.post('/files/rename', async (req, res, next) => { try { metrics_1.cm_docs_operations.inc({ operation: 'rename' }); const { from, to } = req.body; if (!from || !to) { res.status(400).json({ error: { message: 'Both "from" and "to" paths are required', code: 'VALIDATION_ERROR' } }); return; } await docs_files_service_1.docsFilesService.renameFile(from, to); res.json({ success: true }); } catch (err) { handleFileError(err, res, next); } }); // GET /api/docs/files/* — read file content router.get('/files/*', async (req, res, next) => { try { metrics_1.cm_docs_operations.inc({ operation: 'read' }); const filePath = extractWildcardPath(req); if (!filePath) { res.status(400).json({ error: { message: 'File path required', code: 'VALIDATION_ERROR' } }); return; } const content = await docs_files_service_1.docsFilesService.readFileContent(filePath); res.json({ path: filePath, content }); } catch (err) { handleFileError(err, res, next); } }); // PUT /api/docs/files/* — write/update file content router.put('/files/*', async (req, res, next) => { try { metrics_1.cm_docs_operations.inc({ operation: 'write' }); const filePath = extractWildcardPath(req); if (!filePath) { res.status(400).json({ error: { message: 'File path required', code: 'VALIDATION_ERROR' } }); return; } const { content } = req.body; if (typeof content !== 'string') { res.status(400).json({ error: { message: 'Content string required', code: 'VALIDATION_ERROR' } }); return; } await docs_files_service_1.docsFilesService.writeFileContent(filePath, content); res.json({ success: true, path: filePath }); } catch (err) { handleFileError(err, res, next); } }); // POST /api/docs/files/* — create new file or folder router.post('/files/*', async (req, res, next) => { try { metrics_1.cm_docs_operations.inc({ operation: 'create' }); const filePath = extractWildcardPath(req); if (!filePath) { res.status(400).json({ error: { message: 'File path required', code: 'VALIDATION_ERROR' } }); return; } const { content, isDirectory } = req.body; await docs_files_service_1.docsFilesService.createFile(filePath, content, isDirectory); res.status(201).json({ success: true, path: filePath }); } catch (err) { handleFileError(err, res, next); } }); // DELETE /api/docs/files/* — delete file or empty folder router.delete('/files/*', async (req, res, next) => { try { metrics_1.cm_docs_operations.inc({ operation: 'delete' }); const filePath = extractWildcardPath(req); if (!filePath) { res.status(400).json({ error: { message: 'File path required', code: 'VALIDATION_ERROR' } }); return; } await docs_files_service_1.docsFilesService.deleteFile(filePath); res.json({ success: true }); } catch (err) { handleFileError(err, res, next); } }); /** * Extract the wildcard path from Express 5 req.params. * Express 5 uses params[0] for * routes. */ function extractWildcardPath(req) { // Express 5: req.params is array-like for * routes const params = req.params; const wildcardParam = params[0] || params['0']; if (Array.isArray(wildcardParam)) return wildcardParam.join('/'); return wildcardParam || ''; } function handleFileError(err, res, next) { if (err instanceof docs_files_service_1.PathTraversalError) { res.status(403).json({ error: { message: 'Path traversal not allowed', code: 'FORBIDDEN' } }); return; } if (err instanceof docs_files_service_1.FileNotFoundError) { res.status(404).json({ error: { message: err.message, code: 'NOT_FOUND' } }); return; } if (err.message?.startsWith('Already exists')) { res.status(409).json({ error: { message: err.message, code: 'CONFLICT' } }); return; } if (err.message === 'Directory is not empty') { res.status(400).json({ error: { message: 'Directory is not empty', code: 'VALIDATION_ERROR' } }); return; } logger_1.logger.error('Docs file operation failed', err); next(err); } exports.docsRouter = router; //# sourceMappingURL=docs.routes.js.map