add changes

This commit is contained in:
AD2025
2025-11-11 00:25:50 +02:00
commit e3ca132c5e
86 changed files with 22238 additions and 0 deletions

View File

@@ -0,0 +1,481 @@
const { Category, Question } = require('../models');
/**
* @desc Get all active categories
* @route GET /api/categories
* @access Public
*/
exports.getAllCategories = async (req, res) => {
try {
// Check if request is from guest or authenticated user
const isGuest = !req.user; // If no user attached, it's a guest/public request
// Build query conditions
const whereConditions = {
isActive: true
};
// If guest, only show guest-accessible categories
if (isGuest) {
whereConditions.guestAccessible = true;
}
// Fetch categories
const categories = await Category.findAll({
where: whereConditions,
attributes: [
'id',
'name',
'slug',
'description',
'icon',
'color',
'questionCount',
'displayOrder',
'guestAccessible'
],
order: [
['displayOrder', 'ASC'],
['name', 'ASC']
]
});
res.status(200).json({
success: true,
count: categories.length,
data: categories,
message: isGuest
? `${categories.length} guest-accessible categories available`
: `${categories.length} categories available`
});
} catch (error) {
console.error('Error fetching categories:', error);
res.status(500).json({
success: false,
message: 'Error fetching categories',
error: error.message
});
}
};
/**
* @desc Get category details by ID
* @route GET /api/categories/:id
* @access Public (with optional auth for access control)
*/
exports.getCategoryById = async (req, res) => {
try {
const { id } = req.params;
const isGuest = !req.user;
// Validate ID (accepts UUID or numeric)
if (!id) {
return res.status(400).json({
success: false,
message: 'Invalid category ID'
});
}
// UUID format validation (basic check)
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
const isNumeric = !isNaN(id) && Number.isInteger(Number(id));
if (!isUUID && !isNumeric) {
return res.status(400).json({
success: false,
message: 'Invalid category ID format'
});
}
// Find category
const category = await Category.findByPk(id, {
attributes: [
'id',
'name',
'slug',
'description',
'icon',
'color',
'questionCount',
'displayOrder',
'guestAccessible',
'isActive'
]
});
// Check if category exists
if (!category) {
return res.status(404).json({
success: false,
message: 'Category not found'
});
}
// Check if category is active
if (!category.isActive) {
return res.status(404).json({
success: false,
message: 'Category not found'
});
}
// Check guest access
if (isGuest && !category.guestAccessible) {
return res.status(403).json({
success: false,
message: 'This category requires authentication. Please register or login to access.',
requiresAuth: true
});
}
// Get question preview (first 5 questions)
const questionPreview = await Question.findAll({
where: {
categoryId: id,
isActive: true
},
attributes: [
'id',
'questionText',
'questionType',
'difficulty',
'points',
'timesAttempted',
'timesCorrect'
],
order: [['createdAt', 'ASC']],
limit: 5
});
// Calculate category stats
const allQuestions = await Question.findAll({
where: {
categoryId: id,
isActive: true
},
attributes: ['difficulty', 'timesAttempted', 'timesCorrect']
});
const stats = {
totalQuestions: allQuestions.length,
questionsByDifficulty: {
easy: allQuestions.filter(q => q.difficulty === 'easy').length,
medium: allQuestions.filter(q => q.difficulty === 'medium').length,
hard: allQuestions.filter(q => q.difficulty === 'hard').length
},
totalAttempts: allQuestions.reduce((sum, q) => sum + (q.timesAttempted || 0), 0),
totalCorrect: allQuestions.reduce((sum, q) => sum + (q.timesCorrect || 0), 0)
};
// Calculate average accuracy
stats.averageAccuracy = stats.totalAttempts > 0
? Math.round((stats.totalCorrect / stats.totalAttempts) * 100)
: 0;
// Prepare response
const categoryData = {
id: category.id,
name: category.name,
slug: category.slug,
description: category.description,
icon: category.icon,
color: category.color,
questionCount: category.questionCount,
displayOrder: category.displayOrder,
guestAccessible: category.guestAccessible
};
res.status(200).json({
success: true,
data: {
category: categoryData,
questionPreview: questionPreview.map(q => ({
id: q.id,
questionText: q.questionText,
questionType: q.questionType,
difficulty: q.difficulty,
points: q.points,
accuracy: q.timesAttempted > 0
? Math.round((q.timesCorrect / q.timesAttempted) * 100)
: 0
})),
stats
},
message: `Category details retrieved successfully`
});
} catch (error) {
console.error('Error fetching category details:', error);
res.status(500).json({
success: false,
message: 'Error fetching category details',
error: error.message
});
}
};
/**
* @desc Create new category (Admin only)
* @route POST /api/categories
* @access Private/Admin
*/
exports.createCategory = async (req, res) => {
try {
const {
name,
slug,
description,
icon,
color,
guestAccessible,
displayOrder
} = req.body;
// Validate required fields
if (!name) {
return res.status(400).json({
success: false,
message: 'Category name is required'
});
}
// Check if category with same name exists
const existingByName = await Category.findOne({
where: { name }
});
if (existingByName) {
return res.status(400).json({
success: false,
message: 'A category with this name already exists'
});
}
// Check if custom slug provided and if it exists
if (slug) {
const existingBySlug = await Category.findOne({
where: { slug }
});
if (existingBySlug) {
return res.status(400).json({
success: false,
message: 'A category with this slug already exists'
});
}
}
// Create category (slug will be auto-generated by model hook if not provided)
const category = await Category.create({
name,
slug,
description: description || null,
icon: icon || null,
color: color || '#3B82F6',
guestAccessible: guestAccessible !== undefined ? guestAccessible : false,
displayOrder: displayOrder || 0,
isActive: true,
questionCount: 0,
quizCount: 0
});
res.status(201).json({
success: true,
data: {
id: category.id,
name: category.name,
slug: category.slug,
description: category.description,
icon: category.icon,
color: category.color,
guestAccessible: category.guestAccessible,
displayOrder: category.displayOrder,
questionCount: category.questionCount,
isActive: category.isActive
},
message: 'Category created successfully'
});
} catch (error) {
console.error('Error creating category:', error);
res.status(500).json({
success: false,
message: 'Error creating category',
error: error.message
});
}
};
/**
* @desc Update category (Admin only)
* @route PUT /api/categories/:id
* @access Private/Admin
*/
exports.updateCategory = async (req, res) => {
try {
const { id } = req.params;
const {
name,
slug,
description,
icon,
color,
guestAccessible,
displayOrder,
isActive
} = req.body;
// Validate ID
if (!id) {
return res.status(400).json({
success: false,
message: 'Category ID is required'
});
}
// Find category
const category = await Category.findByPk(id);
if (!category) {
return res.status(404).json({
success: false,
message: 'Category not found'
});
}
// Check if new name conflicts with existing category
if (name && name !== category.name) {
const existingByName = await Category.findOne({
where: { name }
});
if (existingByName) {
return res.status(400).json({
success: false,
message: 'A category with this name already exists'
});
}
}
// Check if new slug conflicts with existing category
if (slug && slug !== category.slug) {
const existingBySlug = await Category.findOne({
where: { slug }
});
if (existingBySlug) {
return res.status(400).json({
success: false,
message: 'A category with this slug already exists'
});
}
}
// Update category
const updateData = {};
if (name !== undefined) updateData.name = name;
if (slug !== undefined) updateData.slug = slug;
if (description !== undefined) updateData.description = description;
if (icon !== undefined) updateData.icon = icon;
if (color !== undefined) updateData.color = color;
if (guestAccessible !== undefined) updateData.guestAccessible = guestAccessible;
if (displayOrder !== undefined) updateData.displayOrder = displayOrder;
if (isActive !== undefined) updateData.isActive = isActive;
await category.update(updateData);
res.status(200).json({
success: true,
data: {
id: category.id,
name: category.name,
slug: category.slug,
description: category.description,
icon: category.icon,
color: category.color,
guestAccessible: category.guestAccessible,
displayOrder: category.displayOrder,
questionCount: category.questionCount,
isActive: category.isActive
},
message: 'Category updated successfully'
});
} catch (error) {
console.error('Error updating category:', error);
res.status(500).json({
success: false,
message: 'Error updating category',
error: error.message
});
}
};
/**
* @desc Delete category (soft delete - Admin only)
* @route DELETE /api/categories/:id
* @access Private/Admin
*/
exports.deleteCategory = async (req, res) => {
try {
const { id } = req.params;
// Validate ID
if (!id) {
return res.status(400).json({
success: false,
message: 'Category ID is required'
});
}
// Find category
const category = await Category.findByPk(id);
if (!category) {
return res.status(404).json({
success: false,
message: 'Category not found'
});
}
// Check if already deleted
if (!category.isActive) {
return res.status(400).json({
success: false,
message: 'Category is already deleted'
});
}
// Check if category has questions
const questionCount = await Question.count({
where: {
categoryId: id,
isActive: true
}
});
// Soft delete - set isActive to false
await category.update({ isActive: false });
res.status(200).json({
success: true,
data: {
id: category.id,
name: category.name,
questionCount: questionCount
},
message: questionCount > 0
? `Category deleted successfully. ${questionCount} questions are still associated with this category.`
: 'Category deleted successfully'
});
} catch (error) {
console.error('Error deleting category:', error);
res.status(500).json({
success: false,
message: 'Error deleting category',
error: error.message
});
}
};