add changes
This commit is contained in:
@@ -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",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user