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

@@ -60,6 +60,13 @@ ADMIN_USER=
ADMIN_PASSWORD=
SESSION_SECRET=
# External Admin API Key
# Generate a secure key: openssl rand -hex 32
# Format: sk_live_<your-secure-key> for production, sk_test_<your-secure-key> for testing
ADMIN_API_KEY=
ADMIN_API_JWT_TTL=3600
ADMIN_API_RATE_LIMIT=1000
# Database Configuration (Phase 1.2 & 1.3)
# Set USE_JSON_DATABASE=1 to use legacy JSON files (for rollback)
USE_JSON_DATABASE=

View File

@@ -126,6 +126,16 @@ RUN cd /opt/webchat && npm install --production && chmod -R 755 /opt/webchat
COPY chat_v2 /opt/webchat_v2
RUN chmod -R 755 /opt/webchat_v2
# Copy MCP servers for WordPress validation
COPY opencode/mcp-servers /opt/opencode/mcp-servers
RUN cd /opt/opencode/mcp-servers/wp-cli-testing && npm install --production && \
cd /opt/opencode/mcp-servers/wordpress-validator && npm install --production && \
chmod -R 755 /opt/opencode/mcp-servers
# Copy validation script
COPY scripts/validate-wordpress-plugin.sh /opt/scripts/validate-wordpress-plugin.sh
RUN chmod +x /opt/scripts/validate-wordpress-plugin.sh
# Create data directories
RUN mkdir -p /home/web/data \
&& mkdir -p /var/log/shopify-ai \

677
EXTERNAL_ADMIN_API.md Normal file
View File

