Files
hMarket/backend/src/routes/lists.js
2026-02-03 01:22:08 +03:00

704 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express');
const { body, validationResult, param } = require('express-validator');
const { asyncHandler, formatValidationErrors } = require('../middleware/errorHandler');
const {
authenticateToken,
checkListMembership,
requireListEditPermission
} = require('../middleware/auth');
const {
validateListCreation,
validateListUpdate,
validateListItemCreation,
validateListItemUpdate,
validateUUIDParam,
validateCuidParam,
validateAddListMember,
validatePagination
} = require('../utils/validators');
const router = express.Router();
/**
* Kullanıcının listelerini getir
* GET /api/lists
*/
router.get('/', authenticateToken, asyncHandler(async (req, res) => {
const { page = 1, limit = 10, search } = req.query;
const skip = (page - 1) * limit;
// Arama koşulları
const whereCondition = {
OR: [
{ ownerId: req.user.id },
{
members: {
some: {
userId: req.user.id
}
}
}
],
isActive: true
};
if (search) {
whereCondition.name = {
contains: search,
mode: 'insensitive'
};
}
const [lists, totalCount] = await Promise.all([
req.prisma.shoppingList.findMany({
where: whereCondition,
include: {
owner: {
select: {
id: true,
username: true,
firstName: true,
lastName: true,
avatar: true
}
},
members: {
include: {
user: {
select: {
id: true,
username: true,
firstName: true,
lastName: true,
avatar: true
}
}
}
},
items: {
select: {
id: true,
isPurchased: true
}
},
_count: {
select: {
items: true,
members: true
}
}
},
orderBy: { updatedAt: 'desc' },
skip: parseInt(skip),
take: parseInt(limit)
}),
req.prisma.shoppingList.count({ where: whereCondition })
]);
// Her liste için kullanıcının rolünü belirle ve tamamlanan ürün sayısını hesapla
const listsWithUserRole = lists.map(list => {
let userRole = 'viewer';
if (list.ownerId === req.user.id) {
userRole = 'owner';
} else {
const membership = list.members.find(member => member.userId === req.user.id);
if (membership) {
userRole = membership.role;
}
}
// Tamamlanan ürün sayısını hesapla
const completedItems = list.items.filter(item => item.isPurchased).length;
// Items'ı response'dan çıkar (sadece count için kullandık)
const { items, ...listWithoutItems } = list;
return {
...listWithoutItems,
userRole,
_count: {
...list._count,
completedItems
}
};
});
res.json({
success: true,
data: {
lists: listsWithUserRole,
pagination: {
currentPage: parseInt(page),
totalPages: Math.ceil(totalCount / limit),
totalCount,
hasNext: skip + parseInt(limit) < totalCount,
hasPrev: page > 1
}
}
});
}));
/**
* Yeni liste oluştur
* POST /api/lists
*/
router.post('/', authenticateToken, [
body('name')
.isLength({ min: 1, max: 100 })
.withMessage('Liste adı 1-100 karakter arasında olmalı'),
body('description')
.optional()
.isLength({ max: 500 })
.withMessage('Açıklama en fazla 500 karakter olabilir'),
body('color')
.optional()
.matches(/^#[0-9A-F]{6}$/i)
.withMessage('Geçerli bir hex renk kodu girin'),
body('isShared')
.optional()
.isBoolean()
.withMessage('Paylaşım durumu boolean olmalı')
], asyncHandler(async (req, res) => {
// Validation hatalarını kontrol et
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Girilen bilgilerde hatalar var',
errors: formatValidationErrors(errors)
});
}
const { name, description, color = '#2196F3' } = req.body;
const list = await req.prisma.shoppingList.create({
data: {
name,
description,
color,
ownerId: req.user.id
},
include: {
owner: {
select: {
id: true,
username: true,
firstName: true,
lastName: true,
avatar: true
}
},
_count: {
select: {
items: true,
members: true
}
}
}
});
// Aktivite kaydı oluştur
await req.prisma.activity.create({
data: {
listId: list.id,
userId: req.user.id,
action: 'list_created',
details: {
listName: list.name
}
}
});
// Socket.IO ile gerçek zamanlı bildirim gönder
req.io.emit('list_created', {
list: { ...list, userRole: 'owner' },
user: req.user
});
res.status(201).json({
success: true,
message: 'Liste başarıyla oluşturuldu',
data: {
list: { ...list, userRole: 'owner' }
}
});
}));
/**
* Liste detaylarını getir
* GET /api/lists/:listId
*/
router.get('/:listId', [
authenticateToken,
validateCuidParam('listId'),
checkListMembership
], asyncHandler(async (req, res) => {
const { listId } = req.params;
const list = await req.prisma.shoppingList.findUnique({
where: { id: listId },
include: {
owner: {
select: {
id: true,
username: true,
firstName: true,
lastName: true,
avatar: true
}
},
members: {
include: {
user: {
select: {
id: true,
username: true,
firstName: true,
lastName: true,
avatar: true
}
}
}
},
items: {
include: {
product: {
include: {
category: true
}
}
},
orderBy: [
{ isPurchased: 'asc' },
{ priority: 'desc' },
{ sortOrder: 'asc' },
{ createdAt: 'asc' }
]
},
activities: {
include: {
user: {
select: {
id: true,
username: true,
firstName: true,
lastName: true,
avatar: true
}
}
},
orderBy: { createdAt: 'desc' },
take: 20
}
}
});
res.json({
success: true,
data: {
list: {
...list,
userRole: req.userRole
}
}
});
}));
/**
* Liste güncelle
* PUT /api/lists/:listId
*/
router.put('/:listId', [
authenticateToken,
validateCuidParam('listId'),
checkListMembership,
requireListEditPermission,
body('name')
.optional()
.isLength({ min: 1, max: 100 })
.withMessage('Liste adı 1-100 karakter arasında olmalı'),
body('description')
.optional()
.isLength({ max: 500 })
.withMessage('Açıklama en fazla 500 karakter olabilir'),
body('color')
.optional()
.matches(/^#[0-9A-F]{6}$/i)
.withMessage('Geçerli bir hex renk kodu girin'),
body('isShared')
.optional()
.isBoolean()
.withMessage('Paylaşım durumu boolean olmalı')
], asyncHandler(async (req, res) => {
// Validation hatalarını kontrol et
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Girilen bilgilerde hatalar var',
errors: formatValidationErrors(errors)
});
}
const { listId } = req.params;
const { name, description, color, isShared } = req.body;
const updateData = {};
if (name !== undefined) updateData.name = name;
if (description !== undefined) updateData.description = description;
if (color !== undefined) updateData.color = color;
if (isShared !== undefined) updateData.isShared = isShared;
const updatedList = await req.prisma.shoppingList.update({
where: { id: listId },
data: updateData,
include: {
owner: {
select: {
id: true,
username: true,
firstName: true,
lastName: true,
avatar: true
}
},
members: {
include: {
user: {
select: {
id: true,
username: true,
firstName: true,
lastName: true,
avatar: true
}
}
}
},
_count: {
select: {
items: true,
members: true
}
}
}
});
// Aktivite kaydı oluştur
await req.prisma.activity.create({
data: {
listId: listId,
userId: req.user.id,
action: 'list_updated',
details: {
changes: updateData
}
}
});
// Socket.IO ile gerçek zamanlı güncelleme gönder
req.io.to(`list_${listId}`).emit('list_updated', {
list: { ...updatedList, userRole: req.userRole },
user: req.user,
changes: updateData
});
res.json({
success: true,
message: 'Liste başarıyla güncellendi',
data: {
list: { ...updatedList, userRole: req.userRole }
}
});
}));
/**
* Liste sil
* DELETE /api/lists/:listId
*/
router.delete('/:listId', [
authenticateToken,
validateCuidParam('listId'),
checkListMembership
], asyncHandler(async (req, res) => {
const { listId } = req.params;
// Sadece liste sahibi silebilir
if (req.userRole !== 'owner') {
return res.status(403).json({
success: false,
message: 'Sadece liste sahibi listeyi silebilir'
});
}
// Soft delete (isActive = false)
await req.prisma.shoppingList.update({
where: { id: listId },
data: { isActive: false }
});
// Aktivite kaydı oluştur
await req.prisma.activity.create({
data: {
listId: listId,
userId: req.user.id,
action: 'list_deleted',
details: {
listName: req.list.name
}
}
});
// Socket.IO ile gerçek zamanlı bildirim gönder
req.io.to(`list_${listId}`).emit('list_deleted', {
listId: listId,
user: req.user
});
res.json({
success: true,
message: 'Liste başarıyla silindi'
});
}));
/**
* Listeye üye ekle
* POST /api/lists/:listId/members
*/
router.post('/:listId/members', [
authenticateToken,
validateCuidParam('listId'),
checkListMembership,
requireListEditPermission,
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Geçerli bir e-posta adresi girin'),
body('role')
.optional()
.isIn(['admin', 'member', 'viewer', 'EDITOR', 'VIEWER'])
.withMessage('Geçerli bir rol seçin')
], asyncHandler(async (req, res) => {
// Validation hatalarını kontrol et
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Girilen bilgilerde hatalar var',
errors: formatValidationErrors(errors)
});
}
const { listId } = req.params;
let { email, role = 'member' } = req.body;
// Role mapping
if (role === 'EDITOR') role = 'member';
if (role === 'VIEWER') role = 'viewer';
// Kullanıcıyı bul
const targetUser = await req.prisma.user.findUnique({
where: { email, isActive: true },
select: {
id: true,
email: true,
username: true,
firstName: true,
lastName: true,
avatar: true
}
});
if (!targetUser) {
return res.status(404).json({
success: false,
message: 'Bu e-posta adresine sahip aktif kullanıcı bulunamadı'
});
}
// Zaten üye mi kontrol et
const existingMember = await req.prisma.listMember.findUnique({
where: {
listId_userId: {
listId: listId,
userId: targetUser.id
}
}
});
if (existingMember) {
return res.status(400).json({
success: false,
message: 'Bu kullanıcı zaten liste üyesi'
});
}
// Liste sahibi kendini üye olarak ekleyemez
if (targetUser.id === req.list.ownerId) {
return res.status(400).json({
success: false,
message: 'Liste sahibi zaten tüm yetkilere sahip'
});
}
// Üye ekle
const newMember = await req.prisma.listMember.create({
data: {
listId: listId,
userId: targetUser.id,
role: role
},
include: {
user: {
select: {
id: true,
email: true,
username: true,
firstName: true,
lastName: true,
avatar: true
}
}
}
});
// Aktivite kaydı oluştur
await req.prisma.activity.create({
data: {
listId: listId,
userId: req.user.id,
action: 'member_added',
details: {
addedUser: {
id: targetUser.id,
username: targetUser.username,
firstName: targetUser.firstName,
lastName: targetUser.lastName
},
role: role
}
}
});
// Bildirim oluştur
await req.prisma.notification.create({
data: {
userId: targetUser.id,
title: 'Listeye Eklendi',
message: `${req.user.firstName} ${req.user.lastName} sizi "${req.list.name}" listesine ekledi`,
type: 'list_shared',
data: {
listId: listId,
listName: req.list.name,
invitedBy: {
id: req.user.id,
username: req.user.username,
firstName: req.user.firstName,
lastName: req.user.lastName
}
}
}
});
// Socket.IO ile gerçek zamanlı bildirim gönder
req.io.to(`list_${listId}`).emit('member_added', {
member: newMember,
user: req.user
});
// Yeni üyeye özel bildirim gönder
req.io.to(`user_${targetUser.id}`).emit('list_invitation', {
listId: listId,
listName: req.list.name,
invitedBy: req.user
});
res.status(201).json({
success: true,
message: 'Üye başarıyla eklendi',
data: { member: newMember }
});
}));
/**
* Liste üyesini çıkar
* DELETE /api/lists/:listId/members/:userId
*/
router.delete('/:listId/members/:userId', [
authenticateToken,
validateCuidParam('listId'),
validateCuidParam('userId'),
checkListMembership,
requireListEditPermission
], asyncHandler(async (req, res) => {
const { listId, userId } = req.params;
// Üyeliği bul
const membership = await req.prisma.listMember.findUnique({
where: {
listId_userId: {
listId: listId,
userId: userId
}
},
include: {
user: {
select: {
id: true,
username: true,
firstName: true,
lastName: true,
avatar: true
}
}
}
});
if (!membership) {
return res.status(404).json({
success: false,
message: 'Üye bulunamadı'
});
}
// Üyeliği sil
await req.prisma.listMember.delete({
where: {
listId_userId: {
listId: listId,
userId: userId
}
}
});
// Aktivite kaydı oluştur
await req.prisma.activity.create({
data: {
listId: listId,
userId: req.user.id,
action: 'member_removed',
details: {
removedUser: {
id: membership.user.id,
username: membership.user.username,
firstName: membership.user.firstName,
lastName: membership.user.lastName
}
}
}
});
// Socket.IO ile gerçek zamanlı bildirim gönder
req.io.to(`list_${listId}`).emit('member_removed', {
userId: userId,
user: req.user
});
res.json({
success: true,
message: 'Üye başarıyla çıkarıldı'
});
}));
module.exports = router;