From 46ad6d74a3384b93651831edf5cd26396ed4835e Mon Sep 17 00:00:00 2001 From: Michilis Date: Sun, 21 Dec 2025 01:27:16 -0300 Subject: [PATCH] Remove /v1 API version prefix from all routes - Update routes to serve at root level instead of /v1/* - Simplify nginx configuration and use certbot SSL defaults - Update OpenAPI spec server URLs - Change nginx upstream port from 3000 to 3012 --- deploy/nginx.conf | 122 ++++++++++++++------------------------------ src/docs/openapi.js | 6 +-- src/index.js | 76 +++------------------------ 3 files changed, 48 insertions(+), 156 deletions(-) diff --git a/deploy/nginx.conf b/deploy/nginx.conf index 19cb01f..04202eb 100644 --- a/deploy/nginx.conf +++ b/deploy/nginx.conf @@ -1,35 +1,9 @@ -# Cashumints.space API - Nginx Configuration -# -# Installation: -# sudo cp deploy/nginx.conf /etc/nginx/sites-available/cashumints-api -# sudo ln -s /etc/nginx/sites-available/cashumints-api /etc/nginx/sites-enabled/ -# sudo nginx -t -# sudo systemctl reload nginx -# -# SSL Certificate (Let's Encrypt): -# sudo certbot --nginx -d api.cashumints.space - -# Rate limiting zones -limit_req_zone $binary_remote_addr zone=api_limit:10m rate=30r/s; -limit_req_zone $binary_remote_addr zone=admin_limit:10m rate=5r/s; -limit_conn_zone $binary_remote_addr zone=conn_limit:10m; - -# Upstream API server -upstream cashumints_api { - server 127.0.0.1:3000; - keepalive 32; -} - # Redirect HTTP to HTTPS server { listen 80; listen [::]:80; server_name api.cashumints.space; - location /.well-known/acme-challenge/ { - root /var/www/certbot; - } - location / { return 301 https://$server_name$request_uri; } @@ -37,21 +11,17 @@ server { # Main HTTPS server server { - listen 443 ssl http2; - listen [::]:443 ssl http2; + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name api.cashumints.space; - # SSL Configuration + # Certbot-managed SSL ssl_certificate /etc/letsencrypt/live/api.cashumints.space/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.cashumints.space/privkey.pem; - ssl_session_timeout 1d; - ssl_session_cache shared:SSL:50m; - ssl_session_tickets off; - - # Modern SSL configuration - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; - ssl_prefer_server_ciphers off; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # HSTS add_header Strict-Transport-Security "max-age=63072000" always; @@ -59,84 +29,71 @@ server { # Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; # Logging access_log /var/log/nginx/cashumints-api.access.log; - error_log /var/log/nginx/cashumints-api.error.log; + error_log /var/log/nginx/cashumints-api.error.log; - # Connection limits - limit_conn conn_limit 20; + # Connection limits (GLOBAL zone) + limit_conn cashu_conn_limit 20; - # Gzip compression + # Gzip gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; - gzip_types application/json application/javascript text/plain text/css text/xml; + gzip_types application/json application/javascript text/plain text/css; - # Client settings + # Client limits client_max_body_size 1m; client_body_timeout 10s; client_header_timeout 10s; - # Proxy settings + # Proxy defaults proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Connection ""; + proxy_connect_timeout 5s; proxy_send_timeout 60s; proxy_read_timeout 60s; - proxy_buffering on; - proxy_buffer_size 4k; - proxy_buffers 8 4k; - # Health check (no rate limit) - location = /v1/health { - proxy_pass http://cashumints_api; - proxy_cache_bypass 1; + # Root → main site + location = / { + return 301 https://cashumints.space; } - # Swagger documentation (relaxed rate limit) + # Health (rate-limited like API) + location = /health { + limit_req zone=cashu_api_limit burst=10 nodelay; + proxy_pass http://127.0.0.1:3012; + } + + # Docs location /docs { - limit_req zone=api_limit burst=10 nodelay; - proxy_pass http://cashumints_api; + limit_req zone=cashu_api_limit burst=10 nodelay; + proxy_pass http://127.0.0.1:3012; } location = /openapi.json { - limit_req zone=api_limit burst=10 nodelay; - proxy_pass http://cashumints_api; - # Cache OpenAPI spec - proxy_cache_valid 200 5m; + limit_req zone=cashu_api_limit burst=10 nodelay; + proxy_pass http://127.0.0.1:3012; } - # Admin endpoints (strict rate limit) - location /v1/admin { - limit_req zone=admin_limit burst=5 nodelay; - proxy_pass http://cashumints_api; - - # Only allow from specific IPs (optional) - # allow 10.0.0.0/8; - # allow 192.168.0.0/16; - # deny all; + # Admin (strict) + location /admin { + limit_req zone=cashu_admin_limit burst=5 nodelay; + proxy_pass http://127.0.0.1:3012; } # API endpoints - location /v1 { - limit_req zone=api_limit burst=50 nodelay; - proxy_pass http://cashumints_api; - - # Cache GET requests for 10 seconds - proxy_cache_valid 200 10s; - } - - # Root and favicon - location = / { - proxy_pass http://cashumints_api; + location ~ ^/(stats|reviews|analytics|mints) { + limit_req zone=cashu_api_limit burst=50 nodelay; + proxy_pass http://127.0.0.1:3012; } location = /favicon.ico { @@ -144,17 +101,12 @@ server { access_log off; } - # Block common attack vectors + # Block junk location ~ /\. { deny all; - access_log off; - log_not_found off; } location ~* \.(git|env|sql|bak|old|tmp)$ { deny all; - access_log off; - log_not_found off; } } - diff --git a/src/docs/openapi.js b/src/docs/openapi.js index 7daaf5b..c8e3337 100644 --- a/src/docs/openapi.js +++ b/src/docs/openapi.js @@ -63,15 +63,15 @@ Admin endpoints require the \`X-Admin-Api-Key\` header. All admin actions are au url: 'https://docs.cashu.space' }, servers: [{ - url: '/v1', + url: '/', description: 'Current server (relative)' }, { - url: 'https://api.cashumints.space/v1', + url: 'https://api.cashumints.space', description: 'Production API' }, { - url: 'http://localhost:3000/v1', + url: 'http://localhost:3000', description: 'Local development' } ], diff --git a/src/index.js b/src/index.js index 5a10017..b5a3163 100644 --- a/src/index.js +++ b/src/index.js @@ -145,74 +145,14 @@ app.use('/docs', swaggerUi.serve, swaggerUi.setup(openApiSpec, swaggerUiOptions) // ROUTES // ========================================== -// API version prefix -const API_PREFIX = '/v1'; - -// Health and stats -app.use(API_PREFIX, systemRoutes); -app.get('/health', (req, res) => res.redirect(`${API_PREFIX}/health`)); +// System routes (health, stats, analytics, reviews) +app.use('/', systemRoutes); // Mint routes -app.use(`${API_PREFIX}/mints`, mintRoutes); +app.use('/mints', mintRoutes); // Admin routes -app.use(`${API_PREFIX}/admin`, adminRoutes); - -// Root endpoint -app.get('/', (req, res) => { - res.json({ - name: 'Cashumints.space API', - version: '1.0.0', - description: 'Decentralized observability and reputation API for Cashu mints', - documentation: '/docs', - openapi: '/openapi.json', - endpoints: { - health: `${API_PREFIX}/health`, - stats: `${API_PREFIX}/stats`, - mints: `${API_PREFIX}/mints`, - submit: `${API_PREFIX}/mints/submit` - }, - links: { - website: 'https://cashumints.space', - cashu: 'https://cashu.space', - protocol: 'https://docs.cashu.space', - nostr_nip87: 'https://github.com/nostr-protocol/nips/blob/master/87.md' - } - }); -}); - -// API info -app.get(API_PREFIX, (req, res) => { - res.json({ - version: 'v1', - documentation: '/docs', - openapi: '/openapi.json', - endpoints: [ - { method: 'GET', path: '/v1/health', description: 'Health check' }, - { method: 'GET', path: '/v1/stats', description: 'System statistics' }, - { method: 'GET', path: '/v1/mints', description: 'List all mints' }, - { method: 'GET', path: '/v1/mints/:mint_id', description: 'Get mint by ID' }, - { method: 'GET', path: '/v1/mints/by-url?url=', description: 'Get mint by URL' }, - { method: 'GET', path: '/v1/mints/:mint_id/urls', description: 'Get mint URLs' }, - { method: 'GET', path: '/v1/mints/:mint_id/metadata', description: 'Get mint metadata (NUT-06)' }, - { method: 'GET', path: '/v1/mints/:mint_id/metadata/history', description: 'Get metadata history' }, - { method: 'GET', path: '/v1/mints/:mint_id/status', description: 'Get mint status' }, - { method: 'GET', path: '/v1/mints/:mint_id/uptime', description: 'Get uptime stats' }, - { method: 'GET', path: '/v1/mints/:mint_id/uptime/timeseries', description: 'Get uptime timeseries' }, - { method: 'GET', path: '/v1/mints/:mint_id/incidents', description: 'Get incidents' }, - { method: 'GET', path: '/v1/mints/:mint_id/trust', description: 'Get trust score' }, - { method: 'GET', path: '/v1/mints/:mint_id/reviews', description: 'Get Nostr reviews' }, - { method: 'GET', path: '/v1/mints/:mint_id/views', description: 'Get pageview stats' }, - { method: 'GET', path: '/v1/mints/:mint_id/features', description: 'Get derived features' }, - { method: 'POST', path: '/v1/mints/submit', description: 'Submit a new mint' } - ], - notes: [ - 'All endpoints support both mint_id and by-url variants', - 'All timestamps are ISO-8601 UTC', - 'Rate limit: 100 requests per minute' - ] - }); -}); +app.use('/admin', adminRoutes); // ========================================== // ERROR HANDLING @@ -244,10 +184,10 @@ const server = app.listen(config.port, config.host, () => { ║ • OpenAPI: http://${config.host}:${config.port}/openapi.json ║ ║ ║ ║ Endpoints: ║ -║ • GET /v1/health - Health check ║ -║ • GET /v1/stats - System stats ║ -║ • GET /v1/mints - List mints ║ -║ • POST /v1/mints/submit - Submit mint ║ +║ • GET /health - Health check ║ +║ • GET /stats - System stats ║ +║ • GET /mints - List mints ║ +║ • POST /mints/submit - Submit mint ║ ║ ║ ╚═══════════════════════════════════════════════════════════════╝ `);