hPiBot openclaw ve Opencode ilk versiyonu[B

This commit is contained in:
2026-03-04 05:17:51 +03:00
commit d49edbfba3
75 changed files with 42117 additions and 0 deletions

View File

@@ -0,0 +1,371 @@
const admin = require('firebase-admin');
let firebaseApp = null;
/**
* Firebase Admin SDK'yı başlat
*/
const initializeFirebase = () => {
try {
// Eğer zaten başlatılmışsa, mevcut instance'ı döndür
if (firebaseApp) {
return firebaseApp;
}
// Environment variables'dan Firebase config'i al
const firebaseConfig = {
type: process.env.FIREBASE_TYPE,
project_id: process.env.FIREBASE_PROJECT_ID,
private_key_id: process.env.FIREBASE_PRIVATE_KEY_ID,
private_key: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
client_email: process.env.FIREBASE_CLIENT_EMAIL,
client_id: process.env.FIREBASE_CLIENT_ID,
auth_uri: process.env.FIREBASE_AUTH_URI,
token_uri: process.env.FIREBASE_TOKEN_URI,
auth_provider_x509_cert_url: process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL,
client_x509_cert_url: process.env.FIREBASE_CLIENT_X509_CERT_URL
};
// Gerekli environment variables'ların varlığını kontrol et
const requiredFields = ['project_id', 'private_key', 'client_email'];
const missingFields = requiredFields.filter(field => !firebaseConfig[field]);
if (missingFields.length > 0) {
console.warn(`⚠️ Firebase konfigürasyonu eksik: ${missingFields.join(', ')}`);
console.warn('Push notification özelliği devre dışı bırakıldı.');
return null;
}
// Firebase Admin SDK'yı başlat
firebaseApp = admin.initializeApp({
credential: admin.credential.cert(firebaseConfig),
projectId: firebaseConfig.project_id
});
console.log('✅ Firebase Admin SDK başarıyla başlatıldı');
return firebaseApp;
} catch (error) {
console.error('❌ Firebase başlatma hatası:', error.message);
console.warn('Push notification özelliği devre dışı bırakıldı.');
return null;
}
};
/**
* Firebase Messaging instance'ını al
*/
const getMessaging = () => {
if (!firebaseApp) {
console.warn('Firebase başlatılmamış. Push notification gönderilemez.');
return null;
}
try {
return admin.messaging();
} catch (error) {
console.error('Firebase Messaging alınamadı:', error);
return null;
}
};
/**
* Tek bir cihaza push notification gönder
* @param {string} token - Device token
* @param {object} notification - Notification objesi
* @param {object} data - Ek veri
* @returns {Promise<object>} Gönderim sonucu
*/
const sendToDevice = async (token, notification, data = {}) => {
const messaging = getMessaging();
if (!messaging) {
return { success: false, error: 'Firebase Messaging kullanılamıyor' };
}
try {
const message = {
token: token,
notification: {
title: notification.title,
body: notification.body,
...(notification.imageUrl && { imageUrl: notification.imageUrl })
},
data: {
...data,
timestamp: new Date().toISOString()
},
android: {
notification: {
icon: 'ic_notification',
color: '#FF5722',
sound: 'default',
priority: 'high'
}
},
apns: {
payload: {
aps: {
sound: 'default',
badge: 1
}
}
}
};
const response = await messaging.send(message);
return {
success: true,
messageId: response,
token: token
};
} catch (error) {
console.error('Push notification gönderme hatası:', error);
// Token geçersizse, bu bilgiyi döndür
if (error.code === 'messaging/registration-token-not-registered' ||
error.code === 'messaging/invalid-registration-token') {
return {
success: false,
error: 'Invalid token',
invalidToken: true,
token: token
};
}
return {
success: false,
error: error.message,
token: token
};
}
};
/**
* Birden fazla cihaza push notification gönder
* @param {string[]} tokens - Device token'ları
* @param {object} notification - Notification objesi
* @param {object} data - Ek veri
* @returns {Promise<object>} Gönderim sonucu
*/
const sendToMultipleDevices = async (tokens, notification, data = {}) => {
const messaging = getMessaging();
if (!messaging) {
return { success: false, error: 'Firebase Messaging kullanılamıyor' };
}
if (!tokens || tokens.length === 0) {
return { success: false, error: 'Token listesi boş' };
}
try {
const message = {
notification: {
title: notification.title,
body: notification.body,
...(notification.imageUrl && { imageUrl: notification.imageUrl })
},
data: {
...data,
timestamp: new Date().toISOString()
},
android: {
notification: {
icon: 'ic_notification',
color: '#FF5722',
sound: 'default',
priority: 'high'
}
},
apns: {
payload: {
aps: {
sound: 'default',
badge: 1
}
}
},
tokens: tokens
};
const response = await messaging.sendMulticast(message);
// Başarısız token'ları topla
const failedTokens = [];
const invalidTokens = [];
response.responses.forEach((resp, idx) => {
if (!resp.success) {
const token = tokens[idx];
failedTokens.push({ token, error: resp.error?.message });
if (resp.error?.code === 'messaging/registration-token-not-registered' ||
resp.error?.code === 'messaging/invalid-registration-token') {
invalidTokens.push(token);
}
}
});
return {
success: response.successCount > 0,
successCount: response.successCount,
failureCount: response.failureCount,
failedTokens: failedTokens,
invalidTokens: invalidTokens
};
} catch (error) {
console.error('Toplu push notification gönderme hatası:', error);
return {
success: false,
error: error.message
};
}
};
/**
* Topic'e push notification gönder
* @param {string} topic - Topic adı
* @param {object} notification - Notification objesi
* @param {object} data - Ek veri
* @returns {Promise<object>} Gönderim sonucu
*/
const sendToTopic = async (topic, notification, data = {}) => {
const messaging = getMessaging();
if (!messaging) {
return { success: false, error: 'Firebase Messaging kullanılamıyor' };
}
try {
const message = {
topic: topic,
notification: {
title: notification.title,
body: notification.body,
...(notification.imageUrl && { imageUrl: notification.imageUrl })
},
data: {
...data,
timestamp: new Date().toISOString()
},
android: {
notification: {
icon: 'ic_notification',
color: '#FF5722',
sound: 'default',
priority: 'high'
}
},
apns: {
payload: {
aps: {
sound: 'default',
badge: 1
}
}
}
};
const response = await messaging.send(message);
return {
success: true,
messageId: response,
topic: topic
};
} catch (error) {
console.error('Topic push notification gönderme hatası:', error);
return {
success: false,
error: error.message,
topic: topic
};
}
};
/**
* Cihazı topic'e abone et
* @param {string[]} tokens - Device token'ları
* @param {string} topic - Topic adı
* @returns {Promise<object>} Abonelik sonucu
*/
const subscribeToTopic = async (tokens, topic) => {
const messaging = getMessaging();
if (!messaging) {
return { success: false, error: 'Firebase Messaging kullanılamıyor' };
}
try {
const response = await messaging.subscribeToTopic(tokens, topic);
return {
success: response.successCount > 0,
successCount: response.successCount,
failureCount: response.failureCount,
errors: response.errors
};
} catch (error) {
console.error('Topic abonelik hatası:', error);
return {
success: false,
error: error.message
};
}
};
/**
* Cihazın topic aboneliğini iptal et
* @param {string[]} tokens - Device token'ları
* @param {string} topic - Topic adı
* @returns {Promise<object>} Abonelik iptal sonucu
*/
const unsubscribeFromTopic = async (tokens, topic) => {
const messaging = getMessaging();
if (!messaging) {
return { success: false, error: 'Firebase Messaging kullanılamıyor' };
}
try {
const response = await messaging.unsubscribeFromTopic(tokens, topic);
return {
success: response.successCount > 0,
successCount: response.successCount,
failureCount: response.failureCount,
errors: response.errors
};
} catch (error) {
console.error('Topic abonelik iptal hatası:', error);
return {
success: false,
error: error.message
};
}
};
/**
* Firebase durumunu kontrol et
* @returns {object} Firebase durum bilgisi
*/
const getStatus = () => {
return {
initialized: !!firebaseApp,
messaging: !!getMessaging(),
projectId: firebaseApp?.options?.projectId || null
};
};
module.exports = {
initializeFirebase,
getMessaging,
sendToDevice,
sendToMultipleDevices,
sendToTopic,
subscribeToTopic,
unsubscribeFromTopic,
getStatus
};

View File

@@ -0,0 +1,101 @@
const passport = require('passport');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
// Kullanıcıyı session'a serialize et
passport.serializeUser((user, done) => {
done(null, user.id);
});
// Session'dan kullanıcıyı deserialize et
passport.deserializeUser(async (id, done) => {
try {
const user = await prisma.user.findUnique({
where: { id },
select: {
id: true,
email: true,
username: true,
firstName: true,
lastName: true,
avatar: true,
authProvider: true,
isActive: true,
isAdmin: true
}
});
done(null, user);
} catch (error) {
done(error, null);
}
});
// Google OAuth Strategy - Sadece credentials varsa yükle
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL || "/api/auth/google/callback"
}, async (accessToken, refreshToken, profile, done) => {
try {
// Önce Google ID ile kullanıcı ara
let user = await prisma.user.findUnique({
where: { googleId: profile.id }
});
if (user) {
// Kullanıcı zaten var, son giriş tarihini güncelle
user = await prisma.user.update({
where: { id: user.id },
data: { lastLoginAt: new Date() }
});
return done(null, user);
}
// Email ile kullanıcı ara (mevcut hesap varsa bağla)
user = await prisma.user.findUnique({
where: { email: profile.emails[0].value }
});
if (user) {
// Mevcut hesabı Google ile bağla
user = await prisma.user.update({
where: { id: user.id },
data: {
googleId: profile.id,
authProvider: 'google',
avatar: profile.photos[0]?.value || user.avatar,
lastLoginAt: new Date()
}
});
return done(null, user);
}
// Yeni kullanıcı oluştur
const username = profile.emails[0].value.split('@')[0] + '_' + Math.random().toString(36).substr(2, 4);
user = await prisma.user.create({
data: {
email: profile.emails[0].value,
username: username,
firstName: profile.name.givenName || '',
lastName: profile.name.familyName || '',
googleId: profile.id,
authProvider: 'google',
avatar: profile.photos[0]?.value,
lastLoginAt: new Date()
}
});
return done(null, user);
} catch (error) {
console.error('Google OAuth Error:', error);
return done(error, null);
}
}));
}
module.exports = passport;

View File

@@ -0,0 +1,226 @@
const jwt = require('jsonwebtoken');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
/**
* JWT token doğrulama middleware'i
*/
const authenticateToken = async (req, res, next) => {
try {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
success: false,
message: 'Erişim token\'ı bulunamadı. Lütfen giriş yapın.'
});
}
// Token'ı doğrula
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Kullanıcıyı veritabanından getir
const user = await prisma.user.findUnique({
where: {
id: decoded.userId,
isActive: true
},
select: {
id: true,
email: true,
username: true,
firstName: true,
lastName: true,
avatar: true,
isAdmin: true,
createdAt: true
}
});
if (!user) {
return res.status(401).json({
success: false,
message: 'Geçersiz token. Kullanıcı bulunamadı.'
});
}
// Kullanıcı bilgilerini request'e ekle
req.user = user;
next();
} catch (error) {
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
message: 'Geçersiz token formatı.'
});
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: 'Token süresi dolmuş. Lütfen tekrar giriş yapın.'
});
}
console.error('Auth middleware hatası:', error);
return res.status(500).json({
success: false,
message: 'Kimlik doğrulama sırasında bir hata oluştu.'
});
}
};
/**
* Admin yetkisi kontrolü middleware'i
*/
const requireAdmin = (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Kimlik doğrulama gerekli.'
});
}
if (!req.user.isAdmin) {
return res.status(403).json({
success: false,
message: 'Bu işlem için admin yetkisi gerekli.'
});
}
next();
};
/**
* Liste üyeliği kontrolü middleware'i
*/
const checkListMembership = async (req, res, next) => {
try {
const listId = req.params.listId || req.body.listId;
const userId = req.user.id;
if (!listId) {
return res.status(400).json({
success: false,
message: 'Liste ID\'si gerekli.'
});
}
// Liste sahibi mi kontrol et
const list = await prisma.shoppingList.findFirst({
where: {
id: listId,
ownerId: userId,
isActive: true
}
});
if (list) {
req.userRole = 'owner';
req.list = list;
return next();
}
// Liste üyesi mi kontrol et
const membership = await prisma.listMember.findFirst({
where: {
listId: listId,
userId: userId
},
include: {
list: {
where: {
isActive: true
}
}
}
});
if (!membership || !membership.list) {
return res.status(403).json({
success: false,
message: 'Bu listeye erişim yetkiniz yok.'
});
}
req.userRole = membership.role;
req.list = membership.list;
next();
} catch (error) {
console.error('Liste üyelik kontrolü hatası:', error);
return res.status(500).json({
success: false,
message: 'Liste erişim kontrolü sırasında bir hata oluştu.'
});
}
};
/**
* Liste düzenleme yetkisi kontrolü
*/
const requireListEditPermission = (req, res, next) => {
const allowedRoles = ['owner', 'admin', 'editor'];
if (!allowedRoles.includes(req.userRole)) {
return res.status(403).json({
success: false,
message: 'Bu işlem için yeterli yetkiniz yok.'
});
}
next();
};
/**
* Opsiyonel kimlik doğrulama (token varsa doğrula, yoksa devam et)
*/
const optionalAuth = async (req, res, next) => {
try {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return next(); // Token yoksa devam et
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await prisma.user.findUnique({
where: {
id: decoded.userId,
isActive: true
},
select: {
id: true,
email: true,
username: true,
firstName: true,
lastName: true,
avatar: true,
isAdmin: true,
createdAt: true
}
});
if (user) {
req.user = user;
}
next();
} catch (error) {
// Token geçersizse de devam et
next();
}
};
module.exports = {
authenticateToken,
requireAdmin,
checkListMembership,
requireListEditPermission,
optionalAuth
};

View File

@@ -0,0 +1,125 @@
/**
* 404 - Sayfa bulunamadı middleware'i
*/
const notFound = (req, res, next) => {
const error = new Error(`Bulunamadı - ${req.originalUrl}`);
res.status(404);
next(error);
};
/**
* Genel hata yakalama middleware'i
*/
const errorHandler = (err, req, res, next) => {
let statusCode = res.statusCode === 200 ? 500 : res.statusCode;
let message = err.message;
// Prisma hataları
if (err.code === 'P2002') {
statusCode = 400;
message = 'Bu veri zaten mevcut. Benzersiz alan ihlali.';
}
if (err.code === 'P2025') {
statusCode = 404;
message = 'İstenen kayıt bulunamadı.';
}
if (err.code === 'P2003') {
statusCode = 400;
message = 'İlişkili veri bulunamadı. Yabancı anahtar ihlali.';
}
// Validation hataları
if (err.name === 'ValidationError') {
statusCode = 400;
message = Object.values(err.errors).map(val => val.message).join(', ');
}
// JWT hataları
if (err.name === 'JsonWebTokenError') {
statusCode = 401;
message = 'Geçersiz token.';
}
if (err.name === 'TokenExpiredError') {
statusCode = 401;
message = 'Token süresi dolmuş.';
}
// Multer hataları (dosya yükleme)
if (err.code === 'LIMIT_FILE_SIZE') {
statusCode = 400;
message = 'Dosya boyutu çok büyük.';
}
if (err.code === 'LIMIT_UNEXPECTED_FILE') {
statusCode = 400;
message = 'Beklenmeyen dosya alanı.';
}
// Hata logla (production'da daha detaylı loglama yapılabilir)
if (process.env.NODE_ENV === 'development') {
console.error('🚨 Hata:', {
message: err.message,
stack: err.stack,
url: req.originalUrl,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent'),
timestamp: new Date().toISOString()
});
} else {
console.error('🚨 Hata:', {
message: err.message,
url: req.originalUrl,
method: req.method,
ip: req.ip,
timestamp: new Date().toISOString()
});
}
// Hata yanıtı
const errorResponse = {
success: false,
message: message,
statusCode: statusCode
};
// Development modunda stack trace ekle
if (process.env.NODE_ENV === 'development') {
errorResponse.stack = err.stack;
errorResponse.details = {
url: req.originalUrl,
method: req.method,
timestamp: new Date().toISOString()
};
}
res.status(statusCode).json(errorResponse);
};
/**
* Async fonksiyonlar için hata yakalama wrapper'ı
*/
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
/**
* Validation hata formatter'ı
*/
const formatValidationErrors = (errors) => {
return errors.array().map(error => ({
field: error.param,
message: error.msg,
value: error.value
}));
};
module.exports = {
notFound,
errorHandler,
asyncHandler,
formatValidationErrors
};

598
backend/src/routes/admin.js Normal file
View File

