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:
parent
6a79541493
commit
7d9ef12f69
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue