- Replace placeholder implementation with real database query - Add cross-project checks retrieval for authenticated users - Include detailed check information with hops, timing, and metadata - Add proper database joins across checks → projects → organizations → memberships - Format response to match frontend expectations - Rebuild API with updated implementation Fixes dashboard checks history display - now shows real tracking data instead of empty placeholder
366 lines
13 KiB
JavaScript
366 lines
13 KiB
JavaScript
"use strict";
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const express_1 = __importDefault(require("express"));
|
|
const zod_1 = require("zod");
|
|
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
|
const redirect_tracker_service_1 = require("../services/redirect-tracker.service");
|
|
const prisma_1 = require("../lib/prisma");
|
|
const auth_middleware_1 = require("../middleware/auth.middleware");
|
|
const logger_1 = require("../lib/logger");
|
|
const router = express_1.default.Router();
|
|
const redirectTracker = new redirect_tracker_service_1.RedirectTrackerService();
|
|
router.get('/health', (req, res) => {
|
|
res.json({
|
|
success: true,
|
|
status: 200,
|
|
data: {
|
|
version: 'v2',
|
|
timestamp: new Date().toISOString(),
|
|
environment: process.env.NODE_ENV || 'development'
|
|
}
|
|
});
|
|
});
|
|
const trackingLimiter = (0, express_rate_limit_1.default)({
|
|
windowMs: 60 * 60 * 1000,
|
|
max: 200,
|
|
message: {
|
|
success: false,
|
|
error: 'Rate limit exceeded',
|
|
message: 'Too many tracking requests. Please try again later.'
|
|
},
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
keyGenerator: (req) => {
|
|
return req.user ? `user:${req.user.id}` : `ip:${req.ip}`;
|
|
},
|
|
});
|
|
const anonymousTrackingLimiter = (0, express_rate_limit_1.default)({
|
|
windowMs: 60 * 60 * 1000,
|
|
max: 50,
|
|
message: {
|
|
success: false,
|
|
error: 'Rate limit exceeded',
|
|
message: 'Anonymous users are limited to 50 requests per hour. Please register for higher limits.'
|
|
},
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
skip: (req) => !!req.user,
|
|
});
|
|
const trackUrlSchema = zod_1.z.object({
|
|
url: zod_1.z.string().min(1, 'URL is required'),
|
|
method: zod_1.z.enum(['GET', 'POST', 'HEAD']).default('GET'),
|
|
userAgent: zod_1.z.string().optional(),
|
|
headers: zod_1.z.record(zod_1.z.string()).optional(),
|
|
projectId: zod_1.z.string().optional(),
|
|
followJS: zod_1.z.boolean().default(false),
|
|
maxHops: zod_1.z.number().min(1).max(20).default(10),
|
|
timeout: zod_1.z.number().min(1000).max(30000).default(15000),
|
|
});
|
|
const listChecksSchema = zod_1.z.object({
|
|
projectId: zod_1.z.string(),
|
|
limit: zod_1.z.number().min(1).max(100).default(50),
|
|
offset: zod_1.z.number().min(0).default(0),
|
|
});
|
|
router.post('/test', async (req, res) => {
|
|
res.json({ success: true, message: 'Test endpoint working' });
|
|
});
|
|
router.post('/track', auth_middleware_1.optionalAuth, async (req, res) => {
|
|
try {
|
|
const validatedData = trackUrlSchema.parse(req.body);
|
|
let { url } = validatedData;
|
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
url = 'http://' + url;
|
|
}
|
|
if (!validatedData.projectId) {
|
|
if (req.user) {
|
|
const userMembership = req.user.memberships[0];
|
|
if (userMembership) {
|
|
const defaultProject = await prisma_1.prisma.project.findFirst({
|
|
where: {
|
|
orgId: userMembership.orgId
|
|
}
|
|
});
|
|
if (defaultProject) {
|
|
validatedData.projectId = defaultProject.id;
|
|
}
|
|
else {
|
|
const newProject = await prisma_1.prisma.project.create({
|
|
data: {
|
|
name: 'Default Project',
|
|
orgId: userMembership.orgId,
|
|
settingsJson: {}
|
|
}
|
|
});
|
|
validatedData.projectId = newProject.id;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
let anonymousProject = await prisma_1.prisma.project.findFirst({
|
|
where: {
|
|
name: 'Anonymous Tracking'
|
|
}
|
|
});
|
|
if (!anonymousProject) {
|
|
let anonymousOrg = await prisma_1.prisma.organization.findFirst({
|
|
where: {
|
|
name: 'Anonymous Users'
|
|
}
|
|
});
|
|
if (!anonymousOrg) {
|
|
anonymousOrg = await prisma_1.prisma.organization.create({
|
|
data: {
|
|
name: 'Anonymous Users',
|
|
plan: 'free'
|
|
}
|
|
});
|
|
}
|
|
anonymousProject = await prisma_1.prisma.project.create({
|
|
data: {
|
|
name: 'Anonymous Tracking',
|
|
orgId: anonymousOrg.id,
|
|
settingsJson: {}
|
|
}
|
|
});
|
|
}
|
|
validatedData.projectId = anonymousProject.id;
|
|
}
|
|
}
|
|
const userId = req.user?.id || 'anonymous-user';
|
|
const result = await redirectTracker.trackUrl({ ...validatedData, url }, userId);
|
|
logger_1.logger.info(`Enhanced tracking completed: ${url}`, {
|
|
userId: userId,
|
|
isAnonymous: !req.user,
|
|
projectId: validatedData.projectId,
|
|
checkId: result.id,
|
|
status: result.status,
|
|
redirectCount: result.redirectCount
|
|
});
|
|
res.json({
|
|
success: true,
|
|
status: 200,
|
|
data: {
|
|
check: result,
|
|
url,
|
|
method: result.method,
|
|
redirectCount: result.redirectCount,
|
|
finalUrl: result.finalUrl,
|
|
finalStatusCode: result.hops[result.hops.length - 1]?.statusCode,
|
|
},
|
|
meta: {
|
|
version: 'v2',
|
|
enhanced: true,
|
|
persisted: true,
|
|
checkId: result.id,
|
|
}
|
|
});
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error('Enhanced tracking failed:', error);
|
|
if (error instanceof zod_1.z.ZodError) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Validation error',
|
|
message: error.errors[0]?.message || 'Invalid input',
|
|
details: error.errors
|
|
});
|
|
}
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Tracking failed',
|
|
message: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
});
|
|
}
|
|
});
|
|
router.get('/track/:checkId', auth_middleware_1.optionalAuth, async (req, res) => {
|
|
try {
|
|
const { checkId } = req.params;
|
|
if (!checkId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Check ID required'
|
|
});
|
|
}
|
|
const check = await redirectTracker.getCheck(checkId, req.user?.id);
|
|
if (!check) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Check not found',
|
|
message: 'The requested check does not exist or you do not have access to it'
|
|
});
|
|
}
|
|
res.json({
|
|
success: true,
|
|
status: 200,
|
|
data: { check },
|
|
meta: {
|
|
version: 'v2',
|
|
checkId: check.id,
|
|
}
|
|
});
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error('Failed to retrieve check:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to retrieve check',
|
|
message: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
});
|
|
}
|
|
});
|
|
router.get('/projects/:projectId/checks', auth_middleware_1.requireAuth, async (req, res) => {
|
|
try {
|
|
const { projectId } = req.params;
|
|
const { limit = 50, offset = 0 } = req.query;
|
|
const validatedData = listChecksSchema.parse({
|
|
projectId,
|
|
limit: Number(limit),
|
|
offset: Number(offset),
|
|
});
|
|
if (!projectId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Project ID required'
|
|
});
|
|
}
|
|
const checks = await redirectTracker.listChecks(validatedData.projectId, validatedData.limit, validatedData.offset);
|
|
res.json({
|
|
success: true,
|
|
status: 200,
|
|
data: {
|
|
checks,
|
|
pagination: {
|
|
limit: validatedData.limit,
|
|
offset: validatedData.offset,
|
|
total: checks.length,
|
|
}
|
|
},
|
|
meta: {
|
|
version: 'v2',
|
|
projectId: validatedData.projectId,
|
|
}
|
|
});
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error('Failed to list checks:', error);
|
|
if (error instanceof zod_1.z.ZodError) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Validation error',
|
|
message: error.errors[0]?.message || 'Invalid input',
|
|
details: error.errors
|
|
});
|
|
}
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to list checks',
|
|
message: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
});
|
|
}
|
|
});
|
|
router.get('/checks/recent', auth_middleware_1.requireAuth, async (req, res) => {
|
|
try {
|
|
const { limit = 20 } = req.query;
|
|
const userId = req.user.id;
|
|
const checks = await prisma_1.prisma.check.findMany({
|
|
where: {
|
|
project: {
|
|
organization: {
|
|
memberships: {
|
|
some: {
|
|
userId: userId
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
include: {
|
|
project: {
|
|
select: {
|
|
name: true
|
|
}
|
|
},
|
|
hops: {
|
|
orderBy: {
|
|
hopIndex: 'asc'
|
|
}
|
|
}
|
|
},
|
|
orderBy: {
|
|
startedAt: 'desc'
|
|
},
|
|
take: Number(limit)
|
|
});
|
|
const formattedChecks = checks.map(check => ({
|
|
id: check.id,
|
|
inputUrl: check.inputUrl,
|
|
finalUrl: check.finalUrl,
|
|
method: check.method,
|
|
status: check.status,
|
|
startedAt: check.startedAt,
|
|
finishedAt: check.finishedAt,
|
|
totalTimeMs: check.totalTimeMs,
|
|
redirectCount: check.hops.length,
|
|
projectName: check.project.name,
|
|
hops: check.hops.map(hop => ({
|
|
hopIndex: hop.hopIndex,
|
|
url: hop.url,
|
|
statusCode: hop.statusCode,
|
|
redirectType: hop.redirectType,
|
|
latencyMs: hop.latencyMs,
|
|
contentType: hop.contentType
|
|
}))
|
|
}));
|
|
res.json({
|
|
success: true,
|
|
status: 200,
|
|
data: {
|
|
checks: formattedChecks
|
|
},
|
|
meta: {
|
|
version: 'v2',
|
|
userId: userId,
|
|
total: formattedChecks.length
|
|
}
|
|
});
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error('Failed to get recent checks:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to get recent checks',
|
|
message: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
});
|
|
}
|
|
});
|
|
router.post('/track/bulk', auth_middleware_1.requireAuth, async (req, res) => {
|
|
try {
|
|
logger_1.logger.info(`Bulk tracking request from user: ${req.user.email}`);
|
|
res.json({
|
|
success: true,
|
|
status: 200,
|
|
data: {
|
|
message: 'Bulk tracking will be implemented in Phase 6',
|
|
bulkJobId: null,
|
|
},
|
|
meta: {
|
|
version: 'v2',
|
|
feature: 'bulk-tracking',
|
|
phase: '6 (future)',
|
|
}
|
|
});
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error('Bulk tracking placeholder error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Bulk tracking not yet available',
|
|
message: 'This feature will be implemented in Phase 6'
|
|
});
|
|
}
|
|
});
|
|
exports.default = router;
|
|
//# sourceMappingURL=tracking.routes.js.map
|