@@ -0,0 +1,598 @@
const express = require('express');
const { body, validationResult, param, query } = require('express-validator');
const { asyncHandler, formatValidationErrors } = require('../middleware/errorHandler');
const { authenticateToken, requireAdmin } = require('../middleware/auth');
const router = express.Router();
/**
* Admin dashboard istatistikleri
* GET /api/admin/dashboard
*/
router.get('/dashboard', [
authenticateToken,
requireAdmin
], asyncHandler(async (req, res) => {
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay()));
const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const [
// Kullanıcı istatistikleri
totalUsers,
activeUsers,
newUsersToday,
newUsersThisWeek,
newUsersThisMonth,
// Liste istatistikleri
totalLists,
activeLists,
sharedLists,
newListsToday,
newListsThisWeek,
// Ürün istatistikleri
totalProducts,
activeProducts,
newProductsToday,
newProductsThisWeek,
// Kategori istatistikleri
totalCategories,
activeCategories,
// Bildirim istatistikleri
totalNotifications,
unreadNotifications,
notificationsToday,
// Aktivite istatistikleri
activitiesToday,
activitiesThisWeek
] = await Promise.all([
// Kullanıcılar
req.prisma.user.count(),
req.prisma.user.count({ where: { isActive: true } }),
req.prisma.user.count({ where: { createdAt: { gte: startOfDay } } }),
req.prisma.user.count({ where: { createdAt: { gte: startOfWeek } } }),
req.prisma.user.count({ where: { createdAt: { gte: startOfMonth } } }),
// Listeler
req.prisma.shoppingList.count(),
req.prisma.shoppingList.count({ where: { isActive: true } }),
req.prisma.shoppingList.count({
where: {
isActive: true,
members: {
some: {
role: 'member'
}
}
}
}),
req.prisma.shoppingList.count({ where: { createdAt: { gte: startOfDay } } }),
req.prisma.shoppingList.count({ where: { createdAt: { gte: startOfWeek } } }),
// Ürünler
req.prisma.product.count(),
req.prisma.product.count({ where: { isActive: true } }),
req.prisma.product.count({ where: { createdAt: { gte: startOfDay } } }),
req.prisma.product.count({ where: { createdAt: { gte: startOfWeek } } }),
// Kategoriler
req.prisma.category.count(),
req.prisma.category.count({ where: { isActive: true } }),
// Bildirimler
req.prisma.notification.count(),
req.prisma.notification.count({ where: { isRead: false } }),
req.prisma.notification.count({ where: { createdAt: { gte: startOfDay } } }),
// Aktiviteler
req.prisma.activity.count({ where: { createdAt: { gte: startOfDay } } }),
req.prisma.activity.count({ where: { createdAt: { gte: startOfWeek } } })
]);
res.json({
success: true,
data: {
users: {
total: totalUsers,
active: activeUsers,
inactive: totalUsers - activeUsers,
newToday: newUsersToday,
newThisWeek: newUsersThisWeek,
newThisMonth: newUsersThisMonth
},
lists: {
total: totalLists,
active: activeLists,
inactive: totalLists - activeLists,
shared: sharedLists,
newToday: newListsToday,
newThisWeek: newListsThisWeek
},
products: {
total: totalProducts,
active: activeProducts,
inactive: totalProducts - activeProducts,
newToday: newProductsToday,
newThisWeek: newProductsThisWeek
},
categories: {
total: totalCategories,
active: activeCategories,
inactive: totalCategories - activeCategories
},
notifications: {
total: totalNotifications,
unread: unreadNotifications,
read: totalNotifications - unreadNotifications,
today: notificationsToday
},
activities: {
today: activitiesToday,
thisWeek: activitiesThisWeek
}
}
});
}));
/**
* Son aktiviteleri getir
* GET /api/admin/activities
*/
router.get('/activities', [
authenticateToken,
requireAdmin,
query('page').optional().isInt({ min: 1 }).withMessage('Sayfa numarası pozitif bir sayı olmalı'),
query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('Limit 1-100 arasında olmalı'),
query('type').optional().isIn(['list_created', 'list_updated', 'list_deleted', 'item_added', 'item_updated', 'item_deleted', 'item_purchased', 'member_added', 'member_removed', 'user_registered', 'user_login']).withMessage('Geçerli bir aktivite türü seçin'),
query('userId').optional().isUUID().withMessage('Geçerli bir kullanıcı ID\'si girin')
], 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 {
page = 1,
limit = 50,
type,
userId
} = req.query;
const skip = (page - 1) * limit;
// Filtreleme koşulları
const whereCondition = {};
if (type) {
whereCondition.type = type;
}
if (userId) {
whereCondition.userId = userId;
}
const [activities, totalCount] = await Promise.all([
req.prisma.activity.findMany({
where: whereCondition,
include: {
user: {
select: {
id: true,
username: true,
firstName: true,
lastName: true,
avatar: true
}
},
list: {
select: {
id: true,
name: true
}
}
},
orderBy: {
createdAt: 'desc'
},
skip: parseInt(skip),
take: parseInt(limit)
}),
req.prisma.activity.count({ where: whereCondition })
]);
res.json({
success: true,
data: {
activities,
pagination: {
currentPage: parseInt(page),
totalPages: Math.ceil(totalCount / limit),
totalCount,
hasNext: skip + parseInt(limit) < totalCount,
hasPrev: page > 1
}
}
});
}));
/**
* Sistem ayarlarını getir
* GET /api/admin/settings
*/
router.get('/settings', [
authenticateToken,
requireAdmin
], asyncHandler(async (req, res) => {
const settings = await req.prisma.setting.findMany({
where: {
userId: null // Sistem ayarları
},
orderBy: {
key: 'asc'
}
});
// Ayarları obje formatına çevir
const settingsObject = {};
settings.forEach(setting => {
settingsObject[setting.key] = setting.value;
});
res.json({
success: true,
data: { settings: settingsObject }
});
}));
/**
* Sistem ayarlarını güncelle
* PUT /api/admin/settings
*/
router.put('/settings', [
authenticateToken,
requireAdmin,
body('settings').isObject().withMessage('Ayarlar obje formatında 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 { settings } = req.body;
// Geçerli sistem ayar anahtarları
const validKeys = [
'app_name',
'app_version',
'maintenance_mode',
'registration_enabled',
'max_lists_per_user',
'max_items_per_list',
'max_file_size',
'allowed_file_types',
'email_notifications_enabled',
'push_notifications_enabled',
'backup_enabled',
'backup_frequency',
'log_level',
'session_timeout',
'password_min_length',
'password_require_special_chars'
];
// Geçersiz anahtarları filtrele
const validSettings = {};
Object.keys(settings).forEach(key => {
if (validKeys.includes(key)) {
validSettings[key] = settings[key].toString();
}
});
if (Object.keys(validSettings).length === 0) {
return res.status(400).json({
success: false,
message: 'Geçerli ayar bulunamadı'
});
}
// Ayarları güncelle veya oluştur
const updatePromises = Object.entries(validSettings).map(([key, value]) =>
req.prisma.setting.upsert({
where: {
userId_key: {
userId: null,
key
}
},
update: { value },
create: {
userId: null,
key,
value
}
})
);
await Promise.all(updatePromises);
res.json({
success: true,
message: 'Sistem ayarları güncellendi',
data: { settings: validSettings }
});
}));
/**
* Sistem durumu kontrolü
* GET /api/admin/system-status
*/
router.get('/system-status', [
authenticateToken,
requireAdmin
], asyncHandler(async (req, res) => {
const startTime = Date.now();
try {
// Veritabanı bağlantısını test et
await req.prisma.$queryRaw`SELECT 1`;
const dbResponseTime = Date.now() - startTime;
// Sistem bilgileri
const systemInfo = {
nodeVersion: process.version,
platform: process.platform,
uptime: process.uptime(),
memoryUsage: process.memoryUsage(),
cpuUsage: process.cpuUsage()
};
// Veritabanı istatistikleri
const [
userCount,
listCount,
productCount,
notificationCount
] = await Promise.all([
req.prisma.user.count(),
req.prisma.shoppingList.count(),
req.prisma.product.count(),
req.prisma.notification.count()
]);
res.json({
success: true,
data: {
status: 'healthy',
timestamp: new Date().toISOString(),
database: {
status: 'connected',
responseTime: `${dbResponseTime}ms`
},
system: systemInfo,
statistics: {
users: userCount,
lists: listCount,
products: productCount,
notifications: notificationCount
}
}
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Sistem durumu kontrolünde hata oluştu',
data: {
status: 'unhealthy',
timestamp: new Date().toISOString(),
error: error.message
}
});
}
}));
/**
* Veritabanı yedekleme (Placeholder)
* POST /api/admin/backup
*/
router.post('/backup', [
authenticateToken,
requireAdmin
], asyncHandler(async (req, res) => {
// Bu endpoint gerçek bir yedekleme işlemi yapmaz
// Üretim ortamında uygun yedekleme araçları kullanılmalıdır
res.json({
success: true,
message: 'Yedekleme işlemi başlatıldı',
data: {
backupId: `backup_${Date.now()}`,
timestamp: new Date().toISOString(),
note: 'Bu bir placeholder endpoint\'tir. Gerçek yedekleme işlemi için uygun araçlar kullanılmalıdır.'
}
});
}));
/**
* Sistem loglarını getir (Placeholder)
* GET /api/admin/logs
*/
router.get('/logs', [
authenticateToken,
requireAdmin,
query('level').optional().isIn(['error', 'warn', 'info', 'debug']).withMessage('Geçerli bir log seviyesi seçin'),
query('limit').optional().isInt({ min: 1, max: 1000 }).withMessage('Limit 1-1000 arasında olmalı')
], asyncHandler(async (req, res) => {
const { level = 'info', limit = 100 } = req.query;
// Bu endpoint gerçek log dosyalarını okumaz
// Üretim ortamında uygun log yönetim sistemi kullanılmalıdır
const mockLogs = [
{
timestamp: new Date().toISOString(),
level: 'info',
message: 'Server started successfully',
source: 'server.js'
},
{
timestamp: new Date(Date.now() - 60000).toISOString(),
level: 'info',
message: 'Database connection established',
source: 'database.js'
},
{
timestamp: new Date(Date.now() - 120000).toISOString(),
level: 'warn',
message: 'High memory usage detected',
source: 'monitor.js'
}
];
res.json({
success: true,
data: {
logs: mockLogs.slice(0, parseInt(limit)),
note: 'Bu mock log verileridir. Gerçek log sistemi için uygun araçlar kullanılmalıdır.'
}
});
}));
/**
* Kullanıcı aktivite raporu
* GET /api/admin/reports/user-activity
*/
router.get('/reports/user-activity', [
authenticateToken,
requireAdmin,
query('startDate').optional().isISO8601().withMessage('Geçerli bir başlangıç tarihi girin'),
query('endDate').optional().isISO8601().withMessage('Geçerli bir bitiş tarihi girin'),
query('userId').optional().isUUID().withMessage('Geçerli bir kullanıcı ID\'si girin')
], 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 {
startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(), // Son 30 gün
endDate = new Date().toISOString(),
userId
} = req.query;
const whereCondition = {
createdAt: {
gte: new Date(startDate),
lte: new Date(endDate)
}
};
if (userId) {
whereCondition.userId = userId;
}
const [
activities,
activityCounts,
userStats
] = await Promise.all([
req.prisma.activity.findMany({
where: whereCondition,
include: {
user: {
select: {
id: true,
username: true,
firstName: true,
lastName: true
}
}
},
orderBy: {
createdAt: 'desc'
},
take: 100
}),
req.prisma.activity.groupBy({
by: ['type'],
where: whereCondition,
_count: {
id: true
},
orderBy: {
_count: {
id: 'desc'
}
}
}),
userId ? req.prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
username: true,
firstName: true,
lastName: true,
email: true,
createdAt: true,
lastLoginAt: true,
_count: {
select: {
ownedLists: { where: { isActive: true } },
sharedLists: true,
activities: {
where: {
createdAt: {
gte: new Date(startDate),
lte: new Date(endDate)
}
}
}
}
}
}
}) : null
]);
const activityTypeStats = {};
activityCounts.forEach(item => {
activityTypeStats[item.type] = item._count.id;
});
res.json({
success: true,
data: {
period: {
startDate,
endDate
},
user: userStats,
activities: activities,
statistics: {
totalActivities: activities.length,
byType: activityTypeStats
}
}
});
}));
module.exports = router;

420
backend/src/routes/auth.js Normal file
View File

@@ -0,0 +1,420 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { body, validationResult } = require('express-validator');
const { asyncHandler, formatValidationErrors } = require('../middleware/errorHandler');
const { authenticateToken } = require('../middleware/auth');
const passport = require('../config/passport');
const router = express.Router();
/**
* JWT token oluştur
*/
const generateToken = (userId) => {
return jwt.sign(
{ userId },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
);
};
/**
* Kullanıcı kaydı
* POST /api/auth/register
*/
router.post('/register', [
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Geçerli bir e-posta adresi girin'),
body('firstName')
.isLength({ min: 2, max: 50 })
.withMessage('Ad 2-50 karakter arasında olmalı'),
body('lastName')
.isLength({ min: 2, max: 50 })
.withMessage('Soyad 2-50 karakter arasında olmalı'),
body('password')
.isLength({ min: 6 })
.withMessage('Şifre en az 6 karakter olmalı'),
body('confirmPassword')
.custom((value, { req }) => {
if (value !== req.body.password) {
throw new Error('Şifreler eşleşmiyor');
}
return true;
})
], 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 { email, firstName, lastName, password } = req.body;
// E-posta benzersizlik kontrolü
const existingUser = await req.prisma.user.findFirst({
where: {
email: email
}
});
if (existingUser) {
return res.status(400).json({
success: false,
message: 'E-posta zaten kullanımda'
});
}
// Email'den username oluştur (@ işaretinden önceki kısım)
const username = email.split('@')[0];
// Username benzersizlik kontrolü
const existingUsername = await req.prisma.user.findFirst({
where: {
username: username
}
});
if (existingUsername) {
return res.status(400).json({
success: false,
message: 'Bu kullanıcı adı zaten kullanımda'
});
}
// Şifreyi hashle
const hashedPassword = await bcrypt.hash(password, 12);
// Kullanıcıyı oluştur
const user = await req.prisma.user.create({
data: {
email,
username,
firstName,
lastName,
password: hashedPassword
},
select: {
id: true,
email: true,
firstName: true,
lastName: true,
avatar: true,
isAdmin: true,
createdAt: true
}
});
// JWT token oluştur
const token = generateToken(user.id);
res.status(201).json({
success: true,
message: 'Kullanıcı başarıyla oluşturuldu',
data: {
user,
token
}
});
}));
/**
* Kullanıcı girişi
* POST /api/auth/login
*/
router.post('/login', [
body('login')
.notEmpty()
.withMessage('E-posta veya kullanıcı adı gerekli'),
body('password')
.notEmpty()
.withMessage('Şifre gerekli')
], 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 { login, password } = req.body;
// Kullanıcıyı bul (e-posta veya kullanıcı adı ile)
const user = await req.prisma.user.findFirst({
where: {
OR: [
{ email: login },
{ username: login }
],
isActive: true
}
});
if (!user) {
return res.status(401).json({
success: false,
message: 'Geçersiz giriş bilgileri'
});
}
// Şifreyi kontrol et
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: 'Geçersiz giriş bilgileri'
});
}
// Son giriş tarihini güncelle
await req.prisma.user.update({
where: { id: user.id },
data: { lastLoginAt: new Date() }
});
// JWT token oluştur
const token = generateToken(user.id);
// Şifreyi yanıttan çıkar
const { password: _, ...userWithoutPassword } = user;
res.json({
success: true,
message: 'Giriş başarılı',
data: {
user: userWithoutPassword,
token
}
});
}));
/**
* Profil bilgilerini getir
* GET /api/auth/profile
*/
router.get('/profile', authenticateToken, asyncHandler(async (req, res) => {
const user = await req.prisma.user.findUnique({
where: { id: req.user.id },
select: {
id: true,
email: true,
username: true,
firstName: true,
lastName: true,
avatar: true,
isAdmin: true,
createdAt: true,
lastLoginAt: true,
_count: {
select: {
ownedLists: true,
sharedLists: true,
notifications: {
where: { isRead: false }
}
}
}
}
});
res.json({
success: true,
data: { user }
});
}));
/**
* Profil güncelleme
* PUT /api/auth/profile
*/
router.put('/profile', authenticateToken, [
body('firstName')
.optional()
.isLength({ min: 2, max: 50 })
.withMessage('Ad 2-50 karakter arasında olmalı'),
body('lastName')
.optional()
.isLength({ min: 2, max: 50 })
.withMessage('Soyad 2-50 karakter arasında olmalı'),
body('username')
.optional()
.isLength({ min: 3, max: 20 })
.matches(/^[a-zA-Z0-9_]+$/)
.withMessage('Kullanıcı adı 3-20 karakter olmalı ve sadece harf, rakam ve alt çizgi içermeli')
], 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 { firstName, lastName, username } = req.body;
const updateData = {};
if (firstName) updateData.firstName = firstName;
if (lastName) updateData.lastName = lastName;
if (username) {
// Kullanıcı adı benzersizlik kontrolü
const existingUser = await req.prisma.user.findFirst({
where: {
username: username,
id: { not: req.user.id }
}
});
if (existingUser) {
return res.status(400).json({
success: false,
message: 'Bu kullanıcı adı zaten kullanımda'
});
}
updateData.username = username;
}
const updatedUser = await req.prisma.user.update({
where: { id: req.user.id },
data: updateData,
select: {
id: true,
email: true,
username: true,
firstName: true,
lastName: true,
avatar: true,
isAdmin: true,
createdAt: true,
updatedAt: true
}
});
res.json({
success: true,
message: 'Profil başarıyla güncellendi',
data: { user: updatedUser }
});
}));
/**
* Şifre değiştirme
* PUT /api/auth/change-password
*/
router.put('/change-password', authenticateToken, [
body('currentPassword')
.notEmpty()
.withMessage('Mevcut şifre gerekli'),
body('newPassword')
.isLength({ min: 6 })
.withMessage('Yeni şifre en az 6 karakter 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 { currentPassword, newPassword } = req.body;
// Mevcut kullanıcıyı şifresi ile birlikte getir
const user = await req.prisma.user.findUnique({
where: { id: req.user.id }
});
// Mevcut şifreyi kontrol et
const isCurrentPasswordValid = await bcrypt.compare(currentPassword, user.password);
if (!isCurrentPasswordValid) {
return res.status(400).json({
success: false,
message: 'Mevcut şifre yanlış'
});
}
// Yeni şifreyi hashle
const hashedNewPassword = await bcrypt.hash(newPassword, 12);
// Şifreyi güncelle
await req.prisma.user.update({
where: { id: req.user.id },
data: { password: hashedNewPassword }
});
res.json({
success: true,
message: 'Şifre başarıyla değiştirildi'
});
}));
/**
* Token doğrulama
* POST /api/auth/verify-token
*/
router.post('/verify-token', authenticateToken, (req, res) => {
res.json({
success: true,
message: 'Token geçerli',
data: { user: req.user }
});
});
/**
* Google OAuth başlat
* GET /api/auth/google
*/
router.get('/google',
passport.authenticate('google', {
scope: ['profile', 'email']
})
);
/**
* Google OAuth callback
* GET /api/auth/google/callback
*/
router.get('/google/callback',
passport.authenticate('google', {
failureRedirect: `${process.env.FRONTEND_URL}/login?error=oauth_failed`,
session: false
}),
async (req, res) => {
try {
// Token oluştur
const token = generateToken(req.user.id);
// Frontend'e token ile redirect et
res.redirect(`${process.env.FRONTEND_URL}/login?token=${token}&success=true`);
} catch (error) {
console.error('Google callback error:', error);
res.redirect(`${process.env.FRONTEND_URL}/login?error=callback_failed`);
}
}
);
/**
* Çıkış yap
* POST /api/auth/logout
*/
router.post('/logout', authenticateToken, (req, res) => {
res.json({
success: true,
message: 'Başarıyla çıkış yapıldı'
});
});
module.exports = router;

View File

