Files
hMarket/backend/src/routes/users.js

612 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;