/** * 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 = `
This API allows you to programmatically track and analyze URL redirect chains with detailed information.
The API is limited to 100 requests per hour per IP address.
Track a URL and get the full redirect chain using a POST request.
| 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 |
curl -X POST http://localhost:${PORT}/api/v1/track \\
-H "Content-Type: application/json" \\
-d '{
"url": "github.com",
"method": "GET"
}'
Track a URL and get the full redirect chain using a GET request with query parameters.
curl "http://localhost:${PORT}/api/v1/track?url=github.com&method=GET"
`;
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;