612 lines
14 KiB
JavaScript
612 lines
14 KiB
JavaScript
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;
|