@@ -0,0 +1,602 @@
const express = require('express');
const { body, validationResult, param, query } = require('express-validator');
const { asyncHandler, formatValidationErrors } = require('../middleware/errorHandler');
const { authenticateToken, requireAdmin } = require('../middleware/auth');
const router = express.Router();
/**
* Tüm kategorileri listele
* GET /api/categories
*/
router.get('/', [
authenticateToken,
query('includeInactive').optional().isBoolean().withMessage('includeInactive boolean değer olmalı')
], asyncHandler(async (req, res) => {
const { includeInactive = false } = req.query;
const whereCondition = {};
// Admin değilse sadece aktif kategorileri göster
if (!req.user.isAdmin || !includeInactive) {
whereCondition.isActive = true;
}
const categories = await req.prisma.category.findMany({
where: whereCondition,
include: {
_count: {
select: {
products: {
where: { isActive: true }
}
}
}
},
orderBy: {
name: 'asc'
}
});
// Ürün sayısını ekle
const categoriesWithCount = categories.map(category => ({
...category,
productCount: category._count.products,
_count: undefined
}));
res.json({
success: true,
data: { categories: categoriesWithCount }
});
}));
/**
* Kategori detayı getir
* GET /api/categories/:categoryId
*/
router.get('/:categoryId', [
authenticateToken,
param('categoryId').isUUID().withMessage('Geçerli bir kategori ID\'si girin')
], asyncHandler(async (req, res) => {
const { categoryId } = req.params;
const whereCondition = { id: categoryId };
// Admin değilse sadece aktif kategorileri göster
if (!req.user.isAdmin) {
whereCondition.isActive = true;
}
const category = await req.prisma.category.findUnique({
where: whereCondition,
include: {
_count: {
select: {
products: {
where: { isActive: true }
}
}
}
}
});
if (!category) {
return res.status(404).json({
success: false,
message: 'Kategori bulunamadı'
});
}
res.json({
success: true,
data: {
category: {
...category,
productCount: category._count.products,
_count: undefined
}
}
});
}));
/**
* Kategoriye ait ürünleri listele
* GET /api/categories/:categoryId/products
*/
router.get('/:categoryId/products', [
authenticateToken,
param('categoryId').isUUID().withMessage('Geçerli bir kategori ID\'si girin'),
query('page').optional().isInt({ min: 1 }).withMessage('Sayfa numarası pozitif bir sayı olmalı'),
query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('Limit 1-100 arasında olmalı'),
query('search').optional().isLength({ min: 2 }).withMessage('Arama terimi en az 2 karakter 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 { categoryId } = req.params;
const { page = 1, limit = 20, search } = req.query;
const skip = (page - 1) * limit;
// Kategori kontrolü
const categoryWhereCondition = { id: categoryId };
if (!req.user.isAdmin) {
categoryWhereCondition.isActive = true;
}
const category = await req.prisma.category.findUnique({
where: categoryWhereCondition
});
if (!category) {
return res.status(404).json({
success: false,
message: 'Kategori bulunamadı'
});
}
// Ürün filtreleme koşulları
const whereCondition = {
categoryId,
isActive: true
};
if (search) {
whereCondition.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ barcode: { contains: search, mode: 'insensitive' } }
];
}
const [products, totalCount] = await Promise.all([
req.prisma.product.findMany({
where: whereCondition,
include: {
priceHistory: {
orderBy: {
createdAt: 'desc'
},
take: 1,
select: {
price: true,
createdAt: true
}
},
_count: {
select: {
listItems: {
where: {
list: {
isActive: true
}
}
}
}
}
},
orderBy: {
name: 'asc'
},
skip: parseInt(skip),
take: parseInt(limit)
}),
req.prisma.product.count({ where: whereCondition })
]);
// Son fiyatları ekle
const productsWithPrice = products.map(product => ({
...product,
currentPrice: product.priceHistory[0]?.price || null,
lastPriceUpdate: product.priceHistory[0]?.createdAt || null,
usageCount: product._count.listItems,
priceHistory: undefined,
_count: undefined
}));
res.json({
success: true,
data: {
category: {
id: category.id,
name: category.name,
color: category.color
},
products: productsWithPrice,
pagination: {
currentPage: parseInt(page),
totalPages: Math.ceil(totalCount / limit),
totalCount,
hasNext: skip + parseInt(limit) < totalCount,
hasPrev: page > 1
}
}
});
}));
/**
* Yeni kategori oluştur (Admin)
* POST /api/categories
*/
router.post('/', [
authenticateToken,
requireAdmin,
body('name')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Kategori adı 2-50 karakter arasında olmalı'),
body('color')
.matches(/^#[0-9A-F]{6}$/i)
.withMessage('Geçerli bir hex renk kodu girin (örn: #FF5722)'),
body('description')
.optional({ checkFalsy: true })
.trim()
.isLength({ max: 200 })
.withMessage('Açıklama en fazla 200 karakter olmalı')
, body('icon')
.optional({ checkFalsy: true })
.isString()
.isLength({ max: 50 })
.withMessage('Icon en fazla 50 karakter 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, color, description, icon } = req.body;
// Kategori adı benzersizlik kontrolü
const existingCategory = await req.prisma.category.findFirst({
where: {
name: name,
isActive: true
}
});
if (existingCategory) {
return res.status(400).json({
success: false,
message: 'Bu isimde bir kategori zaten mevcut'
});
}
const category = await req.prisma.category.create({
data: {
name,
color,
description: description || null,
icon: icon || null
}
});
res.status(201).json({
success: true,
message: 'Kategori başarıyla oluşturuldu',
data: {
category: {
...category,
productCount: 0
}
}
});
}));
/**
* Kategori güncelle (Admin)
* PUT /api/categories/:categoryId
*/
router.put('/:categoryId', [
authenticateToken,
requireAdmin,
// Prisma'da ID'ler CUID formatında String; bu yüzden UUID doğrulaması kaldırıldı
param('categoryId').isString().withMessage('Geçerli bir kategori ID\'si girin'),
body('name')
.optional({ checkFalsy: true })
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Kategori adı 2-50 karakter arasında olmalı'),
body('color')
.optional({ checkFalsy: true })
.matches(/^#[0-9A-F]{6}$/i)
.withMessage('Geçerli bir hex renk kodu girin (örn: #FF5722)'),
body('description')
.optional({ checkFalsy: true })
.trim()
.isLength({ max: 200 })
.withMessage('Açıklama en fazla 200 karakter olmalı'),
body('icon')
.optional({ checkFalsy: true })
.isString()
.isLength({ max: 50 })
.withMessage('Icon en fazla 50 karakter 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 { categoryId } = req.params;
const { name, color, description, icon } = req.body;
// Kategori kontrolü
const existingCategory = await req.prisma.category.findUnique({
where: { id: categoryId }
});
if (!existingCategory) {
return res.status(404).json({
success: false,
message: 'Kategori bulunamadı'
});
}
// Kategori adı benzersizlik kontrolü
if (name && name !== existingCategory.name) {
const duplicateCategory = await req.prisma.category.findFirst({
where: {
name: {
equals: name,
mode: 'insensitive'
},
isActive: true,
id: { not: categoryId }
}
});
if (duplicateCategory) {
return res.status(400).json({
success: false,
message: 'Bu isimde bir kategori zaten mevcut'
});
}
}
const updateData = {};
if (name) updateData.name = name;
if (color) updateData.color = color;
if (description !== undefined) updateData.description = description;
if (icon !== undefined) updateData.icon = icon || null;
const updatedCategory = await req.prisma.category.update({
where: { id: categoryId },
data: updateData,
include: {
_count: {
select: {
products: {
where: { isActive: true }
}
}
}
}
});
res.json({
success: true,
message: 'Kategori başarıyla güncellendi',
data: {
category: {
...updatedCategory,
productCount: updatedCategory._count.products,
_count: undefined
}
}
});
}));
/**
* Kategori durumunu güncelle (Admin)
* PUT /api/categories/:categoryId/status
*/
router.put('/:categoryId/status', [
authenticateToken,
requireAdmin,
// CUID string ID desteği
param('categoryId').isString().withMessage('Geçerli bir kategori ID\'si girin'),
body('isActive').isBoolean().withMessage('Durum boolean değer 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 { categoryId } = req.params;
const { isActive } = req.body;
const category = await req.prisma.category.findUnique({
where: { id: categoryId },
include: {
_count: {
select: {
products: {
where: { isActive: true }
}
}
}
}
});
if (!category) {
return res.status(404).json({
success: false,
message: 'Kategori bulunamadı'
});
}
// Eğer kategori deaktive ediliyorsa ve ürünleri varsa uyarı ver
if (!isActive && category._count.products > 0) {
return res.status(400).json({
success: false,
message: `Bu kategoride ${category._count.products} aktif ürün bulunuyor. Önce ürünleri başka kategorilere taşıyın veya silin.`
});
}
const updatedCategory = await req.prisma.category.update({
where: { id: categoryId },
data: { isActive },
include: {
_count: {
select: {
products: {
where: { isActive: true }
}
}
}
}
});
res.json({
success: true,
message: `Kategori ${isActive ? 'aktif' : 'pasif'} hale getirildi`,
data: {
category: {
...updatedCategory,
productCount: updatedCategory._count.products,
_count: undefined
}
}
});
}));
/**
* Kategori sil (Admin)
* DELETE /api/categories/:categoryId
*/
router.delete('/:categoryId', [
authenticateToken,
requireAdmin,
param('categoryId').isUUID().withMessage('Geçerli bir kategori ID\'si girin')
], asyncHandler(async (req, res) => {
const { categoryId } = req.params;
const category = await req.prisma.category.findUnique({
where: { id: categoryId },
include: {
_count: {
select: {
products: {
where: { isActive: true }
}
}
}
}
});
if (!category) {
return res.status(404).json({
success: false,
message: 'Kategori bulunamadı'
});
}
// Eğer kategoride ürünler varsa silinmesine izin verme
if (category._count.products > 0) {
return res.status(400).json({
success: false,
message: `Bu kategoride ${category._count.products} aktif ürün bulunuyor. Önce ürünleri başka kategorilere taşıyın veya silin.`
});
}
// Soft delete
await req.prisma.category.update({
where: { id: categoryId },
data: { isActive: false }
});
res.json({
success: true,
message: 'Kategori başarıyla silindi'
});
}));
/**
* Kategori istatistikleri (Admin)
* GET /api/categories/stats/overview
*/
router.get('/stats/overview', [
authenticateToken,
requireAdmin
], asyncHandler(async (req, res) => {
const [
totalCategories,
activeCategories,
categoriesWithProducts,
mostUsedCategories
] = await Promise.all([
req.prisma.category.count(),
req.prisma.category.count({ where: { isActive: true } }),
req.prisma.category.count({
where: {
isActive: true,
products: {
some: {
isActive: true
}
}
}
}),
req.prisma.category.findMany({
where: { isActive: true },
include: {
_count: {
select: {
products: {
where: { isActive: true }
}
}
}
},
orderBy: {
products: {
_count: 'desc'
}
},
take: 5
})
]);
const mostUsedCategoriesFormatted = mostUsedCategories.map(category => ({
id: category.id,
name: category.name,
color: category.color,
productCount: category._count.products
}));
res.json({
success: true,
data: {
categories: {
total: totalCategories,
active: activeCategories,
inactive: totalCategories - activeCategories,
withProducts: categoriesWithProducts,
empty: activeCategories - categoriesWithProducts
},
mostUsed: mostUsedCategoriesFormatted
}
});
}));
module.exports = router;

View File

@@ -0,0 +1,154 @@
const express = require('express');
const { PrismaClient } = require('@prisma/client');
const { asyncHandler } = require('../middleware/errorHandler');
const { authenticateToken } = require('../middleware/auth');
const router = express.Router();
const prisma = new PrismaClient();
/**
* Dashboard istatistikleri
* GET /api/dashboard/stats
*/
router.get('/stats', [
authenticateToken
], asyncHandler(async (req, res) => {
const userId = req.user.id;
// Kullanıcının listelerini al
const userLists = await prisma.shoppingList.findMany({
where: {
OR: [
{ ownerId: userId },
{
members: {
some: {
userId: userId
}
}
}
],
isActive: true
},
include: {
items: true
}
});
// İstatistikleri hesapla
const totalLists = userLists.length;
const totalItems = userLists.reduce((sum, list) => sum + list.items.length, 0);
const completedItems = userLists.reduce((sum, list) =>
sum + list.items.filter(item => item.isPurchased).length, 0
);
const pendingItems = totalItems - completedItems;
// Bu ayki harcama
const currentMonth = new Date();
currentMonth.setDate(1);
currentMonth.setHours(0, 0, 0, 0);
const monthlySpending = await prisma.listItem.aggregate({
where: {
isPurchased: true,
purchasedAt: {
gte: currentMonth
},
list: {
OR: [
{ ownerId: userId },
{
members: {
some: {
userId: userId
}
}
}
],
isActive: true
}
},
_sum: {
price: true
}
});
res.json({
success: true,
data: {
totalLists,
totalItems,
completedItems,
pendingItems,
monthlySpending: monthlySpending._sum.price || 0,
completionRate: totalItems > 0 ? Math.round((completedItems / totalItems) * 100) : 0
}
});
}));
/**
* Son aktiviteler
* GET /api/dashboard/activity
*/
router.get('/activity', [
authenticateToken
], asyncHandler(async (req, res) => {
const userId = req.user.id;
const { limit = 10 } = req.query;
// Son eklenen ürünler
const recentItems = await prisma.listItem.findMany({
where: {
list: {
OR: [
{ ownerId: userId },
{
members: {
some: {
userId: userId
}
}
}
],
isActive: true
}
},
include: {
list: {
select: {
id: true,
name: true
}
},
product: {
select: {
id: true,
name: true
}
}
},
orderBy: {
createdAt: 'desc'
},
take: parseInt(limit)
});
// Aktiviteleri formatla
const activities = recentItems.map(item => ({
id: item.id,
type: item.isPurchased ? 'item_purchased' : 'item_added',
message: item.isPurchased
? `${item.product?.name || item.name} satın alındı`
: `${item.product?.name || item.name} listeye eklendi`,
listName: item.list.name,
listId: item.list.id,
createdAt: item.isPurchased ? item.purchasedAt : item.createdAt
}));
res.json({
success: true,
data: { activities }
});
}));
module.exports = router;

651
backend/src/routes/items.js Normal file
View File

@@ -0,0 +1,651 @@
const express = require('express');
const { authenticateToken, checkListMembership, requireListEditPermission } = require('../middleware/auth');
const { asyncHandler } = require('../middleware/errorHandler');
const {
validateListItemCreation,
validateListItemUpdate,
validateUUIDParam,
validateCuidParam,
validateCuid,
validatePagination
} = require('../utils/validators');
const { validationResult, param } = require('express-validator');
const { successResponse, errorResponse, calculatePagination, createPaginationMeta } = require('../utils/helpers');
const notificationService = require('../services/notificationService');
const router = express.Router();
/**
* Liste öğelerini getir
* GET /api/items/:listId
*/
router.get('/:listId',
authenticateToken,
param('listId').custom(validateCuid).withMessage('Geçerli bir liste ID\'si girin'),
validatePagination,
checkListMembership,
asyncHandler(async (req, res) => {
console.log('🔍 Items API called with params:', req.params);
console.log('🔍 Items API called with query:', req.query);
const errors = validationResult(req);
if (!errors.isEmpty()) {
console.log('❌ Validation errors:', errors.array());
return res.status(400).json(errorResponse('Doğrulama hatası', errors.array()));
}
const { listId } = req.params;
const { page = 1, limit = 50, category, purchased, search } = req.query;
const { skip, take } = calculatePagination(page, limit);
// Filtreleme koşulları
const where = {
listId
};
if (category) {
where.product = {
categoryId: category
};
}
if (purchased !== undefined) {
where.isPurchased = purchased === 'true';
}
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ notes: { contains: search, mode: 'insensitive' } },
{ product: { name: { contains: search, mode: 'insensitive' } } }
];
}
console.log('🔍 Database where conditions:', JSON.stringify(where, null, 2));
console.log('🔍 Pagination - skip:', skip, 'take:', take);
// Toplam sayı ve öğeleri getir
const [total, items] = await Promise.all([
req.prisma.listItem.count({ where }),
req.prisma.listItem.findMany({
where,
skip,
take,
include: {
product: {
include: {
category: true
}
}
},
orderBy: [
{ isPurchased: 'asc' },
{ createdAt: 'desc' }
]
})
]);
const meta = createPaginationMeta(total, parseInt(page), parseInt(limit));
// Priority değerlerini string'e çevir
const itemsWithStringPriority = items.map(item => ({
...item,
priority: item.priority === 0 ? 'LOW' : item.priority === 1 ? 'MEDIUM' : 'HIGH'
}));
res.json(successResponse('Liste öğeleri başarıyla getirildi', itemsWithStringPriority, meta));
})
);
/**
* Liste öğesi detayını getir
* GET /api/items/:listId/:itemId
*/
router.get('/:listId/:itemId',
authenticateToken,
param('listId').custom(validateCuid).withMessage('Geçerli bir liste ID\'si girin'),
param('itemId').custom(validateCuid).withMessage('Geçerli bir öğe ID\'si girin'),
checkListMembership,
asyncHandler(async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json(errorResponse('Doğrulama hatası', errors.array()));
}
const { listId, itemId } = req.params;
const item = await req.prisma.listItem.findFirst({
where: {
id: itemId,
listId
},
include: {
product: {
include: {
category: true,
priceHistory: {
orderBy: { createdAt: 'desc' },
take: 10
}
}
},
addedBy: {
select: {
id: true,
username: true,
firstName: true,
lastName: true
}
}
}
});
if (!item) {
return res.status(404).json(errorResponse('Liste öğesi bulunamadı'));
}
// Priority değerini string'e çevir
const itemWithStringPriority = {
...item,
priority: item.priority === 0 ? 'LOW' : item.priority === 1 ? 'MEDIUM' : 'HIGH'
};
res.json(successResponse('Liste öğesi detayı başarıyla getirildi', itemWithStringPriority));
})
);
/**
* Listeye öğe ekle
* POST /api/items/:listId
*/
router.post('/:listId',
authenticateToken,
param('listId').custom(validateCuid).withMessage('Geçerli bir liste ID\'si girin'),
validateListItemCreation,
checkListMembership,
requireListEditPermission,
asyncHandler(async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json(errorResponse('Doğrulama hatası', errors.array()));
}
const { listId } = req.params;
const { name, quantity = 1, unit, notes, productId, estimatedPrice } = req.body;
const userId = req.user.id;
// Eğer productId verilmişse, ürünün var olduğunu kontrol et
let product = null;
if (productId) {
product = await req.prisma.product.findUnique({
where: { id: productId }
});
if (!product) {
return res.status(404).json(errorResponse('Ürün bulunamadı'));
}
}
// Aynı öğenin listede zaten var olup olmadığını kontrol et
const existingItem = await req.prisma.listItem.findFirst({
where: {
listId,
OR: [
{ productId: productId || undefined },
{ customName: productId ? undefined : name }
]
}
});
if (existingItem) {
return res.status(409).json(errorResponse('Bu öğe zaten listede mevcut'));
}
// Yeni öğe oluştur
const newItem = await req.prisma.listItem.create({
data: {
customName: productId ? product.name : name,
quantity,
unit: unit || "adet",
note: notes,
price: estimatedPrice,
listId,
productId
},
include: {
product: {
include: {
category: true
}
}
}
});
// Ürün kullanım sayısını artır (Product modelinde usageCount alanı yok, bu özellik kaldırıldı)
// Liste güncelleme tarihini güncelle
await req.prisma.shoppingList.update({
where: { id: listId },
data: { updatedAt: new Date() }
});
// Aktivite kaydı oluştur (geçici olarak devre dışı)
// await req.prisma.activity.create({
// data: {
// action: 'item_added',
// details: {
// itemId: newItem.id,
// itemName: newItem.customName || newItem.product?.name || 'Öğe',
// userName: `${req.user.firstName} ${req.user.lastName}`
// },
// userId,
// listId
// }
// });
// Liste üyelerine bildirim gönder (geçici olarak devre dışı)
// await notificationService.notifyListMembers(
// listId,
// userId,
// 'ITEM_ADDED',
// `${req.user.firstName} ${req.user.lastName} listeye "${newItem.customName || newItem.product?.name || 'Öğe'}" öğesini ekledi`,
// { itemId: newItem.id, itemName: newItem.customName || newItem.product?.name }
// );
// Socket.IO ile gerçek zamanlı güncelleme (geçici olarak devre dışı)
// const io = req.app.get('io');
// if (io) {
// io.to(`list_${listId}`).emit('itemAdded', {
// item: newItem,
// addedBy: req.user
// });
// }
// Priority değerini string'e çevir
const newItemWithStringPriority = {
...newItem,
priority: newItem.priority === 0 ? 'LOW' : newItem.priority === 1 ? 'MEDIUM' : 'HIGH'
};
res.status(201).json(successResponse('Öğe başarıyla eklendi', newItemWithStringPriority));
})
);
/**
* Liste öğesini güncelle
* PUT /api/items/:listId/:itemId
*/
router.put('/:listId/:itemId',
authenticateToken,
param('listId').custom(validateCuid).withMessage('Geçerli bir liste ID\'si girin'),
param('itemId').custom(validateCuid).withMessage('Geçerli bir öğe ID\'si girin'),
validateListItemUpdate,
checkListMembership,
// Sadece isPurchased güncellemesi değilse edit yetkisi gerekli
(req, res, next) => {
const { name, quantity, unit, notes, price, priority } = req.body;
const isOnlyPurchaseUpdate = !name && !quantity && !unit && !notes && !price && !priority;
if (isOnlyPurchaseUpdate) {
// Sadece isPurchased güncellemesi - tüm üyeler yapabilir
return next();
} else {
// Diğer alanlar güncelleniyor - edit yetkisi gerekli
const allowedRoles = ['owner', 'admin'];
if (!allowedRoles.includes(req.userRole)) {
return res.status(403).json({
success: false,
message: 'Bu işlem için yeterli yetkiniz yok.'
});
}
return next();
}
},
asyncHandler(async (req, res) => {
console.log('🔍 PUT /api/items/:listId/:itemId başladı');
console.log('📝 Request params:', req.params);
console.log('📝 Request body:', req.body);
console.log('👤 User ID:', req.user.id);
const errors = validationResult(req);
if (!errors.isEmpty()) {
console.log('❌ Validation errors:', errors.array());
return res.status(400).json(errorResponse('Doğrulama hatası', errors.array()));
}
const { listId, itemId } = req.params;
const { name, quantity, unit, notes, isPurchased, price, priority } = req.body;
const userId = req.user.id;
// Öğenin var olduğunu kontrol et
console.log('🔍 Öğe aranıyor:', { itemId, listId });
const existingItem = await req.prisma.listItem.findFirst({
where: {
id: itemId,
listId
},
include: {
product: true
}
});
if (!existingItem) {
console.log('❌ Öğe bulunamadı');
return res.status(404).json(errorResponse('Liste öğesi bulunamadı'));
}
console.log('✅ Öğe bulundu:', existingItem.name);
// Güncelleme verilerini hazırla
const updateData = {};
if (name !== undefined) updateData.customName = name;
if (quantity !== undefined) updateData.quantity = quantity;
if (unit !== undefined) updateData.unit = unit;
if (notes !== undefined) updateData.note = notes;
if (price !== undefined) updateData.price = price;
if (priority !== undefined) {
// Priority string'i sayıya çevir
const priorityMap = { 'LOW': 0, 'MEDIUM': 1, 'HIGH': 2 };
updateData.priority = priorityMap[priority] !== undefined ? priorityMap[priority] : 1;
}
// Satın alma durumu değişikliği
if (isPurchased !== undefined && isPurchased !== existingItem.isPurchased) {
updateData.isPurchased = isPurchased;
if (isPurchased) {
updateData.purchasedAt = new Date();
updateData.purchasedBy = userId;
} else {
updateData.purchasedAt = null;
updateData.purchasedBy = null;
}
}
// Öğeyi güncelle
const updatedItem = await req.prisma.listItem.update({
where: { id: itemId },
data: updateData,
include: {
product: {
include: {
category: true
}
}
}
});
// Fiyat geçmişi ekle (eğer fiyat girilmişse ve ürün varsa)
if (price && existingItem.productId && isPurchased) {
await req.prisma.priceHistory.create({
data: {
price: price,
productId: existingItem.productId,
userId,
location: 'Market' // Varsayılan konum
}
});
}
// Liste güncelleme tarihini güncelle
await req.prisma.shoppingList.update({
where: { id: listId },
data: { updatedAt: new Date() }
});
// Aktivite kaydı oluştur
let activityDescription = `${req.user.firstName} ${req.user.lastName} "${updatedItem.name}" öğesini güncelledi`;
if (isPurchased !== undefined) {
activityDescription = isPurchased
? `${req.user.firstName} ${req.user.lastName} "${updatedItem.name}" öğesini satın aldı`
: `${req.user.firstName} ${req.user.lastName} "${updatedItem.name}" öğesinin satın alma durumunu iptal etti`;
}
await req.prisma.activity.create({
data: {
action: isPurchased !== undefined ? (isPurchased ? 'ITEM_PURCHASED' : 'ITEM_UNPURCHASED') : 'ITEM_UPDATED',
details: {
description: activityDescription,
itemId: updatedItem.id,
itemName: updatedItem.name
},
userId,
listId
}
});
// Liste üyelerine bildirim gönder (sadece satın alma durumu değişikliğinde)
if (isPurchased !== undefined) {
await notificationService.notifyListMembers(
listId,
userId,
{
type: isPurchased ? 'ITEM_PURCHASED' : 'ITEM_UNPURCHASED',
message: activityDescription,
data: { itemId: updatedItem.id, itemName: updatedItem.name }
}
);
}
// Socket.IO ile gerçek zamanlı güncelleme
const io = req.app.get('io');
if (io) {
io.to(`list_${listId}`).emit('itemUpdated', {
item: updatedItem,
updatedBy: req.user
});
}
// Priority değerini string'e çevir
const updatedItemWithStringPriority = {
...updatedItem,
priority: updatedItem.priority === 0 ? 'LOW' : updatedItem.priority === 1 ? 'MEDIUM' : 'HIGH'
};
res.json(successResponse('Öğe başarıyla güncellendi', updatedItemWithStringPriority));
})
);
/**
* Liste öğesini sil
* DELETE /api/items/:listId/:itemId
*/
router.delete('/:listId/:itemId',
authenticateToken,
param('listId').custom(validateCuid).withMessage('Geçerli bir liste ID\'si girin'),
param('itemId').custom(validateCuid).withMessage('Geçerli bir öğe ID\'si girin'),
checkListMembership,
requireListEditPermission,
asyncHandler(async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json(errorResponse('Doğrulama hatası', errors.array()));
}
const { listId, itemId } = req.params;
const userId = req.user.id;
// Öğenin var olduğunu kontrol et
const existingItem = await req.prisma.listItem.findFirst({
where: {
id: itemId,
listId
}
});
if (!existingItem) {
return res.status(404).json(errorResponse('Liste öğesi bulunamadı'));
}
// Öğeyi sil
await req.prisma.listItem.delete({
where: { id: itemId }
});
// Liste güncelleme tarihini güncelle
await req.prisma.shoppingList.update({
where: { id: listId },
data: { updatedAt: new Date() }
});
// Aktivite kaydı oluştur (Prisma şemasına uygun)
const itemName = existingItem.customName || existingItem.product?.name || 'Öğe';
await req.prisma.activity.create({
data: {
action: 'ITEM_REMOVED',
details: {
description: `${req.user.firstName} ${req.user.lastName} "${itemName}" öğesini listeden kaldırdı`,
itemId: existingItem.id,
itemName: itemName
},
userId,
listId
}
});
// Liste üyelerine bildirim gönder
await notificationService.notifyListMembers(
listId,
userId,
{
type: 'ITEM_REMOVED',
message: `${req.user.firstName} ${req.user.lastName} "${itemName}" öğesini listeden kaldırdı`,
data: { itemId: existingItem.id, itemName: itemName }
}
);
// Socket.IO ile gerçek zamanlı güncelleme
const io = req.app.get('io');
if (io) {
io.to(`list_${listId}`).emit('itemRemoved', {
itemId: existingItem.id,
itemName: existingItem.name,
removedBy: req.user
});
}
res.json(successResponse('Öğe başarıyla silindi'));
})
);
/**
* Birden fazla öğeyi toplu güncelle
* PATCH /api/items/:listId/bulk
*/
router.patch('/:listId/bulk',
authenticateToken,
validateCuidParam('listId'),
requireListEditPermission,
asyncHandler(async (req, res) => {
const { listId } = req.params;
const { items, action } = req.body; // items: [itemId1, itemId2], action: 'purchase' | 'unpurchase' | 'delete'
const userId = req.user.id;
if (!items || !Array.isArray(items) || items.length === 0) {
return res.status(400).json(errorResponse('Geçerli öğe listesi gerekli'));
}
if (!['purchase', 'unpurchase', 'delete'].includes(action)) {
return res.status(400).json(errorResponse('Geçerli bir işlem seçin'));
}
// Öğelerin var olduğunu kontrol et
const existingItems = await req.prisma.listItem.findMany({
where: {
id: { in: items },
listId
}
});
if (existingItems.length !== items.length) {
return res.status(404).json(errorResponse('Bazı öğeler bulunamadı'));
}
let updateData = {};
let activityType = '';
let activityDescription = '';
switch (action) {
case 'purchase':
updateData = {
isPurchased: true,
purchasedAt: new Date(),
purchasedById: userId
};
activityType = 'ITEMS_PURCHASED';
activityDescription = `${req.user.firstName} ${req.user.lastName} ${existingItems.length} öğeyi satın aldı`;
break;
case 'unpurchase':
updateData = {
isPurchased: false,
purchasedAt: null,
purchasedBy: null
};
activityType = 'ITEMS_UNPURCHASED';
activityDescription = `${req.user.firstName} ${req.user.lastName} ${existingItems.length} öğenin satın alma durumunu iptal etti`;
break;
case 'delete':
activityType = 'ITEMS_REMOVED';
activityDescription = `${req.user.firstName} ${req.user.lastName} ${existingItems.length} öğeyi listeden kaldırdı`;
break;
}
// Toplu güncelleme veya silme
if (action === 'delete') {
await req.prisma.listItem.deleteMany({
where: {
id: { in: items },
listId
}
});
} else {
await req.prisma.listItem.updateMany({
where: {
id: { in: items },
listId
},
data: updateData
});
}
// Liste güncelleme tarihini güncelle
await req.prisma.shoppingList.update({
where: { id: listId },
data: { updatedAt: new Date() }
});
// Aktivite kaydı oluştur
await req.prisma.activity.create({
data: {
type: activityType,
description: activityDescription,
userId,
listId
}
});
// Liste üyelerine bildirim gönder
await notificationService.notifyListMembers(
listId,
userId,
{
type: activityType,
message: activityDescription,
data: { itemCount: existingItems.length }
}
);
// Socket.IO ile gerçek zamanlı güncelleme
const io = req.app.get('io');
if (io) {
io.to(`list_${listId}`).emit('itemsBulkUpdated', {
items: items,
action: action,
updatedBy: req.user
});
}
res.json(successResponse(`${existingItems.length} öğe başarıyla ${action === 'purchase' ? 'satın alındı' : action === 'unpurchase' ? 'satın alma iptal edildi' : 'silindi'}`));
})
);
module.exports = router;

