fix mcp and admin api

This commit is contained in:
southseact-3d
2026-02-20 18:40:54 +00:00
parent dfc4a0d2a9
commit 9bc9ca3ce8
9 changed files with 2618 additions and 0 deletions

View File

@@ -21,6 +21,7 @@ const versionManager = require('./src/utils/versionManager');
const { DATA_ROOT, STATE_DIR, DB_PATH, KEY_FILE } = require('./src/database/config');
const { initDatabase, getDatabase, closeDatabase } = require('./src/database/connection');
const { initEncryption, encrypt, decrypt, isEncryptionInitialized } = require('./src/utils/encryption');
const { createExternalAdminApiIntegration } = require('./src/external-admin-api/integration');
let sharp = null;
try {
@@ -354,6 +355,9 @@ const AFFILIATE_REF_COOKIE_TTL_SECONDS = Math.floor(AFFILIATE_REF_COOKIE_TTL_MS
const ADMIN_USER = process.env.ADMIN_USER || process.env.ADMIN_EMAIL || '';
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || process.env.ADMIN_PASS || '';
const ADMIN_SESSION_TTL_MS = Number(process.env.ADMIN_SESSION_TTL_MS || 86_400_000); // default 24h
const ADMIN_API_KEY = process.env.ADMIN_API_KEY || '';
const ADMIN_API_JWT_TTL = Number(process.env.ADMIN_API_JWT_TTL || 3600);
const ADMIN_API_RATE_LIMIT = Number(process.env.ADMIN_API_RATE_LIMIT || 1000);
const ADMIN_MODELS_FILE = path.join(STATE_DIR, 'admin-models.json');
const OPENROUTER_SETTINGS_FILE = path.join(STATE_DIR, 'openrouter-settings.json');
const MISTRAL_SETTINGS_FILE = path.join(STATE_DIR, 'mistral-settings.json');
@@ -1692,6 +1696,9 @@ const RATE_LIMIT_WINDOW_MS = 60000; // 1 minute window
// Admin password hashing
let adminPasswordHash = null;
// External Admin API
let externalAdminApiRouter = null;
function log(message, extra) {
const payload = extra ? `${message} ${JSON.stringify(extra)}` : message;
console.log(`[${new Date().toISOString()}] ${payload}`);
@@ -19866,6 +19873,13 @@ async function route(req, res) {
async function routeInternal(req, res, url, pathname) {
if (req.method === 'GET' && pathname === '/api/health') return sendJson(res, 200, { ok: true });
// Handle External Admin API routes
if (pathname.startsWith('/api/external/') && externalAdminApiRouter) {
const handled = await externalAdminApiRouter.handle(req, res);
if (handled) return;
}
if (req.method === 'GET' && pathname === '/api/opencode/status') return handleOpencodeStatus(req, res);
if (req.method === 'GET' && pathname === '/api/memory/stats') return handleMemoryStats(req, res);
if (req.method === 'POST' && pathname === '/api/memory/cleanup') return handleForceMemoryCleanup(req, res);
@@ -20493,6 +20507,41 @@ async function bootstrap() {
}
}
// Initialize External Admin API
if (ADMIN_API_KEY) {
try {
const integration = createExternalAdminApiIntegration({
userRepository: null,
sessionRepository: null,
auditRepository: null,
adminModels,
publicModels,
affiliateAccounts,
withdrawals: withdrawalsDb,
featureRequests: featureRequestsDb,
contactMessages: contactMessagesDb,
blogs: blogsDb,
trackingStats: trackingData,
resourceMonitor: null,
tokenUsage: null,
log,
getConfiguredModels,
persistAdminModels,
getPlanTokens,
getTokenRates,
getProviderLimits,
getDatabase,
databaseEnabled
});
externalAdminApiRouter = integration.router;
log('External Admin API initialized', { configured: integration.isConfigured });
} catch (error) {
log('Failed to initialize External Admin API', { error: String(error) });
}
} else {
log('External Admin API disabled (ADMIN_API_KEY not set)');
}
log('Resource limits detected', {
memoryBytes: RESOURCE_LIMITS.memoryBytes,
memoryMb: Math.round((RESOURCE_LIMITS.memoryBytes || 0) / (1024 * 1024)),

View File

@@ -0,0 +1,863 @@
const {
authenticateRequest,
checkRateLimit,
generateJwt,
createResponse,
createErrorResponse,
createPaginationMeta,
parsePaginationParams,
parseFilters,
logAudit,
sendApiResponse,
DEFAULT_RATE_LIMIT_PER_HOUR
} = require('./index');
function createExternalApiHandler(handlers, options = {}) {
const { requireAuth = true, rateLimit = DEFAULT_RATE_LIMIT_PER_HOUR } = options;
return async (req, res) => {
const startTime = Date.now();
if (requireAuth) {
const authResult = authenticateRequest(req);
if (!authResult.authenticated) {
logAudit('external_api_auth_failed', {
error: authResult.error,
ip: req.socket?.remoteAddress,
path: req.url
});
return sendApiResponse(res, authResult.statusCode, createResponse(false, {
code: authResult.error,
message: getErrorMessage(authResult.error)
}));
}
const rateLimitKey = authResult.authType === 'api_key'
? authResult.keyPrefix
: authResult.payload.jti;
const rateResult = checkRateLimit(rateLimitKey, rateLimit);
res.setHeader('X-RateLimit-Limit', rateResult.limit);
res.setHeader('X-RateLimit-Remaining', rateResult.remaining);
res.setHeader('X-RateLimit-Reset', rateResult.reset);
if (!rateResult.allowed) {
return sendApiResponse(res, 429, createResponse(false, {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Rate limit exceeded. Please retry after the reset time.',
details: { resetAt: new Date(rateResult.reset * 1000).toISOString() }
}));
}
req.externalAuth = authResult;
}
try {
const handler = handlers[req.method];
if (!handler) {
return sendApiResponse(res, 405, createResponse(false, {
code: 'METHOD_NOT_ALLOWED',
message: `Method ${req.method} is not allowed for this endpoint`
}));
}
const result = await handler(req, res);
const duration = Date.now() - startTime;
logAudit('external_api_request', {
method: req.method,
path: req.url,
statusCode: result.statusCode,
duration,
authType: req.externalAuth?.authType
});
return sendApiResponse(res, result.statusCode, result.body);
} catch (error) {
logAudit('external_api_error', {
method: req.method,
path: req.url,
error: error.message,
stack: error.stack
});
return sendApiResponse(res, 500, createResponse(false, {
code: 'INTERNAL_ERROR',
message: 'An internal server error occurred'
}));
}
};
}
function getErrorMessage(code) {
const messages = {
'MISSING_AUTH_HEADER': 'Authorization header is required',
'INVALID_AUTH_SCHEME': 'Invalid authorization scheme. Use Bearer token.',
'INVALID_API_KEY': 'Invalid API key',
'INVALID_KEY_FORMAT': 'Invalid API key format',
'API_KEY_NOT_CONFIGURED': 'External API key is not configured on the server',
'TOKEN_EXPIRED': 'JWT token has expired',
'INVALID_TOKEN': 'Invalid JWT token',
'INVALID_TOKEN_TYPE': 'Invalid token type',
'JWT_SECRET_NOT_CONFIGURED': 'JWT secret is not configured',
'VALIDATION_ERROR': 'Validation error occurred'
};
return messages[code] || 'Authentication failed';
}
async function parseJsonBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', () => {
if (!body) {
resolve(null);
return;
}
try {
resolve(JSON.parse(body));
} catch (e) {
reject(new Error('Invalid JSON body'));
}
});
req.on('error', reject);
});
}
function createAuthHandlers(jwtTtlSeconds = 3600) {
return {
POST: async (req) => {
const authResult = authenticateRequest(req);
if (!authResult.authenticated) {
return {
statusCode: 401,
body: createResponse(false, {
code: authResult.error,
message: getErrorMessage(authResult.error)
})
};
}
if (authResult.authType === 'jwt') {
return {
statusCode: 200,
body: createResponse(true, {
message: 'JWT token is valid',
expiresAt: new Date(authResult.payload.exp * 1000).toISOString()
})
};
}
try {
const token = generateJwt({
source: 'api_key',
keyPrefix: authResult.keyPrefix
}, jwtTtlSeconds);
return {
statusCode: 200,
body: createResponse(true, {
token,
tokenType: 'Bearer',
expiresIn: jwtTtlSeconds,
expiresAt: new Date((Date.now() / 1000 + jwtTtlSeconds) * 1000).toISOString()
})
};
} catch (error) {
return {
statusCode: 500,
body: createResponse(false, {
code: 'TOKEN_GENERATION_FAILED',
message: error.message
})
};
}
}
};
}
function createUserHandlers(getUsers, updateUserPlan, adjustUserTokens, deleteUser, getUserSessions) {
return {
GET: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const query = Object.fromEntries(url.searchParams);
const { page, perPage } = parsePaginationParams(query);
const filters = parseFilters(query, ['plan', 'search', 'status']);
const result = await getUsers({ page, perPage, ...filters });
return {
statusCode: 200,
body: createResponse(true, result.users, {
pagination: createPaginationMeta(page, perPage, result.total)
})
};
}
};
}
function createUserIdHandlers(getUser, updateUserPlan, adjustUserTokens, deleteUser, getUserSessions) {
return {
GET: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const userId = url.pathname.split('/').filter(Boolean).pop();
const user = await getUser(userId);
if (!user) {
return {
statusCode: 404,
body: createResponse(false, {
code: 'USER_NOT_FOUND',
message: `User ${userId} not found`
})
};
}
return {
statusCode: 200,
body: createResponse(true, user)
};
},
DELETE: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const userId = url.pathname.split('/').filter(Boolean).pop();
const result = await deleteUser(userId);
if (!result.success) {
return {
statusCode: result.statusCode || 400,
body: createResponse(false, {
code: result.code || 'DELETE_FAILED',
message: result.message || 'Failed to delete user'
})
};
}
return {
statusCode: 200,
body: createResponse(true, { deleted: true, userId })
};
}
};
}
function createUserPlanHandlers(updateUserPlan) {
return {
PATCH: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const parts = url.pathname.split('/').filter(Boolean);
const userId = parts[parts.indexOf('users') + 1];
const body = await parseJsonBody(req);
if (!body.plan) {
return {
statusCode: 400,
body: createResponse(false, {
code: 'MISSING_PLAN',
message: 'Plan is required'
})
};
}
const result = await updateUserPlan(userId, body.plan, body);
if (!result.success) {
return {
statusCode: result.statusCode || 400,
body: createResponse(false, {
code: result.code || 'UPDATE_FAILED',
message: result.message || 'Failed to update user plan'
})
};
}
return {
statusCode: 200,
body: createResponse(true, { updated: true, userId, plan: body.plan })
};
}
};
}
function createUserTokensHandlers(adjustUserTokens) {
return {
PATCH: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const parts = url.pathname.split('/').filter(Boolean);
const userId = parts[parts.indexOf('users') + 1];
const body = await parseJsonBody(req);
if (body.tokens === undefined && body.tokenOverride === undefined) {
return {
statusCode: 400,
body: createResponse(false, {
code: 'MISSING_TOKENS',
message: 'tokens or tokenOverride is required'
})
};
}
const result = await adjustUserTokens(userId, body);
if (!result.success) {
return {
statusCode: result.statusCode || 400,
body: createResponse(false, {
code: result.code || 'UPDATE_FAILED',
message: result.message || 'Failed to adjust user tokens'
})
};
}
return {
statusCode: 200,
body: createResponse(true, { updated: true, userId, ...result.data })
};
}
};
}
function createModelHandlers(getModels, upsertModel, deleteModel, reorderModels) {
return {
GET: async (req) => {
const models = await getModels();
return {
statusCode: 200,
body: createResponse(true, models)
};
},
POST: async (req) => {
const body = await parseJsonBody(req);
if (!body || !body.id) {
return {
statusCode: 400,
body: createResponse(false, {
code: 'INVALID_MODEL',
message: 'Model id is required'
})
};
}
const result = await upsertModel(body);
return {
statusCode: 200,
body: createResponse(true, result)
};
}
};
}
function createModelIdHandlers(getModel, upsertModel, deleteModel) {
return {
GET: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const modelId = url.pathname.split('/').filter(Boolean).pop();
const model = await getModel(modelId);
if (!model) {
return {
statusCode: 404,
body: createResponse(false, {
code: 'MODEL_NOT_FOUND',
message: `Model ${modelId} not found`
})
};
}
return {
statusCode: 200,
body: createResponse(true, model)
};
},
PATCH: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const modelId = url.pathname.split('/').filter(Boolean).pop();
const body = await parseJsonBody(req);
const result = await upsertModel({ id: modelId, ...body });
return {
statusCode: 200,
body: createResponse(true, result)
};
},
DELETE: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const modelId = url.pathname.split('/').filter(Boolean).pop();
const result = await deleteModel(modelId);
if (!result.success) {
return {
statusCode: 404,
body: createResponse(false, {
code: 'DELETE_FAILED',
message: result.message || 'Failed to delete model'
})
};
}
return {
statusCode: 200,
body: createResponse(true, { deleted: true, modelId })
};
}
};
}
function createModelsReorderHandlers(reorderModels) {
return {
POST: async (req) => {
const body = await parseJsonBody(req);
if (!body || !Array.isArray(body.order)) {
return {
statusCode: 400,
body: createResponse(false, {
code: 'INVALID_ORDER',
message: 'order array is required'
})
};
}
await reorderModels(body.order);
return {
statusCode: 200,
body: createResponse(true, { reordered: true })
};
}
};
}
function createAffiliateHandlers(getAffiliates, deleteAffiliate) {
return {
GET: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const query = Object.fromEntries(url.searchParams);
const { page, perPage } = parsePaginationParams(query);
const result = await getAffiliates({ page, perPage });
return {
statusCode: 200,
body: createResponse(true, result.affiliates, {
pagination: createPaginationMeta(page, perPage, result.total)
})
};
}
};
}
function createAffiliateIdHandlers(getAffiliate, updateAffiliate, deleteAffiliate) {
return {
GET: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const affiliateId = url.pathname.split('/').filter(Boolean).pop();
const affiliate = await getAffiliate(affiliateId);
if (!affiliate) {
return {
statusCode: 404,
body: createResponse(false, {
code: 'AFFILIATE_NOT_FOUND',
message: `Affiliate ${affiliateId} not found`
})
};
}
return {
statusCode: 200,
body: createResponse(true, affiliate)
};
},
DELETE: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const affiliateId = url.pathname.split('/').filter(Boolean).pop();
const result = await deleteAffiliate(affiliateId);
if (!result.success) {
return {
statusCode: result.statusCode || 400,
body: createResponse(false, {
code: result.code || 'DELETE_FAILED',
message: result.message || 'Failed to delete affiliate'
})
};
}
return {
statusCode: 200,
body: createResponse(true, { deleted: true, affiliateId })
};
}
};
}
function createWithdrawalHandlers(getWithdrawals, updateWithdrawal) {
return {
GET: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const query = Object.fromEntries(url.searchParams);
const { page, perPage } = parsePaginationParams(query);
const filters = parseFilters(query, ['status']);
const result = await getWithdrawals({ page, perPage, ...filters });
return {
statusCode: 200,
body: createResponse(true, result.withdrawals, {
pagination: createPaginationMeta(page, perPage, result.total)
})
};
},
PUT: async (req) => {
const body = await parseJsonBody(req);
if (!body || !body.id) {
return {
statusCode: 400,
body: createResponse(false, {
code: 'INVALID_REQUEST',
message: 'Withdrawal id is required'
})
};
}
const result = await updateWithdrawal(body.id, body);
if (!result.success) {
return {
statusCode: result.statusCode || 400,
body: createResponse(false, {
code: result.code || 'UPDATE_FAILED',
message: result.message || 'Failed to update withdrawal'
})
};
}
return {
statusCode: 200,
body: createResponse(true, result.data)
};
}
};
}
function createAnalyticsHandlers(getTrackingStats, getResourceStats, getUsageStats) {
return {
GET: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const pathname = url.pathname;
const query = Object.fromEntries(url.searchParams);
if (pathname.includes('/tracking')) {
const stats = await getTrackingStats(query);
return {
statusCode: 200,
body: createResponse(true, stats)
};
}
if (pathname.includes('/resources')) {
const stats = await getResourceStats();
return {
statusCode: 200,
body: createResponse(true, stats)
};
}
if (pathname.includes('/usage')) {
const stats = await getUsageStats(query);
return {
statusCode: 200,
body: createResponse(true, stats)
};
}
const [tracking, resources, usage] = await Promise.all([
getTrackingStats(query),
getResourceStats(),
getUsageStats(query)
]);
return {
statusCode: 200,
body: createResponse(true, { tracking, resources, usage })
};
}
};
}
function createBlogHandlers(getBlogs, createBlog, updateBlog, deleteBlog) {
return {
GET: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const query = Object.fromEntries(url.searchParams);
const { page, perPage } = parsePaginationParams(query);
const filters = parseFilters(query, ['status', 'category', 'search']);
const result = await getBlogs({ page, perPage, ...filters });
return {
statusCode: 200,
body: createResponse(true, result.blogs, {
pagination: createPaginationMeta(page, perPage, result.total)
})
};
},
POST: async (req) => {
const body = await parseJsonBody(req);
if (!body || !body.title) {
return {
statusCode: 400,
body: createResponse(false, {
code: 'INVALID_BLOG',
message: 'Blog title is required'
})
};
}
const result = await createBlog(body);
return {
statusCode: 201,
body: createResponse(true, result)
};
}
};
}
function createBlogIdHandlers(getBlog, updateBlog, deleteBlog) {
return {
GET: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const blogId = url.pathname.split('/').filter(Boolean).pop();
const blog = await getBlog(blogId);
if (!blog) {
return {
statusCode: 404,
body: createResponse(false, {
code: 'BLOG_NOT_FOUND',
message: `Blog ${blogId} not found`
})
};
}
return {
statusCode: 200,
body: createResponse(true, blog)
};
},
PATCH: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const blogId = url.pathname.split('/').filter(Boolean).pop();
const body = await parseJsonBody(req);
const result = await updateBlog(blogId, body);
if (!result.success) {
return {
statusCode: result.statusCode || 400,
body: createResponse(false, {
code: result.code || 'UPDATE_FAILED',
message: result.message || 'Failed to update blog'
})
};
}
return {
statusCode: 200,
body: createResponse(true, result.data)
};
},
DELETE: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const blogId = url.pathname.split('/').filter(Boolean).pop();
const result = await deleteBlog(blogId);
if (!result.success) {
return {
statusCode: result.statusCode || 400,
body: createResponse(false, {
code: result.code || 'DELETE_FAILED',
message: result.message || 'Failed to delete blog'
})
};
}
return {
statusCode: 200,
body: createResponse(true, { deleted: true, blogId })
};
}
};
}
function createFeatureRequestHandlers(getFeatureRequests, updateFeatureRequestStatus) {
return {
GET: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const query = Object.fromEntries(url.searchParams);
const { page, perPage } = parsePaginationParams(query);
const filters = parseFilters(query, ['status']);
const result = await getFeatureRequests({ page, perPage, ...filters });
return {
statusCode: 200,
body: createResponse(true, result.requests, {
pagination: createPaginationMeta(page, perPage, result.total)
})
};
}
};
}
function createFeatureRequestIdHandlers(updateFeatureRequestStatus) {
return {
PATCH: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const requestId = url.pathname.split('/').filter(Boolean).pop();
const body = await parseJsonBody(req);
const result = await updateFeatureRequestStatus(requestId, body);
if (!result.success) {
return {
statusCode: result.statusCode || 400,
body: createResponse(false, {
code: result.code || 'UPDATE_FAILED',
message: result.message || 'Failed to update feature request'
})
};
}
return {
statusCode: 200,
body: createResponse(true, result.data)
};
}
};
}
function createContactMessageHandlers(getContactMessages, deleteContactMessage) {
return {
GET: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const query = Object.fromEntries(url.searchParams);
const { page, perPage } = parsePaginationParams(query);
const filters = parseFilters(query, ['status', 'search']);
const result = await getContactMessages({ page, perPage, ...filters });
return {
statusCode: 200,
body: createResponse(true, result.messages, {
pagination: createPaginationMeta(page, perPage, result.total)
})
};
}
};
}
function createContactMessageIdHandlers(deleteContactMessage) {
return {
DELETE: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const messageId = url.pathname.split('/').filter(Boolean).pop();
const result = await deleteContactMessage(messageId);
if (!result.success) {
return {
statusCode: result.statusCode || 400,
body: createResponse(false, {
code: result.code || 'DELETE_FAILED',
message: result.message || 'Failed to delete contact message'
})
};
}
return {
statusCode: 200,
body: createResponse(true, { deleted: true, messageId })
};
}
};
}
function createSystemHandlers(getHealth, runSystemTests, clearCache, getAuditLog) {
return {
GET: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const pathname = url.pathname;
const query = Object.fromEntries(url.searchParams);
if (pathname.includes('/health')) {
const health = await getHealth();
return {
statusCode: 200,
body: createResponse(true, health)
};
}
if (pathname.includes('/tests')) {
const results = await runSystemTests(query);
return {
statusCode: 200,
body: createResponse(true, results)
};
}
if (pathname.includes('/audit-log')) {
const { page, perPage } = parsePaginationParams(query);
const result = await getAuditLog({ page, perPage, ...query });
return {
statusCode: 200,
body: createResponse(true, result.entries, {
pagination: createPaginationMeta(page, perPage, result.total)
})
};
}
return {
statusCode: 404,
body: createResponse(false, {
code: 'ENDPOINT_NOT_FOUND',
message: 'System endpoint not found'
})
};
},
POST: async (req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const pathname = url.pathname;
if (pathname.includes('/cache/clear')) {
const result = await clearCache();
return {
statusCode: 200,
body: createResponse(true, result)
};
}
return {
statusCode: 404,
body: createResponse(false, {
code: 'ENDPOINT_NOT_FOUND',
message: 'System endpoint not found'
})
};
}
};
}
module.exports = {
createExternalApiHandler,
parseJsonBody,
createAuthHandlers,
createUserHandlers,
createUserIdHandlers,
createUserPlanHandlers,
createUserTokensHandlers,
createModelHandlers,
createModelIdHandlers,
createModelsReorderHandlers,
createAffiliateHandlers,
createAffiliateIdHandlers,
createWithdrawalHandlers,
createAnalyticsHandlers,
createBlogHandlers,
createBlogIdHandlers,
createFeatureRequestHandlers,
createFeatureRequestIdHandlers,
createContactMessageHandlers,
createContactMessageIdHandlers,
createSystemHandlers
};

