fix mcp and admin api
This commit is contained in:
@@ -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)),
|
||||
|
||||
863
chat/src/external-admin-api/handlers.js
Normal file
863
chat/src/external-admin-api/handlers.js
Normal 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
|
||||
};
|
||||
248
chat/src/external-admin-api/index.js
Normal file
248
chat/src/external-admin-api/index.js
Normal 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
|
||||
};
|
||||
589
chat/src/external-admin-api/integration.js
Normal file
589
chat/src/external-admin-api/integration.js
Normal 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 };
|
||||
172
chat/src/external-admin-api/router.js
Normal file
172
chat/src/external-admin-api/router.js
Normal 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 };
|
||||
Reference in New Issue
Block a user