707
backend/src/routes/lists.js Normal file
View File

@@ -0,0 +1,707 @@
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 (geçici olarak devre dışı)
// 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 (geçici olarak devre dışı)
// if (req.io) {
// 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()) {
console.log("Liste paylasim hatasi detaylari:", errors.array());
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;

View File

@@ -0,0 +1,541 @@
const express = require('express');
const { body, validationResult, param, query } = require('express-validator');
const { asyncHandler, formatValidationErrors } = require('../middleware/errorHandler');
const { authenticateToken, requireAdmin } = require('../middleware/auth');
const router = express.Router();
/**
* Kullanıcının bildirimlerini getir
* GET /api/notifications
*/
router.get('/', [
authenticateToken,
query('page').optional().isInt({ min: 1 }).withMessage('Sayfa numarası pozitif bir sayı olmalı'),
query('limit').optional().isInt({ min: 1, max: 50 }).withMessage('Limit 1-50 arasında olmalı'),
query('unreadOnly').optional().isBoolean().withMessage('unreadOnly boolean değer olmalı'),
query('type').optional().isIn(['list_invite', 'item_added', 'item_removed', 'item_purchased', 'list_shared', 'system']).withMessage('Geçerli bir bildirim türü 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 {
page = 1,
limit = 20,
unreadOnly = false,
type
} = req.query;
const skip = (page - 1) * limit;
// Filtreleme koşulları
const whereCondition = {
userId: req.user.id
};
if (unreadOnly) {
whereCondition.isRead = false;
}
if (type) {
whereCondition.type = type;
}
const [notifications, totalCount, unreadCount] = await Promise.all([
req.prisma.notification.findMany({
where: whereCondition,
include: {
relatedList: {
select: {
id: true,
name: true
}
},
relatedUser: {
select: {
id: true,
username: true,
firstName: true,
lastName: true,
avatar: true
}
}
},
orderBy: {
createdAt: 'desc'
},
skip: parseInt(skip),
take: parseInt(limit)
}),
req.prisma.notification.count({ where: whereCondition }),
req.prisma.notification.count({
where: {
userId: req.user.id,
isRead: false
}
})
]);
res.json({
success: true,
data: {
notifications,
pagination: {
currentPage: parseInt(page),
totalPages: Math.ceil(totalCount / limit),
totalCount,
hasNext: skip + parseInt(limit) < totalCount,
hasPrev: page > 1
},
unreadCount
}
});
}));
/**
* Okunmamış bildirim sayısını getir
* GET /api/notifications/unread-count
*/
router.get('/unread-count', [
authenticateToken
], asyncHandler(async (req, res) => {
const unreadCount = await req.prisma.notification.count({
where: {
userId: req.user.id,
isRead: false
}
});
res.json({
success: true,
data: { unreadCount }
});
}));
/**
* Bildirimi okundu olarak işaretle
* PUT /api/notifications/:notificationId/read
*/
router.put('/:notificationId/read', [
authenticateToken,
param('notificationId').isUUID().withMessage('Geçerli bir bildirim ID\'si girin')
], asyncHandler(async (req, res) => {
const { notificationId } = req.params;
const notification = await req.prisma.notification.findUnique({
where: {
id: notificationId,
userId: req.user.id
}
});
if (!notification) {
return res.status(404).json({
success: false,
message: 'Bildirim bulunamadı'
});
}
const updatedNotification = await req.prisma.notification.update({
where: { id: notificationId },
data: {
isRead: true,
readAt: new Date()
}
});
res.json({
success: true,
message: 'Bildirim okundu olarak işaretlendi',
data: { notification: updatedNotification }
});
}));
/**
* Tüm bildirimleri okundu olarak işaretle
* PUT /api/notifications/mark-all-read
*/
router.put('/mark-all-read', [
authenticateToken
], asyncHandler(async (req, res) => {
const result = await req.prisma.notification.updateMany({
where: {
userId: req.user.id,
isRead: false
},
data: {
isRead: true,
readAt: new Date()
}
});
res.json({
success: true,
message: `${result.count} bildirim okundu olarak işaretlendi`,
data: { updatedCount: result.count }
});
}));
/**
* Bildirimi sil
* DELETE /api/notifications/:notificationId
*/
router.delete('/:notificationId', [
authenticateToken,
param('notificationId').isUUID().withMessage('Geçerli bir bildirim ID\'si girin')
], asyncHandler(async (req, res) => {
const { notificationId } = req.params;
const notification = await req.prisma.notification.findUnique({
where: {
id: notificationId,
userId: req.user.id
}
});
if (!notification) {
return res.status(404).json({
success: false,
message: 'Bildirim bulunamadı'
});
}
await req.prisma.notification.delete({
where: { id: notificationId }
});
res.json({
success: true,
message: 'Bildirim başarıyla silindi'
});
}));
/**
* Okunmuş bildirimleri temizle
* DELETE /api/notifications/clear-read
*/
router.delete('/clear-read', [
authenticateToken
], asyncHandler(async (req, res) => {
const result = await req.prisma.notification.deleteMany({
where: {
userId: req.user.id,
isRead: true
}
});
res.json({
success: true,
message: `${result.count} okunmuş bildirim temizlendi`,
data: { deletedCount: result.count }
});
}));
/**
* Tüm bildirimleri temizle
* DELETE /api/notifications/clear-all
*/
router.delete('/clear-all', [
authenticateToken
], asyncHandler(async (req, res) => {
const result = await req.prisma.notification.deleteMany({
where: {
userId: req.user.id
}
});
res.json({
success: true,
message: `${result.count} bildirim temizlendi`,
data: { deletedCount: result.count }
});
}));
/**
* Bildirim ayarlarını getir
* GET /api/notifications/settings
*/
router.get('/settings', [
authenticateToken
], asyncHandler(async (req, res) => {
const settings = await req.prisma.setting.findMany({
where: {
userId: req.user.id,
key: {
startsWith: 'notification_'
}
}
});
// Varsayılan ayarlar
const defaultSettings = {
notification_list_invite: 'true',
notification_item_added: 'true',
notification_item_removed: 'true',
notification_item_purchased: 'true',
notification_list_shared: 'true',
notification_system: 'true',
notification_push_enabled: 'true',
notification_email_enabled: 'false'
};
// Kullanıcı ayarlarını varsayılanlarla birleştir
const userSettings = {};
settings.forEach(setting => {
userSettings[setting.key] = setting.value;
});
const finalSettings = { ...defaultSettings, ...userSettings };
res.json({
success: true,
data: { settings: finalSettings }
});
}));
/**
* Bildirim ayarlarını güncelle
* PUT /api/notifications/settings
*/
router.put('/settings', [
authenticateToken,
body('settings').isObject().withMessage('Ayarlar obje formatında 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 { settings } = req.body;
// Geçerli ayar anahtarları
const validKeys = [
'notification_list_invite',
'notification_item_added',
'notification_item_removed',
'notification_item_purchased',
'notification_list_shared',
'notification_system',
'notification_push_enabled',
'notification_email_enabled'
];
// Geçersiz anahtarları filtrele
const validSettings = {};
Object.keys(settings).forEach(key => {
if (validKeys.includes(key)) {
validSettings[key] = settings[key].toString();
}
});
if (Object.keys(validSettings).length === 0) {
return res.status(400).json({
success: false,
message: 'Geçerli ayar bulunamadı'
});
}
// Ayarları güncelle veya oluştur
const updatePromises = Object.entries(validSettings).map(([key, value]) =>
req.prisma.setting.upsert({
where: {
userId_key: {
userId: req.user.id,
key
}
},
update: { value },
create: {
userId: req.user.id,
key,
value
}
})
);
await Promise.all(updatePromises);
res.json({
success: true,
message: 'Bildirim ayarları güncellendi',
data: { settings: validSettings }
});
}));
/**
* Sistem bildirimi gönder (Admin)
* POST /api/notifications/system
*/
router.post('/system', [
authenticateToken,
requireAdmin,
body('title')
.trim()
.isLength({ min: 1, max: 100 })
.withMessage('Başlık 1-100 karakter arasında olmalı'),
body('message')
.trim()
.isLength({ min: 1, max: 500 })
.withMessage('Mesaj 1-500 karakter arasında olmalı'),
body('targetUsers')
.optional()
.isArray()
.withMessage('Hedef kullanıcılar dizi formatında olmalı'),
body('targetUsers.*')
.optional()
.isUUID()
.withMessage('Geçerli kullanıcı ID\'leri girin')
], 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 { title, message, targetUsers } = req.body;
let users;
if (targetUsers && targetUsers.length > 0) {
// Belirli kullanıcılara gönder
users = await req.prisma.user.findMany({
where: {
id: { in: targetUsers },
isActive: true
},
select: { id: true }
});
} else {
// Tüm aktif kullanıcılara gönder
users = await req.prisma.user.findMany({
where: { isActive: true },
select: { id: true }
});
}
if (users.length === 0) {
return res.status(400).json({
success: false,
message: 'Hedef kullanıcı bulunamadı'
});
}
// Bildirimleri oluştur
const notifications = users.map(user => ({
userId: user.id,
type: 'system',
title,
message,
data: JSON.stringify({
adminId: req.user.id,
timestamp: new Date().toISOString()
})
}));
const result = await req.prisma.notification.createMany({
data: notifications
});
// Socket.IO ile gerçek zamanlı bildirim gönder
if (req.io) {
users.forEach(user => {
req.io.to(`user_${user.id}`).emit('notification', {
type: 'system',
title,
message,
createdAt: new Date()
});
});
}
res.status(201).json({
success: true,
message: `${result.count} kullanıcıya sistem bildirimi gönderildi`,
data: { sentCount: result.count }
});
}));
/**
* Bildirim istatistikleri (Admin)
* GET /api/notifications/stats
*/
router.get('/stats/overview', [
authenticateToken,
requireAdmin
], asyncHandler(async (req, res) => {
const [
totalNotifications,
unreadNotifications,
notificationsByType,
recentNotifications
] = await Promise.all([
req.prisma.notification.count(),
req.prisma.notification.count({ where: { isRead: false } }),
req.prisma.notification.groupBy({
by: ['type'],
_count: {
id: true
},
orderBy: {
_count: {
id: 'desc'
}
}
}),
req.prisma.notification.findMany({
where: {
createdAt: {
gte: new Date(Date.now() - 24 * 60 * 60 * 1000) // Son 24 saat
}
},
include: {
user: {
select: {
username: true,
firstName: true,
lastName: true
}
}
},
orderBy: {
createdAt: 'desc'
},
take: 10
})
]);
const typeStats = {};
notificationsByType.forEach(item => {
typeStats[item.type] = item._count.id;
});
res.json({
success: true,
data: {
overview: {
total: totalNotifications,
unread: unreadNotifications,
read: totalNotifications - unreadNotifications
},
byType: typeStats,
recent: recentNotifications
}
});
}));
module.exports = router;

