Phase 6 complete

This commit is contained in:
Joey Yakimowich-Payne 2026-01-13 16:52:57 -07:00
commit 3a22b42492
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
11 changed files with 735 additions and 103 deletions

View file

@ -11,7 +11,20 @@ app.use(cors({
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
credentials: true,
}));
app.use(express.json({ limit: '10mb' }));
app.use((req: Request, res: Response, next: NextFunction) => {
express.json({ limit: '10mb' })(req, res, (err) => {
if (err instanceof SyntaxError && 'body' in err) {
res.status(400).json({ error: 'Invalid JSON' });
return;
}
if (err) {
next(err);
return;
}
next();
});
});
app.get('/health', (_req: Request, res: Response) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });

View file

@ -84,12 +84,45 @@ router.get('/:id', (req: AuthenticatedRequest, res: Response) => {
});
});
function validateQuizBody(body: QuizBody): string | null {
const { title, source, questions } = body;
if (!title?.trim()) {
return 'Title is required and cannot be empty';
}
if (!source || !['manual', 'ai_generated'].includes(source)) {
return 'Source must be "manual" or "ai_generated"';
}
if (!questions || !Array.isArray(questions) || questions.length === 0) {
return 'At least one question is required';
}
for (let i = 0; i < questions.length; i++) {
const q = questions[i];
if (!q.text?.trim()) {
return `Question ${i + 1} text is required`;
}
if (!q.options || !Array.isArray(q.options) || q.options.length < 2) {
return `Question ${i + 1} must have at least 2 options`;
}
const hasCorrect = q.options.some(o => o.isCorrect);
if (!hasCorrect) {
return `Question ${i + 1} must have at least one correct answer`;
}
}
return null;
}
router.post('/', (req: AuthenticatedRequest, res: Response) => {
const body = req.body as QuizBody;
const { title, source, aiTopic, questions } = body;
if (!title?.trim() || !source || !questions?.length) {
res.status(400).json({ error: 'Missing required fields: title, source, questions' });
const validationError = validateQuizBody(body);
if (validationError) {
res.status(400).json({ error: validationError });
return;
}
@ -157,6 +190,33 @@ router.put('/:id', (req: AuthenticatedRequest, res: Response) => {
const { title, questions } = body;
const quizId = req.params.id;
if (!title?.trim()) {
res.status(400).json({ error: 'Title is required and cannot be empty' });
return;
}
if (!questions || !Array.isArray(questions) || questions.length === 0) {
res.status(400).json({ error: 'At least one question is required' });
return;
}
for (let i = 0; i < questions.length; i++) {
const q = questions[i];
if (!q.text?.trim()) {
res.status(400).json({ error: `Question ${i + 1} text is required` });
return;
}
if (!q.options || !Array.isArray(q.options) || q.options.length < 2) {
res.status(400).json({ error: `Question ${i + 1} must have at least 2 options` });
return;
}
const hasCorrect = q.options.some(o => o.isCorrect);
if (!hasCorrect) {
res.status(400).json({ error: `Question ${i + 1} must have at least one correct answer` });
return;
}
}
const existing = db.prepare(`
SELECT id FROM quizzes WHERE id = ? AND user_id = ?
`).get(quizId, req.user!.sub);