fix: address code review feedback - rate limiting, PAT validation, tier registration

- Add per-route rate limiting to industry routes
- Validate PAT format (ghp_ / github_pat_ prefix)
- Fix industry ID generation to use counter
- Reject unknown devs in quota check instead of defaulting to P2
- Extract admin IDs to constant in github-server.js

Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/914ad98a-eb79-43bf-9d77-414bb7771bcb
This commit is contained in:
copilot-swe-agent[bot] 2026-03-26 05:05:54 +00:00 committed by GitHub
parent 6a79541493
commit 7d9ef12f69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 56 additions and 15 deletions

View File

@ -19,6 +19,32 @@ 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
@ -29,13 +55,14 @@ function isRegisteredDev(devId) {
}
/**
* Generate a new industry ID
* Generate a new industry ID using a counter
* @returns {string}
*/
var industryCounter = 1;
function generateIndustryId() {
// Simple incremental ID; in production this would query existing IDs
var timestamp = Date.now().toString(36).toUpperCase();
return 'IND-' + timestamp.slice(-3).padStart(3, '0');
var id = 'IND-' + String(industryCounter).padStart(3, '0');
industryCounter++;
return id;
}
/**
@ -52,7 +79,7 @@ function hashPAT(pat) {
* Register a new industry with a representative developer
* Requires authenticated session (DEV-XXX)
*/
router.post('/industry/register', requireSession, function(req, res) {
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();
@ -88,13 +115,13 @@ router.post('/industry/register', requireSession, function(req, res) {
});
}
// Step 3: validate PAT format (basic check — real validation against GitHub API
// is done asynchronously after registration)
if (pat.length < 10) {
// 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 格式无效'
message: 'PAT 格式无效 · 需要以 ghp_ 或 github_pat_ 开头的有效 GitHub Personal Access Token'
});
}
@ -148,7 +175,7 @@ router.post('/industry/register', requireSession, function(req, res) {
* GET /api/industry/list
* List all registered industries (admin only)
*/
router.get('/industry/list', requireSession, function(req, res) {
router.get('/industry/list', rateLimit, requireSession, function(req, res) {
var sessionDev = req.sessionDev;
if (!sessionDev || sessionDev.level < 3) {
return res.status(403).json({
@ -177,7 +204,7 @@ router.get('/industry/list', requireSession, function(req, res) {
* GET /api/industry/status/:id
* Get status of a specific industry
*/
router.get('/industry/status/:id', requireSession, function(req, res) {
router.get('/industry/status/:id', rateLimit, requireSession, function(req, res) {
var industryId = req.params.id;
// In production: lookup from database

View File

@ -96,6 +96,9 @@ var tools = [
}
];
// System admin IDs with full access to all repos
var ADMIN_IDS = ['TCS-0002', 'DEV-000'];
/**
* Permission matrix for GitHub tools
* @param {string} devId - Developer ID
@ -103,8 +106,8 @@ var tools = [
* @returns {{ allowed: boolean, reason?: string }}
*/
function checkPermission(devId, repo) {
// TCS-0002 (冰朔) has access to all repos
if (devId === 'TCS-0002' || devId === 'DEV-000') {
// Admin users have access to all repos
if (ADMIN_IDS.indexOf(devId) >= 0) {
return { allowed: true };
}

View File

@ -26,7 +26,9 @@ function loadDailyQuota() {
try {
const data = JSON.parse(fs.readFileSync(QUOTA_LOG_PATH, 'utf8'));
if (data.date === today) return data;
} catch (e) { /* file missing or date mismatch — reset */ }
} catch (e) {
// File missing or corrupted — will reset to fresh daily quota
}
return {
date: today,
@ -49,13 +51,22 @@ function getTier(devId) {
for (const [tier, tierConfig] of Object.entries(config.priority_tiers)) {
if (tierConfig.members && tierConfig.members.includes(devId)) return tier;
}
return 'P2'; // default lowest priority
return null; // unknown developer — not in any tier
}
function checkQuota(devId) {
const config = getQuotaConfig();
const daily = loadDailyQuota();
const tier = getTier(devId);
if (!tier) {
return {
allowed: false,
reason: '开发者 ' + devId + ' 未注册到任何配额层级',
suggestion: '请联系管理员将你添加到配额层级中'
};
}
const tierConfig = config.priority_tiers[tier];
const totalDaily = config.quota_pool.daily_total;