/** * Redirect Intelligence v2 API Server * * This server maintains 100% backward compatibility with existing endpoints * while providing a foundation for new v2 features. */ import 'dotenv/config'; import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import compression from 'compression'; import cookieParser from 'cookie-parser'; import rateLimit from 'express-rate-limit'; import path from 'path'; import { logger } from './lib/logger'; import { trackRedirects } from './services/redirect-legacy.service'; import authRoutes from './routes/auth.routes'; const app = express(); const PORT = process.env.PORT || 3333; // Security middleware app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"], scriptSrc: ["'self'", "https://cdn.jsdelivr.net"], imgSrc: ["'self'", "data:", "https:"], }, }, })); // Compression middleware app.use(compression()); // CORS middleware app.use(cors({ origin: process.env.WEB_URL || 'http://localhost:3000', credentials: true, optionsSuccessStatus: 200 // Some legacy browsers (IE11, various SmartTVs) choke on 204 })); // Body parsing middleware app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); app.use(cookieParser()); // Static files (preserve existing behavior) app.use(express.static(path.join(__dirname, '../../../public'))); // Rate limiting (EXACT same configuration as before) const apiLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 100, // limit each IP to 100 requests per windowMs message: { error: 'Too many requests, please try again later.' }, standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); // ============================================================================ // NEW V2 API ROUTES // ============================================================================ // Authentication routes app.use('/api/v1/auth', authRoutes); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), version: '2.0.0', environment: process.env.NODE_ENV || 'development' }); }); // ============================================================================ // LEGACY ENDPOINTS - EXACT SAME BEHAVIOR AS BEFORE // ============================================================================ // Original endpoint (deprecated but maintained for backward compatibility) app.post('/api/track', async (req, res) => { const { url, method = 'GET', userAgent } = req.body; if (!url) { return res.status(400).json({ error: 'URL is required' }); } try { // Ensure URL has a protocol let inputUrl = url; if (!inputUrl.startsWith('http://') && !inputUrl.startsWith('https://')) { inputUrl = 'http://' + inputUrl; } // Set up request options const options = { method: method.toUpperCase(), userAgent }; const redirectChain = await trackRedirects(inputUrl, [], options); res.json({ redirects: redirectChain }); } catch (error) { logger.error('Legacy /api/track error:', error); res.status(500).json({ error: 'Failed to track redirects' }); } }); // API v1 track endpoint (POST) app.post('/api/v1/track', apiLimiter, async (req, res) => { const { url, method = 'GET', userAgent } = req.body; if (!url) { return res.status(400).json({ error: 'URL is required', status: 400, success: false }); } try { // Ensure URL has a protocol let inputUrl = url; if (!inputUrl.startsWith('http://') && !inputUrl.startsWith('https://')) { inputUrl = 'http://' + inputUrl; } // Set up request options const options = { method: method.toUpperCase(), userAgent }; const redirectChain = await trackRedirects(inputUrl, [], options); // Format the response in a more standardized API format res.json({ success: true, status: 200, data: { url: inputUrl, method: options.method, redirectCount: redirectChain.length - 1, finalUrl: redirectChain[redirectChain.length - 1]?.url, finalStatusCode: redirectChain[redirectChain.length - 1]?.statusCode, redirects: redirectChain } }); } catch (error) { logger.error('API v1 track error:', error); res.status(500).json({ error: 'Failed to track redirects', message: error instanceof Error ? error.message : 'Unknown error', status: 500, success: false }); } }); // API v1 track endpoint with GET method support (for easy browser/curl usage) app.get('/api/v1/track', apiLimiter, async (req, res) => { const { url, method = 'GET', userAgent } = req.query; if (!url) { return res.status(400).json({ error: 'URL parameter is required', status: 400, success: false }); } try { // Ensure URL has a protocol let inputUrl = url as string; if (!inputUrl.startsWith('http://') && !inputUrl.startsWith('https://')) { inputUrl = 'http://' + inputUrl; } // Set up request options const options = { method: ((method as string) || 'GET').toUpperCase(), userAgent: userAgent as string }; const redirectChain = await trackRedirects(inputUrl, [], options); // Format the response in a more standardized API format res.json({ success: true, status: 200, data: { url: inputUrl, method: options.method, redirectCount: redirectChain.length - 1, finalUrl: redirectChain[redirectChain.length - 1]?.url, finalStatusCode: redirectChain[redirectChain.length - 1]?.statusCode, redirects: redirectChain } }); } catch (error) { logger.error('API v1 track GET error:', error); res.status(500).json({ error: 'Failed to track redirects', message: error instanceof Error ? error.message : 'Unknown error', status: 500, success: false }); } }); // API documentation endpoint (preserve existing) app.get('/api/docs', (req, res) => { const apiDocs = ` URL Redirect Tracker API

URL Redirect Tracker API Documentation

This API allows you to programmatically track and analyze URL redirect chains with detailed information.

Rate Limiting

The API is limited to 100 requests per hour per IP address.

Endpoints

POST /api/v1/track

Track a URL and get the full redirect chain using a POST request.

Request Parameters (JSON Body)

Parameter Type Required Description
url string Yes The URL to track (e.g., "example.com")
method string No HTTP method (GET, HEAD, POST). Default: "GET"
userAgent string No Custom User-Agent header

Example Request

curl -X POST http://localhost:${PORT}/api/v1/track \\
  -H "Content-Type: application/json" \\
  -d '{
    "url": "github.com",
    "method": "GET"
  }'
  

GET /api/v1/track

Track a URL and get the full redirect chain using a GET request with query parameters.

Example Request

curl "http://localhost:${PORT}/api/v1/track?url=github.com&method=GET"
  

Back to URL Redirect Tracker

`; res.send(apiDocs); }); // Catch-all for serving the frontend (preserve existing behavior) app.get('/', (req, res) => { res.sendFile(path.join(__dirname, '../../../public', 'index.html')); }); // Global error handler app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { logger.error('Unhandled error:', err); res.status(500).json({ success: false, error: 'Internal server error', message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong' }); }); // 404 handler app.use((req, res) => { res.status(404).json({ success: false, error: 'Not found', message: `Route ${req.method} ${req.path} not found` }); }); // Graceful shutdown process.on('SIGTERM', () => { logger.info('SIGTERM received, shutting down gracefully'); process.exit(0); }); process.on('SIGINT', () => { logger.info('SIGINT received, shutting down gracefully'); process.exit(0); }); app.listen(PORT, () => { logger.info(`🚀 Redirect Intelligence v2 API Server running on http://localhost:${PORT}`); logger.info(`📖 API Documentation: http://localhost:${PORT}/api/docs`); logger.info(`🏥 Health Check: http://localhost:${PORT}/health`); }); export default app;