gitea
This commit is contained in:
@@ -1,371 +1,614 @@
|
||||
import { Router, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import db from '../db';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
import { CreateMonitorInput, UpdateMonitorInput } from '../types';
|
||||
import { checkMonitor } from '../services/monitor';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const createMonitorSchema = z.object({
|
||||
url: z.string().url(),
|
||||
name: z.string().optional(),
|
||||
frequency: z.number().int().positive(),
|
||||
elementSelector: z.string().optional(),
|
||||
ignoreRules: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.enum(['css', 'regex', 'text']),
|
||||
value: z.string(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
keywordRules: z
|
||||
.array(
|
||||
z.object({
|
||||
keyword: z.string(),
|
||||
type: z.enum(['appears', 'disappears', 'count']),
|
||||
threshold: z.number().optional(),
|
||||
caseSensitive: z.boolean().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const updateMonitorSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
frequency: z.number().int().positive().optional(),
|
||||
status: z.enum(['active', 'paused', 'error']).optional(),
|
||||
elementSelector: z.string().optional(),
|
||||
ignoreRules: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.enum(['css', 'regex', 'text']),
|
||||
value: z.string(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
keywordRules: z
|
||||
.array(
|
||||
z.object({
|
||||
keyword: z.string(),
|
||||
type: z.enum(['appears', 'disappears', 'count']),
|
||||
threshold: z.number().optional(),
|
||||
caseSensitive: z.boolean().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
// Get plan limits
|
||||
function getPlanLimits(plan: string) {
|
||||
const limits = {
|
||||
free: {
|
||||
maxMonitors: parseInt(process.env.MAX_MONITORS_FREE || '5'),
|
||||
minFrequency: parseInt(process.env.MIN_FREQUENCY_FREE || '60'),
|
||||
},
|
||||
pro: {
|
||||
maxMonitors: parseInt(process.env.MAX_MONITORS_PRO || '50'),
|
||||
minFrequency: parseInt(process.env.MIN_FREQUENCY_PRO || '5'),
|
||||
},
|
||||
business: {
|
||||
maxMonitors: parseInt(process.env.MAX_MONITORS_BUSINESS || '200'),
|
||||
minFrequency: parseInt(process.env.MIN_FREQUENCY_BUSINESS || '1'),
|
||||
},
|
||||
enterprise: {
|
||||
maxMonitors: 999999,
|
||||
minFrequency: 1,
|
||||
},
|
||||
};
|
||||
|
||||
return limits[plan as keyof typeof limits] || limits.free;
|
||||
}
|
||||
|
||||
// List monitors
|
||||
router.get('/', async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'unauthorized', message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const monitors = await db.monitors.findByUserId(req.user.userId);
|
||||
|
||||
res.json({ monitors });
|
||||
} catch (error) {
|
||||
console.error('List monitors error:', error);
|
||||
res.status(500).json({ error: 'server_error', message: 'Failed to list monitors' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get monitor by ID
|
||||
router.get('/:id', async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'unauthorized', message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const monitor = await db.monitors.findById(req.params.id);
|
||||
|
||||
if (!monitor) {
|
||||
res.status(404).json({ error: 'not_found', message: 'Monitor not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.userId !== req.user.userId) {
|
||||
res.status(403).json({ error: 'forbidden', message: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ monitor });
|
||||
} catch (error) {
|
||||
console.error('Get monitor error:', error);
|
||||
res.status(500).json({ error: 'server_error', message: 'Failed to get monitor' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create monitor
|
||||
router.post('/', async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'unauthorized', message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const input = createMonitorSchema.parse(req.body);
|
||||
|
||||
// Check plan limits
|
||||
const limits = getPlanLimits(req.user.plan);
|
||||
const currentCount = await db.monitors.countByUserId(req.user.userId);
|
||||
|
||||
if (currentCount >= limits.maxMonitors) {
|
||||
res.status(403).json({
|
||||
error: 'limit_exceeded',
|
||||
message: `Your ${req.user.plan} plan allows max ${limits.maxMonitors} monitors`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.frequency < limits.minFrequency) {
|
||||
res.status(403).json({
|
||||
error: 'invalid_frequency',
|
||||
message: `Your ${req.user.plan} plan requires minimum ${limits.minFrequency} minute frequency`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract domain from URL for name if not provided
|
||||
const name = input.name || new URL(input.url).hostname;
|
||||
|
||||
const monitor = await db.monitors.create({
|
||||
userId: req.user.userId,
|
||||
url: input.url,
|
||||
name,
|
||||
frequency: input.frequency,
|
||||
status: 'active',
|
||||
elementSelector: input.elementSelector,
|
||||
ignoreRules: input.ignoreRules,
|
||||
keywordRules: input.keywordRules,
|
||||
});
|
||||
|
||||
// Perform first check immediately
|
||||
checkMonitor(monitor.id).catch((err) =>
|
||||
console.error('Initial check failed:', err)
|
||||
);
|
||||
|
||||
res.status(201).json({ monitor });
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({
|
||||
error: 'validation_error',
|
||||
message: 'Invalid input',
|
||||
details: error.errors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Create monitor error:', error);
|
||||
res.status(500).json({ error: 'server_error', message: 'Failed to create monitor' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update monitor
|
||||
router.put('/:id', async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'unauthorized', message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const monitor = await db.monitors.findById(req.params.id);
|
||||
|
||||
if (!monitor) {
|
||||
res.status(404).json({ error: 'not_found', message: 'Monitor not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.userId !== req.user.userId) {
|
||||
res.status(403).json({ error: 'forbidden', message: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
const input = updateMonitorSchema.parse(req.body);
|
||||
|
||||
// Check frequency limit if being updated
|
||||
if (input.frequency) {
|
||||
const limits = getPlanLimits(req.user.plan);
|
||||
if (input.frequency < limits.minFrequency) {
|
||||
res.status(403).json({
|
||||
error: 'invalid_frequency',
|
||||
message: `Your ${req.user.plan} plan requires minimum ${limits.minFrequency} minute frequency`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await db.monitors.update(req.params.id, input);
|
||||
|
||||
res.json({ monitor: updated });
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({
|
||||
error: 'validation_error',
|
||||
message: 'Invalid input',
|
||||
details: error.errors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Update monitor error:', error);
|
||||
res.status(500).json({ error: 'server_error', message: 'Failed to update monitor' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete monitor
|
||||
router.delete('/:id', async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'unauthorized', message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const monitor = await db.monitors.findById(req.params.id);
|
||||
|
||||
if (!monitor) {
|
||||
res.status(404).json({ error: 'not_found', message: 'Monitor not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.userId !== req.user.userId) {
|
||||
res.status(403).json({ error: 'forbidden', message: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
await db.monitors.delete(req.params.id);
|
||||
|
||||
res.json({ message: 'Monitor deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Delete monitor error:', error);
|
||||
res.status(500).json({ error: 'server_error', message: 'Failed to delete monitor' });
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger manual check
|
||||
router.post('/:id/check', async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'unauthorized', message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const monitor = await db.monitors.findById(req.params.id);
|
||||
|
||||
if (!monitor) {
|
||||
res.status(404).json({ error: 'not_found', message: 'Monitor not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.userId !== req.user.userId) {
|
||||
res.status(403).json({ error: 'forbidden', message: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Trigger check (don't wait for it)
|
||||
checkMonitor(monitor.id).catch((err) => console.error('Manual check failed:', err));
|
||||
|
||||
res.json({ message: 'Check triggered successfully' });
|
||||
} catch (error) {
|
||||
console.error('Trigger check error:', error);
|
||||
res.status(500).json({ error: 'server_error', message: 'Failed to trigger check' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get monitor history (snapshots)
|
||||
router.get('/:id/history', async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'unauthorized', message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const monitor = await db.monitors.findById(req.params.id);
|
||||
|
||||
if (!monitor) {
|
||||
res.status(404).json({ error: 'not_found', message: 'Monitor not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.userId !== req.user.userId) {
|
||||
res.status(403).json({ error: 'forbidden', message: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
const limit = Math.min(parseInt(req.query.limit as string) || 50, 100);
|
||||
const snapshots = await db.snapshots.findByMonitorId(req.params.id, limit);
|
||||
|
||||
res.json({ snapshots });
|
||||
} catch (error) {
|
||||
console.error('Get history error:', error);
|
||||
res.status(500).json({ error: 'server_error', message: 'Failed to get history' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get specific snapshot
|
||||
router.get(
|
||||
'/:id/history/:snapshotId',
|
||||
async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'unauthorized', message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const monitor = await db.monitors.findById(req.params.id);
|
||||
|
||||
if (!monitor) {
|
||||
res.status(404).json({ error: 'not_found', message: 'Monitor not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.userId !== req.user.userId) {
|
||||
res.status(403).json({ error: 'forbidden', message: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = await db.snapshots.findById(req.params.snapshotId);
|
||||
|
||||
if (!snapshot || snapshot.monitorId !== req.params.id) {
|
||||
res.status(404).json({ error: 'not_found', message: 'Snapshot not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ snapshot });
|
||||
}catch (error) {
|
||||
console.error('Get snapshot error:', error);
|
||||
res.status(500).json({ error: 'server_error', message: 'Failed to get snapshot' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
import { Router, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import db from '../db';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
import { checkLimiter } from '../middleware/rateLimiter';
|
||||
import { MonitorFrequency, Monitor } from '../types';
|
||||
import { checkMonitor, scheduleMonitor, unscheduleMonitor, rescheduleMonitor } from '../services/monitor';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const createMonitorSchema = z.object({
|
||||
url: z.string().url(),
|
||||
name: z.string().optional(),
|
||||
frequency: z.number().int().positive(),
|
||||
elementSelector: z.string().optional(),
|
||||
ignoreRules: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.enum(['css', 'regex', 'text']),
|
||||
value: z.string(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
keywordRules: z
|
||||
.array(
|
||||
z.object({
|
||||
keyword: z.string(),
|
||||
type: z.enum(['appears', 'disappears', 'count']),
|
||||
threshold: z.number().optional(),
|
||||
caseSensitive: z.boolean().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const updateMonitorSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
frequency: z.number().int().positive().optional(),
|
||||
status: z.enum(['active', 'paused', 'error']).optional(),
|
||||
elementSelector: z.string().optional(),
|
||||
ignoreRules: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.enum(['css', 'regex', 'text']),
|
||||
value: z.string(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
keywordRules: z
|
||||
.array(
|
||||
z.object({
|
||||
keyword: z.string(),
|
||||
type: z.enum(['appears', 'disappears', 'count']),
|
||||
threshold: z.number().optional(),
|
||||
caseSensitive: z.boolean().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
// Get plan limits
|
||||
function getPlanLimits(plan: string) {
|
||||
const limits = {
|
||||
free: {
|
||||
maxMonitors: parseInt(process.env.MAX_MONITORS_FREE || '5'),
|
||||
minFrequency: parseInt(process.env.MIN_FREQUENCY_FREE || '60'),
|
||||
},
|
||||
pro: {
|
||||
maxMonitors: parseInt(process.env.MAX_MONITORS_PRO || '50'),
|
||||
minFrequency: parseInt(process.env.MIN_FREQUENCY_PRO || '5'),
|
||||
},
|
||||
business: {
|
||||
maxMonitors: parseInt(process.env.MAX_MONITORS_BUSINESS || '200'),
|
||||
minFrequency: parseInt(process.env.MIN_FREQUENCY_BUSINESS || '1'),
|
||||
},
|
||||
enterprise: {
|
||||
maxMonitors: 999999,
|
||||
minFrequency: 1,
|
||||
},
|
||||
};
|
||||
|
||||
return limits[plan as keyof typeof limits] || limits.free;
|
||||
}
|
||||
|
||||
// List monitors
|
||||
router.get('/', async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'unauthorized', message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const monitors = await db.monitors.findByUserId(req.user.userId);
|
||||
|
||||
// Attach recent snapshots to each monitor for sparklines
|
||||
const monitorsWithSnapshots = await Promise.all(monitors.map(async (monitor) => {
|
||||
// Get last 20 snapshots for sparkline
|
||||
const recentSnapshots = await db.snapshots.findByMonitorId(monitor.id, 20);
|
||||
return {
|
||||
...monitor,
|
||||
recentSnapshots
|
||||
};
|
||||
}));
|
||||
|
||||
res.json({ monitors: monitorsWithSnapshots });
|
||||
} catch (error) {
|
||||
console.error('List monitors error:', error);
|
||||
res.status(500).json({ error: 'server_error', message: 'Failed to list monitors' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get monitor by ID
|
||||
router.get('/:id', async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'unauthorized', message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const monitor = await db.monitors.findById(req.params.id);
|
||||
|
||||
if (!monitor) {
|
||||
res.status(404).json({ error: 'not_found', message: 'Monitor not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.userId !== req.user.userId) {
|
||||
res.status(403).json({ error: 'forbidden', message: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ monitor });
|
||||
} catch (error) {
|
||||
console.error('Get monitor error:', error);
|
||||
res.status(500).json({ error: 'server_error', message: 'Failed to get monitor' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create monitor
|
||||
router.post('/', async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'unauthorized', message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const input = createMonitorSchema.parse(req.body);
|
||||
|
||||
// Check plan limits (fetch fresh user data)
|
||||
const currentUser = await db.users.findById(req.user.userId);
|
||||
const plan = currentUser?.plan || req.user.plan;
|
||||
const limits = getPlanLimits(plan);
|
||||
const currentCount = await db.monitors.countByUserId(req.user.userId);
|
||||
|
||||
if (currentCount >= limits.maxMonitors) {
|
||||
res.status(403).json({
|
||||
error: 'limit_exceeded',
|
||||
message: `Your ${plan} plan allows max ${limits.maxMonitors} monitors`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.frequency < limits.minFrequency) {
|
||||
res.status(403).json({
|
||||
error: 'invalid_frequency',
|
||||
message: `Your ${plan} plan requires minimum ${limits.minFrequency} minute frequency`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract domain from URL for name if not provided
|
||||
const name = input.name || new URL(input.url).hostname;
|
||||
|
||||
const monitor = await db.monitors.create({
|
||||
userId: req.user.userId,
|
||||
url: input.url,
|
||||
name,
|
||||
frequency: input.frequency as MonitorFrequency,
|
||||
status: 'active',
|
||||
elementSelector: input.elementSelector,
|
||||
ignoreRules: input.ignoreRules,
|
||||
keywordRules: input.keywordRules,
|
||||
});
|
||||
|
||||
// Schedule recurring checks
|
||||
try {
|
||||
await scheduleMonitor(monitor);
|
||||
console.log(`Monitor ${monitor.id} scheduled successfully`);
|
||||
} catch (err) {
|
||||
console.error('Failed to schedule monitor:', err);
|
||||
}
|
||||
|
||||
// Perform first check immediately
|
||||
checkMonitor(monitor.id).catch((err) =>
|
||||
console.error('Initial check failed:', err)
|
||||
);
|
||||
|
||||
res.status(201).json({ monitor });
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({
|
||||
error: 'validation_error',
|
||||
message: 'Invalid input',
|
||||
details: error.errors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Create monitor error:', error);
|
||||
res.status(500).json({ error: 'server_error', message: 'Failed to create monitor' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update monitor
|
||||
router.put('/:id', async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'unauthorized', message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const monitor = await db.monitors.findById(req.params.id);
|
||||
|
||||
if (!monitor) {
|
||||
res.status(404).json({ error: 'not_found', message: 'Monitor not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.userId !== req.user.userId) {
|
||||
res.status(403).json({ error: 'forbidden', message: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
const input = updateMonitorSchema.parse(req.body);
|
||||
|
||||
// Check frequency limit if being updated
|
||||
if (input.frequency) {
|
||||
// Fetch fresh user data to get current plan
|
||||
const currentUser = await db.users.findById(req.user.userId);
|
||||
const plan = currentUser?.plan || req.user.plan;
|
||||
const limits = getPlanLimits(plan);
|
||||
|
||||
if (input.frequency < limits.minFrequency) {
|
||||
res.status(403).json({
|
||||
error: 'invalid_frequency',
|
||||
message: `Your ${plan} plan requires minimum ${limits.minFrequency} minute frequency`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const updateData: Partial<Monitor> = {
|
||||
...input,
|
||||
frequency: input.frequency as MonitorFrequency | undefined,
|
||||
};
|
||||
const updated = await db.monitors.update(req.params.id, updateData);
|
||||
|
||||
if (!updated) {
|
||||
res.status(500).json({ error: 'update_failed', message: 'Failed to update monitor' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Reschedule if frequency changed or status changed to/from active
|
||||
const needsRescheduling =
|
||||
input.frequency !== undefined ||
|
||||
(input.status && (input.status === 'active' || monitor.status === 'active'));
|
||||
|
||||
if (needsRescheduling) {
|
||||
try {
|
||||
if (updated.status === 'active') {
|
||||
await rescheduleMonitor(updated);
|
||||
console.log(`Monitor ${updated.id} rescheduled`);
|
||||
} else {
|
||||
await unscheduleMonitor(updated.id);
|
||||
console.log(`Monitor ${updated.id} unscheduled (status: ${updated.status})`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to reschedule monitor:', err);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ monitor: updated });
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({
|
||||
error: 'validation_error',
|
||||
message: 'Invalid input',
|
||||
details: error.errors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Update monitor error:', error);
|
||||
res.status(500).json({ error: 'server_error', message: 'Failed to update monitor' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete monitor
|
||||
router.delete('/:id', async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'unauthorized', message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const monitor = await db.monitors.findById(req.params.id);
|
||||
|
||||
if (!monitor) {
|
||||
res.status(404).json({ error: 'not_found', message: 'Monitor not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.userId !== req.user.userId) {
|
||||
res.status(403).json({ error: 'forbidden', message: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Unschedule before deleting
|
||||
try {
|
||||
await unscheduleMonitor(req.params.id);
|
||||
console.log(`Monitor ${req.params.id} unscheduled before deletion`);
|
||||
} catch (err) {
|
||||
console.error('Failed to unschedule monitor:', err);
|
||||
}
|
||||
|
||||
await db.monitors.delete(req.params.id);
|
||||
|
||||
res.json({ message: 'Monitor deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Delete monitor error:', error);
|
||||
res.status(500).json({ error: 'server_error', message: 'Failed to delete monitor' });
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger manual check
|
||||
router.post('/:id/check', checkLimiter, async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'unauthorized', message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const monitor = await db.monitors.findById(req.params.id);
|
||||
|
||||
if (!monitor) {
|
||||
res.status(404).json({ error: 'not_found', message: 'Monitor not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.userId !== req.user.userId) {
|
||||
res.status(403).json({ error: 'forbidden', message: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Await the check so user gets immediate feedback
|
||||
try {
|
||||
await checkMonitor(monitor.id);
|
||||
|
||||
// Get the latest snapshot to return to the user
|
||||
const latestSnapshot = await db.snapshots.findLatestByMonitorId(monitor.id);
|
||||
const updatedMonitor = await db.monitors.findById(monitor.id);
|
||||
|
||||
res.json({
|
||||
message: 'Check completed successfully',
|
||||
monitor: updatedMonitor,
|
||||
snapshot: latestSnapshot ? {
|
||||
id: latestSnapshot.id,
|
||||
changed: latestSnapshot.changed,
|
||||
changePercentage: latestSnapshot.changePercentage,
|
||||
httpStatus: latestSnapshot.httpStatus,
|
||||
responseTime: latestSnapshot.responseTime,
|
||||
createdAt: latestSnapshot.createdAt,
|
||||
errorMessage: latestSnapshot.errorMessage,
|
||||
} : null,
|
||||
});
|
||||
} catch (checkError: any) {
|
||||
console.error('Check failed:', checkError);
|
||||
res.status(500).json({
|
||||
error: 'check_failed',
|
||||
message: checkError.message || 'Failed to check monitor'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Trigger check error:', error);
|
||||
res.status(500).json({ error: 'server_error', message: 'Failed to trigger check' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get monitor history (snapshots)
|
||||
router.get('/:id/history', async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'unauthorized', message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const monitor = await db.monitors.findById(req.params.id);
|
||||
|
||||
if (!monitor) {
|
||||
res.status(404).json({ error: 'not_found', message: 'Monitor not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.userId !== req.user.userId) {
|
||||
res.status(403).json({ error: 'forbidden', message: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
const limit = Math.min(parseInt(req.query.limit as string) || 50, 100);
|
||||
const snapshots = await db.snapshots.findByMonitorId(req.params.id, limit);
|
||||
|
||||
res.json({ snapshots });
|
||||
} catch (error) {
|
||||
console.error('Get history error:', error);
|
||||
res.status(500).json({ error: 'server_error', message: 'Failed to get history' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get specific snapshot
|
||||
router.get(
|
||||
'/:id/history/:snapshotId',
|
||||
async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'unauthorized', message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const monitor = await db.monitors.findById(req.params.id);
|
||||
|
||||
if (!monitor) {
|
||||
res.status(404).json({ error: 'not_found', message: 'Monitor not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.userId !== req.user.userId) {
|
||||
res.status(403).json({ error: 'forbidden', message: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = await db.snapshots.findById(req.params.snapshotId);
|
||||
|
||||
if (!snapshot || snapshot.monitorId !== req.params.id) {
|
||||
res.status(404).json({ error: 'not_found', message: 'Snapshot not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ snapshot });
|
||||
} catch (error) {
|
||||
console.error('Get snapshot error:', error);
|
||||
res.status(500).json({ error: 'server_error', message: 'Failed to get snapshot' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Export monitor audit trail (JSON or CSV)
|
||||
router.get('/:id/export', async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'unauthorized', message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user has export feature (PRO+)
|
||||
const user = await db.users.findById(req.user.userId);
|
||||
if (!user) {
|
||||
res.status(404).json({ error: 'not_found', message: 'User not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow export for all users for now, but in production check plan
|
||||
// if (!hasFeature(user.plan, 'audit_export')) {
|
||||
// res.status(403).json({ error: 'forbidden', message: 'Export feature requires Pro plan' });
|
||||
// return;
|
||||
// }
|
||||
|
||||
const monitor = await db.monitors.findById(req.params.id);
|
||||
|
||||
if (!monitor) {
|
||||
res.status(404).json({ error: 'not_found', message: 'Monitor not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.userId !== req.user.userId) {
|
||||
res.status(403).json({ error: 'forbidden', message: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
const format = (req.query.format as string)?.toLowerCase() || 'json';
|
||||
const fromDate = req.query.from ? new Date(req.query.from as string) : undefined;
|
||||
const toDate = req.query.to ? new Date(req.query.to as string) : undefined;
|
||||
|
||||
// Get all snapshots (up to 1000)
|
||||
let snapshots = await db.snapshots.findByMonitorId(monitor.id, 1000);
|
||||
|
||||
// Filter by date range if provided
|
||||
if (fromDate) {
|
||||
snapshots = snapshots.filter(s => new Date(s.createdAt) >= fromDate);
|
||||
}
|
||||
if (toDate) {
|
||||
snapshots = snapshots.filter(s => new Date(s.createdAt) <= toDate);
|
||||
}
|
||||
|
||||
// Get alerts for this monitor
|
||||
const allAlerts = await db.alerts.findByUserId(req.user.userId, 1000);
|
||||
const monitorAlerts = allAlerts.filter(a => a.monitorId === monitor.id);
|
||||
|
||||
// Filter alerts by date range if provided
|
||||
let filteredAlerts = monitorAlerts;
|
||||
if (fromDate) {
|
||||
filteredAlerts = filteredAlerts.filter(a => new Date(a.createdAt) >= fromDate);
|
||||
}
|
||||
if (toDate) {
|
||||
filteredAlerts = filteredAlerts.filter(a => new Date(a.createdAt) <= toDate);
|
||||
}
|
||||
|
||||
const exportData = {
|
||||
monitor: {
|
||||
id: monitor.id,
|
||||
name: monitor.name,
|
||||
url: monitor.url,
|
||||
frequency: monitor.frequency,
|
||||
status: monitor.status,
|
||||
createdAt: monitor.createdAt,
|
||||
},
|
||||
exportedAt: new Date().toISOString(),
|
||||
dateRange: {
|
||||
from: fromDate?.toISOString() || 'start',
|
||||
to: toDate?.toISOString() || 'now',
|
||||
},
|
||||
summary: {
|
||||
totalChecks: snapshots.length,
|
||||
changesDetected: snapshots.filter(s => s.changed).length,
|
||||
errorsDetected: snapshots.filter(s => s.errorMessage).length,
|
||||
totalAlerts: filteredAlerts.length,
|
||||
},
|
||||
checks: snapshots.map(s => ({
|
||||
id: s.id,
|
||||
timestamp: s.createdAt,
|
||||
changed: s.changed,
|
||||
changePercentage: s.changePercentage,
|
||||
httpStatus: s.httpStatus,
|
||||
responseTime: s.responseTime,
|
||||
errorMessage: s.errorMessage,
|
||||
})),
|
||||
alerts: filteredAlerts.map(a => ({
|
||||
id: a.id,
|
||||
type: a.type,
|
||||
title: a.title,
|
||||
summary: a.summary,
|
||||
channels: a.channels,
|
||||
createdAt: a.createdAt,
|
||||
deliveredAt: a.deliveredAt,
|
||||
})),
|
||||
};
|
||||
|
||||
if (format === 'csv') {
|
||||
// Generate CSV
|
||||
const csvLines: string[] = [];
|
||||
|
||||
// Header
|
||||
csvLines.push('Type,Timestamp,Changed,Change %,HTTP Status,Response Time (ms),Error,Alert Type,Alert Title');
|
||||
|
||||
// Checks
|
||||
for (const check of exportData.checks) {
|
||||
csvLines.push([
|
||||
'check',
|
||||
check.timestamp,
|
||||
check.changed ? 'true' : 'false',
|
||||
check.changePercentage?.toFixed(2) || '',
|
||||
check.httpStatus,
|
||||
check.responseTime,
|
||||
`"${(check.errorMessage || '').replace(/"/g, '""')}"`,
|
||||
'',
|
||||
'',
|
||||
].join(','));
|
||||
}
|
||||
|
||||
// Alerts
|
||||
for (const alert of exportData.alerts) {
|
||||
csvLines.push([
|
||||
'alert',
|
||||
alert.createdAt,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
alert.type,
|
||||
`"${(alert.title || '').replace(/"/g, '""')}"`,
|
||||
].join(','));
|
||||
}
|
||||
|
||||
const csv = csvLines.join('\n');
|
||||
const filename = `${monitor.name.replace(/[^a-zA-Z0-9]/g, '_')}_audit_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
res.send(csv);
|
||||
} else {
|
||||
// JSON format
|
||||
const filename = `${monitor.name.replace(/[^a-zA-Z0-9]/g, '_')}_audit_${new Date().toISOString().split('T')[0]}.json`;
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
res.json(exportData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Export error:', error);
|
||||
res.status(500).json({ error: 'server_error', message: 'Failed to export audit trail' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user