feat(phase-2): implement enhanced redirect tracking with database persistence
🚀 Core Features: - Complete database-persisted redirect tracking system - Enhanced hop analysis with timing, headers, and metadata - Intelligent redirect type detection (301, 302, 307, 308, meta, JS, final) - Automatic redirect loop detection and prevention - Comprehensive status tracking (OK, ERROR, TIMEOUT, LOOP) - Real-time latency measurement per hop 🔧 Technical Implementation: - Production-grade RedirectTrackerService with Prisma integration - Type-safe request/response handling with Zod validation - Advanced rate limiting (200/hour authenticated, 50/hour anonymous) - Flexible authentication (optional auth for broader access) - Robust error handling and structured logging - Comprehensive input validation and sanitization 🌐 API Endpoints: - POST /api/v2/track - Enhanced tracking with database persistence - GET /api/v2/track/:checkId - Retrieve specific check with full hop details - GET /api/v2/projects/:projectId/checks - List project checks with pagination - GET /api/v2/checks/recent - Recent checks for authenticated users - POST /api/v2/track/bulk - Placeholder for Phase 6 bulk processing 📊 Enhanced Data Model: - Persistent check records with complete metadata - Detailed hop tracking with response headers and timing - SSL scheme detection and protocol analysis - Content-Type extraction and analysis - Comprehensive redirect chain preservation 🔒 Security & Performance: - User-based rate limiting for authenticated requests - IP-based rate limiting for anonymous requests - Configurable timeouts and hop limits (1-20 hops, 1-30s timeout) - Request validation prevents malicious input - Structured error responses for API consistency 🔄 Backward Compatibility: - All existing endpoints preserved and functional - Legacy response formats maintained exactly - Zero breaking changes to existing integrations - Enhanced features available only in v2 endpoints 📋 Database Schema: - Checks table for persistent tracking records - Hops table for detailed redirect chain analysis - Foreign key relationships for data integrity - Optimized indexes for performance queries 🧪 Quality Assurance: - Comprehensive test suite for all endpoints - Authentication flow testing - Rate limiting verification - Error handling validation - Legacy compatibility verification Ready for Phase 3: SSL/SEO/Security analysis integration
This commit is contained in:
@@ -11,6 +11,7 @@ export const ApiResponseSchema = z.object({
|
||||
data: z.any().optional(),
|
||||
error: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
meta: z.record(z.any()).optional(),
|
||||
});
|
||||
|
||||
export type ApiResponse<T = any> = {
|
||||
@@ -19,6 +20,7 @@ export type ApiResponse<T = any> = {
|
||||
data?: T;
|
||||
error?: string;
|
||||
message?: string;
|
||||
meta?: Record<string, any>;
|
||||
};
|
||||
|
||||
// Legacy redirect result (for backward compatibility)
|
||||
@@ -38,7 +40,7 @@ export const LegacyRedirectSchema = z.object({
|
||||
|
||||
export type LegacyRedirect = z.infer<typeof LegacyRedirectSchema>;
|
||||
|
||||
// Track request schemas
|
||||
// Legacy track request (v1)
|
||||
export const TrackRequestSchema = z.object({
|
||||
url: z.string().url(),
|
||||
method: z.enum(['GET', 'HEAD', 'POST']).default('GET'),
|
||||
@@ -47,7 +49,7 @@ export const TrackRequestSchema = z.object({
|
||||
|
||||
export type TrackRequest = z.infer<typeof TrackRequestSchema>;
|
||||
|
||||
// Track response schema
|
||||
// Legacy track response (v1)
|
||||
export const TrackResponseSchema = z.object({
|
||||
url: z.string(),
|
||||
method: z.string(),
|
||||
@@ -58,3 +60,166 @@ export const TrackResponseSchema = z.object({
|
||||
});
|
||||
|
||||
export type TrackResponse = z.infer<typeof TrackResponseSchema>;
|
||||
|
||||
// ============================================================================
|
||||
// V2 ENHANCED TYPES
|
||||
// ============================================================================
|
||||
|
||||
// Enhanced track request (v2)
|
||||
export const TrackRequestV2Schema = z.object({
|
||||
url: z.string().url(),
|
||||
method: z.enum(['GET', 'POST', 'HEAD']).default('GET'),
|
||||
userAgent: z.string().optional(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
projectId: z.string().optional(),
|
||||
followJS: z.boolean().default(false),
|
||||
maxHops: z.number().min(1).max(20).default(10),
|
||||
timeout: z.number().min(1000).max(30000).default(15000),
|
||||
});
|
||||
|
||||
export type TrackRequestV2 = z.infer<typeof TrackRequestV2Schema>;
|
||||
|
||||
// Hop result (v2)
|
||||
export const HopResultSchema = z.object({
|
||||
hopIndex: z.number(),
|
||||
url: z.string(),
|
||||
scheme: z.string().optional(),
|
||||
statusCode: z.number().optional(),
|
||||
redirectType: z.enum(['HTTP_301', 'HTTP_302', 'HTTP_307', 'HTTP_308', 'META_REFRESH', 'JS', 'FINAL', 'OTHER']),
|
||||
latencyMs: z.number().optional(),
|
||||
contentType: z.string().optional(),
|
||||
reason: z.string().optional(),
|
||||
responseHeaders: z.record(z.string()),
|
||||
});
|
||||
|
||||
export type HopResult = z.infer<typeof HopResultSchema>;
|
||||
|
||||
// Check result (v2)
|
||||
export const CheckResultSchema = z.object({
|
||||
id: z.string(),
|
||||
inputUrl: z.string(),
|
||||
method: z.string(),
|
||||
status: z.enum(['OK', 'ERROR', 'TIMEOUT', 'LOOP']),
|
||||
finalUrl: z.string().optional(),
|
||||
totalTimeMs: z.number(),
|
||||
startedAt: z.date(),
|
||||
finishedAt: z.date(),
|
||||
hops: z.array(HopResultSchema),
|
||||
redirectCount: z.number(),
|
||||
loopDetected: z.boolean().optional(),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
export type CheckResult = z.infer<typeof CheckResultSchema>;
|
||||
|
||||
// Enhanced track response (v2)
|
||||
export const TrackResponseV2Schema = z.object({
|
||||
success: z.boolean(),
|
||||
status: z.number(),
|
||||
data: z.object({
|
||||
check: CheckResultSchema,
|
||||
url: z.string(),
|
||||
method: z.string(),
|
||||
redirectCount: z.number(),
|
||||
finalUrl: z.string().optional(),
|
||||
finalStatusCode: z.number().optional(),
|
||||
}),
|
||||
meta: z.object({
|
||||
version: z.literal('v2'),
|
||||
enhanced: z.boolean(),
|
||||
persisted: z.boolean(),
|
||||
checkId: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type TrackResponseV2 = z.infer<typeof TrackResponseV2Schema>;
|
||||
|
||||
// ============================================================================
|
||||
// AUTHENTICATION TYPES
|
||||
// ============================================================================
|
||||
|
||||
export const AuthUserSchema = z.object({
|
||||
id: z.string(),
|
||||
email: z.string().email(),
|
||||
name: z.string(),
|
||||
memberships: z.array(z.object({
|
||||
orgId: z.string(),
|
||||
role: z.string(),
|
||||
organization: z.object({
|
||||
name: z.string(),
|
||||
plan: z.string(),
|
||||
}),
|
||||
})),
|
||||
});
|
||||
|
||||
export type AuthUser = z.infer<typeof AuthUserSchema>;
|
||||
|
||||
export const LoginRequestSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
});
|
||||
|
||||
export type LoginRequest = z.infer<typeof LoginRequestSchema>;
|
||||
|
||||
export const RegisterRequestSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(2),
|
||||
password: z.string().min(8),
|
||||
organizationName: z.string().min(2).optional(),
|
||||
});
|
||||
|
||||
export type RegisterRequest = z.infer<typeof RegisterRequestSchema>;
|
||||
|
||||
export const AuthResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
status: z.number(),
|
||||
data: z.object({
|
||||
user: AuthUserSchema,
|
||||
token: z.string().optional(),
|
||||
}),
|
||||
message: z.string().optional(),
|
||||
});
|
||||
|
||||
export type AuthResponse = z.infer<typeof AuthResponseSchema>;
|
||||
|
||||
// ============================================================================
|
||||
// PROJECT & ORGANIZATION TYPES
|
||||
// ============================================================================
|
||||
|
||||
export const ProjectSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
orgId: z.string(),
|
||||
settingsJson: z.record(z.any()),
|
||||
createdAt: z.date(),
|
||||
});
|
||||
|
||||
export type Project = z.infer<typeof ProjectSchema>;
|
||||
|
||||
export const OrganizationSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
plan: z.string(),
|
||||
createdAt: z.date(),
|
||||
});
|
||||
|
||||
export type Organization = z.infer<typeof OrganizationSchema>;
|
||||
|
||||
// ============================================================================
|
||||
// UTILITY TYPES
|
||||
// ============================================================================
|
||||
|
||||
export type CheckStatus = 'OK' | 'ERROR' | 'TIMEOUT' | 'LOOP';
|
||||
export type RedirectType = 'HTTP_301' | 'HTTP_302' | 'HTTP_307' | 'HTTP_308' | 'META_REFRESH' | 'JS' | 'FINAL' | 'OTHER';
|
||||
export type UserRole = 'OWNER' | 'ADMIN' | 'MEMBER';
|
||||
|
||||
// Error response
|
||||
export const ErrorResponseSchema = z.object({
|
||||
success: z.literal(false),
|
||||
error: z.string(),
|
||||
message: z.string(),
|
||||
status: z.number(),
|
||||
details: z.any().optional(),
|
||||
});
|
||||
|
||||
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
|
||||
Reference in New Issue
Block a user