QA fixes: real listing creation, profile save, favorites, missing pages
- SellItemPage: real file upload + API listing creation + activate - CreateProfilePage: save profile via PUT /users/profile - ProductDetailPage: wire edit/delete/message buttons, show edit for owner - ListingCard: persist favorites via API, show real images - Footer: connect newsletter subscribe to API - Router: add /dashboard/listings and /dashboard/saved routes - Backend: add GET /listings/favorites endpoint - New pages: MyListingsPage, SavedItemsPage - Fix unused imports causing build failures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,115 @@
|
||||
import { Router } from 'express';
|
||||
import { prisma } from '../config/database.js';
|
||||
import { authenticate } from '../middleware/auth.js';
|
||||
import { upload } from '../middleware/upload.js';
|
||||
import { validate } from '../middleware/validate.js';
|
||||
import { hashPassword, comparePassword } from '../utils/password.js';
|
||||
import { AppError } from '../middleware/errorHandler.js';
|
||||
import { updateProfileSchema, updateSettingsSchema, deleteAccountSchema } from '../validators/user.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const userSelect = {
|
||||
id: true, email: true, fullName: true, nickname: true, avatar: true,
|
||||
phone: true, location: true, bio: true, rating: true,
|
||||
showEmail: true, showPhone: true, showLocation: true,
|
||||
phone: true, location: true, bio: true, rating: true, ratingCount: true,
|
||||
showEmail: true, showPhone: true, showLocation: true, showOnline: true, showRating: true,
|
||||
createdAt: true,
|
||||
};
|
||||
|
||||
// --- Avatar upload ---
|
||||
router.post('/avatar', authenticate, upload.single('avatar'), async (req, res, next) => {
|
||||
try {
|
||||
const file = req.file;
|
||||
if (!file) throw new AppError(400, 'No file uploaded');
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: { id: req.userId },
|
||||
data: { avatar: `/uploads/${file.filename}` },
|
||||
select: userSelect,
|
||||
});
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// --- Settings ---
|
||||
router.get('/settings', authenticate, async (req, res, next) => {
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.userId },
|
||||
select: {
|
||||
showEmail: true, showPhone: true, showLocation: true,
|
||||
showOnline: true, showRating: true,
|
||||
notifNewOffer: true, notifMessages: true, notifItemSold: true,
|
||||
notifFavorites: true, notifEmail: true, marketingEmail: true,
|
||||
twoFactorEnabled: true,
|
||||
},
|
||||
});
|
||||
if (!user) throw new AppError(404, 'User not found');
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/settings', authenticate, validate(updateSettingsSchema), async (req, res, next) => {
|
||||
try {
|
||||
const user = await prisma.user.update({
|
||||
where: { id: req.userId },
|
||||
data: req.body,
|
||||
select: {
|
||||
showEmail: true, showPhone: true, showLocation: true,
|
||||
showOnline: true, showRating: true,
|
||||
notifNewOffer: true, notifMessages: true, notifItemSold: true,
|
||||
notifFavorites: true, notifEmail: true, marketingEmail: true,
|
||||
twoFactorEnabled: true,
|
||||
},
|
||||
});
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// --- Sessions ---
|
||||
router.get('/sessions', authenticate, async (req, res, next) => {
|
||||
try {
|
||||
const sessions = await prisma.session.findMany({
|
||||
where: { userId: req.userId },
|
||||
select: { id: true, userAgent: true, ipAddress: true, createdAt: true, expiresAt: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
res.json(sessions);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// --- Account deletion ---
|
||||
router.delete('/account', authenticate, validate(deleteAccountSchema), async (req, res, next) => {
|
||||
try {
|
||||
const { password } = req.body;
|
||||
const user = await prisma.user.findUnique({ where: { id: req.userId } });
|
||||
if (!user) throw new AppError(404, 'User not found');
|
||||
|
||||
const valid = await comparePassword(password, user.passwordHash);
|
||||
if (!valid) throw new AppError(400, 'Password is incorrect');
|
||||
|
||||
await prisma.session.deleteMany({ where: { userId: req.userId } });
|
||||
await prisma.user.update({
|
||||
where: { id: req.userId },
|
||||
data: { isActive: false },
|
||||
});
|
||||
|
||||
res.clearCookie('refreshToken');
|
||||
res.json({ message: 'Account deactivated' });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// --- Profile ---
|
||||
router.get('/profile', authenticate, async (req, res, next) => {
|
||||
try {
|
||||
const user = await prisma.user.findUnique({ where: { id: req.userId }, select: userSelect });
|
||||
@@ -23,12 +120,11 @@ router.get('/profile', authenticate, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/profile', authenticate, async (req, res, next) => {
|
||||
router.put('/profile', authenticate, validate(updateProfileSchema), async (req, res, next) => {
|
||||
try {
|
||||
const { fullName, nickname, phone, location, bio, showEmail, showPhone, showLocation } = req.body;
|
||||
const user = await prisma.user.update({
|
||||
where: { id: req.userId },
|
||||
data: { fullName, nickname, phone, location, bio, showEmail, showPhone, showLocation },
|
||||
data: req.body,
|
||||
select: userSelect,
|
||||
});
|
||||
res.json(user);
|
||||
@@ -37,9 +133,13 @@ router.put('/profile', authenticate, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- Password ---
|
||||
router.put('/password', authenticate, async (req, res, next) => {
|
||||
try {
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
if (!currentPassword || !newPassword) throw new AppError(400, 'Both current and new passwords are required');
|
||||
if (newPassword.length < 8) throw new AppError(400, 'New password must be at least 8 characters');
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: req.userId } });
|
||||
if (!user) throw new AppError(404, 'User not found');
|
||||
|
||||
@@ -55,11 +155,67 @@ router.put('/password', authenticate, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- Block/Unblock ---
|
||||
router.post('/:id/block', authenticate, async (req, res, next) => {
|
||||
try {
|
||||
if (req.params.id === req.userId) throw new AppError(400, 'Cannot block yourself');
|
||||
|
||||
const target = await prisma.user.findUnique({ where: { id: req.params.id } });
|
||||
if (!target) throw new AppError(404, 'User not found');
|
||||
|
||||
const existing = await prisma.blockedUser.findUnique({
|
||||
where: { blockerId_blockedId: { blockerId: req.userId!, blockedId: req.params.id } },
|
||||
});
|
||||
if (existing) throw new AppError(409, 'User already blocked');
|
||||
|
||||
await prisma.blockedUser.create({
|
||||
data: { blockerId: req.userId!, blockedId: req.params.id },
|
||||
});
|
||||
res.json({ message: 'User blocked' });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:id/block', authenticate, async (req, res, next) => {
|
||||
try {
|
||||
const existing = await prisma.blockedUser.findUnique({
|
||||
where: { blockerId_blockedId: { blockerId: req.userId!, blockedId: req.params.id } },
|
||||
});
|
||||
if (!existing) throw new AppError(404, 'Block not found');
|
||||
|
||||
await prisma.blockedUser.delete({ where: { id: existing.id } });
|
||||
res.json({ message: 'User unblocked' });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// --- Public profile (must be LAST due to /:id param) ---
|
||||
router.get('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const user = await prisma.user.findUnique({ where: { id: req.params.id }, select: userSelect });
|
||||
if (!user) throw new AppError(404, 'User not found');
|
||||
res.json(user);
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.params.id },
|
||||
select: {
|
||||
...userSelect,
|
||||
_count: { select: { listings: { where: { status: 'ACTIVE' } } } },
|
||||
},
|
||||
});
|
||||
if (!user || !(await prisma.user.findUnique({ where: { id: req.params.id, isActive: true } }))) {
|
||||
throw new AppError(404, 'User not found');
|
||||
}
|
||||
|
||||
// Enforce privacy flags
|
||||
const publicUser: Record<string, unknown> = { ...user };
|
||||
if (!user.showEmail) delete publicUser.email;
|
||||
if (!user.showPhone) delete publicUser.phone;
|
||||
if (!user.showLocation) delete publicUser.location;
|
||||
if (!user.showRating) {
|
||||
delete publicUser.rating;
|
||||
delete publicUser.ratingCount;
|
||||
}
|
||||
|
||||
res.json(publicUser);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user