fix: resolve production deployment issues and switch to in-memory rate limiting
- Fix CORS configuration to use CORS_ORIGIN env variable - Switch from Redis-based to in-memory rate limiting for stability - Fix frontend authentication error handling for public API - Disable problematic trackingRateLimit middleware - Update environment configuration for production This resolves hanging issues with tracking API and enables frontend forms to work properly on production.
This commit is contained in:
41
.env
41
.env
@@ -1,23 +1,32 @@
|
|||||||
|
# Production Environment Variables for Redirect Intelligence v2
|
||||||
|
# Copy this to .env and customize for your deployment
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/redirect_intelligence"
|
DB_PASSWORD=R9dbMfTkksXlboeFRD+wtw==
|
||||||
|
|
||||||
# Redis
|
# JWT Secret (generate a strong secret)
|
||||||
REDIS_URL="redis://localhost:6379"
|
JWT_SECRET=syiIpEqLvhXjbpyC2+VccCMfhz6rznIObRdZMfqf7Hg=
|
||||||
|
|
||||||
# API
|
# CORS Origin (your domain)
|
||||||
PORT=3333
|
CORS_ORIGIN=https://urltrackertool.com
|
||||||
NODE_ENV=development
|
|
||||||
JWT_SECRET="your-super-secret-jwt-key-change-in-production"
|
|
||||||
|
|
||||||
# Frontend
|
# API URL for frontend
|
||||||
WEB_URL="http://localhost:3000"
|
VITE_API_URL=https://api.urltrackertool.com
|
||||||
REACT_APP_API_URL="http://localhost:3333"
|
|
||||||
|
|
||||||
# Optional: Google Safe Browsing API
|
# Optional: Monitoring and Analytics
|
||||||
GOOGLE_SAFE_BROWSING_API_KEY=""
|
# SENTRY_DSN=your_sentry_dsn_here
|
||||||
|
# ANALYTICS_ID=your_analytics_id_here
|
||||||
|
|
||||||
# Logging
|
# Optional: Email Configuration (for notifications)
|
||||||
LOG_LEVEL=info
|
# SMTP_HOST=smtp.gmail.com
|
||||||
|
# SMTP_PORT=587
|
||||||
|
# SMTP_USER=your_email@gmail.com
|
||||||
|
# SMTP_PASS=your_app_password
|
||||||
|
|
||||||
# Worker
|
# Optional: Rate Limiting
|
||||||
WORKER_CONCURRENCY=5
|
DEFAULT_RATE_LIMIT=100
|
||||||
|
AUTHENTICATED_RATE_LIMIT=1000
|
||||||
|
|
||||||
|
# Optional: File Upload Limits
|
||||||
|
MAX_FILE_SIZE=10485760
|
||||||
|
MAX_BULK_URLS=10000
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import trackingRoutes from './routes/tracking.routes';
|
|||||||
import analysisRoutes from './routes/analysis.routes';
|
import analysisRoutes from './routes/analysis.routes';
|
||||||
import exportRoutes from './routes/export.routes';
|
import exportRoutes from './routes/export.routes';
|
||||||
import bulkRoutes from './routes/bulk.routes';
|
import bulkRoutes from './routes/bulk.routes';
|
||||||
|
import docsRoutes from './routes/docs.routes';
|
||||||
import { legacyRateLimit, requestLogger, rateLimitErrorHandler } from './middleware/rate-limit.middleware';
|
import { legacyRateLimit, requestLogger, rateLimitErrorHandler } from './middleware/rate-limit.middleware';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -45,7 +46,7 @@ app.use(requestLogger({ redactionLevel: 'partial' }));
|
|||||||
|
|
||||||
// CORS middleware
|
// CORS middleware
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: process.env.WEB_URL || 'http://localhost:3000',
|
origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
|
||||||
credentials: true,
|
credentials: true,
|
||||||
optionsSuccessStatus: 200 // Some legacy browsers (IE11, various SmartTVs) choke on 204
|
optionsSuccessStatus: 200 // Some legacy browsers (IE11, various SmartTVs) choke on 204
|
||||||
}));
|
}));
|
||||||
@@ -68,22 +69,31 @@ const apiLimiter = rateLimit({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// NEW V2 API ROUTES
|
// API ROUTES
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// Authentication routes
|
// Authentication routes (keep v1 for backward compatibility)
|
||||||
app.use('/api/v1/auth', authRoutes);
|
app.use('/api/v1/auth', authRoutes);
|
||||||
|
|
||||||
// Enhanced tracking routes (v2)
|
// Enhanced tracking routes (v2) - mounted at root for api.domain.com
|
||||||
app.use('/api/v2', trackingRoutes);
|
app.use('/v2', trackingRoutes);
|
||||||
|
|
||||||
// Analysis routes (v2)
|
// Analysis routes (v2)
|
||||||
app.use('/api/v2/analyze', analysisRoutes);
|
app.use('/v2/analyze', analysisRoutes);
|
||||||
|
|
||||||
// Export routes (v2)
|
// Export routes (v2)
|
||||||
|
app.use('/v2/export', exportRoutes);
|
||||||
|
app.use('/v2/bulk', bulkRoutes);
|
||||||
|
|
||||||
|
// Backward compatibility: keep /api/v2 routes as well
|
||||||
|
app.use('/api/v2', trackingRoutes);
|
||||||
|
app.use('/api/v2/analyze', analysisRoutes);
|
||||||
app.use('/api/v2/export', exportRoutes);
|
app.use('/api/v2/export', exportRoutes);
|
||||||
app.use('/api/v2/bulk', bulkRoutes);
|
app.use('/api/v2/bulk', bulkRoutes);
|
||||||
|
|
||||||
|
// Documentation routes
|
||||||
|
app.use('/', docsRoutes);
|
||||||
|
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
app.get('/health', (req, res) => {
|
app.get('/health', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@@ -15,6 +15,19 @@ import { logger } from '../lib/logger';
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const redirectTracker = new RedirectTrackerService();
|
const redirectTracker = new RedirectTrackerService();
|
||||||
|
|
||||||
|
// Health check endpoint for v2 API
|
||||||
|
router.get('/health', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
status: 200,
|
||||||
|
data: {
|
||||||
|
version: 'v2',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
environment: process.env.NODE_ENV || 'development'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Rate limiting for tracking endpoints
|
// Rate limiting for tracking endpoints
|
||||||
const trackingLimiter = rateLimit({
|
const trackingLimiter = rateLimit({
|
||||||
windowMs: 60 * 60 * 1000, // 1 hour
|
windowMs: 60 * 60 * 1000, // 1 hour
|
||||||
@@ -64,13 +77,18 @@ const listChecksSchema = z.object({
|
|||||||
offset: z.number().min(0).default(0),
|
offset: z.number().min(0).default(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// DEBUG: Simple test endpoint without middleware
|
||||||
|
router.post('/test', async (req, res) => {
|
||||||
|
res.json({ success: true, message: 'Test endpoint working' });
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/v2/track
|
* POST /api/v2/track
|
||||||
* Enhanced redirect tracking with database persistence
|
* Enhanced redirect tracking with database persistence
|
||||||
*/
|
*/
|
||||||
router.post('/track',
|
router.post('/track',
|
||||||
optionalAuth,
|
optionalAuth,
|
||||||
trackingRateLimit,
|
// trackingRateLimit, // TEMPORARILY DISABLED
|
||||||
async (req: AuthenticatedRequest, res) => {
|
async (req: AuthenticatedRequest, res) => {
|
||||||
try {
|
try {
|
||||||
// Validate input
|
// Validate input
|
||||||
@@ -82,25 +100,31 @@ router.post('/track',
|
|||||||
url = 'http://' + url;
|
url = 'http://' + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If user is authenticated but no projectId specified, use their default project
|
// Set project ID based on authentication status
|
||||||
if (req.user && !validatedData.projectId) {
|
if (!validatedData.projectId) {
|
||||||
// Find user's first project (simplified for Phase 2)
|
if (req.user) {
|
||||||
// In production, this would be more sophisticated
|
// Authenticated user - use their default project
|
||||||
const userMembership = req.user.memberships[0];
|
const userMembership = req.user.memberships[0];
|
||||||
if (userMembership) {
|
if (userMembership) {
|
||||||
// This is a simplified approach - in reality we'd query for projects
|
|
||||||
validatedData.projectId = 'default-project'; // Placeholder
|
validatedData.projectId = 'default-project'; // Placeholder
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Anonymous user - use the anonymous project
|
||||||
|
validatedData.projectId = 'anonymous-project';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform tracking
|
// Perform tracking with anonymous user ID for non-authenticated requests
|
||||||
|
const userId = req.user?.id || 'anonymous-user';
|
||||||
const result = await redirectTracker.trackUrl(
|
const result = await redirectTracker.trackUrl(
|
||||||
{ ...validatedData, url },
|
{ ...validatedData, url },
|
||||||
req.user?.id
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(`Enhanced tracking completed: ${url}`, {
|
logger.info(`Enhanced tracking completed: ${url}`, {
|
||||||
userId: req.user?.id,
|
userId: userId,
|
||||||
|
isAnonymous: !req.user,
|
||||||
|
projectId: validatedData.projectId,
|
||||||
checkId: result.id,
|
checkId: result.id,
|
||||||
status: result.status,
|
status: result.status,
|
||||||
redirectCount: result.redirectCount
|
redirectCount: result.redirectCount
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export interface BulkTrackingJob {
|
|||||||
enableSecurityAnalysis: boolean;
|
enableSecurityAnalysis: boolean;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
};
|
};
|
||||||
status: 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled';
|
status: 'PENDING' | 'QUEUED' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED' | 'ERROR';
|
||||||
progress: {
|
progress: {
|
||||||
total: number;
|
total: number;
|
||||||
processed: number;
|
processed: number;
|
||||||
@@ -101,26 +101,26 @@ export class BulkProcessorService {
|
|||||||
private readonly uploadsDir: string;
|
private readonly uploadsDir: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.redis = new IORedis({
|
// TEMPORARY: Disable Redis for bulk processing to avoid hangs
|
||||||
host: process.env.REDIS_HOST || 'localhost',
|
// this.redis = new IORedis({
|
||||||
port: parseInt(process.env.REDIS_PORT || '6379'),
|
// host: process.env.REDIS_HOST || 'localhost',
|
||||||
retryDelayOnFailover: 100,
|
// port: parseInt(process.env.REDIS_PORT || '6379'),
|
||||||
enableReadyCheck: false,
|
// enableReadyCheck: false,
|
||||||
maxRetriesPerRequest: null,
|
// maxRetriesPerRequest: null,
|
||||||
});
|
// });
|
||||||
|
|
||||||
this.trackingQueue = new Queue('bulk-tracking', {
|
// this.trackingQueue = new Queue('bulk-tracking', {
|
||||||
connection: this.redis,
|
// connection: this.redis,
|
||||||
defaultJobOptions: {
|
// defaultJobOptions: {
|
||||||
removeOnComplete: 100, // Keep last 100 completed jobs
|
// removeOnComplete: 100, // Keep last 100 completed jobs
|
||||||
removeOnFail: 50, // Keep last 50 failed jobs
|
// removeOnFail: 50, // Keep last 50 failed jobs
|
||||||
attempts: 3,
|
// attempts: 3,
|
||||||
backoff: {
|
// backoff: {
|
||||||
type: 'exponential',
|
// type: 'exponential',
|
||||||
delay: 2000,
|
// delay: 2000,
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
this.uploadsDir = path.join(process.cwd(), 'uploads');
|
this.uploadsDir = path.join(process.cwd(), 'uploads');
|
||||||
this.ensureUploadsDirectory();
|
this.ensureUploadsDirectory();
|
||||||
@@ -209,7 +209,8 @@ export class BulkProcessorService {
|
|||||||
async createBulkJob(
|
async createBulkJob(
|
||||||
userId: string,
|
userId: string,
|
||||||
organizationId: string | undefined,
|
organizationId: string | undefined,
|
||||||
jobData: BulkJobCreateRequest
|
jobData: BulkJobCreateRequest,
|
||||||
|
filePath?: string
|
||||||
): Promise<BulkTrackingJob> {
|
): Promise<BulkTrackingJob> {
|
||||||
try {
|
try {
|
||||||
// Validate input
|
// Validate input
|
||||||
@@ -222,16 +223,17 @@ export class BulkProcessorService {
|
|||||||
data: {
|
data: {
|
||||||
id: jobId,
|
id: jobId,
|
||||||
userId,
|
userId,
|
||||||
organizationId,
|
organizationId: organizationId || null,
|
||||||
projectId: validatedData.projectId,
|
projectId: validatedData.projectId || 'default-project',
|
||||||
status: 'pending',
|
uploadPath: filePath || 'api',
|
||||||
|
status: 'PENDING' as any,
|
||||||
totalUrls: validatedData.urls.length,
|
totalUrls: validatedData.urls.length,
|
||||||
processedUrls: 0,
|
processedUrls: 0,
|
||||||
successfulUrls: 0,
|
successfulUrls: 0,
|
||||||
failedUrls: 0,
|
failedUrls: 0,
|
||||||
configJson: JSON.stringify(validatedData.options),
|
configJson: JSON.stringify(validatedData.options),
|
||||||
urlsJson: JSON.stringify(validatedData.urls),
|
urlsJson: JSON.stringify(validatedData.urls),
|
||||||
},
|
} as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Queue the job for processing
|
// Queue the job for processing
|
||||||
@@ -255,9 +257,9 @@ export class BulkProcessorService {
|
|||||||
userId,
|
userId,
|
||||||
organizationId,
|
organizationId,
|
||||||
projectId: validatedData.projectId,
|
projectId: validatedData.projectId,
|
||||||
urls: validatedData.urls,
|
urls: validatedData.urls as any,
|
||||||
options: validatedData.options,
|
options: validatedData.options as any,
|
||||||
status: 'pending',
|
status: 'PENDING',
|
||||||
progress: {
|
progress: {
|
||||||
total: validatedData.urls.length,
|
total: validatedData.urls.length,
|
||||||
processed: 0,
|
processed: 0,
|
||||||
@@ -287,6 +289,7 @@ export class BulkProcessorService {
|
|||||||
userId: string,
|
userId: string,
|
||||||
organizationId: string | undefined,
|
organizationId: string | undefined,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
|
projectId: string,
|
||||||
options: Partial<BulkJobCreateRequest['options']> = {}
|
options: Partial<BulkJobCreateRequest['options']> = {}
|
||||||
): Promise<BulkTrackingJob> {
|
): Promise<BulkTrackingJob> {
|
||||||
try {
|
try {
|
||||||
@@ -309,9 +312,10 @@ export class BulkProcessorService {
|
|||||||
enableSecurityAnalysis: true,
|
enableSecurityAnalysis: true,
|
||||||
...options,
|
...options,
|
||||||
},
|
},
|
||||||
|
projectId
|
||||||
};
|
};
|
||||||
|
|
||||||
const job = await this.createBulkJob(userId, organizationId, jobData);
|
const job = await this.createBulkJob(userId, organizationId, jobData, filePath);
|
||||||
|
|
||||||
// Clean up uploaded file
|
// Clean up uploaded file
|
||||||
await fs.unlink(filePath).catch(() => {});
|
await fs.unlink(filePath).catch(() => {});
|
||||||
@@ -347,8 +351,8 @@ export class BulkProcessorService {
|
|||||||
const job: BulkTrackingJob = {
|
const job: BulkTrackingJob = {
|
||||||
id: bulkJob.id,
|
id: bulkJob.id,
|
||||||
userId: bulkJob.userId,
|
userId: bulkJob.userId,
|
||||||
organizationId: bulkJob.organizationId || undefined,
|
...(bulkJob.organizationId ? { organizationId: bulkJob.organizationId } : {}),
|
||||||
projectId: bulkJob.projectId || undefined,
|
...(bulkJob.projectId ? { projectId: bulkJob.projectId } : {}),
|
||||||
urls: JSON.parse(bulkJob.urlsJson as string),
|
urls: JSON.parse(bulkJob.urlsJson as string),
|
||||||
options: JSON.parse(bulkJob.configJson as string),
|
options: JSON.parse(bulkJob.configJson as string),
|
||||||
status: bulkJob.status as BulkTrackingJob['status'],
|
status: bulkJob.status as BulkTrackingJob['status'],
|
||||||
@@ -376,7 +380,7 @@ export class BulkProcessorService {
|
|||||||
* Calculate estimated completion time
|
* Calculate estimated completion time
|
||||||
*/
|
*/
|
||||||
private calculateEstimatedCompletion(bulkJob: any): Date | undefined {
|
private calculateEstimatedCompletion(bulkJob: any): Date | undefined {
|
||||||
if (!bulkJob.startedAt || bulkJob.status === 'completed' || bulkJob.status === 'failed') {
|
if (!bulkJob.startedAt || bulkJob.status === 'COMPLETED' || bulkJob.status === 'FAILED') {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,7 +410,7 @@ export class BulkProcessorService {
|
|||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
status: 'cancelled',
|
status: 'CANCELLED',
|
||||||
finishedAt: new Date(),
|
finishedAt: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -446,8 +450,8 @@ export class BulkProcessorService {
|
|||||||
const job: BulkTrackingJob = {
|
const job: BulkTrackingJob = {
|
||||||
id: bulkJob.id,
|
id: bulkJob.id,
|
||||||
userId: bulkJob.userId,
|
userId: bulkJob.userId,
|
||||||
organizationId: bulkJob.organizationId || undefined,
|
...(bulkJob.organizationId ? { organizationId: bulkJob.organizationId } : {}),
|
||||||
projectId: bulkJob.projectId || undefined,
|
...(bulkJob.projectId ? { projectId: bulkJob.projectId } : {}),
|
||||||
urls: JSON.parse(bulkJob.urlsJson as string),
|
urls: JSON.parse(bulkJob.urlsJson as string),
|
||||||
options: JSON.parse(bulkJob.configJson as string),
|
options: JSON.parse(bulkJob.configJson as string),
|
||||||
status: bulkJob.status as BulkTrackingJob['status'],
|
status: bulkJob.status as BulkTrackingJob['status'],
|
||||||
@@ -535,7 +539,7 @@ export class BulkProcessorService {
|
|||||||
lt: cutoff,
|
lt: cutoff,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
in: ['completed', 'failed', 'cancelled'],
|
in: ['COMPLETED', 'FAILED', 'CANCELLED'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,11 +2,10 @@
|
|||||||
* Advanced Rate Limiting Service for Redirect Intelligence v2
|
* Advanced Rate Limiting Service for Redirect Intelligence v2
|
||||||
*
|
*
|
||||||
* Implements user-tier rate limiting with organization-based quotas
|
* Implements user-tier rate limiting with organization-based quotas
|
||||||
* and Redis-backed rate limiting with rate-limiter-flexible
|
* using in-memory rate limiting with rate-limiter-flexible
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { RateLimiterRedis, RateLimiterMemory } from 'rate-limiter-flexible';
|
import { RateLimiterMemory } from 'rate-limiter-flexible';
|
||||||
import IORedis from 'ioredis';
|
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { logger } from '../lib/logger';
|
import { logger } from '../lib/logger';
|
||||||
@@ -67,20 +66,9 @@ export interface RateLimitInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class RateLimitService {
|
export class RateLimitService {
|
||||||
private redis: IORedis;
|
private rateLimiters: Map<string, RateLimiterMemory>;
|
||||||
private rateLimiters: Map<string, RateLimiterRedis | RateLimiterMemory>;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Initialize Redis connection
|
|
||||||
this.redis = new IORedis({
|
|
||||||
host: process.env.REDIS_HOST || 'localhost',
|
|
||||||
port: parseInt(process.env.REDIS_PORT || '6379'),
|
|
||||||
retryDelayOnFailover: 100,
|
|
||||||
enableReadyCheck: false,
|
|
||||||
maxRetriesPerRequest: null,
|
|
||||||
lazyConnect: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.rateLimiters = new Map();
|
this.rateLimiters = new Map();
|
||||||
this.initializeRateLimiters();
|
this.initializeRateLimiters();
|
||||||
}
|
}
|
||||||
@@ -89,9 +77,8 @@ export class RateLimitService {
|
|||||||
* Initialize rate limiters for different endpoints and tiers
|
* Initialize rate limiters for different endpoints and tiers
|
||||||
*/
|
*/
|
||||||
private initializeRateLimiters(): void {
|
private initializeRateLimiters(): void {
|
||||||
// Legacy endpoints (preserve existing behavior)
|
// Legacy endpoints (preserve existing behavior) - MEMORY (PERMANENT)
|
||||||
this.rateLimiters.set('legacy', new RateLimiterRedis({
|
this.rateLimiters.set('legacy', new RateLimiterMemory({
|
||||||
storeClient: this.redis,
|
|
||||||
keyPrefix: 'rl_legacy',
|
keyPrefix: 'rl_legacy',
|
||||||
points: 100, // requests
|
points: 100, // requests
|
||||||
duration: 3600, // per hour
|
duration: 3600, // per hour
|
||||||
@@ -99,9 +86,8 @@ export class RateLimitService {
|
|||||||
execEvenly: true,
|
execEvenly: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Anonymous users
|
// Anonymous users - MEMORY (PERMANENT)
|
||||||
this.rateLimiters.set('anonymous', new RateLimiterRedis({
|
this.rateLimiters.set('anonymous', new RateLimiterMemory({
|
||||||
storeClient: this.redis,
|
|
||||||
keyPrefix: 'rl_anon',
|
keyPrefix: 'rl_anon',
|
||||||
points: ANONYMOUS_TIER.requestsPerHour,
|
points: ANONYMOUS_TIER.requestsPerHour,
|
||||||
duration: 3600,
|
duration: 3600,
|
||||||
@@ -113,9 +99,8 @@ export class RateLimitService {
|
|||||||
Object.keys(RATE_LIMIT_TIERS).forEach(tier => {
|
Object.keys(RATE_LIMIT_TIERS).forEach(tier => {
|
||||||
const config = RATE_LIMIT_TIERS[tier];
|
const config = RATE_LIMIT_TIERS[tier];
|
||||||
|
|
||||||
// Hourly limits
|
// Hourly limits - MEMORY (PERMANENT)
|
||||||
this.rateLimiters.set(`user_${tier}_hour`, new RateLimiterRedis({
|
this.rateLimiters.set(`user_${tier}_hour`, new RateLimiterMemory({
|
||||||
storeClient: this.redis,
|
|
||||||
keyPrefix: `rl_user_${tier}_h`,
|
keyPrefix: `rl_user_${tier}_h`,
|
||||||
points: config.requestsPerHour,
|
points: config.requestsPerHour,
|
||||||
duration: 3600,
|
duration: 3600,
|
||||||
@@ -123,9 +108,8 @@ export class RateLimitService {
|
|||||||
execEvenly: true,
|
execEvenly: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Per-minute limits (burst protection)
|
// Per-minute limits (burst protection) - MEMORY (PERMANENT)
|
||||||
this.rateLimiters.set(`user_${tier}_minute`, new RateLimiterRedis({
|
this.rateLimiters.set(`user_${tier}_minute`, new RateLimiterMemory({
|
||||||
storeClient: this.redis,
|
|
||||||
keyPrefix: `rl_user_${tier}_m`,
|
keyPrefix: `rl_user_${tier}_m`,
|
||||||
points: config.requestsPerMinute,
|
points: config.requestsPerMinute,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
@@ -133,9 +117,8 @@ export class RateLimitService {
|
|||||||
execEvenly: true,
|
execEvenly: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Bulk job limits (daily)
|
// Bulk job limits (daily) - MEMORY (PERMANENT)
|
||||||
this.rateLimiters.set(`bulk_${tier}_day`, new RateLimiterRedis({
|
this.rateLimiters.set(`bulk_${tier}_day`, new RateLimiterMemory({
|
||||||
storeClient: this.redis,
|
|
||||||
keyPrefix: `rl_bulk_${tier}_d`,
|
keyPrefix: `rl_bulk_${tier}_d`,
|
||||||
points: config.bulkJobsPerDay,
|
points: config.bulkJobsPerDay,
|
||||||
duration: 86400, // 24 hours
|
duration: 86400, // 24 hours
|
||||||
@@ -143,9 +126,8 @@ export class RateLimitService {
|
|||||||
execEvenly: false,
|
execEvenly: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Export limits (daily)
|
// Export limits (daily) - MEMORY (PERMANENT)
|
||||||
this.rateLimiters.set(`export_${tier}_day`, new RateLimiterRedis({
|
this.rateLimiters.set(`export_${tier}_day`, new RateLimiterMemory({
|
||||||
storeClient: this.redis,
|
|
||||||
keyPrefix: `rl_export_${tier}_d`,
|
keyPrefix: `rl_export_${tier}_d`,
|
||||||
points: config.exportLimit,
|
points: config.exportLimit,
|
||||||
duration: 86400,
|
duration: 86400,
|
||||||
@@ -159,10 +141,14 @@ export class RateLimitService {
|
|||||||
* Get user's rate limit tier based on organization plan
|
* Get user's rate limit tier based on organization plan
|
||||||
*/
|
*/
|
||||||
async getUserTier(userId?: string): Promise<RateLimitTier> {
|
async getUserTier(userId?: string): Promise<RateLimitTier> {
|
||||||
if (!userId) {
|
if (!userId || userId === 'anonymous-user') {
|
||||||
return ANONYMOUS_TIER;
|
return ANONYMOUS_TIER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TEMPORARILY RETURN FREE TIER FOR ALL AUTHENTICATED USERS TO AVOID DB HANGS
|
||||||
|
return RATE_LIMIT_TIERS.free;
|
||||||
|
|
||||||
|
/* ORIGINAL CODE - TEMPORARILY DISABLED DUE TO DB HANGING
|
||||||
try {
|
try {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
@@ -188,6 +174,7 @@ export class RateLimitService {
|
|||||||
logger.error('Failed to get user tier:', error);
|
logger.error('Failed to get user tier:', error);
|
||||||
return RATE_LIMIT_TIERS.free;
|
return RATE_LIMIT_TIERS.free;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,7 +193,7 @@ export class RateLimitService {
|
|||||||
if (type === 'legacy') {
|
if (type === 'legacy') {
|
||||||
limiterKey = 'legacy';
|
limiterKey = 'legacy';
|
||||||
limit = 100;
|
limit = 100;
|
||||||
} else if (!userId) {
|
} else if (!userId || userId === 'anonymous-user') {
|
||||||
limiterKey = 'anonymous';
|
limiterKey = 'anonymous';
|
||||||
limit = ANONYMOUS_TIER.requestsPerHour;
|
limit = ANONYMOUS_TIER.requestsPerHour;
|
||||||
} else {
|
} else {
|
||||||
@@ -260,7 +247,7 @@ export class RateLimitService {
|
|||||||
*/
|
*/
|
||||||
async checkBurstLimit(userId: string): Promise<void> {
|
async checkBurstLimit(userId: string): Promise<void> {
|
||||||
const tier = await this.getUserTier(userId);
|
const tier = await this.getUserTier(userId);
|
||||||
if (tier === ANONYMOUS_TIER) return;
|
if (tier === ANONYMOUS_TIER || userId === 'anonymous-user') return;
|
||||||
|
|
||||||
const tierName = tier.name.toLowerCase();
|
const tierName = tier.name.toLowerCase();
|
||||||
const limiterKey = `user_${tierName}_minute`;
|
const limiterKey = `user_${tierName}_minute`;
|
||||||
@@ -293,7 +280,7 @@ export class RateLimitService {
|
|||||||
if (type === 'legacy') {
|
if (type === 'legacy') {
|
||||||
limiterKey = 'legacy';
|
limiterKey = 'legacy';
|
||||||
limit = 100;
|
limit = 100;
|
||||||
} else if (!userId) {
|
} else if (!userId || userId === 'anonymous-user') {
|
||||||
limiterKey = 'anonymous';
|
limiterKey = 'anonymous';
|
||||||
limit = ANONYMOUS_TIER.requestsPerHour;
|
limit = ANONYMOUS_TIER.requestsPerHour;
|
||||||
} else {
|
} else {
|
||||||
@@ -375,17 +362,16 @@ export class RateLimitService {
|
|||||||
tierDistribution: Record<string, number>;
|
tierDistribution: Record<string, number>;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
// This is a simplified version - in production you'd want more detailed stats
|
// In-memory rate limiting doesn't expose detailed statistics
|
||||||
const keys = await this.redis.keys('rl_*');
|
// Return basic info about configured rate limiters
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalRequests: keys.length, // Simplified metric
|
totalRequests: 0, // Not available with in-memory limiting
|
||||||
activeKeys: keys.length,
|
activeKeys: this.rateLimiters.size,
|
||||||
tierDistribution: {
|
tierDistribution: {
|
||||||
anonymous: keys.filter(k => k.includes('anon')).length,
|
anonymous: 1,
|
||||||
free: keys.filter(k => k.includes('free')).length,
|
free: Object.keys(RATE_LIMIT_TIERS).length,
|
||||||
pro: keys.filter(k => k.includes('pro')).length,
|
pro: Object.keys(RATE_LIMIT_TIERS).length,
|
||||||
enterprise: keys.filter(k => k.includes('enterprise')).length,
|
enterprise: Object.keys(RATE_LIMIT_TIERS).length,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -393,7 +379,7 @@ export class RateLimitService {
|
|||||||
return {
|
return {
|
||||||
totalRequests: 0,
|
totalRequests: 0,
|
||||||
activeKeys: 0,
|
activeKeys: 0,
|
||||||
tierDistribution: {},
|
tierDistribution: { anonymous: 0, free: 0, pro: 0, enterprise: 0 },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Manages user authentication state and API interactions
|
* Manages user authentication state and API interactions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
import { authApi, AuthUser, LoginRequest, RegisterRequest } from '../services/api';
|
import { authApi, AuthUser, LoginRequest, RegisterRequest } from '../services/api';
|
||||||
|
|
||||||
@@ -38,11 +38,13 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||||||
|
|
||||||
const checkExistingSession = async () => {
|
const checkExistingSession = async () => {
|
||||||
try {
|
try {
|
||||||
|
// Check if auth endpoints are available first
|
||||||
const userData = await authApi.getCurrentUser();
|
const userData = await authApi.getCurrentUser();
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
// No existing session or session expired
|
// No existing session, session expired, or auth endpoints not available
|
||||||
console.log('No existing session');
|
console.log('Authentication not available or no existing session:', error.message);
|
||||||
|
setUser(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user