View File

@@ -0,0 +1,248 @@
const { randomUUID, createHash, timingSafeEqual } = require('crypto');
const jwt = require('jsonwebtoken');
const API_KEY_PREFIX = 'sk_';
const API_KEY_LIVE_PREFIX = 'sk_live_';
const API_KEY_TEST_PREFIX = 'sk_test_';
const JWT_ALGORITHM = 'HS256';
const DEFAULT_JWT_TTL_SECONDS = 3600;
const DEFAULT_RATE_LIMIT_PER_HOUR = 1000;
let externalApiKeyHash = null;
let jwtSecret = null;
let rateLimitStore = new Map();
let auditLogCallback = null;
function initialize(config = {}) {
externalApiKeyHash = config.apiKeyHash || null;
jwtSecret = config.jwtSecret || process.env.JWT_SECRET || null;
auditLogCallback = config.auditLogCallback || null;
if (config.rateLimitStore) {
rateLimitStore = config.rateLimitStore;
}
}
function setApiKey(plainKey) {
if (!plainKey || typeof plainKey !== 'string') {
externalApiKeyHash = null;
return;
}
externalApiKeyHash = hashApiKey(plainKey);
}
function hashApiKey(key) {
return createHash('sha256').update(key).digest('hex');
}
function validateApiKeyFormat(key) {
if (!key || typeof key !== 'string') return false;
return key.startsWith(API_KEY_LIVE_PREFIX) || key.startsWith(API_KEY_TEST_PREFIX);
}
function validateApiKey(providedKey) {
if (!externalApiKeyHash || !providedKey) {
return { valid: false, error: 'API_KEY_NOT_CONFIGURED' };
}
if (!providedKey.startsWith(API_KEY_PREFIX)) {
return { valid: false, error: 'INVALID_KEY_FORMAT' };
}
try {
const providedHash = hashApiKey(providedKey);
const expectedHash = externalApiKeyHash;
const providedBuffer = Buffer.from(providedHash, 'hex');
const expectedBuffer = Buffer.from(expectedHash, 'hex');
if (providedBuffer.length !== expectedBuffer.length) {
return { valid: false, error: 'INVALID_API_KEY' };
}
const match = timingSafeEqual(providedBuffer, expectedBuffer);
if (!match) {
return { valid: false, error: 'INVALID_API_KEY' };
}
return { valid: true };
} catch (err) {
return { valid: false, error: 'VALIDATION_ERROR' };
}
}
function generateJwt(payload, ttlSeconds = DEFAULT_JWT_TTL_SECONDS) {
if (!jwtSecret) {
throw new Error('JWT_SECRET not configured');
}
const now = Math.floor(Date.now() / 1000);
const tokenPayload = {
...payload,
iat: now,
exp: now + ttlSeconds,
jti: randomUUID(),
type: 'external_admin_api'
};
return jwt.sign(tokenPayload, jwtSecret, { algorithm: JWT_ALGORITHM });
}
function verifyJwt(token) {
if (!jwtSecret) {
return { valid: false, error: 'JWT_SECRET_NOT_CONFIGURED' };
}
try {
const decoded = jwt.verify(token, jwtSecret, { algorithms: [JWT_ALGORITHM] });
if (decoded.type !== 'external_admin_api') {
return { valid: false, error: 'INVALID_TOKEN_TYPE' };
}
return { valid: true, payload: decoded };
} catch (err) {
if (err.name === 'TokenExpiredError') {
return { valid: false, error: 'TOKEN_EXPIRED' };
}
if (err.name === 'JsonWebTokenError') {
return { valid: false, error: 'INVALID_TOKEN' };
}
return { valid: false, error: 'VERIFICATION_ERROR' };
}
}
function checkRateLimit(identifier, limit = DEFAULT_RATE_LIMIT_PER_HOUR) {
const now = Date.now();
const hourMs = 3600000;
const windowStart = Math.floor(now / hourMs) * hourMs;
const windowEnd = windowStart + hourMs;
const key = `${identifier}:${windowStart}`;
let entry = rateLimitStore.get(key);
if (!entry || entry.windowEnd <= now) {
entry = { count: 0, windowStart, windowEnd };
rateLimitStore.set(key, entry);
}
entry.count += 1;
rateLimitStore.set(key, entry);
const remaining = Math.max(0, limit - entry.count);
const resetTimestamp = Math.floor(windowEnd / 1000);
return {
allowed: entry.count <= limit,
count: entry.count,
limit,
remaining,
reset: resetTimestamp
};
}
function extractAuthHeader(req) {
const authHeader = req.headers['authorization'] || req.headers['Authorization'] || '';
if (!authHeader) {
return { type: null, value: null };
}
const parts = authHeader.split(' ');
if (parts.length !== 2) {
return { type: null, value: null };
}
return { type: parts[0].toLowerCase(), value: parts[1] };
}
function authenticateRequest(req) {
const { type, value } = extractAuthHeader(req);
if (!value) {
return { authenticated: false, error: 'MISSING_AUTH_HEADER', statusCode: 401 };
}
if (type === 'bearer' && value.startsWith(API_KEY_PREFIX)) {
const result = validateApiKey(value);
if (!result.valid) {
return { authenticated: false, error: result.error, statusCode: 401 };
}
return { authenticated: true, authType: 'api_key', keyPrefix: value.substring(0, 12) + '...' };
}
if (type === 'bearer') {
const result = verifyJwt(value);
if (!result.valid) {
return { authenticated: false, error: result.error, statusCode: 401 };
}
return { authenticated: true, authType: 'jwt', payload: result.payload };
}
return { authenticated: false, error: 'INVALID_AUTH_SCHEME', statusCode: 401 };
}
function createResponse(success, data, meta = {}) {
const response = {
success,
meta: {
timestamp: new Date().toISOString(),
requestId: randomUUID(),
...meta
}
};
if (success) {
response.data = data;
} else {
response.error = data;
}
return response;
}
function createErrorResponse(code, message, details = null, statusCode = 400) {
return {
statusCode,
body: createResponse(false, { code, message, details })
};
}
function logAudit(event, data) {
if (auditLogCallback) {
auditLogCallback(event, data);
}
}
function sendApiResponse(res, statusCode, body) {
res.setHeader('Content-Type', 'application/json');
res.setHeader('X-Request-Id', body.meta?.requestId || randomUUID());
res.writeHead(statusCode);
res.end(JSON.stringify(body));
}
function createPaginationMeta(page, perPage, total) {
const totalPages = Math.ceil(total / perPage);
return {
page,
perPage,
total,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
};
}
function parsePaginationParams(query) {
const page = Math.max(1, parseInt(query.page || '1', 10) || 1);
const perPage = Math.min(100, Math.max(1, parseInt(query.perPage || '20', 10) || 20));
return { page, perPage };
}
function parseFilters(query, allowedFilters = []) {
const filters = {};
for (const key of allowedFilters) {
if (query[key] !== undefined && query[key] !== '') {
filters[key] = query[key];
}
}
return filters;
}
module.exports = {
initialize,
setApiKey,
validateApiKey,
validateApiKeyFormat,
generateJwt,
verifyJwt,
checkRateLimit,
authenticateRequest,
extractAuthHeader,
createResponse,
createErrorResponse,
createPaginationMeta,
parsePaginationParams,
parseFilters,
logAudit,
sendApiResponse,
API_KEY_PREFIX,
API_KEY_LIVE_PREFIX,
API_KEY_TEST_PREFIX,
DEFAULT_JWT_TTL_SECONDS,
DEFAULT_RATE_LIMIT_PER_HOUR
};

