initial wcag-ada
This commit is contained in:
parent
042b8cb83a
commit
d52cfe7de2
112 changed files with 9069 additions and 0 deletions
283
apps/wcag-ada/api/src/routes/scans.ts
Normal file
283
apps/wcag-ada/api/src/routes/scans.ts
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
import { Hono } from 'hono';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { z } from 'zod';
|
||||
import { Queue } from 'bullmq';
|
||||
import Redis from 'ioredis';
|
||||
import { prisma } from '../utils/prisma';
|
||||
import { authenticate } from '../middleware/auth';
|
||||
import { getWorkerConfig } from '@wcag-ada/config';
|
||||
import type { AccessibilityScanOptions } from '@wcag-ada/shared';
|
||||
|
||||
const scanRoutes = new Hono();
|
||||
|
||||
// Get worker config for queue connection
|
||||
const workerConfig = getWorkerConfig();
|
||||
|
||||
// Create queue connection
|
||||
const scanQueue = new Queue('accessibility-scans', {
|
||||
connection: new Redis({
|
||||
host: workerConfig.redis.host,
|
||||
port: workerConfig.redis.port,
|
||||
password: workerConfig.redis.password,
|
||||
db: workerConfig.redis.db,
|
||||
}),
|
||||
});
|
||||
|
||||
// Apply authentication to all routes
|
||||
scanRoutes.use('*', authenticate);
|
||||
|
||||
// Validation schemas
|
||||
const createScanSchema = z.object({
|
||||
websiteId: z.string(),
|
||||
url: z.string().url().optional(),
|
||||
options: z.object({
|
||||
wcagLevel: z.object({
|
||||
level: z.enum(['A', 'AA', 'AAA']),
|
||||
version: z.enum(['2.0', '2.1', '2.2']),
|
||||
}).optional(),
|
||||
viewport: z.object({
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
deviceScaleFactor: z.number().optional(),
|
||||
isMobile: z.boolean().optional(),
|
||||
}).optional(),
|
||||
includeScreenshots: z.boolean().optional(),
|
||||
excludeSelectors: z.array(z.string()).optional(),
|
||||
waitForSelector: z.string().optional(),
|
||||
timeout: z.number().optional(),
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
// Start a new scan
|
||||
scanRoutes.post('/', zValidator('json', createScanSchema), async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const { websiteId, url, options } = c.req.valid('json');
|
||||
|
||||
// Get website
|
||||
const website = await prisma.website.findFirst({
|
||||
where: {
|
||||
id: websiteId,
|
||||
userId,
|
||||
active: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!website) {
|
||||
return c.json({ error: 'Website not found' }, 404);
|
||||
}
|
||||
|
||||
// Merge options with website defaults
|
||||
const scanOptions: AccessibilityScanOptions = {
|
||||
url: url || website.url,
|
||||
...website.scanOptions as any,
|
||||
...options,
|
||||
authenticate: website.authConfig as any,
|
||||
};
|
||||
|
||||
// Create scan job
|
||||
const job = await prisma.scanJob.create({
|
||||
data: {
|
||||
websiteId,
|
||||
userId,
|
||||
url: scanOptions.url,
|
||||
options: scanOptions as any,
|
||||
status: 'PENDING',
|
||||
},
|
||||
});
|
||||
|
||||
// Queue the scan
|
||||
await scanQueue.add('scan', {
|
||||
jobId: job.id,
|
||||
websiteId,
|
||||
userId,
|
||||
options: scanOptions,
|
||||
}, {
|
||||
jobId: job.id,
|
||||
});
|
||||
|
||||
return c.json({
|
||||
job: {
|
||||
id: job.id,
|
||||
status: job.status,
|
||||
url: job.url,
|
||||
scheduledAt: job.scheduledAt,
|
||||
},
|
||||
message: 'Scan queued successfully',
|
||||
}, 202);
|
||||
});
|
||||
|
||||
// Get scan status
|
||||
scanRoutes.get('/:id', async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const scanId = c.req.param('id');
|
||||
|
||||
const job = await prisma.scanJob.findFirst({
|
||||
where: {
|
||||
id: scanId,
|
||||
userId,
|
||||
},
|
||||
include: {
|
||||
result: {
|
||||
select: {
|
||||
id: true,
|
||||
summary: true,
|
||||
wcagCompliance: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!job) {
|
||||
return c.json({ error: 'Scan not found' }, 404);
|
||||
}
|
||||
|
||||
return c.json(job);
|
||||
});
|
||||
|
||||
// Get scan result
|
||||
scanRoutes.get('/:id/result', async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const scanId = c.req.param('id');
|
||||
|
||||
const result = await prisma.scanResult.findFirst({
|
||||
where: {
|
||||
jobId: scanId,
|
||||
job: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return c.json({ error: 'Scan result not found' }, 404);
|
||||
}
|
||||
|
||||
return c.json(result);
|
||||
});
|
||||
|
||||
// Get scan violations
|
||||
scanRoutes.get('/:id/violations', async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const scanId = c.req.param('id');
|
||||
const { impact, tag } = c.req.query();
|
||||
|
||||
const result = await prisma.scanResult.findFirst({
|
||||
where: {
|
||||
jobId: scanId,
|
||||
job: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
violations: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return c.json({ error: 'Scan result not found' }, 404);
|
||||
}
|
||||
|
||||
let violations = result.violations as any[];
|
||||
|
||||
// Filter by impact if provided
|
||||
if (impact) {
|
||||
violations = violations.filter(v => v.impact === impact);
|
||||
}
|
||||
|
||||
// Filter by tag if provided
|
||||
if (tag) {
|
||||
violations = violations.filter(v => v.tags.includes(tag));
|
||||
}
|
||||
|
||||
return c.json({ violations });
|
||||
});
|
||||
|
||||
// Cancel a pending scan
|
||||
scanRoutes.delete('/:id', async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const scanId = c.req.param('id');
|
||||
|
||||
const job = await prisma.scanJob.findFirst({
|
||||
where: {
|
||||
id: scanId,
|
||||
userId,
|
||||
status: 'PENDING',
|
||||
},
|
||||
});
|
||||
|
||||
if (!job) {
|
||||
return c.json({ error: 'Scan not found or cannot be cancelled' }, 404);
|
||||
}
|
||||
|
||||
// Update job status
|
||||
await prisma.scanJob.update({
|
||||
where: { id: scanId },
|
||||
data: {
|
||||
status: 'FAILED',
|
||||
error: 'Cancelled by user',
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// Remove from queue if possible
|
||||
const queueJob = await scanQueue.getJob(scanId);
|
||||
if (queueJob) {
|
||||
await queueJob.remove();
|
||||
}
|
||||
|
||||
return c.json({ message: 'Scan cancelled successfully' });
|
||||
});
|
||||
|
||||
// Get user's scan history
|
||||
scanRoutes.get('/', async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const { page = '1', limit = '20', status, websiteId } = c.req.query();
|
||||
|
||||
const pageNum = parseInt(page);
|
||||
const limitNum = parseInt(limit);
|
||||
const skip = (pageNum - 1) * limitNum;
|
||||
|
||||
const where = {
|
||||
userId,
|
||||
...(status && { status }),
|
||||
...(websiteId && { websiteId }),
|
||||
};
|
||||
|
||||
const [jobs, total] = await Promise.all([
|
||||
prisma.scanJob.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limitNum,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
website: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
select: {
|
||||
id: true,
|
||||
summary: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.scanJob.count({ where }),
|
||||
]);
|
||||
|
||||
return c.json({
|
||||
scans: jobs,
|
||||
pagination: {
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limitNum),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export { scanRoutes };
|
||||
Loading…
Add table
Add a link
Reference in a new issue