- 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>
225 lines
7.1 KiB
TypeScript
225 lines
7.1 KiB
TypeScript
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, 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 });
|
|
if (!user) throw new AppError(404, 'User not found');
|
|
res.json(user);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
router.put('/profile', authenticate, validate(updateProfileSchema), async (req, res, next) => {
|
|
try {
|
|
const user = await prisma.user.update({
|
|
where: { id: req.userId },
|
|
data: req.body,
|
|
select: userSelect,
|
|
});
|
|
res.json(user);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// --- 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');
|
|
|
|
const valid = await comparePassword(currentPassword, user.passwordHash);
|
|
if (!valid) throw new AppError(400, 'Current password is incorrect');
|
|
|
|
const passwordHash = await hashPassword(newPassword);
|
|
await prisma.user.update({ where: { id: req.userId }, data: { passwordHash } });
|
|
|
|
res.json({ message: 'Password updated' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// --- 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,
|
|
_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);
|
|
}
|
|
});
|
|
|
|
export default router;
|