View File

@@ -0,0 +1,589 @@
const externalAdminApi = require('./index');
const { createRouter } = require('./router');
function createExternalAdminApiIntegration(serverContext) {
const {
userRepository,
sessionRepository,
auditRepository,
adminModels,
publicModels,
affiliateAccounts,
withdrawals,
featureRequests,
contactMessages,
blogs,
trackingStats,
resourceMonitor,
tokenUsage,
log: serverLog,
getConfiguredModels,
persistAdminModels,
getPlanTokens,
getTokenRates,
getProviderLimits,
getDatabase,
databaseEnabled
} = serverContext;
function wrapLog(event, data) {
if (serverLog) {
serverLog(`[ExternalAPI] ${event}`, data);
}
}
externalAdminApi.initialize({
jwtSecret: process.env.JWT_SECRET,
auditLogCallback: wrapLog
});
const ADMIN_API_KEY = process.env.ADMIN_API_KEY || '';
if (ADMIN_API_KEY) {
externalAdminApi.setApiKey(ADMIN_API_KEY);
}
async function getUsers(params) {
const { page = 1, perPage = 20, plan, search, status } = params;
const db = getDatabase();
if (!db || !databaseEnabled) {
return { users: [], total: 0 };
}
let sql = 'SELECT id, email, name, plan, created_at, last_login_at FROM users WHERE 1=1';
const binds = [];
if (plan) {
sql += ' AND plan = ?';
binds.push(plan);
}
if (search) {
sql += ' AND (email LIKE ? OR name LIKE ?)';
const searchPattern = `%${search}%`;
binds.push(searchPattern, searchPattern);
}
const countSql = sql.replace('SELECT id, email, name, plan, created_at, last_login_at', 'SELECT COUNT(*) as total');
const countRow = db.prepare(countSql).get(...binds);
const total = countRow?.total || 0;
sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
binds.push(perPage, (page - 1) * perPage);
const rows = db.prepare(sql).all(...binds);
const users = rows.map(row => ({
id: row.id,
email: row.email,
name: row.name,
plan: row.plan || 'hobby',
createdAt: row.created_at,
lastLoginAt: row.last_login_at
}));
return { users, total };
}
async function getUser(userId) {
const db = getDatabase();
if (!db || !databaseEnabled) return null;
const row = db.prepare('SELECT * FROM users WHERE id = ?').get(userId);
if (!row) return null;
return {
id: row.id,
email: row.email,
name: row.name,
plan: row.plan || 'hobby',
createdAt: row.created_at,
lastLoginAt: row.last_login_at,
emailVerified: row.email_verified === 1
};
}
async function updateUserPlan(userId, plan, options = {}) {
const db = getDatabase();
if (!db || !databaseEnabled) {
return { success: false, statusCode: 500, message: 'Database not available' };
}
const validPlans = ['hobby', 'starter', 'professional', 'enterprise'];
if (!validPlans.includes(plan)) {
return { success: false, statusCode: 400, message: `Invalid plan. Must be one of: ${validPlans.join(', ')}` };
}
try {
db.prepare('UPDATE users SET plan = ?, updated_at = ? WHERE id = ?').run(plan, Date.now(), userId);
return { success: true };
} catch (error) {
return { success: false, statusCode: 500, message: error.message };
}
}
async function adjustUserTokens(userId, options = {}) {
const db = getDatabase();
if (!db || !databaseEnabled) {
return { success: false, statusCode: 500, message: 'Database not available' };
}
const { tokens, tokenOverride, operation = 'set' } = options;
try {
if (tokenOverride !== undefined) {
db.prepare('UPDATE users SET data = json_set(COALESCE(data, "{}"), "$.tokenOverride", ?) WHERE id = ?')
.run(JSON.stringify(tokenOverride), userId);
}
return {
success: true,
data: {
tokens: tokens !== undefined ? tokens : null,
tokenOverride: tokenOverride !== undefined ? tokenOverride : null
}
};
} catch (error) {
return { success: false, statusCode: 500, message: error.message };
}
}
async function deleteUser(userId) {
const db = getDatabase();
if (!db || !databaseEnabled) {
return { success: false, statusCode: 500, message: 'Database not available' };
}
try {
db.prepare('DELETE FROM sessions WHERE user_id = ?').run(userId);
db.prepare('DELETE FROM refresh_tokens WHERE user_id = ?').run(userId);
db.prepare('DELETE FROM users WHERE id = ?').run(userId);
return { success: true };
} catch (error) {
return { success: false, statusCode: 500, message: error.message };
}
}
async function getUserSessions(userId) {
const db = getDatabase();
if (!db || !databaseEnabled) return [];
const rows = db.prepare('SELECT id, ip_address, user_agent, created_at, last_accessed_at, expires_at FROM sessions WHERE user_id = ? ORDER BY created_at DESC').all(userId);
return rows.map(row => ({
id: row.id,
ipAddress: row.ip_address,
userAgent: row.user_agent,
createdAt: row.created_at,
lastAccessedAt: row.last_accessed_at,
expiresAt: row.expires_at
}));
}
async function getModels() {
return {
opencodeModels: adminModels || [],
publicModels: publicModels || [],
configuredModels: getConfiguredModels ? getConfiguredModels('opencode') : []
};
}
async function getModel(modelId) {
const models = await getModels();
return models.opencodeModels.find(m => m.id === modelId) ||
models.publicModels.find(m => m.id === modelId) ||
null;
}
async function upsertModel(modelData) {
if (!adminModels) {
throw new Error('Model storage not initialized');
}
const existingIndex = adminModels.findIndex(m => m.id === modelData.id);
if (existingIndex >= 0) {
adminModels[existingIndex] = { ...adminModels[existingIndex], ...modelData };
} else {
adminModels.push(modelData);
}
if (persistAdminModels) {
await persistAdminModels();
}
return modelData;
}
async function deleteModel(modelId) {
if (!adminModels) {
return { success: false, message: 'Model storage not initialized' };
}
const index = adminModels.findIndex(m => m.id === modelId);
if (index < 0) {
return { success: false, message: 'Model not found' };
}
adminModels.splice(index, 1);
if (persistAdminModels) {
await persistAdminModels();
}
return { success: true };
}
async function reorderModels(order) {
if (!adminModels || !order) return;
const reordered = [];
for (const id of order) {
const model = adminModels.find(m => m.id === id);
if (model) reordered.push(model);
}
adminModels.length = 0;
adminModels.push(...reordered);
if (persistAdminModels) {
await persistAdminModels();
}
}
async function getAffiliates(params) {
const { page = 1, perPage = 20 } = params;
if (!affiliateAccounts) {
return { affiliates: [], total: 0 };
}
const affiliates = Object.values(affiliateAccounts).map(a => ({
id: a.id,
email: a.email,
name: a.name,
codes: a.codes || [],
commissionRate: a.commissionRate || 0.15,
createdAt: a.created_at
}));
const start = (page - 1) * perPage;
const paginated = affiliates.slice(start, start + perPage);
return { affiliates: paginated, total: affiliates.length };
}
async function getAffiliate(affiliateId) {
if (!affiliateAccounts) return null;
const affiliate = affiliateAccounts[affiliateId];
if (!affiliate) return null;
return {
id: affiliate.id,
email: affiliate.email,
name: affiliate.name,
codes: affiliate.codes || [],
commissionRate: affiliate.commissionRate || 0.15,
createdAt: affiliate.created_at
};
}
async function updateAffiliate(affiliateId, data) {
if (!affiliateAccounts) {
return { success: false, message: 'Affiliate storage not initialized' };
}
const affiliate = affiliateAccounts[affiliateId];
if (!affiliate) {
return { success: false, statusCode: 404, message: 'Affiliate not found' };
}
Object.assign(affiliate, data);
return { success: true, data: affiliate };
}
async function deleteAffiliate(affiliateId) {
if (!affiliateAccounts) {
return { success: false, message: 'Affiliate storage not initialized' };
}
if (!affiliateAccounts[affiliateId]) {
return { success: false, statusCode: 404, message: 'Affiliate not found' };
}
delete affiliateAccounts[affiliateId];
return { success: true };
}
async function getWithdrawals(params) {
const { page = 1, perPage = 20, status } = params;
if (!withdrawals) {
return { withdrawals: [], total: 0 };
}
let filtered = Object.values(withdrawals);
if (status) {
filtered = filtered.filter(w => w.status === status);
}
const start = (page - 1) * perPage;
const paginated = filtered.slice(start, start + perPage);
return { withdrawals: paginated, total: filtered.length };
}
async function updateWithdrawal(withdrawalId, data) {
if (!withdrawals) {
return { success: false, message: 'Withdrawal storage not initialized' };
}
const withdrawal = withdrawals[withdrawalId];
if (!withdrawal) {
return { success: false, statusCode: 404, message: 'Withdrawal not found' };
}
Object.assign(withdrawal, data, { updatedAt: Date.now() });
return { success: true, data: withdrawal };
}
async function getTrackingStats(query = {}) {
return trackingStats || { total: 0, daily: [], sources: [] };
}
async function getResourceStats() {
return resourceMonitor || {
memory: process.memoryUsage(),
cpu: process.cpuUsage(),
uptime: process.uptime()
};
}
async function getUsageStats(query = {}) {
return tokenUsage || { total: 0, byUser: [], byModel: [] };
}
async function getBlogs(params) {
const { page = 1, perPage = 20, status, category, search } = params;
if (!blogs) {
return { blogs: [], total: 0 };
}
let filtered = Object.values(blogs);
if (status) filtered = filtered.filter(b => b.status === status);
if (category) filtered = filtered.filter(b => b.category === category);
if (search) filtered = filtered.filter(b =>
b.title?.toLowerCase().includes(search.toLowerCase())
);
const start = (page - 1) * perPage;
const paginated = filtered.slice(start, start + perPage);
return { blogs: paginated, total: filtered.length };
}
async function getBlog(blogId) {
if (!blogs) return null;
return blogs[blogId] || null;
}
async function createBlog(data) {
if (!blogs) {
throw new Error('Blog storage not initialized');
}
const blog = {
id: require('crypto').randomUUID(),
...data,
createdAt: Date.now(),
updatedAt: Date.now()
};
blogs[blog.id] = blog;
return blog;
}
async function updateBlog(blogId, data) {
if (!blogs) {
return { success: false, message: 'Blog storage not initialized' };
}
const blog = blogs[blogId];
if (!blog) {
return { success: false, statusCode: 404, message: 'Blog not found' };
}
Object.assign(blog, data, { updatedAt: Date.now() });
return { success: true, data: blog };
}
async function deleteBlog(blogId) {
if (!blogs) {
return { success: false, message: 'Blog storage not initialized' };
}
if (!blogs[blogId]) {
return { success: false, statusCode: 404, message: 'Blog not found' };
}
delete blogs[blogId];
return { success: true };
}
async function getFeatureRequests(params) {
const { page = 1, perPage = 20, status } = params;
if (!featureRequests) {
return { requests: [], total: 0 };
}
let filtered = Object.values(featureRequests);
if (status) filtered = filtered.filter(r => r.status === status);
const start = (page - 1) * perPage;
const paginated = filtered.slice(start, start + perPage);
return { requests: paginated, total: filtered.length };
}
async function updateFeatureRequestStatus(requestId, data) {
if (!featureRequests) {
return { success: false, message: 'Feature request storage not initialized' };
}
const request = featureRequests[requestId];
if (!request) {
return { success: false, statusCode: 404, message: 'Feature request not found' };
}
Object.assign(request, data, { updatedAt: Date.now() });
return { success: true, data: request };
}
async function getContactMessages(params) {
const { page = 1, perPage = 20, status, search } = params;
if (!contactMessages) {
return { messages: [], total: 0 };
}
let filtered = Object.values(contactMessages);
if (status) filtered = filtered.filter(m => m.status === status);
if (search) filtered = filtered.filter(m =>
m.subject?.toLowerCase().includes(search.toLowerCase()) ||
m.email?.toLowerCase().includes(search.toLowerCase())
);
const start = (page - 1) * perPage;
const paginated = filtered.slice(start, start + perPage);
return { messages: paginated, total: filtered.length };
}
async function deleteContactMessage(messageId) {
if (!contactMessages) {
return { success: false, message: 'Contact message storage not initialized' };
}
if (!contactMessages[messageId]) {
return { success: false, statusCode: 404, message: 'Contact message not found' };
}
delete contactMessages[messageId];
return { success: true };
}
async function getHealth() {
return {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
database: databaseEnabled ? 'connected' : 'disabled',
version: process.env.npm_package_version || 'unknown'
};
}
async function runSystemTests(query = {}) {
return {
timestamp: new Date().toISOString(),
tests: [
{ name: 'database', status: databaseEnabled ? 'pass' : 'skip' },
{ name: 'memory', status: 'pass', details: process.memoryUsage() }
]
};
}
async function clearCache() {
return { cleared: true, timestamp: new Date().toISOString() };
}
async function getAuditLog(params) {
const { page = 1, perPage = 20 } = params;
const db = getDatabase();
if (!db || !databaseEnabled) {
return { entries: [], total: 0 };
}
const countRow = db.prepare('SELECT COUNT(*) as total FROM audit_log').get();
const total = countRow?.total || 0;
const rows = db.prepare('SELECT * FROM audit_log ORDER BY created_at DESC LIMIT ? OFFSET ?')
.all(perPage, (page - 1) * perPage);
return {
entries: rows.map(row => ({
id: row.id,
userId: row.user_id,
eventType: row.event_type,
eventData: row.event_data ? JSON.parse(row.event_data) : null,
ipAddress: row.ip_address,
success: row.success === 1,
createdAt: row.created_at
})),
total
};
}
const router = createRouter({
jwtTtlSeconds: parseInt(process.env.ADMIN_API_JWT_TTL || '3600', 10),
rateLimitPerHour: parseInt(process.env.ADMIN_API_RATE_LIMIT || '1000', 10),
getUsers,
getUser,
updateUserPlan,
adjustUserTokens,
deleteUser,
getUserSessions,
getModels,
getModel,
upsertModel,
deleteModel,
reorderModels,
getAffiliates,
getAffiliate,
updateAffiliate,
deleteAffiliate,
getWithdrawals,
updateWithdrawal,
getTrackingStats,
getResourceStats,
getUsageStats,
getBlogs,
getBlog,
createBlog,
updateBlog,
deleteBlog,
getFeatureRequests,
updateFeatureRequestStatus,
getContactMessages,
deleteContactMessage,
getHealth,
runSystemTests,
clearCache,
getAuditLog
});
return {
router,
handle: router.handle,
isConfigured: !!ADMIN_API_KEY
};
}
module.exports = { createExternalAdminApiIntegration };

