hMarket Trae ilk versiyon
This commit is contained in:
704
backend/src/routes/lists.js
Normal file
704
backend/src/routes/lists.js
Normal file
@@ -0,0 +1,704 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user