Files
marketplace/server/src/routes/user.ts
delta-lynx-89e8 d09c998d51 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>
2026-02-22 12:30:03 -08:00

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;