@@ -0,0 +1,677 @@
# External Admin API Documentation
## Overview
The External Admin API provides programmatic access to all admin functionality via a RESTful API. This enables automation, integrations with external tools, and custom admin dashboards.
## Table of Contents
1. [Authentication](#authentication)
2. [Configuration](#configuration)
3. [API Endpoints](#api-endpoints)
4. [Request/Response Format](#requestresponse-format)
5. [Rate Limiting](#rate-limiting)
6. [Error Handling](#error-handling)
7. [Examples](#examples)
8. [Security Best Practices](#security-best-practices)
---
## Authentication
The External Admin API supports two authentication methods:
### Option 1: API Key (Direct)
Include your API key in the `Authorization` header with the `Bearer` scheme:
```http
Authorization: Bearer sk_live_your_api_key_here
```
**Key Format:**
- Production keys: `sk_live_` prefix
- Test keys: `sk_test_` prefix
### Option 2: API Key → JWT Token (Recommended)
For better performance, exchange your API key for a short-lived JWT token:
1. **Obtain JWT Token:**
```http
POST /api/external/auth/validate
Authorization: Bearer sk_live_your_api_key_here
```
2. **Response:**
```json
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 3600,
"expiresAt": "2026-02-20T11:00:00.000Z"
},
"meta": {
"timestamp": "2026-02-20T10:00:00.000Z",
"requestId": "req_abc123"
}
}
```
3. **Use JWT Token:**
```http
GET /api/external/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
### Token Validation
Validate your current authentication:
```http
GET /api/external/auth/me
Authorization: Bearer <your_token>
```
---
## Configuration
### Environment Variables
Add these to your `.env` file or Docker environment:
| Variable | Description | Default | Required |
|----------|-------------|---------|----------|
| `ADMIN_API_KEY` | Your secret API key | - | Yes (to enable API) |
| `ADMIN_API_JWT_TTL` | JWT token lifetime in seconds | 3600 (1 hour) | No |
| `ADMIN_API_RATE_LIMIT` | Requests per hour per key | 1000 | No |
| `JWT_SECRET` | Secret for JWT signing | - | Yes |
### Docker Compose Setup
```yaml
# docker-compose.yml
services:
shopify-ai-builder:
environment:
- ADMIN_API_KEY=${ADMIN_API_KEY:-}
- ADMIN_API_JWT_TTL=${ADMIN_API_JWT_TTL:-3600}
- ADMIN_API_RATE_LIMIT=${ADMIN_API_RATE_LIMIT:-1000}
- JWT_SECRET=${JWT_SECRET:-}
```
### Generating a Secure API Key
```bash
# Generate a 32-byte hex key
openssl rand -hex 32
# Example output: a1b2c3d4e5f6... (64 hex characters)
# Set in .env:
ADMIN_API_KEY=sk_live_a1b2c3d4e5f6...
```
---
## API Endpoints
### Base URL
All external API endpoints are prefixed with `/api/external`.
### Authentication
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/auth/validate` | Exchange API key for JWT token |
| GET | `/auth/me` | Get current authentication info |
### User Management
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/users` | List all users (paginated) |
| GET | `/users/:id` | Get user by ID |
| PATCH | `/users/:id/plan` | Update user's subscription plan |
| PATCH | `/users/:id/tokens` | Adjust user's token allocation |
| DELETE | `/users/:id` | Delete a user |
| GET | `/users/:id/sessions` | List user's active sessions |
| DELETE | `/users/:id/sessions/:sessionId` | Revoke a user session |
### Model Management
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/models` | List all configured models |
| POST | `/models` | Create a new model configuration |
| GET | `/models/:id` | Get model by ID |
| PATCH | `/models/:id` | Update model configuration |
| DELETE | `/models/:id` | Delete a model |
| POST | `/models/reorder` | Reorder model display priority |
### Affiliate Management
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/affiliates` | List all affiliates (paginated) |
| GET | `/affiliates/:id` | Get affiliate by ID |
| DELETE | `/affiliates/:id` | Delete an affiliate |
### Withdrawal Management
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/withdrawals` | List withdrawal requests (paginated) |
| PUT | `/withdrawals` | Update withdrawal status |
### Analytics & Monitoring
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/analytics/overview` | Get dashboard overview stats |
| GET | `/analytics/tracking` | Get visitor tracking stats |
| GET | `/analytics/resources` | Get resource monitoring data |
| GET | `/analytics/usage` | Get token usage statistics |
### Content Management
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/blogs` | List all blogs (paginated) |
| POST | `/blogs` | Create a new blog post |
| GET | `/blogs/:id` | Get blog by ID |
| PATCH | `/blogs/:id` | Update blog post |
| DELETE | `/blogs/:id` | Delete a blog post |
| GET | `/feature-requests` | List feature requests (paginated) |
| PATCH | `/feature-requests/:id` | Update feature request status |
| GET | `/contact-messages` | List contact messages (paginated) |
| DELETE | `/contact-messages/:id` | Delete a contact message |
### System Operations
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/system/health` | Health check (no auth required) |
| GET | `/system/tests` | Run system tests |
| POST | `/system/tests` | Run specific system tests |
| POST | `/system/cache/clear` | Clear server cache |
| GET | `/system/audit-log` | Query audit log (paginated) |
---
## Request/Response Format
### Standard Response
```json
{
"success": true,
"data": {
// Response data here
},
"meta": {
"timestamp": "2026-02-20T10:00:00.000Z",
"requestId": "req_abc123"
}
}
```
### Paginated Response
```json
{
"success": true,
"data": [
// Array of items
],
"meta": {
"timestamp": "2026-02-20T10:00:00.000Z",
"requestId": "req_abc123"
},
"pagination": {
"page": 1,
"perPage": 20,
"total": 150,
"totalPages": 8,
"hasNext": true,
"hasPrev": false
}
}
```
### Query Parameters
| Parameter | Description | Default | Max |
|-----------|-------------|---------|-----|
| `page` | Page number | 1 | - |
| `perPage` | Items per page | 20 | 100 |
| `search` | Search query | - | - |
| `status` | Filter by status | - | - |
| `plan` | Filter by plan | - | - |
---
## Rate Limiting
All authenticated requests are rate-limited per API key.
### Default Limits
- **Requests per hour:** 1000 (configurable via `ADMIN_API_RATE_LIMIT`)
- **Burst:** Not applicable (smooth rate limiting)
### Rate Limit Headers
Every response includes rate limit information:
```http
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 856
X-RateLimit-Reset: 1708407600
```
### Rate Limit Exceeded Response
```json
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Please retry after the reset time.",
"details": {
"resetAt": "2026-02-20T11:00:00.000Z"
}
},
"meta": {
"timestamp": "2026-02-20T10:45:00.000Z",
"requestId": "req_xyz789"
}
}
```
---
## Error Handling
### Error Response Format
```json
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message",
"details": {
// Additional context
}
},
"meta": {
"timestamp": "2026-02-20T10:00:00.000Z",
"requestId": "req_abc123"
}
}
```
### Error Codes
| Code | HTTP Status | Description |
|------|-------------|-------------|
| `MISSING_AUTH_HEADER` | 401 | No Authorization header provided |
| `INVALID_AUTH_SCHEME` | 401 | Invalid authorization scheme (use Bearer) |
| `INVALID_API_KEY` | 401 | API key is invalid |
| `TOKEN_EXPIRED` | 401 | JWT token has expired |
| `INVALID_TOKEN` | 401 | JWT token is malformed or invalid |
| `RATE_LIMIT_EXCEEDED` | 429 | Rate limit exceeded |
| `ENDPOINT_NOT_FOUND` | 404 | Endpoint does not exist |
| `METHOD_NOT_ALLOWED` | 405 | HTTP method not supported |
| `VALIDATION_ERROR` | 400 | Request validation failed |
| `NOT_FOUND` | 404 | Resource not found |
| `INTERNAL_ERROR` | 500 | Internal server error |
---
## Examples
### cURL Examples
#### Get JWT Token
```bash
curl -X POST https://your-domain.com/api/external/auth/validate \
-H "Authorization: Bearer sk_live_your_api_key_here"
```
#### List Users
```bash
curl -X GET "https://your-domain.com/api/external/users?page=1&perPage=10" \
-H "Authorization: Bearer your_jwt_token_here"
```
#### Update User Plan
```bash
curl -X PATCH https://your-domain.com/api/external/users/user_123/plan \
-H "Authorization: Bearer your_jwt_token_here" \
-H "Content-Type: application/json" \
-d '{"plan": "professional"}'
```
#### Create Model
```bash
curl -X POST https://your-domain.com/api/external/models \
-H "Authorization: Bearer your_jwt_token_here" \
-H "Content-Type: application/json" \
-d '{
"name": "gpt-4-turbo",
"label": "GPT-4 Turbo",
"tier": "premium",
"supportsMedia": true
}'
```
### JavaScript/Node.js Examples
#### Setup
```javascript
const API_BASE = 'https://your-domain.com/api/external';
const API_KEY = 'sk_live_your_api_key_here';
let jwtToken = null;
let tokenExpiresAt = null;
async function getJwtToken() {
if (jwtToken && tokenExpiresAt && Date.now() < tokenExpiresAt) {
return jwtToken;
}
const response = await fetch(`${API_BASE}/auth/validate`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`
}
});
const data = await response.json();
if (!data.success) {
throw new Error(data.error.message);
}
jwtToken = data.data.token;
tokenExpiresAt = new Date(data.data.expiresAt).getTime();
return jwtToken;
}
async function apiRequest(method, path, body = null) {
const token = await getJwtToken();
const options = {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
};
if (body) {
options.body = JSON.stringify(body);
}
const response = await fetch(`${API_BASE}${path}`, options);
return response.json();
}
```
#### Usage Examples
```javascript
// List users
const users = await apiRequest('GET', '/users?page=1&perPage=20');
console.log(users);
// Get specific user
const user = await apiRequest('GET', '/users/user_123');
console.log(user);
// Update user plan
const result = await apiRequest('PATCH', '/users/user_123/plan', {
plan: 'professional'
});
console.log(result);
// Adjust user tokens
const tokensResult = await apiRequest('PATCH', '/users/user_123/tokens', {
tokens: 1000000,
operation: 'set'
});
console.log(tokensResult);
// List models
const models = await apiRequest('GET', '/models');
console.log(models);
// Create model
const newModel = await apiRequest('POST', '/models', {
name: 'claude-3-opus',
label: 'Claude 3 Opus',
tier: 'premium',
supportsMedia: true
});
console.log(newModel);
// Get analytics
const analytics = await apiRequest('GET', '/analytics/overview');
console.log(analytics);
```
### Python Examples
```python
import requests
from datetime import datetime
API_BASE = 'https://your-domain.com/api/external'
API_KEY = 'sk_live_your_api_key_here'
class ExternalAdminAPI:
def __init__(self, api_base, api_key):
self.api_base = api_base
self.api_key = api_key
self.jwt_token = None
self.token_expires_at = None
def get_jwt_token(self):
if self.jwt_token and self.token_expires_at:
if datetime.now() < self.token_expires_at:
return self.jwt_token
response = requests.post(
f'{self.api_base}/auth/validate',
headers={'Authorization': f'Bearer {self.api_key}'}
)
data = response.json()
if not data['success']:
raise Exception(data['error']['message'])
self.jwt_token = data['data']['token']
self.token_expires_at = datetime.fromisoformat(
data['data']['expiresAt'].replace('Z', '+00:00')
)
return self.jwt_token
def request(self, method, path, body=None):
token = self.get_jwt_token()
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
response = requests.request(
method,
f'{self.api_base}{path}',
headers=headers,
json=body
)
return response.json()
# Usage
api = ExternalAdminAPI(API_BASE, API_KEY)
# List users
users = api.request('GET', '/users?page=1&perPage=20')
print(users)
# Update user plan
result = api.request('PATCH', '/users/user_123/plan', {'plan': 'professional'})
print(result)
```
---
## Security Best Practices
### API Key Management
1. **Never commit API keys to version control**
- Use environment variables
- Use secrets management (Docker secrets, Kubernetes secrets, AWS Secrets Manager)
2. **Use different keys for different environments**
- `sk_test_` prefix for development/testing
- `sk_live_` prefix for production
3. **Rotate keys periodically**
- Generate new keys at least every 90 days
- Immediately revoke compromised keys
4. **Limit key exposure**
- Only store keys on secure servers
- Never expose keys in client-side code
- Use IP whitelisting if possible (future feature)
### JWT Token Security
1. **Short token lifetime**
- Default: 1 hour
- Configure via `ADMIN_API_JWT_TTL`
2. **Token storage**
- Store tokens in memory, not persistent storage
- Clear tokens on application shutdown
3. **Token refresh**
- Implement token refresh before expiration
- Handle `TOKEN_EXPIRED` errors gracefully
### Network Security
1. **Use HTTPS**
- Never transmit API keys over HTTP
- Ensure SSL/TLS certificates are valid
2. **IP Restrictions** (if applicable)
- Restrict API access to known IP ranges
- Use VPN or private networks
3. **Rate Limiting**
- Respect rate limit headers
- Implement client-side throttling
- Handle 429 responses with exponential backoff
### Audit & Monitoring
1. **Monitor API Usage**
- Review audit logs regularly
- Set up alerts for unusual activity
2. **Log Retention**
- Maintain audit logs for compliance
- Use `/system/audit-log` endpoint to query logs
3. **Anomaly Detection**
- Monitor for unusual request patterns
- Alert on failed authentication attempts
---
## Troubleshooting
### Common Issues
#### "API_KEY_NOT_CONFIGURED"
**Cause:** `ADMIN_API_KEY` environment variable not set.
**Solution:** Set the environment variable and restart the server:
```bash
export ADMIN_API_KEY=sk_live_your_secure_key_here
```
#### "INVALID_API_KEY"
**Cause:** API key doesn't match the configured key.
**Solution:**
1. Verify the API key is correct
2. Check for extra whitespace or encoding issues
3. Ensure the key prefix is correct (`sk_live_` or `sk_test_`)
#### "TOKEN_EXPIRED"
**Cause:** JWT token has exceeded its lifetime.
**Solution:** Request a new token using the API key:
```bash
POST /api/external/auth/validate
Authorization: Bearer sk_live_your_api_key
```
#### "RATE_LIMIT_EXCEEDED"
**Cause:** Too many requests in the current hour.
**Solution:**
1. Wait until the reset time (check `X-RateLimit-Reset` header)
2. Reduce request frequency
3. Increase rate limit via `ADMIN_API_RATE_LIMIT` environment variable
### Debugging Tips
1. **Enable verbose logging**
- Check server logs for detailed error messages
- Look for `[ExternalAPI]` prefixed log entries
2. **Test with health endpoint**
```bash
curl https://your-domain.com/api/external/system/health
```
3. **Verify authentication**
```bash
curl -X GET https://your-domain.com/api/external/auth/me \
-H "Authorization: Bearer your_token"
```
---
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.0.0 | 2026-02-20 | Initial release |
---
## Support
For issues or questions:
- GitHub Issues: [project-repo]/issues
- Documentation: This file
- Server Logs: Check container logs for `[ExternalAPI]` entries

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 };

View File

@@ -90,6 +90,9 @@ services:
- ADMIN_USER=${ADMIN_USER:-}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
- ADMIN_SESSION_TTL_MS=${ADMIN_SESSION_TTL_MS:-}
- ADMIN_API_KEY=${ADMIN_API_KEY:-}
- ADMIN_API_JWT_TTL=${ADMIN_API_JWT_TTL:-3600}
- ADMIN_API_RATE_LIMIT=${ADMIN_API_RATE_LIMIT:-1000}
- COOKIE_SECURE=${COOKIE_SECURE:-}
# Database configuration
- USE_JSON_DATABASE=${USE_JSON_DATABASE:-}