704 lines
15 KiB
JavaScript
704 lines
15 KiB
JavaScript
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; |