View File

@@ -0,0 +1,722 @@
const express = require('express');
const { body, validationResult, param, query } = require('express-validator');
const { asyncHandler, formatValidationErrors } = require('../middleware/errorHandler');
const { authenticateToken, requireAdmin } = require('../middleware/auth');
const {
validateProductCreation,
validatePagination,
validateSearch,
validateDateRange,
validateUUIDParam,
validateCuid
} = require('../utils/validators');
const router = express.Router();
/**
* Ürün arama
* GET /api/products/search
*/
router.get('/search', [
authenticateToken,
query('q')
.isLength({ min: 2 })
.withMessage('Arama terimi en az 2 karakter olmalı'),
query('categoryId').optional().custom(validateCuid).withMessage('Geçerli bir kategori ID\'si girin'),
query('limit').optional().isInt({ min: 1, max: 50 }).withMessage('Limit 1-50 arasında olmalı')
], asyncHandler(async (req, res) => {
console.log('🔍 Products API called with query:', req.query);
// Validation hatalarını kontrol et
const errors = validationResult(req);
if (!errors.isEmpty()) {
console.log('❌ Products validation errors:', errors.array());
return res.status(400).json({
success: false,
message: 'Girilen bilgilerde hatalar var',
errors: formatValidationErrors(errors)
});
}
const { q, categoryId, limit = 20 } = req.query;
const whereCondition = {
isActive: true,
OR: [
{
name: {
contains: q
}
},
{
barcode: {
contains: q
}
}
]
};
if (categoryId) {
whereCondition.categoryId = categoryId;
}
const products = await req.prisma.product.findMany({
where: whereCondition,
include: {
category: {
select: {
id: true,
name: true,
color: true,
icon: true
}
},
priceHistory: {
orderBy: {
createdAt: 'desc'
},
take: 1,
select: {
price: true,
createdAt: true
}
}
},
take: parseInt(limit),
orderBy: [
{ name: 'asc' }
]
});
// Son fiyatı ekle
const productsWithPrice = products.map(product => ({
...product,
currentPrice: product.priceHistory[0]?.price || null,
lastPriceUpdate: product.priceHistory[0]?.createdAt || null,
priceHistory: undefined // Gereksiz veriyi kaldır
}));
res.json({
success: true,
data: { products: productsWithPrice }
});
}));
/**
* Barkod ile ürün arama
* GET /api/products/barcode/:barcode
*/
router.get('/barcode/:barcode', [
authenticateToken,
param('barcode').isLength({ min: 8, max: 20 }).withMessage('Geçerli bir barkod girin')
], asyncHandler(async (req, res) => {
const { barcode } = req.params;
const product = await req.prisma.product.findFirst({
where: {
barcode,
isActive: true
},
include: {
category: {
select: {
id: true,
name: true,
color: true
}
},
priceHistory: {
orderBy: {
createdAt: 'desc'
},
take: 5,
select: {
price: true,
createdAt: true,
location: true
}
}
}
});
if (!product) {
return res.status(404).json({
success: false,
message: 'Ürün bulunamadı'
});
}
// Son fiyatı ekle
const productWithPrice = {
...product,
currentPrice: product.priceHistory[0]?.price || null,
lastPriceUpdate: product.priceHistory[0]?.createdAt || null
};
res.json({
success: true,
data: { product: productWithPrice }
});
}));
/**
* Ürün detayı getir
* GET /api/products/:productId
*/
router.get('/:productId', [
authenticateToken,
param('productId').custom(validateCuid).withMessage('Geçerli bir ürün ID\'si girin')
], asyncHandler(async (req, res) => {
const { productId } = req.params;
const product = await req.prisma.product.findUnique({
where: {
id: productId,
isActive: true
},
include: {
category: {
select: {
id: true,
name: true,
color: true
}
},
priceHistory: {
orderBy: {
createdAt: 'desc'
},
take: 10,
select: {
price: true,
createdAt: true,
location: true
}
},
_count: {
select: {
listItems: {
where: {
list: {
isActive: true
}
}
}
}
}
}
});
if (!product) {
return res.status(404).json({
success: false,
message: 'Ürün bulunamadı'
});
}
// Son fiyatı ve istatistikleri ekle
const productWithStats = {
...product,
currentPrice: product.priceHistory[0]?.price || null,
lastPriceUpdate: product.priceHistory[0]?.createdAt || null,
usageCount: product._count.listItems
};
res.json({
success: true,
data: { product: productWithStats }
});
}));
/**
* Tüm ürünleri listele
* GET /api/products
*/
router.get('/', [
authenticateToken,
query('page').optional().isInt({ min: 1 }).withMessage('Sayfa numarası pozitif bir sayı olmalı'),
query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('Limit 1-100 arasında olmalı'),
query('search').optional().isLength({ min: 2 }).withMessage('Arama terimi en az 2 karakter olmalı'),
query('categoryId').optional().custom(validateCuid).withMessage('Geçerli bir kategori ID\'si girin'),
query('sortBy').optional().isIn(['name', 'createdAt', 'usageCount']).withMessage('Geçerli bir sıralama kriteri seçin'),
query('sortOrder').optional().isIn(['asc', 'desc']).withMessage('Geçerli bir sıralama yönü 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 {
page = 1,
limit = 20,
search,
categoryId,
sortBy = 'name',
sortOrder = 'asc'
} = req.query;
const skip = (page - 1) * limit;
// Filtreleme koşulları
const whereCondition = {
isActive: true
};
if (search) {
whereCondition.OR = [
{ name: { contains: search } },
{ barcode: { contains: search } }
];
}
if (categoryId) {
whereCondition.categoryId = categoryId;
}
// Sıralama
let orderBy = {};
if (sortBy === 'usageCount') {
orderBy = {
listItems: {
_count: sortOrder
}
};
} else {
orderBy[sortBy] = sortOrder;
}
const [products, totalCount] = await Promise.all([
req.prisma.product.findMany({
where: whereCondition,
include: {
category: {
select: {
id: true,
name: true,
color: true,
icon: true
}
},
priceHistory: {
orderBy: {
createdAt: 'desc'
},
take: 1,
select: {
price: true,
createdAt: true
}
},
_count: {
select: {
listItems: {
where: {
list: {
isActive: true
}
}
}
}
}
},
orderBy,
skip: parseInt(skip),
take: parseInt(limit)
}),
req.prisma.product.count({ where: whereCondition })
]);
// Son fiyatları ekle
const productsWithPrice = products.map(product => ({
...product,
currentPrice: product.priceHistory[0]?.price || null,
lastPriceUpdate: product.priceHistory[0]?.createdAt || null,
usageCount: product._count.listItems,
priceHistory: undefined,
_count: undefined
}));
res.json({
success: true,
data: {
products: productsWithPrice,
pagination: {
currentPage: parseInt(page),
totalPages: Math.ceil(totalCount / limit),
totalCount,
hasNext: skip + parseInt(limit) < totalCount,
hasPrev: page > 1
}
}
});
}));
/**
* Yeni ürün oluştur
* POST /api/products
*/
router.post('/', [
authenticateToken,
body('name')
.trim()
.isLength({ min: 2, max: 100 })
.withMessage('Ürün adı 2-100 karakter arasında olmalı'),
body('barcode')
.optional({ checkFalsy: true })
.isLength({ min: 8, max: 20 })
.withMessage('Barkod 8-20 karakter arasında olmalı'),
body('categoryId')
.custom(validateCuid)
.withMessage('Geçerli bir kategori ID\'si girin'),
body('price')
.optional()
.isFloat({ min: 0 })
.withMessage('Fiyat pozitif bir sayı olmalı'),
body('location')
.optional()
.trim()
.isLength({ max: 100 })
.withMessage('Konum en fazla 100 karakter olmalı'),
body('unit')
.optional()
.trim()
.isLength({ max: 20 })
.withMessage('Birim en fazla 20 karakter 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, barcode, categoryId, price, location, unit } = req.body;
// Kategori kontrolü
const category = await req.prisma.category.findUnique({
where: { id: categoryId, isActive: true }
});
if (!category) {
return res.status(404).json({
success: false,
message: 'Kategori bulunamadı'
});
}
// Barkod benzersizlik kontrolü
if (barcode) {
const existingProduct = await req.prisma.product.findFirst({
where: {
barcode,
isActive: true
}
});
if (existingProduct) {
return res.status(400).json({
success: false,
message: 'Bu barkoda sahip bir ürün zaten mevcut'
});
}
}
// Transaction ile ürün ve fiyat geçmişi oluştur
const result = await req.prisma.$transaction(async (prisma) => {
const product = await prisma.product.create({
data: {
name,
barcode,
categoryId,
unit
},
include: {
category: {
select: {
id: true,
name: true,
color: true
}
}
}
});
// Eğer fiyat verilmişse fiyat geçmişine ekle
if (price !== undefined && price !== null) {
await prisma.priceHistory.create({
data: {
productId: product.id,
price: parseFloat(price),
location: location || null
}
});
}
return product;
});
res.status(201).json({
success: true,
message: 'Ürün başarıyla oluşturuldu',
data: {
product: {
...result,
currentPrice: price || null
}
}
});
}));
/**
* Ürün güncelle
* PUT /api/products/:productId
*/
router.put('/:productId', [
authenticateToken,
param('productId').custom(validateCuid).withMessage('Geçerli bir ürün ID\'si girin'),
body('name')
.optional()
.trim()
.isLength({ min: 2, max: 100 })
.withMessage('Ürün adı 2-100 karakter arasında olmalı'),
body('barcode')
.optional({ checkFalsy: true })
.isLength({ min: 8, max: 20 })
.withMessage('Barkod 8-20 karakter arasında olmalı'),
body('categoryId')
.optional()
.custom(validateCuid)
.withMessage('Geçerli bir kategori ID\'si girin'),
body('description')
.optional()
.trim()
.isLength({ max: 500 })
.withMessage('Açıklama en fazla 500 karakter olabilir'),
body('brand')
.optional()
.trim()
.isLength({ max: 100 })
.withMessage('Marka en fazla 100 karakter olabilir')
], 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 { productId } = req.params;
const { name, barcode, categoryId, description, brand } = req.body;
// Ürün kontrolü
const existingProduct = await req.prisma.product.findUnique({
where: { id: productId, isActive: true }
});
if (!existingProduct) {
return res.status(404).json({
success: false,
message: 'Ürün bulunamadı'
});
}
// Admin değilse sadece kendi oluşturduğu ürünleri güncelleyebilir
if (!req.user.isAdmin && existingProduct.createdBy !== req.user.id) {
return res.status(403).json({
success: false,
message: 'Bu ürünü güncelleme yetkiniz yok'
});
}
// Kategori kontrolü
if (categoryId) {
const category = await req.prisma.category.findUnique({
where: { id: categoryId, isActive: true }
});
if (!category) {
return res.status(404).json({
success: false,
message: 'Kategori bulunamadı'
});
}
}
// Barkod benzersizlik kontrolü
if (barcode && barcode !== existingProduct.barcode) {
const duplicateProduct = await req.prisma.product.findFirst({
where: {
barcode,
isActive: true,
id: { not: productId }
}
});
if (duplicateProduct) {
return res.status(400).json({
success: false,
message: 'Bu barkoda sahip bir ürün zaten mevcut'
});
}
}
const updateData = {};
if (name) updateData.name = name;
if (barcode !== undefined) updateData.barcode = barcode;
if (categoryId) updateData.categoryId = categoryId;
if (description !== undefined) updateData.description = description;
if (brand !== undefined) updateData.brand = brand;
if (unit !== undefined) updateData.unit = unit;
const updatedProduct = await req.prisma.product.update({
where: { id: productId },
data: updateData,
include: {
category: {
select: {
id: true,
name: true,
color: true
}
},
priceHistory: {
orderBy: {
createdAt: 'desc'
},
take: 1,
select: {
price: true,
createdAt: true
}
}
}
});
res.json({
success: true,
message: 'Ürün başarıyla güncellendi',
data: {
product: {
...updatedProduct,
currentPrice: updatedProduct.priceHistory[0]?.price || null,
lastPriceUpdate: updatedProduct.priceHistory[0]?.createdAt || null,
priceHistory: undefined
}
}
});
}));
/**
* Ürün fiyatı güncelle
* POST /api/products/:productId/price
*/
router.post('/:productId/price', [
authenticateToken,
param('productId').custom(validateCuid).withMessage('Geçerli bir ürün ID\'si girin'),
body('price')
.isFloat({ min: 0 })
.withMessage('Fiyat pozitif bir sayı olmalı'),
body('location')
.optional()
.trim()
.isLength({ max: 100 })
.withMessage('Konum en fazla 100 karakter 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 { productId } = req.params;
const { price, location } = req.body;
// Ürün kontrolü
const product = await req.prisma.product.findUnique({
where: { id: productId, isActive: true }
});
if (!product) {
return res.status(404).json({
success: false,
message: 'Ürün bulunamadı'
});
}
// Fiyat geçmişine ekle
const priceHistory = await req.prisma.priceHistory.create({
data: {
productId,
price: parseFloat(price),
location: location || null,
createdBy: req.user.id
}
});
res.status(201).json({
success: true,
message: 'Fiyat başarıyla eklendi',
data: { priceHistory }
});
}));
/**
* Ürün sil
* DELETE /api/products/:productId
*/
router.delete('/:productId', [
authenticateToken,
requireAdmin,
param('productId').custom(validateCuid).withMessage('Geçerli bir ürün ID\'si girin')
], asyncHandler(async (req, res) => {
const { productId } = req.params;
// Ürün kontrolü
const product = await req.prisma.product.findUnique({
where: { id: productId, isActive: true }
});
if (!product) {
return res.status(404).json({
success: false,
message: 'Ürün bulunamadı'
});
}
// Admin değilse sadece kendi oluşturduğu ürünleri silebilir
if (!req.user.isAdmin && product.createdBy !== req.user.id) {
return res.status(403).json({
success: false,
message: 'Bu ürünü silme yetkiniz yok'
});
}
// Soft delete
await req.prisma.product.update({
where: { id: productId },
data: { isActive: false }
});
res.json({
success: true,
message: 'Ürün başarıyla silindi'
});
}));
module.exports = router;

