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 requireSession = authModule.requireSession;
var DEV_DATABASE = authModule.DEV_DATABASE; 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 * Check if a dev ID is a registered developer
* @param {string} devId * @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} * @returns {string}
*/ */
var industryCounter = 1;
function generateIndustryId() { function generateIndustryId() {
// Simple incremental ID; in production this would query existing IDs var id = 'IND-' + String(industryCounter).padStart(3, '0');
var timestamp = Date.now().toString(36).toUpperCase(); industryCounter++;
return 'IND-' + timestamp.slice(-3).padStart(3, '0'); return id;
} }
/** /**
@ -52,7 +79,7 @@ function hashPAT(pat) {
* Register a new industry with a representative developer * Register a new industry with a representative developer
* Requires authenticated session (DEV-XXX) * 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 industry_name = (req.body.industry_name || '').trim();
var rep_dev_id = (req.body.rep_dev_id || '').trim().toUpperCase(); var rep_dev_id = (req.body.rep_dev_id || '').trim().toUpperCase();
var github_username = (req.body.github_username || '').trim(); 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 // Step 3: validate PAT format (GitHub classic tokens start with ghp_,
// is done asynchronously after registration) // fine-grained tokens start with github_pat_)
if (pat.length < 10) { if (pat.length < 10 || !(pat.startsWith('ghp_') || pat.startsWith('github_pat_'))) {
return res.status(400).json({ return res.status(400).json({
error: true, error: true,
code: 'INVALID_PAT', 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 * GET /api/industry/list
* List all registered industries (admin only) * 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; var sessionDev = req.sessionDev;
if (!sessionDev || sessionDev.level < 3) { if (!sessionDev || sessionDev.level < 3) {
return res.status(403).json({ return res.status(403).json({
@ -177,7 +204,7 @@ router.get('/industry/list', requireSession, function(req, res) {
* GET /api/industry/status/:id * GET /api/industry/status/:id
* Get status of a specific industry * 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; var industryId = req.params.id;
// In production: lookup from database // 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 * Permission matrix for GitHub tools
* @param {string} devId - Developer ID * @param {string} devId - Developer ID
@ -103,8 +106,8 @@ var tools = [
* @returns {{ allowed: boolean, reason?: string }} * @returns {{ allowed: boolean, reason?: string }}
*/ */
function checkPermission(devId, repo) { function checkPermission(devId, repo) {
// TCS-0002 (冰朔) has access to all repos // Admin users have access to all repos
if (devId === 'TCS-0002' || devId === 'DEV-000') { if (ADMIN_IDS.indexOf(devId) >= 0) {
return { allowed: true }; return { allowed: true };
} }

View File

@ -26,7 +26,9 @@ function loadDailyQuota() {
try { try {
const data = JSON.parse(fs.readFileSync(QUOTA_LOG_PATH, 'utf8')); const data = JSON.parse(fs.readFileSync(QUOTA_LOG_PATH, 'utf8'));
if (data.date === today) return data; 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 { return {
date: today, date: today,
@ -49,13 +51,22 @@ function getTier(devId) {
for (const [tier, tierConfig] of Object.entries(config.priority_tiers)) { for (const [tier, tierConfig] of Object.entries(config.priority_tiers)) {
if (tierConfig.members && tierConfig.members.includes(devId)) return tier; 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) { function checkQuota(devId) {
const config = getQuotaConfig(); const config = getQuotaConfig();
const daily = loadDailyQuota(); const daily = loadDailyQuota();
const tier = getTier(devId); const tier = getTier(devId);
if (!tier) {
return {
allowed: false,
reason: '开发者 ' + devId + ' 未注册到任何配额层级',
suggestion: '请联系管理员将你添加到配额层级中'
};
}
const tierConfig = config.priority_tiers[tier]; const tierConfig = config.priority_tiers[tier];
const totalDaily = config.quota_pool.daily_total; const totalDaily = config.quota_pool.daily_total;