607 lines
17 KiB
JavaScript
607 lines
17 KiB
JavaScript
import http from 'node:http';
|
|
import {
|
|
buildTemplateMigrationPreviewRequest,
|
|
calculateTemplateFieldStatsRequest,
|
|
detectDangerousTemplateChanges,
|
|
normalizeUploadTemplateSaveRequest,
|
|
planHomeFilterCustomFieldCountsRequest,
|
|
planDepartmentScopeRequest,
|
|
planMaterialListQueryRequest,
|
|
planMaterialCustomSearchRequest,
|
|
planHomeFilterFieldCountsRequest,
|
|
planTemplateMigrationBatchRequest,
|
|
planDepartmentStructureChangeRequest,
|
|
planUploadTemplateDeleteRequest,
|
|
planUploadTemplateUpdateRequest,
|
|
prepareMaterialMetadataRequest,
|
|
prepareTemplateMigrationMetadataRequest,
|
|
projectPublicTemplateOptionsRequest,
|
|
validateSubmittedTemplateFieldsRequest,
|
|
validateAndFormatUploadTemplateFields,
|
|
} from './template-validation.mjs';
|
|
|
|
const moduleId = 'paid-core';
|
|
const port = Number.parseInt(process.env.PORT || '8787', 10);
|
|
const tunnelProvider = (process.env.TUNNEL_PROVIDER || '').trim();
|
|
|
|
const capabilities = [
|
|
'customTemplates',
|
|
'departmentIsolation',
|
|
'systemSettings',
|
|
'intranetPenetration',
|
|
'apiAccess',
|
|
];
|
|
|
|
const allowedActions = new Set([
|
|
'customTemplates:use',
|
|
'customTemplates:create',
|
|
'customTemplates:update',
|
|
'customTemplates:delete',
|
|
'customTemplates:migrate',
|
|
'customTemplates:read',
|
|
'customTemplates:list',
|
|
'customTemplates:validate',
|
|
'customTemplates:filter',
|
|
'customTemplates:planDelete',
|
|
'customTemplates:planUpdate',
|
|
'customTemplates:planMigrationBatch',
|
|
'customTemplates:planMaterialListQuery',
|
|
'customTemplates:planHomeFilterFields',
|
|
'departmentIsolation:use',
|
|
'departmentIsolation:read',
|
|
'departmentIsolation:manage',
|
|
'departmentIsolation:planStructureChange',
|
|
'systemSettings:use',
|
|
'systemSettings:read',
|
|
'systemSettings:update',
|
|
'intranetPenetration:use',
|
|
'intranetPenetration:status',
|
|
'intranetPenetration:start',
|
|
'intranetPenetration:stop',
|
|
'intranetPenetration:read',
|
|
'apiAccess:use',
|
|
'apiAccess:read',
|
|
]);
|
|
|
|
function sendJson(res, statusCode, payload) {
|
|
const body = JSON.stringify(payload);
|
|
res.writeHead(statusCode, {
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
'Content-Length': Buffer.byteLength(body),
|
|
});
|
|
res.end(body);
|
|
}
|
|
|
|
function readJsonBody(req) {
|
|
return new Promise((resolve, reject) => {
|
|
let body = '';
|
|
|
|
req.on('data', (chunk) => {
|
|
body += chunk;
|
|
if (body.length > 1024 * 1024) {
|
|
req.destroy();
|
|
reject(new Error('request_body_too_large'));
|
|
}
|
|
});
|
|
|
|
req.on('end', () => {
|
|
if (!body.trim()) {
|
|
resolve({});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
resolve(JSON.parse(body));
|
|
} catch {
|
|
reject(new Error('invalid_json'));
|
|
}
|
|
});
|
|
|
|
req.on('error', reject);
|
|
});
|
|
}
|
|
|
|
function sendAuthorization(res, action) {
|
|
const allowed = typeof action === 'string' && allowedActions.has(action);
|
|
sendJson(res, allowed ? 200 : 403, {
|
|
moduleId,
|
|
allowed,
|
|
action,
|
|
capabilities,
|
|
});
|
|
}
|
|
|
|
function providerNotConfigured(res, action) {
|
|
sendJson(res, 501, {
|
|
moduleId,
|
|
status: 'provider_not_configured',
|
|
action,
|
|
error: '付费增强包已加载,但尚未配置真实内网穿透提供商',
|
|
});
|
|
}
|
|
|
|
const server = http.createServer((req, res) => {
|
|
const url = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
|
|
if (req.method === 'GET' && url.pathname === '/health') {
|
|
sendJson(res, 200, { moduleId, status: 'healthy' });
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'GET' && url.pathname === '/status') {
|
|
sendJson(res, 200, {
|
|
moduleId,
|
|
status: 'ready',
|
|
capabilities,
|
|
note: 'paid-core 是统一付费增强包;业务核心迁移会继续推进',
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'GET' && url.pathname === '/capabilities') {
|
|
sendJson(res, 200, {
|
|
moduleId,
|
|
capabilities,
|
|
actions: Array.from(allowedActions),
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/authorize') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
sendAuthorization(res, body.action);
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, { moduleId, allowed: false, error: error.message });
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/templates/validate-fields') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
const result = validateAndFormatUploadTemplateFields(body.fields);
|
|
sendJson(res, result.valid ? 200 : 400, {
|
|
moduleId,
|
|
action: 'customTemplates:validate',
|
|
...result,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'customTemplates:validate',
|
|
valid: false,
|
|
error: error.message,
|
|
field: 'fields',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/templates/normalize-save') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
const result = normalizeUploadTemplateSaveRequest(body.payload, body.mode);
|
|
sendJson(res, result.valid ? 200 : 400, {
|
|
moduleId,
|
|
action: 'customTemplates:normalizeSave',
|
|
...result,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'customTemplates:normalizeSave',
|
|
valid: false,
|
|
error: error.message,
|
|
field: 'template',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/templates/plan-delete') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
const result = planUploadTemplateDeleteRequest(body);
|
|
sendJson(res, result.valid ? 200 : 400, {
|
|
moduleId,
|
|
action: 'customTemplates:planDelete',
|
|
...result,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'customTemplates:planDelete',
|
|
valid: false,
|
|
allowed: false,
|
|
error: error.message,
|
|
field: 'template',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/templates/plan-update') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
const result = planUploadTemplateUpdateRequest(body);
|
|
sendJson(res, result.valid ? 200 : result.statusCode || 400, {
|
|
moduleId,
|
|
action: 'customTemplates:planUpdate',
|
|
...result,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'customTemplates:planUpdate',
|
|
valid: false,
|
|
error: error.message,
|
|
errorCode: 'TEMPLATE_UPDATE_PLAN_FAILED',
|
|
field: 'template',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/templates/plan-migration-batch') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
const result = planTemplateMigrationBatchRequest(body);
|
|
sendJson(res, result.allowed ? 200 : result.statusCode || 400, {
|
|
moduleId,
|
|
action: 'customTemplates:planMigrationBatch',
|
|
...result,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'customTemplates:planMigrationBatch',
|
|
valid: false,
|
|
allowed: false,
|
|
error: error.message,
|
|
errorCode: 'TEMPLATE_MIGRATION_BATCH_PLAN_FAILED',
|
|
field: 'template',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/templates/detect-structure-changes') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
if (!body.existingTemplate || !Array.isArray(body.existingTemplate.fields)) {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'customTemplates:detectStructureChanges',
|
|
valid: false,
|
|
error: '缺少现有模板字段信息',
|
|
field: 'existingTemplate',
|
|
});
|
|
return;
|
|
}
|
|
|
|
const changes = detectDangerousTemplateChanges(body);
|
|
sendJson(res, 200, {
|
|
moduleId,
|
|
action: 'customTemplates:detectStructureChanges',
|
|
valid: true,
|
|
changes,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'customTemplates:detectStructureChanges',
|
|
valid: false,
|
|
error: error.message,
|
|
field: 'template',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/materials/validate-template-fields') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
const result = validateSubmittedTemplateFieldsRequest(body);
|
|
sendJson(res, result.valid ? 200 : 400, {
|
|
moduleId,
|
|
action: 'customTemplates:validateSubmittedFields',
|
|
...result,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'customTemplates:validateSubmittedFields',
|
|
valid: false,
|
|
error: error.message,
|
|
errorCode: 'TEMPLATE_FIELD_VALIDATION_FAILED',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/materials/prepare-metadata') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
const result = prepareMaterialMetadataRequest(body);
|
|
sendJson(res, result.valid ? 200 : 400, {
|
|
moduleId,
|
|
action: 'customTemplates:prepareMaterialMetadata',
|
|
...result,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'customTemplates:prepareMaterialMetadata',
|
|
valid: false,
|
|
error: error.message,
|
|
errorCode: 'METADATA_PREPARE_FAILED',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/templates/prepare-migration-metadata') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
const result = prepareTemplateMigrationMetadataRequest(body);
|
|
sendJson(res, result.valid ? 200 : 400, {
|
|
moduleId,
|
|
action: 'customTemplates:prepareMigrationMetadata',
|
|
...result,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'customTemplates:prepareMigrationMetadata',
|
|
valid: false,
|
|
error: error.message,
|
|
errorCode: 'TEMPLATE_MIGRATION_METADATA_PREPARE_FAILED',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/templates/build-migration-preview') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
const result = buildTemplateMigrationPreviewRequest(body);
|
|
sendJson(res, result.valid ? 200 : 400, {
|
|
moduleId,
|
|
action: 'customTemplates:buildMigrationPreview',
|
|
...result,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'customTemplates:buildMigrationPreview',
|
|
valid: false,
|
|
error: error.message,
|
|
errorCode: 'TEMPLATE_MIGRATION_PREVIEW_BUILD_FAILED',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/templates/project-public-options') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
const result = projectPublicTemplateOptionsRequest(body);
|
|
sendJson(res, result.valid ? 200 : 400, {
|
|
moduleId,
|
|
action: 'customTemplates:projectPublicOptions',
|
|
...result,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'customTemplates:projectPublicOptions',
|
|
valid: false,
|
|
error: error.message,
|
|
errorCode: 'TEMPLATE_PUBLIC_OPTIONS_PROJECT_FAILED',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/templates/calculate-field-stats') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
const result = calculateTemplateFieldStatsRequest(body);
|
|
sendJson(res, result.valid ? 200 : 400, {
|
|
moduleId,
|
|
action: 'customTemplates:calculateFieldStats',
|
|
...result,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'customTemplates:calculateFieldStats',
|
|
valid: false,
|
|
error: error.message,
|
|
errorCode: 'TEMPLATE_FIELD_STATS_CALCULATE_FAILED',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/materials/plan-custom-search') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
const result = planMaterialCustomSearchRequest(body);
|
|
sendJson(res, result.valid ? 200 : 400, {
|
|
moduleId,
|
|
action: 'customTemplates:planMaterialCustomSearch',
|
|
...result,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'customTemplates:planMaterialCustomSearch',
|
|
valid: false,
|
|
error: error.message,
|
|
errorCode: 'MATERIAL_CUSTOM_SEARCH_PLAN_FAILED',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/materials/plan-home-filter-counts') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
const result = planHomeFilterCustomFieldCountsRequest(body);
|
|
sendJson(res, result.valid ? 200 : 400, {
|
|
moduleId,
|
|
action: 'customTemplates:planHomeFilterCounts',
|
|
...result,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'customTemplates:planHomeFilterCounts',
|
|
valid: false,
|
|
error: error.message,
|
|
errorCode: 'HOME_FILTER_COUNTS_PLAN_FAILED',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/materials/plan-list-query') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
const result = planMaterialListQueryRequest(body);
|
|
sendJson(res, result.valid ? 200 : 400, {
|
|
moduleId,
|
|
action: 'customTemplates:planMaterialListQuery',
|
|
...result,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'customTemplates:planMaterialListQuery',
|
|
valid: false,
|
|
error: error.message,
|
|
errorCode: 'MATERIAL_LIST_QUERY_PLAN_FAILED',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/materials/plan-home-filter-fields') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
const result = planHomeFilterFieldCountsRequest(body);
|
|
sendJson(res, result.valid ? 200 : 400, {
|
|
moduleId,
|
|
action: 'customTemplates:planHomeFilterFields',
|
|
...result,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'customTemplates:planHomeFilterFields',
|
|
valid: false,
|
|
error: error.message,
|
|
errorCode: 'HOME_FILTER_FIELDS_PLAN_FAILED',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/departments/plan-scope') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
const result = planDepartmentScopeRequest(body);
|
|
sendJson(res, result.valid ? 200 : 400, {
|
|
moduleId,
|
|
action: 'departmentIsolation:planScope',
|
|
...result,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'departmentIsolation:planScope',
|
|
valid: false,
|
|
error: error.message,
|
|
errorCode: 'DEPARTMENT_SCOPE_PLAN_FAILED',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/departments/plan-structure-change') {
|
|
readJsonBody(req)
|
|
.then((body) => {
|
|
const result = planDepartmentStructureChangeRequest(body);
|
|
sendJson(res, result.allowed ? 200 : result.statusCode || 400, {
|
|
moduleId,
|
|
action: 'departmentIsolation:planStructureChange',
|
|
...result,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
sendJson(res, 400, {
|
|
moduleId,
|
|
action: 'departmentIsolation:planStructureChange',
|
|
valid: false,
|
|
allowed: false,
|
|
error: error.message,
|
|
errorCode: 'DEPARTMENT_STRUCTURE_CHANGE_PLAN_FAILED',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'GET' && url.pathname === '/tunnel/status') {
|
|
if (!tunnelProvider) {
|
|
providerNotConfigured(res, 'status');
|
|
return;
|
|
}
|
|
providerNotConfigured(res, 'status');
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/tunnel/start') {
|
|
if (!tunnelProvider) {
|
|
providerNotConfigured(res, 'start');
|
|
return;
|
|
}
|
|
providerNotConfigured(res, 'start');
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && url.pathname === '/tunnel/stop') {
|
|
if (!tunnelProvider) {
|
|
providerNotConfigured(res, 'stop');
|
|
return;
|
|
}
|
|
providerNotConfigured(res, 'stop');
|
|
return;
|
|
}
|
|
|
|
sendJson(res, 404, { moduleId, error: '接口不存在' });
|
|
});
|
|
|
|
server.listen(port, '0.0.0.0', () => {
|
|
console.log(`${moduleId} listening on ${port}`);
|
|
});
|