611
backend/src/routes/users.js Normal file
View File

@@ -0,0 +1,611 @@
const express = require('express');
const { body, validationResult, param, query } = require('express-validator');
const { asyncHandler, formatValidationErrors } = require('../middleware/errorHandler');
const { authenticateToken, requireAdmin } = require('../middleware/auth');
const router = express.Router();
/**
* Kullanıcı bildirim/tercih ayarlarını getir
* GET /api/users/settings
* Not: Özel route'u parametre yakalayıcılarından (/:userId) ÖNCE tanımlayın
*/
router.get('/settings', [
authenticateToken,
], asyncHandler(async (req, res) => {
const key = `user:${req.user.id}:settings`;
const setting = await req.prisma.setting.findUnique({ where: { key } });
let settings;
if (setting) {
try {
settings = JSON.parse(setting.value);
} catch (e) {
settings = {};
}
} else {
const now = new Date().toISOString();
settings = {
id: `user-settings-${req.user.id}`,
userId: req.user.id,
emailNotifications: true,
pushNotifications: true,
listInviteNotifications: true,
itemUpdateNotifications: true,
priceAlertNotifications: false,
theme: 'system',
language: 'tr',
currency: 'TL',
timezone: 'Europe/Istanbul',
createdAt: now,
updatedAt: now,
};
}
res.json({
success: true,
data: { settings }
});
}));
/**
* Kullanıcı bildirim/tercih ayarlarını güncelle
* PUT /api/users/settings
* Not: Özel route'u parametre yakalayıcılarından (/:userId) ÖNCE tanımlayın
*/
router.put('/settings', [
authenticateToken,
body('emailNotifications').optional().isBoolean(),
body('pushNotifications').optional().isBoolean(),
body('listInviteNotifications').optional().isBoolean(),
body('itemUpdateNotifications').optional().isBoolean(),
body('priceAlertNotifications').optional().isBoolean(),
body('theme').optional().isIn(['light', 'dark', 'system']),
body('language').optional().isString(),
body('currency').optional().isString(),
body('timezone').optional().isString(),
], asyncHandler(async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Girilen bilgilerde hatalar var',
errors: formatValidationErrors(errors)
});
}
const key = `user:${req.user.id}:settings`;
const existing = await req.prisma.setting.findUnique({ where: { key } });
let current = {};
if (existing) {
try {
current = JSON.parse(existing.value);
} catch (e) {
current = {};
}
}
const now = new Date().toISOString();
const merged = {
...current,
...req.body,
id: current.id || `user-settings-${req.user.id}`,
userId: req.user.id,
updatedAt: now,
createdAt: current.createdAt || now,
};
await req.prisma.setting.upsert({
where: { key },
update: { value: JSON.stringify(merged), type: 'json' },
create: { key, value: JSON.stringify(merged), type: 'json' },
});
res.json({
success: true,
message: 'Ayarlar güncellendi',
data: { settings: merged }
});
}));
/**
* Kullanıcı arama
* GET /api/users/search
*/
router.get('/search', [
authenticateToken,
query('q')
.isLength({ min: 2 })
.withMessage('Arama terimi en az 2 karakter 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 { q, limit = 10 } = req.query;
const users = await req.prisma.user.findMany({
where: {
isActive: true,
OR: [
{
username: {
contains: q,
mode: 'insensitive'
}
},
{
firstName: {
contains: q,
mode: 'insensitive'
}
},
{
lastName: {
contains: q,
mode: 'insensitive'
}
},
{
email: {
contains: q,
mode: 'insensitive'
}
}
]
},
select: {
id: true,
username: true,
firstName: true,
lastName: true,
email: true,
avatar: true,
createdAt: true
},
take: parseInt(limit),
orderBy: {
username: 'asc'
}
});
res.json({
success: true,
data: { users }
});
}));
/**
* Kullanıcı profili getir
* GET /api/users/:userId
*/
router.get('/:userId', [
authenticateToken,
param('userId').isUUID().withMessage('Geçerli bir kullanıcı ID\'si girin')
], asyncHandler(async (req, res) => {
const { userId } = req.params;
const user = await req.prisma.user.findUnique({
where: {
id: userId,
isActive: true
},
select: {
id: true,
username: true,
firstName: true,
lastName: true,
email: true,
avatar: true,
createdAt: true,
_count: {
select: {
ownedLists: {
where: { isActive: true }
},
sharedLists: true
}
}
}
});
if (!user) {
return res.status(404).json({
success: false,
message: 'Kullanıcı bulunamadı'
});
}
// Eğer kendi profili değilse, e-posta adresini gizle
if (userId !== req.user.id && !req.user.isAdmin) {
delete user.email;
}
res.json({
success: true,
data: { user }
});
}));
/**
* Tüm kullanıcıları listele (Admin)
* GET /api/users
*/
router.get('/', [
authenticateToken,
requireAdmin,
query('page').optional().isInt({ min: 1 }).withMessage('Sayfa numarası pozitif bir sayı olmalı'),
query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('Limit 1-100 arasında olmalı'),
query('search').optional().isLength({ min: 2 }).withMessage('Arama terimi en az 2 karakter olmalı'),
query('status').optional().isIn(['active', 'inactive', 'all']).withMessage('Geçerli bir durum 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 {
page = 1,
limit = 20,
search,
status = 'active',
sortBy = 'createdAt',
sortOrder = 'desc'
} = req.query;
const skip = (page - 1) * limit;
// Filtreleme koşulları
const whereCondition = {};
if (status === 'active') {
whereCondition.isActive = true;
} else if (status === 'inactive') {
whereCondition.isActive = false;
}
if (search) {
whereCondition.OR = [
{ username: { contains: search, mode: 'insensitive' } },
{ firstName: { contains: search, mode: 'insensitive' } },
{ lastName: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } }
];
}
// Sıralama
const orderBy = {};
orderBy[sortBy] = sortOrder;
const [users, totalCount] = await Promise.all([
req.prisma.user.findMany({
where: whereCondition,
select: {
id: true,
username: true,
firstName: true,
lastName: true,
email: true,
avatar: true,
isActive: true,
isAdmin: true,
createdAt: true,
lastLoginAt: true,
_count: {
select: {
ownedLists: {
where: { isActive: true }
},
sharedLists: true,
notifications: {
where: { isRead: false }
}
}
}
},
orderBy,
skip: parseInt(skip),
take: parseInt(limit)
}),
req.prisma.user.count({ where: whereCondition })
]);
res.json({
success: true,
data: {
users,
pagination: {
currentPage: parseInt(page),
totalPages: Math.ceil(totalCount / limit),
totalCount,
hasNext: skip + parseInt(limit) < totalCount,
hasPrev: page > 1
}
}
});
}));
/**
* Kullanıcı durumunu güncelle (Admin)
* PUT /api/users/:userId/status
*/
router.put('/:userId/status', [
authenticateToken,
requireAdmin,
param('userId').isString().isLength({ min: 1 }).withMessage('Geçerli bir kullanıcı ID\'si girin'),
body('isActive').isBoolean().withMessage('Durum boolean değer 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 { userId } = req.params;
const { isActive } = req.body;
// Kendi hesabını deaktive edemez
if (userId === req.user.id) {
return res.status(400).json({
success: false,
message: 'Kendi hesabınızı deaktive edemezsiniz'
});
}
const user = await req.prisma.user.findUnique({
where: { id: userId }
});
if (!user) {
return res.status(404).json({
success: false,
message: 'Kullanıcı bulunamadı'
});
}
const updatedUser = await req.prisma.user.update({
where: { id: userId },
data: { isActive },
select: {
id: true,
username: true,
firstName: true,
lastName: true,
email: true,
isActive: true,
isAdmin: true,
updatedAt: true
}
});
res.json({
success: true,
message: `Kullanıcı ${isActive ? 'aktif' : 'pasif'} hale getirildi`,
data: { user: updatedUser }
});
}));
/**
* Kullanıcı admin yetkisi güncelle (Admin)
* PUT /api/users/:userId/admin
*/
router.put('/:userId/admin', [
authenticateToken,
requireAdmin,
param('userId').isString().isLength({ min: 1 }).withMessage('Geçerli bir kullanıcı ID\'si girin'),
body('isAdmin').isBoolean().withMessage('Admin durumu boolean değer 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 { userId } = req.params;
const { isAdmin } = req.body;
// Kendi admin yetkisini kaldıramaz
if (userId === req.user.id && !isAdmin) {
return res.status(400).json({
success: false,
message: 'Kendi admin yetkinizi kaldıramazsınız'
});
}
const user = await req.prisma.user.findUnique({
where: { id: userId }
});
if (!user) {
return res.status(404).json({
success: false,
message: 'Kullanıcı bulunamadı'
});
}
const updatedUser = await req.prisma.user.update({
where: { id: userId },
data: { isAdmin },
select: {
id: true,
username: true,
firstName: true,
lastName: true,
email: true,
isActive: true,
isAdmin: true,
updatedAt: true
}
});
res.json({
success: true,
message: `Kullanıcı ${isAdmin ? 'admin' : 'normal kullanıcı'} yapıldı`,
data: { user: updatedUser }
});
}));
/**
* Kullanıcı sil (Admin)
* DELETE /api/users/:userId
*/
router.delete('/:userId', [
authenticateToken,
requireAdmin,
param('userId').isUUID().withMessage('Geçerli bir kullanıcı ID\'si girin')
], asyncHandler(async (req, res) => {
const { userId } = req.params;
// Kendi hesabını silemez
if (userId === req.user.id) {
return res.status(400).json({
success: false,
message: 'Kendi hesabınızı silemezsiniz'
});
}
const user = await req.prisma.user.findUnique({
where: { id: userId }
});
if (!user) {
return res.status(404).json({
success: false,
message: 'Kullanıcı bulunamadı'
});
}
// Soft delete (isActive = false)
await req.prisma.user.update({
where: { id: userId },
data: {
isActive: false,
email: `deleted_${Date.now()}_${user.email}`, // E-posta çakışmasını önle
username: `deleted_${Date.now()}_${user.username}` // Kullanıcı adı çakışmasını önle
}
});
res.json({
success: true,
message: 'Kullanıcı başarıyla silindi'
});
}));
/**
* Kullanıcı istatistikleri (Admin)
* GET /api/users/stats
*/
router.get('/stats/overview', [
authenticateToken,
requireAdmin
], asyncHandler(async (req, res) => {
const [
totalUsers,
activeUsers,
adminUsers,
newUsersThisMonth,
totalLists,
totalProducts
] = await Promise.all([
req.prisma.user.count(),
req.prisma.user.count({ where: { isActive: true } }),
req.prisma.user.count({ where: { isAdmin: true, isActive: true } }),
req.prisma.user.count({
where: {
createdAt: {
gte: new Date(new Date().getFullYear(), new Date().getMonth(), 1)
}
}
}),
req.prisma.shoppingList.count({ where: { isActive: true } }),
req.prisma.product.count({ where: { isActive: true } })
]);
res.json({
success: true,
data: {
users: {
total: totalUsers,
active: activeUsers,
inactive: totalUsers - activeUsers,
admins: adminUsers,
newThisMonth: newUsersThisMonth
},
lists: {
total: totalLists
},
products: {
total: totalProducts
}
}
});
}));
/**
* Kullanıcı bildirim/tercih ayarlarını getir
* GET /api/users/settings
*/
/**
* Admin: Kullanıcı şifresini sıfırla
* PUT /api/users/:userId/password
*/
router.put('/:userId/password', [
authenticateToken,
requireAdmin,
param('userId').isUUID().withMessage('Geçerli bir kullanıcı ID\'si girin'),
body('newPassword').isLength({ min: 6 }).withMessage('Yeni şifre en az 6 karakter olmalı'),
], asyncHandler(async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Girilen bilgilerde hatalar var',
errors: formatValidationErrors(errors)
});
}
const { userId } = req.params;
const { newPassword } = req.body;
const user = await req.prisma.user.findUnique({ where: { id: userId } });
if (!user) {
return res.status(404).json({
success: false,
message: 'Kullanıcı bulunamadı'
});
}
const bcrypt = require('bcryptjs');
const hashedPassword = await bcrypt.hash(newPassword, 12);
await req.prisma.user.update({
where: { id: userId },
data: { password: hashedPassword }
});
res.json({
success: true,
message: 'Kullanıcı şifresi güncellendi'
});
}));
module.exports = router;

164
backend/src/server.js Normal file
View File

@@ -0,0 +1,164 @@
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
const session = require('express-session');
const { createServer } = require('http');
const { Server } = require('socket.io');
require('dotenv').config();
const passport = require('./config/passport');
const { PrismaClient } = require('@prisma/client');
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/users');
const listRoutes = require('./routes/lists');
const itemRoutes = require('./routes/items');
const productRoutes = require('./routes/products');
const categoryRoutes = require('./routes/categories');
const notificationRoutes = require('./routes/notifications');
const dashboardRoutes = require('./routes/dashboard');
const adminRoutes = require('./routes/admin');
const socketHandler = require('./services/socketHandler');
const { errorHandler, notFound } = require('./middleware/errorHandler');
const { createNotificationService } = require('./services/notificationService');
const { setupUtf8mb4 } = require('./utils/dbCharsetSetup');
// Prisma client'ı başlat
const prisma = new PrismaClient();
// Ensure DB charset supports emojis (utf8mb4)
setupUtf8mb4(prisma).catch(() => {});
// Express uygulamasını oluştur
const app = express();
const server = createServer(app);
// Socket.IO'yu başlat
const io = new Server(server, {
cors: {
origin: process.env.CORS_ORIGIN || "http://localhost:3000",
methods: ["GET", "POST", "PUT", "DELETE"],
credentials: true
}
});
// NotificationService'i initialize et
const notificationService = createNotificationService(prisma, io);
console.log('📧 NotificationService initialized');
// Rate limiting
const limiter = rateLimit({
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 dakika
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100, // İstek limiti
message: {
error: 'Çok fazla istek gönderdiniz. Lütfen daha sonra tekrar deneyin.'
},
standardHeaders: true,
legacyHeaders: false,
});
// Middleware'ler
app.use(helmet()); // Güvenlik başlıkları
app.use(compression()); // Gzip sıkıştırma
app.use(morgan('combined')); // HTTP isteklerini logla
app.use(limiter); // Rate limiting
app.use(cors({
origin: process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',').map(origin => origin.trim()) : ["http://localhost:3000"],
credentials: true
}));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Session middleware (Google OAuth için gerekli)
app.use(session({
secret: process.env.SESSION_SECRET || 'hmarket-session-secret',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000 // 24 saat
}
}));
// Passport middleware
app.use(passport.initialize());
app.use(passport.session());
// Static dosyalar
app.use('/uploads', express.static('uploads'));
// Prisma'yı request'e ekle
app.use((req, res, next) => {
req.prisma = prisma;
req.io = io;
next();
});
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV || 'development'
});
});
// API rotaları
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
app.use('/api/lists', listRoutes);
app.use('/api/items', itemRoutes);
app.use('/api/products', productRoutes);
app.use('/api/categories', categoryRoutes);
app.use('/api/notifications', notificationRoutes);
app.use('/api/dashboard', dashboardRoutes);
app.use('/api/admin', adminRoutes);
// Socket.IO event handler'ları
socketHandler(io, prisma);
// Error handling middleware'leri
app.use(notFound);
app.use(errorHandler);
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('🛑 SIGTERM sinyali alındı. Sunucu kapatılıyor...');
await prisma.$disconnect();
server.close(() => {
console.log('✅ Sunucu başarıyla kapatıldı.');
process.exit(0);
});
});
process.on('SIGINT', async () => {
console.log('🛑 SIGINT sinyali alındı. Sunucu kapatılıyor...');
await prisma.$disconnect();
server.close(() => {
console.log('✅ Sunucu başarıyla kapatıldı.');
process.exit(0);
});
});
// Sunucuyu başlat
const PORT = process.env.PORT || 7001;
server.listen(PORT, () => {
console.log(`🚀 Server ${PORT} portunda çalışıyor - port 7001`);
console.log(`📊 Environment: ${process.env.NODE_ENV || 'development'}`);
console.log(`🌐 CORS Origin: ${process.env.CORS_ORIGIN || 'http://localhost:7000'}`);
console.log(`${new Date().toLocaleString('tr-TR')}`);
// Server başlatıldı - port 7002
});
module.exports = { app, server, io, prisma };

View File

