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 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
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue