Upgrade system finished
This commit is contained in:
14
api/dist/modules/docs/docs-files.service.d.ts
vendored
14
api/dist/modules/docs/docs-files.service.d.ts
vendored
@@ -26,6 +26,17 @@ declare function createFile(relativePath: string, content?: string, isDirectory?
|
||||
declare function deleteFile(relativePath: string): Promise<void>;
|
||||
declare function renameFile(fromPath: string, toPath: string): Promise<void>;
|
||||
declare function isEditableFile(relativePath: string): boolean;
|
||||
declare function uploadFile(relativePath: string, sourcePath: string): Promise<void>;
|
||||
declare function invalidateTreeCache(): Promise<void>;
|
||||
/**
|
||||
* Flatten a FileNode tree into file-only entries, then filter by
|
||||
* case-insensitive query match on name or path. Name matches score
|
||||
* higher so they sort first.
|
||||
*/
|
||||
declare function searchFiles(query: string, limit?: number): Promise<{
|
||||
name: string;
|
||||
path: string;
|
||||
}[]>;
|
||||
export declare const docsFilesService: {
|
||||
listTree: typeof listTree;
|
||||
readFileContent: typeof readFileContent;
|
||||
@@ -33,8 +44,11 @@ export declare const docsFilesService: {
|
||||
createFile: typeof createFile;
|
||||
deleteFile: typeof deleteFile;
|
||||
renameFile: typeof renameFile;
|
||||
uploadFile: typeof uploadFile;
|
||||
safeResolve: typeof safeResolve;
|
||||
isEditableFile: typeof isEditableFile;
|
||||
invalidateTreeCache: typeof invalidateTreeCache;
|
||||
searchFiles: typeof searchFiles;
|
||||
};
|
||||
export {};
|
||||
//# sourceMappingURL=docs-files.service.d.ts.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"docs-files.service.d.ts","sourceRoot":"","sources":["../../../src/modules/docs/docs-files.service.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;CACvB;AAaD;;;GAGG;AACH,iBAAS,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED,qBAAa,kBAAmB,SAAQ,KAAK;;CAK5C;AAED,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,QAAQ,EAAE,MAAM;CAI7B;AAED;;;GAGG;AACH,iBAAe,QAAQ,CAAC,GAAG,GAAE,MAAkB,EAAE,OAAO,GAAE,MAAW,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAiD1F;AAED,iBAAe,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAmCpE;AAED,iBAAe,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYpF;AAED,iBAAe,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BtG;AAED,iBAAe,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA8B7D;AAED,iBAAe,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBzE;AAED,iBAAS,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAGrD;AAED,eAAO,MAAM,gBAAgB;;;;;;;;;CAS5B,CAAC"}
|
||||
{"version":3,"file":"docs-files.service.d.ts","sourceRoot":"","sources":["../../../src/modules/docs/docs-files.service.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;CACvB;AAcD;;;GAGG;AACH,iBAAS,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAQjD;AAED,qBAAa,kBAAmB,SAAQ,KAAK;;CAK5C;AAED,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,QAAQ,EAAE,MAAM;CAI7B;AAED;;;GAGG;AACH,iBAAe,QAAQ,CAAC,GAAG,GAAE,MAAkB,EAAE,OAAO,GAAE,MAAW,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAiD1F;AAED,iBAAe,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAmCpE;AAED,iBAAe,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYpF;AAED,iBAAe,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BtG;AAED,iBAAe,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA8B7D;AAED,iBAAe,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBzE;AAED,iBAAS,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAGrD;AAOD,iBAAe,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBjF;AAED,iBAAe,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAMlD;AAED;;;;GAIG;AACH,iBAAe,WAAW,CACxB,KAAK,EAAE,MAAM,EACb,KAAK,SAAI,GACR,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAAC,CAyB3C;AAED,eAAO,MAAM,gBAAgB;;;;;;;;;;;;CAY5B,CAAC"}
|
||||
72
api/dist/modules/docs/docs-files.service.js
vendored
72
api/dist/modules/docs/docs-files.service.js
vendored
@@ -15,7 +15,8 @@ const DOCS_ROOT = (0, path_1.resolve)(env_1.env.MKDOCS_DOCS_PATH);
|
||||
// Redis cache configuration
|
||||
const CACHE_KEY_PREFIX = 'DOCS_CACHE:';
|
||||
const TREE_CACHE_KEY = `${CACHE_KEY_PREFIX}tree`;
|
||||
const FILE_CACHE_TTL = 60 * 60; // 1 hour
|
||||
const TREE_CACHE_TTL = 30; // 30 seconds — short so external changes show quickly
|
||||
const FILE_CONTENT_CACHE_TTL = 60 * 60; // 1 hour for file content
|
||||
function hashFilePath(path) {
|
||||
return crypto_1.default.createHash('sha256').update(path).digest('hex').substring(0, 16);
|
||||
}
|
||||
@@ -26,7 +27,8 @@ function hashFilePath(path) {
|
||||
function safeResolve(relativePath) {
|
||||
const normalized = (0, path_1.normalize)(relativePath).replace(/^(\.\.(\/|\\|$))+/, '');
|
||||
const resolved = (0, path_1.resolve)(DOCS_ROOT, normalized);
|
||||
if (!resolved.startsWith(DOCS_ROOT)) {
|
||||
// Use DOCS_ROOT + sep to prevent prefix attacks (e.g., /mkdocs/docs-evil matching /mkdocs/docs)
|
||||
if (resolved !== DOCS_ROOT && !resolved.startsWith(DOCS_ROOT + '/')) {
|
||||
throw new PathTraversalError();
|
||||
}
|
||||
return resolved;
|
||||
@@ -89,7 +91,7 @@ async function listTree(dir = DOCS_ROOT, relBase = '') {
|
||||
// Cache root result
|
||||
if (dir === DOCS_ROOT && !relBase) {
|
||||
try {
|
||||
await redis_1.redis.setex(TREE_CACHE_KEY, FILE_CACHE_TTL, JSON.stringify(nodes));
|
||||
await redis_1.redis.setex(TREE_CACHE_KEY, TREE_CACHE_TTL, JSON.stringify(nodes));
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.warn('Failed to cache docs tree:', err);
|
||||
@@ -118,7 +120,7 @@ async function readFileContent(relativePath) {
|
||||
const content = await (0, promises_1.readFile)(fullPath, 'utf-8');
|
||||
// Cache the result
|
||||
try {
|
||||
await redis_1.redis.setex(cacheKey, FILE_CACHE_TTL, content);
|
||||
await redis_1.redis.setex(cacheKey, FILE_CONTENT_CACHE_TTL, content);
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.warn('Failed to cache file content:', err);
|
||||
@@ -235,6 +237,65 @@ function isEditableFile(relativePath) {
|
||||
const ext = (0, path_1.extname)(relativePath).toLowerCase();
|
||||
return ['.md', '.txt', '.yml', '.yaml', '.json', '.css', '.html', '.js'].includes(ext);
|
||||
}
|
||||
const ALLOWED_UPLOAD_EXTENSIONS = new Set([
|
||||
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico',
|
||||
'.pdf', '.zip',
|
||||
]);
|
||||
async function uploadFile(relativePath, sourcePath) {
|
||||
const ext = (0, path_1.extname)(relativePath).toLowerCase();
|
||||
if (!ALLOWED_UPLOAD_EXTENSIONS.has(ext)) {
|
||||
throw new Error(`File type not allowed: ${ext}`);
|
||||
}
|
||||
const fullPath = safeResolve(relativePath);
|
||||
await (0, promises_1.mkdir)((0, path_1.dirname)(fullPath), { recursive: true });
|
||||
await (0, promises_1.copyFile)(sourcePath, fullPath);
|
||||
// Invalidate tree cache (structure changed)
|
||||
try {
|
||||
await redis_1.redis.del(TREE_CACHE_KEY);
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.warn('Failed to invalidate tree cache after upload:', err);
|
||||
}
|
||||
}
|
||||
async function invalidateTreeCache() {
|
||||
try {
|
||||
await redis_1.redis.del(TREE_CACHE_KEY);
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.warn('Failed to invalidate tree cache:', err);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Flatten a FileNode tree into file-only entries, then filter by
|
||||
* case-insensitive query match on name or path. Name matches score
|
||||
* higher so they sort first.
|
||||
*/
|
||||
async function searchFiles(query, limit = 5) {
|
||||
const tree = await listTree();
|
||||
const q = query.toLowerCase();
|
||||
const matches = [];
|
||||
function walk(nodes) {
|
||||
for (const node of nodes) {
|
||||
if (node.isDirectory) {
|
||||
if (node.children)
|
||||
walk(node.children);
|
||||
}
|
||||
else {
|
||||
const nameLower = node.name.toLowerCase();
|
||||
const pathLower = node.path.toLowerCase();
|
||||
if (nameLower.includes(q)) {
|
||||
matches.push({ name: node.name, path: node.path, score: 2 });
|
||||
}
|
||||
else if (pathLower.includes(q)) {
|
||||
matches.push({ name: node.name, path: node.path, score: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(tree);
|
||||
matches.sort((a, b) => b.score - a.score);
|
||||
return matches.slice(0, limit).map(({ name, path }) => ({ name, path }));
|
||||
}
|
||||
exports.docsFilesService = {
|
||||
listTree,
|
||||
readFileContent,
|
||||
@@ -242,7 +303,10 @@ exports.docsFilesService = {
|
||||
createFile,
|
||||
deleteFile,
|
||||
renameFile,
|
||||
uploadFile,
|
||||
safeResolve,
|
||||
isEditableFile,
|
||||
invalidateTreeCache,
|
||||
searchFiles,
|
||||
};
|
||||
//# sourceMappingURL=docs-files.service.js.map
|
||||
File diff suppressed because one or more lines are too long
2
api/dist/modules/docs/docs.routes.d.ts.map
vendored
2
api/dist/modules/docs/docs.routes.d.ts.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"docs.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/docs/docs.routes.ts"],"names":[],"mappings":"AAiQA,eAAO,MAAM,UAAU,4CAAS,CAAC"}
|
||||
{"version":3,"file":"docs.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/docs/docs.routes.ts"],"names":[],"mappings":"AA4YA,eAAO,MAAM,UAAU,4CAAS,CAAC"}
|
||||
151
api/dist/modules/docs/docs.routes.js
vendored
151
api/dist/modules/docs/docs.routes.js
vendored
@@ -1,21 +1,31 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.docsRouter = void 0;
|
||||
const express_1 = require("express");
|
||||
const multer_1 = __importDefault(require("multer"));
|
||||
const promises_1 = require("fs/promises");
|
||||
const path_1 = require("path");
|
||||
const auth_middleware_1 = require("../../middleware/auth.middleware");
|
||||
const rbac_middleware_1 = require("../../middleware/rbac.middleware");
|
||||
const env_1 = require("../../config/env");
|
||||
const roles_1 = require("../../utils/roles");
|
||||
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 docs_collab_service_1 = require("./docs-collab.service");
|
||||
const mkdocs_config_service_1 = require("./mkdocs-config.service");
|
||||
const header_builder_service_1 = require("./header-builder.service");
|
||||
const header_builder_schemas_1 = require("./header-builder.schemas");
|
||||
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) => {
|
||||
// GET /api/docs/status — check MkDocs and Code Server availability (content editors only)
|
||||
router.get('/status', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, next) => {
|
||||
try {
|
||||
const [mkdocsOnline, codeServerOnline, siteServerOnline] = await Promise.all([
|
||||
(0, health_check_1.isServiceOnline)(env_1.env.MKDOCS_PREVIEW_URL),
|
||||
@@ -33,8 +43,8 @@ router.get('/status', async (_req, res, next) => {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// GET /api/docs/config — return public-facing port numbers for iframe URLs
|
||||
router.get('/config', async (_req, res, _next) => {
|
||||
// GET /api/docs/config — return public-facing port numbers for iframe URLs (content editors only)
|
||||
router.get('/config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, _next) => {
|
||||
res.json({
|
||||
codeServerPort: env_1.env.CODE_SERVER_PORT,
|
||||
mkdocsPort: env_1.env.MKDOCS_PORT,
|
||||
@@ -42,8 +52,8 @@ router.get('/config', async (_req, res, _next) => {
|
||||
});
|
||||
});
|
||||
// --- MkDocs Config Endpoints ---
|
||||
// GET /api/docs/mkdocs-config — read raw mkdocs.yml content
|
||||
router.get('/mkdocs-config', async (_req, res, next) => {
|
||||
// GET /api/docs/mkdocs-config — read raw mkdocs.yml content (content editors only)
|
||||
router.get('/mkdocs-config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, next) => {
|
||||
try {
|
||||
const content = await mkdocs_config_service_1.mkdocsConfigService.readConfig();
|
||||
res.json({ content });
|
||||
@@ -54,7 +64,7 @@ router.get('/mkdocs-config', async (_req, res, next) => {
|
||||
}
|
||||
});
|
||||
// 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) => {
|
||||
router.put('/mkdocs-config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
const { content } = req.body;
|
||||
if (typeof content !== 'string') {
|
||||
@@ -73,8 +83,8 @@ router.put('/mkdocs-config', (0, rbac_middleware_1.requireRole)('SUPER_ADMIN'),
|
||||
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) => {
|
||||
// POST /api/docs/build — trigger mkdocs build in container
|
||||
router.post('/build', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, next) => {
|
||||
try {
|
||||
const result = await mkdocs_config_service_1.mkdocsConfigService.triggerBuild();
|
||||
res.json(result);
|
||||
@@ -84,11 +94,95 @@ router.post('/build', (0, rbac_middleware_1.requireRole)('SUPER_ADMIN'), async (
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// --- Header Builder ---
|
||||
// GET /api/docs/header-config — read header nav bar config (content editors only)
|
||||
router.get('/header-config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, next) => {
|
||||
try {
|
||||
const config = await header_builder_service_1.headerBuilderService.readConfig();
|
||||
res.json(config);
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to read header config', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// PUT /api/docs/header-config — save header nav bar config + regenerate template
|
||||
router.put('/header-config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
const parsed = header_builder_schemas_1.headerConfigSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({
|
||||
error: { message: 'Invalid header config', code: 'VALIDATION_ERROR', details: parsed.error.flatten().fieldErrors },
|
||||
});
|
||||
return;
|
||||
}
|
||||
await header_builder_service_1.headerBuilderService.writeConfig(parsed.data);
|
||||
// Invalidate docs file tree cache so the new main.html shows up
|
||||
await docs_files_service_1.docsFilesService.invalidateTreeCache();
|
||||
res.json({ success: true });
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to save header config', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// --- File Upload ---
|
||||
const ALLOWED_UPLOAD_EXTENSIONS = new Set([
|
||||
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico',
|
||||
'.pdf', '.zip',
|
||||
]);
|
||||
const upload = (0, multer_1.default)({
|
||||
storage: multer_1.default.diskStorage({}), // temp dir
|
||||
limits: { fileSize: 20 * 1024 * 1024 }, // 20MB
|
||||
fileFilter: (_req, file, cb) => {
|
||||
const ext = (0, path_1.extname)(file.originalname).toLowerCase();
|
||||
if (ALLOWED_UPLOAD_EXTENSIONS.has(ext)) {
|
||||
cb(null, true);
|
||||
}
|
||||
else {
|
||||
cb(new Error(`File type not allowed: ${ext}`));
|
||||
}
|
||||
},
|
||||
});
|
||||
// POST /api/docs/upload — upload binary file (image, pdf, etc.)
|
||||
router.post('/upload', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), upload.single('file'), async (req, res, next) => {
|
||||
const tempPath = req.file?.path;
|
||||
try {
|
||||
metrics_1.cm_docs_operations.inc({ operation: 'upload' });
|
||||
if (!req.file) {
|
||||
res.status(400).json({ error: { message: 'No file provided', code: 'VALIDATION_ERROR' } });
|
||||
return;
|
||||
}
|
||||
const targetDir = req.body.path || '';
|
||||
const fileName = (0, path_1.basename)(req.file.originalname).replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const relativePath = targetDir ? `${targetDir}/${fileName}` : fileName;
|
||||
await docs_files_service_1.docsFilesService.uploadFile(relativePath, req.file.path);
|
||||
// Clean up temp file
|
||||
try {
|
||||
await (0, promises_1.rm)(req.file.path);
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
res.json({ success: true, path: relativePath });
|
||||
}
|
||||
catch (err) {
|
||||
// Clean up temp file on error
|
||||
if (tempPath) {
|
||||
try {
|
||||
await (0, promises_1.rm)(tempPath);
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
}
|
||||
handleFileError(err, res, next);
|
||||
}
|
||||
});
|
||||
// --- File Management Endpoints ---
|
||||
// GET /api/docs/files — list file tree
|
||||
router.get('/files', async (_req, res, next) => {
|
||||
// GET /api/docs/files — list file tree (content editors only)
|
||||
router.get('/files', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
metrics_1.cm_docs_operations.inc({ operation: 'list' });
|
||||
if (req.query['force'] === 'true') {
|
||||
await docs_files_service_1.docsFilesService.invalidateTreeCache();
|
||||
}
|
||||
const tree = await docs_files_service_1.docsFilesService.listTree();
|
||||
res.json(tree);
|
||||
}
|
||||
@@ -97,8 +191,25 @@ router.get('/files', async (_req, res, next) => {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// GET /api/docs/files/search — search files by name/path (content editors only)
|
||||
router.get('/files/search', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
const search = String(req.query['search'] ?? req.query['q'] ?? '').trim();
|
||||
if (!search) {
|
||||
res.json({ files: [] });
|
||||
return;
|
||||
}
|
||||
const limit = Math.min(Math.max(Number(req.query['limit']) || 5, 1), 20);
|
||||
const files = await docs_files_service_1.docsFilesService.searchFiles(search, limit);
|
||||
res.json({ files });
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to search docs files', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/docs/files/rename — rename/move file
|
||||
router.post('/files/rename', async (req, res, next) => {
|
||||
router.post('/files/rename', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
metrics_1.cm_docs_operations.inc({ operation: 'rename' });
|
||||
const { from, to } = req.body;
|
||||
@@ -107,14 +218,16 @@ router.post('/files/rename', async (req, res, next) => {
|
||||
return;
|
||||
}
|
||||
await docs_files_service_1.docsFilesService.renameFile(from, to);
|
||||
// Invalidate old path's collaboration state
|
||||
docs_collab_service_1.docsCollabService.invalidateDocument(from).catch(() => { });
|
||||
res.json({ success: true });
|
||||
}
|
||||
catch (err) {
|
||||
handleFileError(err, res, next);
|
||||
}
|
||||
});
|
||||
// GET /api/docs/files/* — read file content
|
||||
router.get('/files/*', async (req, res, next) => {
|
||||
// GET /api/docs/files/* — read file content (content editors only)
|
||||
router.get('/files/*', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
metrics_1.cm_docs_operations.inc({ operation: 'read' });
|
||||
const filePath = extractWildcardPath(req);
|
||||
@@ -130,7 +243,7 @@ router.get('/files/*', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
// PUT /api/docs/files/* — write/update file content
|
||||
router.put('/files/*', async (req, res, next) => {
|
||||
router.put('/files/*', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
metrics_1.cm_docs_operations.inc({ operation: 'write' });
|
||||
const filePath = extractWildcardPath(req);
|
||||
@@ -144,6 +257,8 @@ router.put('/files/*', async (req, res, next) => {
|
||||
return;
|
||||
}
|
||||
await docs_files_service_1.docsFilesService.writeFileContent(filePath, content);
|
||||
// Invalidate collaboration state so next session starts fresh from disk
|
||||
docs_collab_service_1.docsCollabService.invalidateDocument(filePath).catch(() => { });
|
||||
res.json({ success: true, path: filePath });
|
||||
}
|
||||
catch (err) {
|
||||
@@ -151,7 +266,7 @@ router.put('/files/*', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
// POST /api/docs/files/* — create new file or folder
|
||||
router.post('/files/*', async (req, res, next) => {
|
||||
router.post('/files/*', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
metrics_1.cm_docs_operations.inc({ operation: 'create' });
|
||||
const filePath = extractWildcardPath(req);
|
||||
@@ -168,7 +283,7 @@ router.post('/files/*', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
// DELETE /api/docs/files/* — delete file or empty folder
|
||||
router.delete('/files/*', async (req, res, next) => {
|
||||
router.delete('/files/*', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
metrics_1.cm_docs_operations.inc({ operation: 'delete' });
|
||||
const filePath = extractWildcardPath(req);
|
||||
@@ -177,6 +292,8 @@ router.delete('/files/*', async (req, res, next) => {
|
||||
return;
|
||||
}
|
||||
await docs_files_service_1.docsFilesService.deleteFile(filePath);
|
||||
// Invalidate collaboration state for deleted file
|
||||
docs_collab_service_1.docsCollabService.invalidateDocument(filePath).catch(() => { });
|
||||
res.json({ success: true });
|
||||
}
|
||||
catch (err) {
|
||||
|
||||
2
api/dist/modules/docs/docs.routes.js.map
vendored
2
api/dist/modules/docs/docs.routes.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -12,7 +12,8 @@ declare function validateYaml(content: string): string | null;
|
||||
declare function readConfig(): Promise<string>;
|
||||
declare function writeConfig(content: string): Promise<void>;
|
||||
/**
|
||||
* Execute `mkdocs build` inside the running MkDocs container via Docker Engine API.
|
||||
* Trigger `mkdocs build --clean` via the build trigger HTTP server
|
||||
* running inside the MkDocs container on port 8001.
|
||||
*/
|
||||
declare function triggerBuild(): Promise<{
|
||||
success: boolean;
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"mkdocs-config.service.d.ts","sourceRoot":"","sources":["../../../src/modules/docs/mkdocs-config.service.ts"],"names":[],"mappings":"AAIA,OAAO,EAAiB,QAAQ,EAAE,MAAM,MAAM,CAAC;AAuB/C;;;GAGG;AACH,iBAAS,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAS9C;AAED;;;GAGG;AACH,iBAAS,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWpD;AAED,iBAAe,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAE3C;AAED,iBAAe,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAezD;AA8DD;;GAEG;AACH,iBAAe,YAAY,IAAI,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAgE7F;AAED,eAAO,MAAM,mBAAmB;;;;;;CAM/B,CAAC"}
|
||||
{"version":3,"file":"mkdocs-config.service.d.ts","sourceRoot":"","sources":["../../../src/modules/docs/mkdocs-config.service.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,QAAQ,EAAE,MAAM,MAAM,CAAC;AAqB/C;;;GAGG;AACH,iBAAS,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAS9C;AAED;;;GAGG;AACH,iBAAS,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWpD;AAED,iBAAe,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAE3C;AAED,iBAAe,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAezD;AAED;;;GAGG;AACH,iBAAe,YAAY,IAAI,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAyB7F;AAED,eAAO,MAAM,mBAAmB;;;;;;CAM/B,CAAC"}
|
||||
104
api/dist/modules/docs/mkdocs-config.service.js
vendored
104
api/dist/modules/docs/mkdocs-config.service.js
vendored
@@ -2,11 +2,9 @@
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.mkdocsConfigService = void 0;
|
||||
const promises_1 = require("fs/promises");
|
||||
const http_1 = require("http");
|
||||
const env_1 = require("../../config/env");
|
||||
const logger_1 = require("../../utils/logger");
|
||||
const yaml_1 = require("yaml");
|
||||
const DOCKER_SOCKET = '/var/run/docker.sock';
|
||||
/**
|
||||
* Custom YAML tag schema to preserve !!python/name: and !!python/object: tags.
|
||||
* Without this, the yaml library would reject these custom tags.
|
||||
@@ -73,102 +71,22 @@ async function writeConfig(content) {
|
||||
await (0, promises_1.writeFile)(env_1.env.MKDOCS_CONFIG_PATH, content, 'utf-8');
|
||||
}
|
||||
/**
|
||||
* Make a request to the Docker Engine API over Unix socket.
|
||||
*/
|
||||
function dockerRequest(method, path, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
socketPath: DOCKER_SOCKET,
|
||||
path,
|
||||
method,
|
||||
headers: body
|
||||
? { 'Content-Type': 'application/json' }
|
||||
: undefined,
|
||||
};
|
||||
const req = (0, http_1.request)(options, (res) => {
|
||||
const chunks = [];
|
||||
res.on('data', (chunk) => chunks.push(chunk));
|
||||
res.on('end', () => {
|
||||
resolve({
|
||||
statusCode: res.statusCode || 0,
|
||||
body: Buffer.concat(chunks).toString(),
|
||||
});
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (body) {
|
||||
req.write(JSON.stringify(body));
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Read raw output from a Docker exec start stream.
|
||||
* Docker multiplexes stdout/stderr with 8-byte headers.
|
||||
*/
|
||||
function demuxDockerStream(raw) {
|
||||
const lines = [];
|
||||
let offset = 0;
|
||||
while (offset < raw.length) {
|
||||
if (offset + 8 > raw.length)
|
||||
break;
|
||||
// byte 0: stream type (1=stdout, 2=stderr)
|
||||
const size = raw.readUInt32BE(offset + 4);
|
||||
offset += 8;
|
||||
if (offset + size > raw.length) {
|
||||
lines.push(raw.subarray(offset).toString('utf-8'));
|
||||
break;
|
||||
}
|
||||
lines.push(raw.subarray(offset, offset + size).toString('utf-8'));
|
||||
offset += size;
|
||||
}
|
||||
return lines.join('');
|
||||
}
|
||||
/**
|
||||
* Execute `mkdocs build` inside the running MkDocs container via Docker Engine API.
|
||||
* Trigger `mkdocs build --clean` via the build trigger HTTP server
|
||||
* running inside the MkDocs container on port 8001.
|
||||
*/
|
||||
async function triggerBuild() {
|
||||
const containerName = env_1.env.MKDOCS_CONTAINER_NAME;
|
||||
const buildUrl = `${env_1.env.MKDOCS_PREVIEW_URL.replace(':8000', ':8001')}/build`;
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
// 1. Create exec instance
|
||||
const execCreate = await dockerRequest('POST', `/containers/${containerName}/exec`, {
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Cmd: ['mkdocs', 'build', '--clean'],
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 130_000);
|
||||
const response = await fetch(buildUrl, {
|
||||
method: 'POST',
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (execCreate.statusCode !== 201) {
|
||||
throw new Error(`Failed to create exec: ${execCreate.body}`);
|
||||
}
|
||||
const { Id: execId } = JSON.parse(execCreate.body);
|
||||
// 2. Start exec and collect output
|
||||
const execOutput = await new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
socketPath: DOCKER_SOCKET,
|
||||
path: `/exec/${execId}/start`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
const req = (0, http_1.request)(options, (res) => {
|
||||
const chunks = [];
|
||||
res.on('data', (chunk) => chunks.push(chunk));
|
||||
res.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.write(JSON.stringify({ Detach: false, Tty: false }));
|
||||
req.end();
|
||||
});
|
||||
const output = demuxDockerStream(execOutput);
|
||||
// 3. Check exit code
|
||||
const execInspect = await dockerRequest('GET', `/exec/${execId}/json`);
|
||||
const inspectData = JSON.parse(execInspect.body);
|
||||
const exitCode = inspectData.ExitCode ?? -1;
|
||||
const duration = Date.now() - startTime;
|
||||
return {
|
||||
success: exitCode === 0,
|
||||
output: output || '(no output)',
|
||||
duration,
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
catch (err) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user