Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

220 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* /api/industry — 行业代表制注册与管理路由
*
* 行业代表制架构:铸渊不是接入个人,而是接入行业。
* 每个行业有一个代表人,代表人开自己的企业会员,
* 铸渊管理代表人的仓库。
*
* 指令ZY-AGEOS-TOWER-2026-0326-001
* 版权:国作登字-2026-A-00037559
*/
'use strict';
var express = require('express');
var crypto = require('crypto');
var router = express.Router();
var authModule = require('./auth');
var requireSession = authModule.requireSession;
var DEV_DATABASE = authModule.DEV_DATABASE;
// Rate limiting (same pattern as auth.js)
var requestCounts = new Map();
var RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute window
var RATE_LIMIT_MAX = 10; // max 10 requests per minute
function rateLimit(req, res, next) {
var ip = req.ip || req.connection.remoteAddress || 'unknown';
var now = Date.now();
var entry = requestCounts.get(ip);
if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW_MS) {
requestCounts.set(ip, { windowStart: now, count: 1 });
return next();
}
entry.count++;
if (entry.count > RATE_LIMIT_MAX) {
return res.status(429).json({
error: true,
code: 'RATE_LIMITED',
message: '请求过于频繁,请稍后再试'
});
}
next();
}
// Apply rate limiting to all industry routes via per-route middleware
/**
* Check if a dev ID is a registered developer
* @param {string} devId
* @returns {boolean}
*/
function isRegisteredDev(devId) {
return !!(DEV_DATABASE[devId]);
}
/**
* Generate a new industry ID using a counter
* @returns {string}
*/
var industryCounter = 1;
function generateIndustryId() {
var id = 'IND-' + String(industryCounter).padStart(3, '0');
industryCounter++;
return id;
}
/**
* Hash a PAT for storage (never store raw PAT)
* @param {string} pat
* @returns {string}
*/
function hashPAT(pat) {
return crypto.createHash('sha256').update(pat).digest('hex');
}
/**
* POST /api/industry/register
* Register a new industry with a representative developer
* Requires authenticated session (DEV-XXX)
*/
router.post('/industry/register', rateLimit, requireSession, function(req, res) {
var industry_name = (req.body.industry_name || '').trim();
var rep_dev_id = (req.body.rep_dev_id || '').trim().toUpperCase();
var github_username = (req.body.github_username || '').trim();
var repo_name = (req.body.repo_name || '').trim();
var pat = (req.body.pat || '').trim();
// Validate required fields
if (!industry_name || !rep_dev_id || !github_username || !repo_name || !pat) {
return res.status(400).json({
error: true,
code: 'MISSING_FIELDS',
message: '缺少必填字段industry_name, rep_dev_id, github_username, repo_name, pat'
});
}
// Step 1: verify representative is a registered developer
if (!isRegisteredDev(rep_dev_id)) {
return res.status(400).json({
error: true,
code: 'INVALID_REPRESENTATIVE',
message: '代表人必须是已注册开发者'
});
}
// Step 2: only the representative themselves or admin can register
var sessionDevId = req.sessionDevId;
var sessionDev = req.sessionDev;
if (sessionDevId !== rep_dev_id && (!sessionDev || sessionDev.level < 3)) {
return res.status(403).json({
error: true,
code: 'PERMISSION_DENIED',
message: '只有代表人本人或管理者可以注册行业'
});
}
// Step 3: validate PAT format (GitHub classic tokens start with ghp_,
// fine-grained tokens start with github_pat_)
if (pat.length < 10 || !(pat.startsWith('ghp_') || pat.startsWith('github_pat_'))) {
return res.status(400).json({
error: true,
code: 'INVALID_PAT',
message: 'PAT 格式无效 · 需要以 ghp_ 或 github_pat_ 开头的有效 GitHub Personal Access Token'
});
}
// Step 4: create industry record
var industryId = generateIndustryId();
var patHash = hashPAT(pat);
var record = {
industry_id: industryId,
name: industry_name,
representative: {
dev_id: rep_dev_id,
name: DEV_DATABASE[rep_dev_id] ? DEV_DATABASE[rep_dev_id].name : '',
github_username: github_username,
repo_url: 'https://github.com/' + github_username + '/' + repo_name,
pat_hash: patHash,
pat_status: 'pending_verification',
membership: 'yearly_enterprise',
membership_start: null
},
status: 'pending_init',
created_at: new Date().toISOString(),
created_by: sessionDevId
};
// In production: store to database / encrypted file.
// For now: return the record (minus PAT).
console.log('[INDUSTRY] New industry registered: ' + industryId + ' ' + industry_name + ' by ' + rep_dev_id);
res.json({
status: 'ok',
industry_id: industryId,
message: '行业「' + industry_name + '」已注册 · 铸渊正在初始化仓库',
record: {
industry_id: record.industry_id,
name: record.name,
representative: {
dev_id: record.representative.dev_id,
name: record.representative.name,
github_username: record.representative.github_username,
repo_url: record.representative.repo_url,
pat_status: record.representative.pat_status
},
status: record.status,
created_at: record.created_at
}
});
});
/**
* GET /api/industry/list
* List all registered industries (admin only)
*/
router.get('/industry/list', rateLimit, requireSession, function(req, res) {
var sessionDev = req.sessionDev;
if (!sessionDev || sessionDev.level < 3) {
return res.status(403).json({
error: true,
code: 'PERMISSION_DENIED',
message: '仅管理者可查看行业列表'
});
}
// In production: read from database.
// For now: return placeholder.
res.json({
status: 'ok',
industries: [
{
industry_id: 'IND-001',
name: '网文行业',
representative: { dev_id: 'DEV-002', name: '肥猫' },
status: 'pending_setup'
}
]
});
});
/**
* GET /api/industry/status/:id
* Get status of a specific industry
*/
router.get('/industry/status/:id', rateLimit, requireSession, function(req, res) {
var industryId = req.params.id;
// In production: lookup from database
res.json({
status: 'ok',
industry_id: industryId,
message: '行业状态查询 · 功能开发中',
note: '待铸渊初始化行业仓库后可查询完整状态'
});
});
module.exports = router;