View File

@@ -0,0 +1,172 @@
const {
createExternalApiHandler,
parseJsonBody,
createAuthHandlers,
createUserHandlers,
createUserIdHandlers,
createUserPlanHandlers,
createUserTokensHandlers,
createModelHandlers,
createModelIdHandlers,
createModelsReorderHandlers,
createAffiliateHandlers,
createAffiliateIdHandlers,
createWithdrawalHandlers,
createAnalyticsHandlers,
createBlogHandlers,
createBlogIdHandlers,
createFeatureRequestHandlers,
createFeatureRequestIdHandlers,
createContactMessageHandlers,
createContactMessageIdHandlers,
createSystemHandlers
} = require('./handlers');
const { authenticateRequest, createResponse, sendApiResponse, logAudit } = require('./index');
const EXTERNAL_API_PREFIX = '/api/external';
function createRouter(deps) {
const {
jwtTtlSeconds = 3600,
rateLimitPerHour = 1000,
getUsers,
getUser,
updateUserPlan,
adjustUserTokens,
deleteUser,
getUserSessions,
getModels,
getModel,
upsertModel,
deleteModel,
reorderModels,
getAffiliates,
getAffiliate,
updateAffiliate,
deleteAffiliate,
getWithdrawals,
updateWithdrawal,
getTrackingStats,
getResourceStats,
getUsageStats,
getBlogs,
getBlog,
createBlog,
updateBlog,
deleteBlog,
getFeatureRequests,
updateFeatureRequestStatus,
getContactMessages,
deleteContactMessage,
getHealth,
runSystemTests,
clearCache,
getAuditLog
} = deps;
const routes = [];
function register(method, pattern, handler) {
const regex = patternToRegex(pattern);
routes.push({ method, pattern, regex, handler });
}
function patternToRegex(pattern) {
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const withParams = escaped.replace(/:([^/]+)/g, '([^/]+)');
return new RegExp(`^${withParams}$`);
}
function matchRoute(method, pathname) {
for (const route of routes) {
if (route.method === method && route.regex.test(pathname)) {
const match = pathname.match(route.regex);
const params = {};
const paramNames = (route.pattern.match(/:[^/]+/g) || []).map(p => p.slice(1));
paramNames.forEach((name, i) => {
params[name] = match[i + 1];
});
return { handler: route.handler, params };
}
}
return null;
}
register('POST', `${EXTERNAL_API_PREFIX}/auth/validate`, createExternalApiHandler(createAuthHandlers(jwtTtlSeconds), { requireAuth: true }));
register('GET', `${EXTERNAL_API_PREFIX}/auth/me`, createExternalApiHandler({ GET: async () => ({ statusCode: 200, body: createResponse(true, { authenticated: true }) }) }));
register('GET', `${EXTERNAL_API_PREFIX}/users`, createExternalApiHandler(createUserHandlers(getUsers, updateUserPlan, adjustUserTokens, deleteUser, getUserSessions), { rateLimit }));
register('GET', `${EXTERNAL_API_PREFIX}/users/:id`, createExternalApiHandler(createUserIdHandlers(getUser, updateUserPlan, adjustUserTokens, deleteUser, getUserSessions)));
register('DELETE', `${EXTERNAL_API_PREFIX}/users/:id`, createExternalApiHandler(createUserIdHandlers(getUser, updateUserPlan, adjustUserTokens, deleteUser, getUserSessions)));
register('PATCH', `${EXTERNAL_API_PREFIX}/users/:id/plan`, createExternalApiHandler(createUserPlanHandlers(updateUserPlan)));
register('PATCH', `${EXTERNAL_API_PREFIX}/users/:id/tokens`, createExternalApiHandler(createUserTokensHandlers(adjustUserTokens)));
register('GET', `${EXTERNAL_API_PREFIX}/users/:id/sessions`, createExternalApiHandler({
GET: async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const parts = url.pathname.split('/').filter(Boolean);
const userId = parts[parts.indexOf('users') + 1];
const sessions = await getUserSessions(userId);
return { statusCode: 200, body: createResponse(true, sessions) };
}
}));
register('GET', `${EXTERNAL_API_PREFIX}/models`, createExternalApiHandler(createModelHandlers(getModels, upsertModel, deleteModel, reorderModels), { rateLimit }));
register('POST', `${EXTERNAL_API_PREFIX}/models`, createExternalApiHandler(createModelHandlers(getModels, upsertModel, deleteModel, reorderModels)));
register('GET', `${EXTERNAL_API_PREFIX}/models/:id`, createExternalApiHandler(createModelIdHandlers(getModel, upsertModel, deleteModel)));
register('PATCH', `${EXTERNAL_API_PREFIX}/models/:id`, createExternalApiHandler(createModelIdHandlers(getModel, upsertModel, deleteModel)));
register('DELETE', `${EXTERNAL_API_PREFIX}/models/:id`, createExternalApiHandler(createModelIdHandlers(getModel, upsertModel, deleteModel)));
register('POST', `${EXTERNAL_API_PREFIX}/models/reorder`, createExternalApiHandler(createModelsReorderHandlers(reorderModels)));
register('GET', `${EXTERNAL_API_PREFIX}/affiliates`, createExternalApiHandler(createAffiliateHandlers(getAffiliates, deleteAffiliate), { rateLimit }));
register('GET', `${EXTERNAL_API_PREFIX}/affiliates/:id`, createExternalApiHandler(createAffiliateIdHandlers(getAffiliate, updateAffiliate, deleteAffiliate)));
register('DELETE', `${EXTERNAL_API_PREFIX}/affiliates/:id`, createExternalApiHandler(createAffiliateIdHandlers(getAffiliate, updateAffiliate, deleteAffiliate)));
register('GET', `${EXTERNAL_API_PREFIX}/withdrawals`, createExternalApiHandler(createWithdrawalHandlers(getWithdrawals, updateWithdrawal), { rateLimit }));
register('PUT', `${EXTERNAL_API_PREFIX}/withdrawals`, createExternalApiHandler(createWithdrawalHandlers(getWithdrawals, updateWithdrawal)));
register('GET', `${EXTERNAL_API_PREFIX}/analytics/overview`, createExternalApiHandler(createAnalyticsHandlers(getTrackingStats, getResourceStats, getUsageStats)));
register('GET', `${EXTERNAL_API_PREFIX}/analytics/tracking`, createExternalApiHandler(createAnalyticsHandlers(getTrackingStats, getResourceStats, getUsageStats)));
register('GET', `${EXTERNAL_API_PREFIX}/analytics/resources`, createExternalApiHandler(createAnalyticsHandlers(getTrackingStats, getResourceStats, getUsageStats)));
register('GET', `${EXTERNAL_API_PREFIX}/analytics/usage`, createExternalApiHandler(createAnalyticsHandlers(getTrackingStats, getResourceStats, getUsageStats)));
register('GET', `${EXTERNAL_API_PREFIX}/blogs`, createExternalApiHandler(createBlogHandlers(getBlogs, createBlog, updateBlog, deleteBlog), { rateLimit }));
register('POST', `${EXTERNAL_API_PREFIX}/blogs`, createExternalApiHandler(createBlogHandlers(getBlogs, createBlog, updateBlog, deleteBlog)));
register('GET', `${EXTERNAL_API_PREFIX}/blogs/:id`, createExternalApiHandler(createBlogIdHandlers(getBlog, updateBlog, deleteBlog)));
register('PATCH', `${EXTERNAL_API_PREFIX}/blogs/:id`, createExternalApiHandler(createBlogIdHandlers(getBlog, updateBlog, deleteBlog)));
register('DELETE', `${EXTERNAL_API_PREFIX}/blogs/:id`, createExternalApiHandler(createBlogIdHandlers(getBlog, updateBlog, deleteBlog)));
register('GET', `${EXTERNAL_API_PREFIX}/feature-requests`, createExternalApiHandler(createFeatureRequestHandlers(getFeatureRequests, updateFeatureRequestStatus), { rateLimit }));
register('PATCH', `${EXTERNAL_API_PREFIX}/feature-requests/:id`, createExternalApiHandler(createFeatureRequestIdHandlers(updateFeatureRequestStatus)));
register('GET', `${EXTERNAL_API_PREFIX}/contact-messages`, createExternalApiHandler(createContactMessageHandlers(getContactMessages, deleteContactMessage), { rateLimit }));
register('DELETE', `${EXTERNAL_API_PREFIX}/contact-messages/:id`, createExternalApiHandler(createContactMessageIdHandlers(deleteContactMessage)));
register('GET', `${EXTERNAL_API_PREFIX}/system/health`, createExternalApiHandler(createSystemHandlers(getHealth, runSystemTests, clearCache, getAuditLog), { requireAuth: false }));
register('GET', `${EXTERNAL_API_PREFIX}/system/tests`, createExternalApiHandler(createSystemHandlers(getHealth, runSystemTests, clearCache, getAuditLog)));
register('POST', `${EXTERNAL_API_PREFIX}/system/tests`, createExternalApiHandler(createSystemHandlers(getHealth, runSystemTests, clearCache, getAuditLog)));
register('POST', `${EXTERNAL_API_PREFIX}/system/cache/clear`, createExternalApiHandler(createSystemHandlers(getHealth, runSystemTests, clearCache, getAuditLog)));
register('GET', `${EXTERNAL_API_PREFIX}/system/audit-log`, createExternalApiHandler(createSystemHandlers(getHealth, runSystemTests, clearCache, getAuditLog)));
async function handle(req, res) {
const url = new URL(req.url, `http://${req.headers.host}`);
const pathname = url.pathname;
if (!pathname.startsWith(EXTERNAL_API_PREFIX)) {
return false;
}
const matched = matchRoute(req.method, pathname);
if (!matched) {
const authResult = authenticateRequest(req);
if (!authResult.authenticated) {
return sendApiResponse(res, authResult.statusCode, createResponse(false, {
code: authResult.error,
message: 'Authentication failed'
}));
}
return sendApiResponse(res, 404, createResponse(false, {
code: 'ENDPOINT_NOT_FOUND',
message: `No endpoint found for ${req.method} ${pathname}`
}));
}
req.params = matched.params;
await matched.handler(req, res);
return true;
}
return { handle, routes };
}
module.exports = { createRouter, EXTERNAL_API_PREFIX };