hMarket Trae ilk versiyon

This commit is contained in:
hOLOlu
2026-02-03 01:22:08 +03:00
commit 2b861156fe
74 changed files with 42127 additions and 0 deletions

704
backend/src/routes/lists.js Normal file
View 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;