@@ -0,0 +1,534 @@
const admin = require('firebase-admin');
class NotificationService {
constructor(prisma, io) {
this.prisma = prisma;
this.io = io;
this.initializeFirebase();
}
initializeFirebase() {
try {
// Firebase credentials kontrolü
const hasCredentials = process.env.FIREBASE_PROJECT_ID &&
process.env.FIREBASE_PRIVATE_KEY &&
process.env.FIREBASE_CLIENT_EMAIL;
if (!hasCredentials) {
console.log('⚠️ Firebase credentials not configured. Push notifications disabled.');
return;
}
if (!admin.apps.length) {
const serviceAccount = {
type: process.env.FIREBASE_TYPE,
project_id: process.env.FIREBASE_PROJECT_ID,
private_key_id: process.env.FIREBASE_PRIVATE_KEY_ID,
private_key: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
client_email: process.env.FIREBASE_CLIENT_EMAIL,
client_id: process.env.FIREBASE_CLIENT_ID,
auth_uri: process.env.FIREBASE_AUTH_URI,
token_uri: process.env.FIREBASE_TOKEN_URI,
auth_provider_x509_cert_url: process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL,
client_x509_cert_url: process.env.FIREBASE_CLIENT_X509_CERT_URL
};
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
console.log('✅ Firebase Admin SDK initialized successfully');
}
} catch (error) {
console.error('Firebase initialization error:', error.message);
console.warn('Push notifications will be disabled.');
}
}
/**
* Veritabanına bildirim kaydet
*/
async createNotification(data) {
try {
const notification = await this.prisma.notification.create({
data: {
userId: data.userId,
type: data.type,
title: data.title,
message: data.message,
data: data.data ? JSON.stringify(data.data) : null,
relatedListId: data.relatedListId || null,
relatedUserId: data.relatedUserId || null
},
include: {
relatedList: {
select: {
id: true,
name: true
}
},
relatedUser: {
select: {
id: true,
username: true,
firstName: true,
lastName: true,
avatar: true
}
}
}
});
return notification;
} catch (error) {
console.error('Error creating notification:', error);
throw error;
}
}
/**
* Socket.IO ile gerçek zamanlı bildirim gönder
*/
async sendRealtimeNotification(userId, notification) {
try {
if (this.io) {
this.io.to(`user_${userId}`).emit('notification', notification);
}
} catch (error) {
console.error('Error sending realtime notification:', error);
}
}
/**
* Push notification gönder
*/
async sendPushNotification(userId, notification) {
try {
// Kullanıcının push notification ayarını kontrol et
const setting = await this.prisma.setting.findUnique({
where: {
userId_key: {
userId,
key: 'notification_push_enabled'
}
}
});
if (setting && setting.value === 'false') {
return; // Push notification kapalı
}
// Kullanıcının device token'larını al
const deviceTokens = await this.prisma.deviceToken.findMany({
where: {
userId,
isActive: true
}
});
if (deviceTokens.length === 0) {
return; // Device token yok
}
const tokens = deviceTokens.map(dt => dt.token);
const message = {
notification: {
title: notification.title,
body: notification.message
},
data: {
type: notification.type,
notificationId: notification.id,
...(notification.data ? JSON.parse(notification.data) : {})
},
tokens
};
const response = await admin.messaging().sendMulticast(message);
// Başarısız token'ları temizle
if (response.failureCount > 0) {
const failedTokens = [];
response.responses.forEach((resp, idx) => {
if (!resp.success) {
failedTokens.push(tokens[idx]);
}
});
if (failedTokens.length > 0) {
await this.prisma.deviceToken.updateMany({
where: {
token: { in: failedTokens }
},
data: {
isActive: false
}
});
}
}
console.log(`Push notification sent to ${response.successCount} devices`);
} catch (error) {
console.error('Error sending push notification:', error);
}
}
/**
* Tam bildirim gönderme (DB + Realtime + Push)
*/
async sendNotification(data) {
try {
// Kullanıcının bildirim ayarını kontrol et
const notificationKey = `notification_${data.type}`;
const setting = await this.prisma.setting.findUnique({
where: {
userId_key: {
userId: data.userId,
key: notificationKey
}
}
});
if (setting && setting.value === 'false') {
return; // Bu tür bildirim kapalı
}
// Veritabanına kaydet
const notification = await this.createNotification(data);
// Gerçek zamanlı gönder
await this.sendRealtimeNotification(data.userId, notification);
// Push notification gönder
await this.sendPushNotification(data.userId, notification);
return notification;
} catch (error) {
console.error('Error in sendNotification:', error);
throw error;
}
}
/**
* Birden fazla kullanıcıya bildirim gönder
*/
async sendNotificationToMultipleUsers(userIds, data) {
try {
const promises = userIds.map(userId =>
this.sendNotification({ ...data, userId })
);
const results = await Promise.allSettled(promises);
const successful = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
console.log(`Notifications sent: ${successful} successful, ${failed} failed`);
return { successful, failed };
} catch (error) {
console.error('Error sending notifications to multiple users:', error);
throw error;
}
}
/**
* Liste üyelerine bildirim gönder
*/
async sendNotificationToListMembers(listId, data, excludeUserId = null) {
try {
const members = await this.prisma.listMember.findMany({
where: {
listId,
userId: excludeUserId ? { not: excludeUserId } : undefined
},
select: {
userId: true
}
});
const userIds = members.map(m => m.userId);
if (userIds.length === 0) {
return { successful: 0, failed: 0 };
}
return await this.sendNotificationToMultipleUsers(userIds, {
...data,
relatedListId: listId
});
} catch (error) {
console.error('Error sending notifications to list members:', error);
throw error;
}
}
/**
* Liste daveti bildirimi
*/
async sendListInviteNotification(invitedUserId, inviterUser, list) {
return await this.sendNotification({
userId: invitedUserId,
type: 'list_invite',
title: 'Liste Daveti',
message: `${inviterUser.firstName} ${inviterUser.lastName} sizi "${list.name}" listesine davet etti`,
data: {
inviterId: inviterUser.id,
listId: list.id
},
relatedListId: list.id,
relatedUserId: inviterUser.id
});
}
/**
* Ürün eklendi bildirimi
*/
async sendItemAddedNotification(listId, adderUser, itemName, excludeUserId = null) {
return await this.sendNotificationToListMembers(listId, {
type: 'item_added',
title: 'Ürün Eklendi',
message: `${adderUser.firstName} "${itemName}" ürününü listeye ekledi`,
data: {
adderId: adderUser.id,
itemName
},
relatedUserId: adderUser.id
}, excludeUserId);
}
/**
* Ürün silindi bildirimi
*/
async sendItemRemovedNotification(listId, removerUser, itemName, excludeUserId = null) {
return await this.sendNotificationToListMembers(listId, {
type: 'item_removed',
title: 'Ürün Silindi',
message: `${removerUser.firstName} "${itemName}" ürününü listeden sildi`,
data: {
removerId: removerUser.id,
itemName
},
relatedUserId: removerUser.id
}, excludeUserId);
}
/**
* Ürün satın alındı bildirimi
*/
async sendItemPurchasedNotification(listId, purchaserUser, itemName, excludeUserId = null) {
return await this.sendNotificationToListMembers(listId, {
type: 'item_purchased',
title: 'Ürün Satın Alındı',
message: `${purchaserUser.firstName} "${itemName}" ürününü satın aldı`,
data: {
purchaserId: purchaserUser.id,
itemName
},
relatedUserId: purchaserUser.id
}, excludeUserId);
}
/**
* Liste paylaşıldı bildirimi
*/
async sendListSharedNotification(listId, sharerUser, sharedWithUser, listName) {
return await this.sendNotification({
userId: sharedWithUser.id,
type: 'list_shared',
title: 'Liste Paylaşıldı',
message: `${sharerUser.firstName} "${listName}" listesini sizinle paylaştı`,
data: {
sharerId: sharerUser.id,
listName
},
relatedListId: listId,
relatedUserId: sharerUser.id
});
}
/**
* Device token kaydet
*/
async registerDeviceToken(userId, token, deviceInfo = {}) {
try {
// Mevcut token'ı kontrol et
const existingToken = await this.prisma.deviceToken.findFirst({
where: {
token,
userId
}
});
if (existingToken) {
// Token'ı güncelle
return await this.prisma.deviceToken.update({
where: { id: existingToken.id },
data: {
isActive: true,
deviceInfo: deviceInfo ? JSON.stringify(deviceInfo) : null,
updatedAt: new Date()
}
});
} else {
// Yeni token oluştur
return await this.prisma.deviceToken.create({
data: {
userId,
token,
deviceInfo: deviceInfo ? JSON.stringify(deviceInfo) : null
}
});
}
} catch (error) {
console.error('Error registering device token:', error);
throw error;
}
}
/**
* Device token'ı deaktive et
*/
async unregisterDeviceToken(userId, token) {
try {
return await this.prisma.deviceToken.updateMany({
where: {
userId,
token
},
data: {
isActive: false
}
});
} catch (error) {
console.error('Error unregistering device token:', error);
throw error;
}
}
/**
* Liste üyelerine bildirim gönder
*/
async notifyListMembers(listId, excludeUserId, notificationData) {
try {
// Liste üyelerini getir
const listMembers = await this.prisma.listMember.findMany({
where: {
listId,
userId: {
not: excludeUserId // Aksiyonu yapan kullanıcıyı hariç tut
}
},
include: {
user: {
select: {
id: true,
firstName: true,
lastName: true
}
}
}
});
// Liste sahibini de dahil et (eğer üye değilse)
const list = await this.prisma.shoppingList.findUnique({
where: { id: listId },
include: {
owner: {
select: {
id: true,
firstName: true,
lastName: true
}
}
}
});
const memberUserIds = listMembers.map(member => member.userId);
if (list.owner.id !== excludeUserId && !memberUserIds.includes(list.owner.id)) {
listMembers.push({
user: list.owner,
userId: list.owner.id
});
}
// Her üyeye bildirim gönder
const notifications = [];
for (const member of listMembers) {
// Socket.IO ile gerçek zamanlı bildirim
if (this.io) {
this.io.to(`user_${member.userId}`).emit('notification', {
type: notificationData.type,
title: notificationData.title,
message: notificationData.message,
data: notificationData.data,
timestamp: new Date()
});
}
// Veritabanına bildirim kaydet
const notification = await this.createNotification({
userId: member.userId,
type: notificationData.type,
title: notificationData.title,
message: notificationData.message,
data: notificationData.data,
relatedListId: listId,
relatedUserId: excludeUserId
});
notifications.push(notification);
// Push notification gönder
await this.sendPushNotification(member.userId, {
title: notificationData.title,
body: notificationData.message,
data: notificationData.data
});
}
return notifications;
} catch (error) {
console.error('Error notifying list members:', error);
throw error;
}
}
}
// Singleton instance
let notificationServiceInstance = null;
// Factory function
const createNotificationService = (prisma, io) => {
if (!notificationServiceInstance) {
notificationServiceInstance = new NotificationService(prisma, io);
}
return notificationServiceInstance;
};
// Export both class and factory
module.exports = {
NotificationService,
createNotificationService,
// Default export for backward compatibility
notifyListMembers: async (...args) => {
if (!notificationServiceInstance) {
throw new Error('NotificationService not initialized. Call createNotificationService first.');
}
return notificationServiceInstance.notifyListMembers(...args);
},
createNotification: async (...args) => {
if (!notificationServiceInstance) {
throw new Error('NotificationService not initialized. Call createNotificationService first.');
}
return notificationServiceInstance.createNotification(...args);
},
sendPushNotification: async (...args) => {
if (!notificationServiceInstance) {
throw new Error('NotificationService not initialized. Call createNotificationService first.');
}
return notificationServiceInstance.sendPushNotification(...args);
}
};

View File

@@ -0,0 +1,359 @@
const jwt = require('jsonwebtoken');
/**
* Socket.IO event handler'ları
* Gerçek zamanlı senkronizasyon için
*/
function socketHandler(io, prisma) {
// Socket kimlik doğrulama middleware'i
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token || socket.handshake.headers.authorization?.split(' ')[1];
if (!token) {
return next(new Error('Token bulunamadı'));
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await prisma.user.findUnique({
where: {
id: decoded.userId,
isActive: true
},
select: {
id: true,
email: true,
username: true,
firstName: true,
lastName: true,
avatar: true,
isAdmin: true
}
});
if (!user) {
return next(new Error('Kullanıcı bulunamadı'));
}
socket.user = user;
next();
} catch (error) {
next(new Error('Geçersiz token'));
}
});
io.on('connection', async (socket) => {
console.log(`👤 Kullanıcı bağlandı: ${socket.user.username} (${socket.id})`);
// Kullanıcıyı kendi odasına ekle (kişisel bildirimler için)
socket.join(`user_${socket.user.id}`);
// Kullanıcının listelerini getir ve ilgili odalara ekle
try {
const userLists = await prisma.shoppingList.findMany({
where: {
OR: [
{ ownerId: socket.user.id },
{
members: {
some: {
userId: socket.user.id
}
}
}
],
isActive: true
},
select: { id: true }
});
// Her liste için odaya katıl
userLists.forEach(list => {
socket.join(`list_${list.id}`);
});
console.log(`📋 ${socket.user.username} ${userLists.length} listeye bağlandı`);
} catch (error) {
console.error('Liste odalarına katılım hatası:', error);
}
// Liste odalarına katılma
socket.on('join_list', async (data) => {
try {
const { listId } = data;
// Kullanıcının bu listeye erişimi var mı kontrol et
const hasAccess = await checkListAccess(socket.user.id, listId, prisma);
if (!hasAccess) {
socket.emit('error', { message: 'Bu listeye erişim yetkiniz yok' });
return;
}
socket.join(`list_${listId}`);
socket.emit('joined_list', { listId });
// Diğer kullanıcılara bildir
socket.to(`list_${listId}`).emit('user_joined_list', {
user: socket.user,
listId
});
console.log(`📋 ${socket.user.username} liste ${listId} odasına katıldı`);
} catch (error) {
console.error('Liste odası katılım hatası:', error);
socket.emit('error', { message: 'Liste odası katılımında hata oluştu' });
}
});
// Liste odalarından ayrılma
socket.on('leave_list', (data) => {
const { listId } = data;
socket.leave(`list_${listId}`);
socket.to(`list_${listId}`).emit('user_left_list', {
user: socket.user,
listId
});
console.log(`📋 ${socket.user.username} liste ${listId} odasından ayrıldı`);
});
// Ürün ekleme
socket.on('item_added', async (data) => {
try {
const { listId, item } = data;
// Erişim kontrolü
const hasAccess = await checkListAccess(socket.user.id, listId, prisma);
if (!hasAccess) {
socket.emit('error', { message: 'Bu listeye erişim yetkiniz yok' });
return;
}
// Aktivite kaydı oluştur
await prisma.activity.create({
data: {
listId: listId,
userId: socket.user.id,
action: 'item_added',
details: {
itemName: item.customName || item.product?.name,
quantity: item.quantity,
unit: item.unit
}
}
});
// Diğer kullanıcılara bildir
socket.to(`list_${listId}`).emit('item_added', {
item,
user: socket.user,
listId
});
console.log(` ${socket.user.username} liste ${listId}'e ürün ekledi`);
} catch (error) {
console.error('Ürün ekleme hatası:', error);
socket.emit('error', { message: 'Ürün ekleme sırasında hata oluştu' });
}
});
// Ürün güncelleme
socket.on('item_updated', async (data) => {
try {
const { listId, itemId, changes } = data;
// Erişim kontrolü
const hasAccess = await checkListAccess(socket.user.id, listId, prisma);
if (!hasAccess) {
socket.emit('error', { message: 'Bu listeye erişim yetkiniz yok' });
return;
}
// Aktivite kaydı oluştur
await prisma.activity.create({
data: {
listId: listId,
userId: socket.user.id,
action: 'item_updated',
details: {
itemId,
changes
}
}
});
// Diğer kullanıcılara bildir
socket.to(`list_${listId}`).emit('item_updated', {
itemId,
changes,
user: socket.user,
listId
});
console.log(`✏️ ${socket.user.username} liste ${listId}'de ürün güncelledi`);
} catch (error) {
console.error('Ürün güncelleme hatası:', error);
socket.emit('error', { message: 'Ürün güncelleme sırasında hata oluştu' });
}
});
// Ürün satın alma durumu değişikliği
socket.on('item_purchased', async (data) => {
try {
const { listId, itemId, isPurchased } = data;
// Erişim kontrolü
const hasAccess = await checkListAccess(socket.user.id, listId, prisma);
if (!hasAccess) {
socket.emit('error', { message: 'Bu listeye erişim yetkiniz yok' });
return;
}
// Aktivite kaydı oluştur
await prisma.activity.create({
data: {
listId: listId,
userId: socket.user.id,
action: isPurchased ? 'item_purchased' : 'item_unpurchased',
details: {
itemId
}
}
});
// Diğer kullanıcılara bildir
socket.to(`list_${listId}`).emit('item_purchased', {
itemId,
isPurchased,
purchasedBy: socket.user.id,
purchasedAt: isPurchased ? new Date() : null,
user: socket.user,
listId
});
console.log(`${isPurchased ? '✅' : '❌'} ${socket.user.username} liste ${listId}'de ürün durumunu değiştirdi`);
} catch (error) {
console.error('Ürün satın alma durumu hatası:', error);
socket.emit('error', { message: 'Ürün durumu değişikliğinde hata oluştu' });
}
});
// Ürün silme
socket.on('item_deleted', async (data) => {
try {
const { listId, itemId } = data;
// Erişim kontrolü
const hasAccess = await checkListAccess(socket.user.id, listId, prisma);
if (!hasAccess) {
socket.emit('error', { message: 'Bu listeye erişim yetkiniz yok' });
return;
}
// Aktivite kaydı oluştur
await prisma.activity.create({
data: {
listId: listId,
userId: socket.user.id,
action: 'item_deleted',
details: {
itemId
}
}
});
// Diğer kullanıcılara bildir
socket.to(`list_${listId}`).emit('item_deleted', {
itemId,
user: socket.user,
listId
});
console.log(`🗑️ ${socket.user.username} liste ${listId}'den ürün sildi`);
} catch (error) {
console.error('Ürün silme hatası:', error);
socket.emit('error', { message: 'Ürün silme sırasında hata oluştu' });
}
});
// Kullanıcı yazıyor bildirimi
socket.on('typing_start', (data) => {
const { listId } = data;
socket.to(`list_${listId}`).emit('user_typing', {
user: socket.user,
listId
});
});
socket.on('typing_stop', (data) => {
const { listId } = data;
socket.to(`list_${listId}`).emit('user_stopped_typing', {
user: socket.user,
listId
});
});
// Ping-pong (bağlantı kontrolü)
socket.on('ping', () => {
socket.emit('pong');
});
// Bağlantı kopma
socket.on('disconnect', (reason) => {
console.log(`👋 Kullanıcı ayrıldı: ${socket.user.username} (${reason})`);
// Tüm liste odalarına ayrılma bildirimi gönder
socket.rooms.forEach(room => {
if (room.startsWith('list_')) {
socket.to(room).emit('user_disconnected', {
user: socket.user
});
}
});
});
// Hata yakalama
socket.on('error', (error) => {
console.error(`Socket hatası (${socket.user.username}):`, error);
});
});
// Bağlantı hatası yakalama
io.on('connect_error', (error) => {
console.error('Socket.IO bağlantı hatası:', error);
});
console.log('🔌 Socket.IO handler başlatıldı');
}
/**
* Kullanıcının listeye erişim yetkisi var mı kontrol et
*/
async function checkListAccess(userId, listId, prisma) {
try {
const access = await prisma.shoppingList.findFirst({
where: {
id: listId,
isActive: true,
OR: [
{ ownerId: userId },
{
members: {
some: {
userId: userId
}
}
}
]
}
});
return !!access;
} catch (error) {
console.error('Liste erişim kontrolü hatası:', error);
return false;
}
}
module.exports = socketHandler;

View File

