add changes

This commit is contained in:
AD2025
2025-11-12 23:06:27 +02:00
parent c664d0a341
commit ec6534fcc2
42 changed files with 11854 additions and 299 deletions

View File

@@ -1,4 +1,4 @@
const { User, QuizSession, Category, sequelize } = require('../models');
const { User, QuizSession, Category, Question, UserBookmark, sequelize } = require('../models');
const { Op } = require('sequelize');
/**
@@ -697,3 +697,411 @@ exports.updateUserProfile = async (req, res) => {
});
}
};
/**
* Add bookmark for a question
* POST /api/users/:userId/bookmarks
*/
exports.addBookmark = async (req, res) => {
try {
const { userId } = req.params;
const requestUserId = req.user.userId;
const { questionId } = req.body;
// Validate userId UUID format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(userId)) {
return res.status(400).json({
success: false,
message: 'Invalid user ID format'
});
}
// Check if user exists
const user = await User.findByPk(userId);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
// Authorization check - users can only manage their own bookmarks
if (userId !== requestUserId) {
return res.status(403).json({
success: false,
message: 'You are not authorized to add bookmarks for this user'
});
}
// Validate questionId is provided
if (!questionId) {
return res.status(400).json({
success: false,
message: 'Question ID is required'
});
}
// Validate questionId UUID format
if (!uuidRegex.test(questionId)) {
return res.status(400).json({
success: false,
message: 'Invalid question ID format'
});
}
// Check if question exists and is active
const question = await Question.findOne({
where: { id: questionId, isActive: true },
include: [{
model: Category,
as: 'category',
attributes: ['id', 'name', 'slug']
}]
});
if (!question) {
return res.status(404).json({
success: false,
message: 'Question not found or not available'
});
}
// Check if already bookmarked
const existingBookmark = await UserBookmark.findOne({
where: { userId, questionId }
});
if (existingBookmark) {
return res.status(409).json({
success: false,
message: 'Question is already bookmarked'
});
}
// Create bookmark
const bookmark = await UserBookmark.create({
userId,
questionId
});
// Return success with bookmark details
return res.status(201).json({
success: true,
data: {
id: bookmark.id,
questionId: bookmark.questionId,
question: {
id: question.id,
questionText: question.questionText,
difficulty: question.difficulty,
category: question.category
},
bookmarkedAt: bookmark.createdAt
},
message: 'Question bookmarked successfully'
});
} catch (error) {
console.error('Error adding bookmark:', error);
return res.status(500).json({
success: false,
message: 'An error occurred while adding bookmark',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
/**
* Remove bookmark for a question
* DELETE /api/users/:userId/bookmarks/:questionId
*/
exports.removeBookmark = async (req, res) => {
try {
const { userId, questionId } = req.params;
const requestUserId = req.user.userId;
// Validate userId UUID format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(userId)) {
return res.status(400).json({
success: false,
message: 'Invalid user ID format'
});
}
// Validate questionId UUID format
if (!uuidRegex.test(questionId)) {
return res.status(400).json({
success: false,
message: 'Invalid question ID format'
});
}
// Check if user exists
const user = await User.findByPk(userId);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
// Authorization check - users can only manage their own bookmarks
if (userId !== requestUserId) {
return res.status(403).json({
success: false,
message: 'You are not authorized to remove bookmarks for this user'
});
}
// Find the bookmark
const bookmark = await UserBookmark.findOne({
where: { userId, questionId }
});
if (!bookmark) {
return res.status(404).json({
success: false,
message: 'Bookmark not found'
});
}
// Delete the bookmark
await bookmark.destroy();
return res.status(200).json({
success: true,
data: {
questionId
},
message: 'Bookmark removed successfully'
});
} catch (error) {
console.error('Error removing bookmark:', error);
return res.status(500).json({
success: false,
message: 'An error occurred while removing bookmark',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
/**
* Get user bookmarks with pagination and filtering
* @route GET /api/users/:userId/bookmarks
*/
exports.getUserBookmarks = async (req, res) => {
try {
const { userId } = req.params;
const requestUserId = req.user.userId;
// Validate userId format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(userId)) {
return res.status(400).json({
success: false,
message: "Invalid user ID format",
});
}
// Check if user exists
const user = await User.findByPk(userId);
if (!user) {
return res.status(404).json({
success: false,
message: "User not found",
});
}
// Authorization: users can only view their own bookmarks
if (userId !== requestUserId) {
return res.status(403).json({
success: false,
message: "You are not authorized to view these bookmarks",
});
}
// Pagination parameters
const page = Math.max(parseInt(req.query.page) || 1, 1);
const limit = Math.min(Math.max(parseInt(req.query.limit) || 10, 1), 50);
const offset = (page - 1) * limit;
// Category filter (optional)
let categoryId = req.query.category;
if (categoryId) {
if (!uuidRegex.test(categoryId)) {
return res.status(400).json({
success: false,
message: "Invalid category ID format",
});
}
}
// Difficulty filter (optional)
const difficulty = req.query.difficulty;
if (difficulty && !["easy", "medium", "hard"].includes(difficulty)) {
return res.status(400).json({
success: false,
message: "Invalid difficulty value. Must be: easy, medium, or hard",
});
}
// Sort options
const sortBy = req.query.sortBy || "date"; // 'date' or 'difficulty'
const sortOrder = (req.query.sortOrder || "desc").toLowerCase();
if (!["asc", "desc"].includes(sortOrder)) {
return res.status(400).json({
success: false,
message: "Invalid sort order. Must be: asc or desc",
});
}
// Build query conditions
const whereConditions = {
userId: userId,
};
const questionWhereConditions = {
isActive: true,
};
if (categoryId) {
questionWhereConditions.categoryId = categoryId;
}
if (difficulty) {
questionWhereConditions.difficulty = difficulty;
}
// Determine sort order
let orderClause;
if (sortBy === "difficulty") {
// Custom order for difficulty: easy, medium, hard
const difficultyOrder = sortOrder === "asc"
? ["easy", "medium", "hard"]
: ["hard", "medium", "easy"];
orderClause = [
[sequelize.literal(`FIELD(Question.difficulty, '${difficultyOrder.join("','")}')`)],
["createdAt", "DESC"]
];
} else {
// Sort by bookmark date (createdAt)
orderClause = [["createdAt", sortOrder.toUpperCase()]];
}
// Get total count with filters
const totalCount = await UserBookmark.count({
where: whereConditions,
include: [
{
model: Question,
as: "Question",
where: questionWhereConditions,
required: true,
},
],
});
// Get bookmarks with pagination
const bookmarks = await UserBookmark.findAll({
where: whereConditions,
include: [
{
model: Question,
as: "Question",
where: questionWhereConditions,
attributes: [
"id",
"questionText",
"questionType",
"options",
"difficulty",
"points",
"explanation",
"tags",
"keywords",
"timesAttempted",
"timesCorrect",
],
include: [
{
model: Category,
as: "category",
attributes: ["id", "name", "slug", "icon", "color"],
},
],
},
],
order: orderClause,
limit: limit,
offset: offset,
});
// Format response
const formattedBookmarks = bookmarks.map((bookmark) => {
const question = bookmark.Question;
const accuracy =
question.timesAttempted > 0
? Math.round((question.timesCorrect / question.timesAttempted) * 100)
: 0;
return {
bookmarkId: bookmark.id,
bookmarkedAt: bookmark.createdAt,
notes: bookmark.notes,
question: {
id: question.id,
questionText: question.questionText,
questionType: question.questionType,
options: question.options,
difficulty: question.difficulty,
points: question.points,
explanation: question.explanation,
tags: question.tags,
keywords: question.keywords,
statistics: {
timesAttempted: question.timesAttempted,
timesCorrect: question.timesCorrect,
accuracy: accuracy,
},
category: question.category,
},
};
});
// Calculate pagination metadata
const totalPages = Math.ceil(totalCount / limit);
return res.status(200).json({
success: true,
data: {
bookmarks: formattedBookmarks,
pagination: {
currentPage: page,
totalPages: totalPages,
totalItems: totalCount,
itemsPerPage: limit,
hasNextPage: page < totalPages,
hasPreviousPage: page > 1,
},
filters: {
category: categoryId || null,
difficulty: difficulty || null,
},
sorting: {
sortBy: sortBy,
sortOrder: sortOrder,
},
},
message: "User bookmarks retrieved successfully",
});
} catch (error) {
console.error("Error getting user bookmarks:", error);
return res.status(500).json({
success: false,
message: "Internal server error",
});
}
};