@@ -0,0 +1,48 @@
const url = require('url');
function getDatabaseName(databaseUrl) {
try {
const parsed = new url.URL(databaseUrl);
// pathname like "/dbname"; strip leading '/'
const dbPath = parsed.pathname || '';
const dbName = dbPath.startsWith('/') ? dbPath.slice(1) : dbPath;
return dbName.split('?')[0];
} catch {
// Fallback: basic parsing
const parts = (databaseUrl || '').split('/');
return (parts[parts.length - 1] || '').split('?')[0];
}
}
async function setupUtf8mb4(prisma) {
const databaseUrl = process.env.DATABASE_URL || '';
// SQLite için bu fonksiyonu atla
if (databaseUrl.includes('sqlite') || databaseUrl.includes('.db')) {
console.log(' SQLite kullanılıyor. UTF8MB4 kurulumu atlandı.');
return;
}
const dbName = getDatabaseName(databaseUrl);
if (!dbName) {
console.warn('⚠️ DATABASE_URL bulunamadı veya veritabanı adı çözümlenemedi. UTF8MB4 kurulumu atlandı.');
return;
}
// Only run for MySQL/MariaDB
try {
await prisma.$executeRawUnsafe(`ALTER DATABASE \`${dbName}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
console.log('✅ Veritabanı varsayılan karakter seti/collation utf8mb4 olarak ayarlandı.');
} catch (err) {
console.warn('⚠️ Veritabanı charset ayarlanırken hata oluştu:', err?.message || err);
}
try {
await prisma.$executeRawUnsafe('ALTER TABLE `categories` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci');
console.log('✅ `categories` tablosu utf8mb4 olarak dönüştürüldü.');
} catch (err) {
console.warn('⚠️ `categories` tablosu dönüştürülürken hata oluştu:', err?.message || err);
}
}
module.exports = { setupUtf8mb4 };

View File

@@ -0,0 +1,379 @@
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
/**
* Sayfalama hesaplamaları
* @param {number} page - Sayfa numarası
* @param {number} limit - Sayfa başına öğe sayısı
* @returns {object} Skip ve take değerleri
*/
const calculatePagination = (page = 1, limit = 10) => {
const pageNum = Math.max(1, parseInt(page));
const limitNum = Math.min(100, Math.max(1, parseInt(limit)));
return {
skip: (pageNum - 1) * limitNum,
take: limitNum,
page: pageNum,
limit: limitNum
};
};
/**
* Sayfalama meta bilgilerini oluştur
* @param {number} total - Toplam öğe sayısı
* @param {number} page - Mevcut sayfa
* @param {number} limit - Sayfa başına öğe sayısı
* @returns {object} Meta bilgileri
*/
const createPaginationMeta = (total, page, limit) => {
const totalPages = Math.ceil(total / limit);
return {
total,
page,
limit,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
};
};
/**
* API yanıt formatı
* @param {boolean} success - İşlem başarılı mı
* @param {string} message - Mesaj
* @param {any} data - Veri
* @param {object} meta - Meta bilgileri
* @returns {object} Formatlanmış yanıt
*/
const createResponse = (success, message, data = null, meta = null) => {
const response = {
success,
message,
timestamp: new Date().toISOString()
};
if (data !== null) {
response.data = data;
}
if (meta !== null) {
response.meta = meta;
}
return response;
};
/**
* Başarılı yanıt oluştur
* @param {string} message - Mesaj
* @param {any} data - Veri
* @param {object} meta - Meta bilgileri
* @returns {object} Başarılı yanıt
*/
const successResponse = (message, data = null, meta = null) => {
return createResponse(true, message, data, meta);
};
/**
* Hata yanıtı oluştur
* @param {string} message - Hata mesajı
* @param {any} data - Hata detayları
* @returns {object} Hata yanıtı
*/
const errorResponse = (message, data = null) => {
return createResponse(false, message, data);
};
/**
* JWT token oluştur
* @param {object} payload - Token içeriği
* @param {string} expiresIn - Geçerlilik süresi
* @returns {string} JWT token
*/
const generateToken = (payload, expiresIn = process.env.JWT_EXPIRES_IN || '7d') => {
return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn });
};
/**
* JWT token doğrula
* @param {string} token - JWT token
* @returns {object} Decoded token
*/
const verifyToken = (token) => {
return jwt.verify(token, process.env.JWT_SECRET);
};
/**
* Rastgele string oluştur
* @param {number} length - String uzunluğu
* @returns {string} Rastgele string
*/
const generateRandomString = (length = 32) => {
return crypto.randomBytes(length).toString('hex');
};
/**
* Güvenli karşılaştırma
* @param {string} a - İlk string
* @param {string} b - İkinci string
* @returns {boolean} Eşit mi
*/
const safeCompare = (a, b) => {
if (a.length !== b.length) {
return false;
}
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
};
/**
* Slug oluştur
* @param {string} text - Metin
* @returns {string} Slug
*/
const createSlug = (text) => {
return text
.toLowerCase()
.replace(/ç/g, 'c')
.replace(/ğ/g, 'g')
.replace(/ı/g, 'i')
.replace(/ö/g, 'o')
.replace(/ş/g, 's')
.replace(/ü/g, 'u')
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim('-');
};
/**
* Tarih formatla
* @param {Date} date - Tarih
* @param {string} locale - Dil kodu
* @returns {string} Formatlanmış tarih
*/
const formatDate = (date, locale = 'tr-TR') => {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(new Date(date));
};
/**
* Fiyat formatla
* @param {number} price - Fiyat
* @param {string} currency - Para birimi
* @returns {string} Formatlanmış fiyat
*/
const formatPrice = (price, currency = 'TRY') => {
return new Intl.NumberFormat('tr-TR', {
style: 'currency',
currency: currency
}).format(price);
};
/**
* Dosya boyutu formatla
* @param {number} bytes - Byte cinsinden boyut
* @returns {string} Formatlanmış boyut
*/
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
/**
* E-posta adresi maskele
* @param {string} email - E-posta adresi
* @returns {string} Maskelenmiş e-posta
*/
const maskEmail = (email) => {
const [username, domain] = email.split('@');
const maskedUsername = username.charAt(0) + '*'.repeat(username.length - 2) + username.charAt(username.length - 1);
return `${maskedUsername}@${domain}`;
};
/**
* Telefon numarası maskele
* @param {string} phone - Telefon numarası
* @returns {string} Maskelenmiş telefon
*/
const maskPhone = (phone) => {
if (phone.length < 4) return phone;
return phone.substring(0, 2) + '*'.repeat(phone.length - 4) + phone.substring(phone.length - 2);
};
/**
* Dizi elemanlarını karıştır
* @param {Array} array - Karıştırılacak dizi
* @returns {Array} Karıştırılmış dizi
*/
const shuffleArray = (array) => {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
};
/**
* Dizi elemanlarını grupla
* @param {Array} array - Gruplandırılacak dizi
* @param {string} key - Gruplama anahtarı
* @returns {object} Gruplandırılmış obje
*/
const groupBy = (array, key) => {
return array.reduce((groups, item) => {
const group = item[key];
groups[group] = groups[group] || [];
groups[group].push(item);
return groups;
}, {});
};
/**
* Debounce fonksiyonu
* @param {Function} func - Debounce edilecek fonksiyon
* @param {number} wait - Bekleme süresi (ms)
* @returns {Function} Debounce edilmiş fonksiyon
*/
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
/**
* Throttle fonksiyonu
* @param {Function} func - Throttle edilecek fonksiyon
* @param {number} limit - Limit süresi (ms)
* @returns {Function} Throttle edilmiş fonksiyon
*/
const throttle = (func, limit) => {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
};
/**
* Renk hex kodunu RGB'ye çevir
* @param {string} hex - Hex renk kodu
* @returns {object} RGB değerleri
*/
const hexToRgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
};
/**
* RGB değerlerini hex koduna çevir
* @param {number} r - Kırmızı değeri
* @param {number} g - Yeşil değeri
* @param {number} b - Mavi değeri
* @returns {string} Hex renk kodu
*/
const rgbToHex = (r, g, b) => {
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
};
/**
* URL'den query parametrelerini çıkar
* @param {string} url - URL
* @returns {object} Query parametreleri
*/
const parseQueryParams = (url) => {
const params = {};
const urlObj = new URL(url);
urlObj.searchParams.forEach((value, key) => {
params[key] = value;
});
return params;
};
/**
* Nesne değerlerini temizle (null, undefined, empty string)
* @param {object} obj - Temizlenecek nesne
* @returns {object} Temizlenmiş nesne
*/
const cleanObject = (obj) => {
const cleaned = {};
Object.keys(obj).forEach(key => {
if (obj[key] !== null && obj[key] !== undefined && obj[key] !== '') {
cleaned[key] = obj[key];
}
});
return cleaned;
};
/**
* İki tarih arasındaki farkı hesapla
* @param {Date} date1 - İlk tarih
* @param {Date} date2 - İkinci tarih
* @returns {object} Tarih farkı
*/
const dateDifference = (date1, date2) => {
const diffTime = Math.abs(date2 - date1);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
const diffHours = Math.ceil(diffTime / (1000 * 60 * 60));
const diffMinutes = Math.ceil(diffTime / (1000 * 60));
return {
milliseconds: diffTime,
minutes: diffMinutes,
hours: diffHours,
days: diffDays
};
};
module.exports = {
calculatePagination,
createPaginationMeta,
createResponse,
successResponse,
errorResponse,
generateToken,
verifyToken,
generateRandomString,
safeCompare,
createSlug,
formatDate,
formatPrice,
formatFileSize,
maskEmail,
maskPhone,
shuffleArray,
groupBy,
debounce,
throttle,
hexToRgb,
rgbToHex,
parseQueryParams,
cleanObject,
dateDifference
};

View File

@@ -0,0 +1,407 @@
const { body, param, query } = require('express-validator');
/**
* CUID validation helper
*/
const validateCuid = (value) => {
if (!value) return false;
// CUID format: 20-30 characters, lowercase letters and numbers
const cuidRegex = /^[a-z0-9]{20,30}$/;
return cuidRegex.test(value);
};
/**
* Kullanıcı kayıt doğrulama kuralları
*/
const validateUserRegistration = [
body('username')
.trim()
.isLength({ min: 3, max: 30 })
.withMessage('Kullanıcı adı 3-30 karakter arasında olmalı')
.matches(/^[a-zA-Z0-9_]+$/)
.withMessage('Kullanıcı adı sadece harf, rakam ve alt çizgi içerebilir'),
body('email')
.isEmail()
.withMessage('Geçerli bir e-posta adresi girin')
.normalizeEmail(),
body('password')
.isLength({ min: 6 })
.withMessage('Şifre en az 6 karakter olmalı')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.withMessage('Şifre en az bir küçük harf, bir büyük harf ve bir rakam içermeli'),
body('firstName')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Ad 2-50 karakter arasında olmalı')
.matches(/^[a-zA-ZçğıöşüÇĞIİÖŞÜ\s]+$/)
.withMessage('Ad sadece harf içerebilir'),
body('lastName')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Soyad 2-50 karakter arasında olmalı')
.matches(/^[a-zA-ZçğıöşüÇĞIİÖŞÜ\s]+$/)
.withMessage('Soyad sadece harf içerebilir')
];
/**
* Kullanıcı giriş doğrulama kuralları
*/
const validateUserLogin = [
body('username')
.trim()
.notEmpty()
.withMessage('Kullanıcı adı veya e-posta gerekli'),
body('password')
.notEmpty()
.withMessage('Şifre gerekli')
];
/**
* Şifre değiştirme doğrulama kuralları
*/
const validatePasswordChange = [
body('currentPassword')
.notEmpty()
.withMessage('Mevcut şifre gerekli'),
body('newPassword')
.isLength({ min: 6 })
.withMessage('Yeni şifre en az 6 karakter olmalı')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.withMessage('Yeni şifre en az bir küçük harf, bir büyük harf ve bir rakam içermeli')
];
/**
* Profil güncelleme doğrulama kuralları
*/
const validateProfileUpdate = [
body('firstName')
.optional()
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Ad 2-50 karakter arasında olmalı')
.matches(/^[a-zA-ZçğıöşüÇĞIİÖŞÜ\s]+$/)
.withMessage('Ad sadece harf içerebilir'),
body('lastName')
.optional()
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Soyad 2-50 karakter arasında olmalı')
.matches(/^[a-zA-ZçğıöşüÇĞIİÖŞÜ\s]+$/)
.withMessage('Soyad sadece harf içerebilir'),
body('email')
.optional()
.isEmail()
.withMessage('Geçerli bir e-posta adresi girin')
.normalizeEmail()
];
/**
* Liste oluşturma doğrulama kuralları
*/
const validateListCreation = [
body('name')
.trim()
.isLength({ min: 2, max: 100 })
.withMessage('Liste adı 2-100 karakter arasında olmalı'),
body('description')
.optional()
.trim()
.isLength({ max: 500 })
.withMessage('Açıklama en fazla 500 karakter olmalı'),
body('color')
.optional()
.matches(/^#[0-9A-F]{6}$/i)
.withMessage('Geçerli bir hex renk kodu girin (örn: #FF5722)')
];
/**
* Liste güncelleme doğrulama kuralları
*/
const validateListUpdate = [
body('name')
.optional()
.trim()
.isLength({ min: 2, max: 100 })
.withMessage('Liste adı 2-100 karakter arasında olmalı'),
body('description')
.optional()
.trim()
.isLength({ max: 500 })
.withMessage('Açıklama en fazla 500 karakter olmalı'),
body('color')
.optional()
.matches(/^#[0-9A-F]{6}$/i)
.withMessage('Geçerli bir hex renk kodu girin (örn: #FF5722)')
];
/**
* Liste öğesi ekleme doğrulama kuralları
*/
const validateListItemCreation = [
body('name')
.optional()
.trim()
.isLength({ min: 1, max: 100 })
.withMessage('Ürün adı 1-100 karakter arasında olmalı'),
body('quantity')
.optional()
.isInt({ min: 1 })
.withMessage('Miktar pozitif bir sayı olmalı'),
body('unit')
.optional()
.trim()
.isLength({ max: 20 })
.withMessage('Birim en fazla 20 karakter olmalı'),
body('notes')
.optional()
.trim()
.isLength({ max: 200 })
.withMessage('Notlar en fazla 200 karakter olmalı'),
body('productId')
.optional()
.custom(validateCuid)
.withMessage('Geçerli bir ürün ID\'si girin'),
body('estimatedPrice')
.optional()
.isFloat({ min: 0 })
.withMessage('Tahmini fiyat pozitif bir sayı olmalı'),
body('priority')
.optional()
.isIn(['LOW', 'MEDIUM', 'HIGH'])
.withMessage('Öncelik LOW, MEDIUM veya HIGH olmalı')
];
/**
* Liste öğesi güncelleme doğrulama kuralları
*/
const validateListItemUpdate = [
body('name')
.optional()
.trim()
.isLength({ min: 1, max: 100 })
.withMessage('Ürün adı 1-100 karakter arasında olmalı'),
body('quantity')
.optional()
.isInt({ min: 1 })
.withMessage('Miktar pozitif bir sayı olmalı'),
body('unit')
.optional()
.trim()
.isLength({ max: 20 })
.withMessage('Birim en fazla 20 karakter olmalı'),
body('notes')
.optional()
.trim()
.isLength({ max: 200 })
.withMessage('Notlar en fazla 200 karakter olmalı'),
body('isPurchased')
.optional()
.isBoolean()
.withMessage('Satın alındı durumu boolean değer olmalı'),
body('actualPrice')
.optional()
.isFloat({ min: 0 })
.withMessage('Gerçek fiyat pozitif bir sayı olmalı')
];
/**
* Ürün oluşturma doğrulama kuralları
*/
const validateProductCreation = [
body('name')
.trim()
.isLength({ min: 2, max: 100 })
.withMessage('Ürün adı 2-100 karakter arasında olmalı'),
body('barcode')
.optional()
.isLength({ min: 8, max: 20 })
.withMessage('Barkod 8-20 karakter arasında olmalı'),
body('categoryId')
.isUUID()
.withMessage('Geçerli bir kategori ID\'si girin'),
body('price')
.optional()
.isFloat({ min: 0 })
.withMessage('Fiyat pozitif bir sayı olmalı'),
body('location')
.optional()
.trim()
.isLength({ max: 100 })
.withMessage('Konum en fazla 100 karakter olmalı')
];
/**
* Kategori oluşturma doğrulama kuralları
*/
const validateCategoryCreation = [
body('name')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Kategori adı 2-50 karakter arasında olmalı'),
body('color')
.matches(/^#[0-9A-F]{6}$/i)
.withMessage('Geçerli bir hex renk kodu girin (örn: #FF5722)'),
body('description')
.optional()
.trim()
.isLength({ max: 200 })
.withMessage('Açıklama en fazla 200 karakter olmalı')
];
/**
* UUID parametresi doğrulama
*/
const validateUUIDParam = (paramName) => [
param(paramName).isUUID().withMessage(`Geçerli bir ${paramName} girin`)
];
/**
* CUID parametresi doğrulama
*/
const validateCuidParam = (paramName) => [
param(paramName).custom(validateCuid).withMessage(`Geçerli bir ${paramName} girin`)
];
/**
* Sayfalama doğrulama kuralları
*/
const validatePagination = [
query('page')
.optional()
.isInt({ min: 1 })
.withMessage('Sayfa numarası pozitif bir sayı olmalı'),
query('limit')
.optional()
.isInt({ min: 1, max: 100 })
.withMessage('Limit 1-100 arasında olmalı')
];
/**
* Arama doğrulama kuralları
*/
const validateSearch = [
query('q')
.isLength({ min: 2 })
.withMessage('Arama terimi en az 2 karakter olmalı')
];
/**
* Tarih aralığı doğrulama kuralları
*/
const validateDateRange = [
query('startDate')
.optional()
.isISO8601()
.withMessage('Geçerli bir başlangıç tarihi girin'),
query('endDate')
.optional()
.isISO8601()
.withMessage('Geçerli bir bitiş tarihi girin')
];
/**
* Device token doğrulama kuralları
*/
const validateDeviceToken = [
body('token')
.trim()
.isLength({ min: 10 })
.withMessage('Geçerli bir device token girin'),
body('deviceInfo')
.optional()
.isObject()
.withMessage('Device bilgisi obje formatında olmalı')
];
/**
* Liste üyesi ekleme doğrulama kuralları
*/
const validateAddListMember = [
body('userId')
.isUUID()
.withMessage('Geçerli bir kullanıcı ID\'si girin'),
body('role')
.optional()
.isIn(['member', 'editor'])
.withMessage('Geçerli bir rol seçin (member, editor)')
];
/**
* Bildirim ayarları doğrulama kuralları
*/
const validateNotificationSettings = [
body('settings')
.isObject()
.withMessage('Ayarlar obje formatında olmalı')
];
/**
* Fiyat geçmişi ekleme doğrulama kuralları
*/
const validatePriceHistory = [
body('price')
.isFloat({ min: 0 })
.withMessage('Fiyat pozitif bir sayı olmalı'),
body('location')
.optional()
.trim()
.isLength({ max: 100 })
.withMessage('Konum en fazla 100 karakter olmalı')
];
module.exports = {
validateUserRegistration,
validateUserLogin,
validatePasswordChange,
validateProfileUpdate,
validateListCreation,
validateListUpdate,
validateListItemCreation,
validateListItemUpdate,
validateProductCreation,
validateCategoryCreation,
validateUUIDParam,
validateCuidParam,
validatePagination,
validateSearch,
validateDateRange,
validateDeviceToken,
validateAddListMember,
validateNotificationSettings,
validatePriceHistory,
validateCuid
};