Full-stack marketplace for buying/selling second-hand items. React 19 + TypeScript + Tailwind CSS v4 frontend with 17 screens, Express + Prisma + Socket.io backend, Stripe payments, JWT auth. Deployed at https://marketplace.173.212.212.157.sslip.io/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
149 lines
4.9 KiB
TypeScript
149 lines
4.9 KiB
TypeScript
import { Router } from 'express';
|
|
import { prisma } from '../config/database.js';
|
|
import { hashPassword, comparePassword } from '../utils/password.js';
|
|
import { generateAccessToken, generateRefreshToken, verifyRefreshToken } from '../utils/jwt.js';
|
|
import { validate } from '../middleware/validate.js';
|
|
import { authenticate } from '../middleware/auth.js';
|
|
import { registerSchema, loginSchema } from '../validators/auth.js';
|
|
import { AppError } from '../middleware/errorHandler.js';
|
|
|
|
const router = Router();
|
|
|
|
router.post('/register', validate(registerSchema), async (req, res, next) => {
|
|
try {
|
|
const { fullName, email, password } = req.body;
|
|
|
|
const existing = await prisma.user.findUnique({ where: { email } });
|
|
if (existing) throw new AppError(409, 'Email already registered');
|
|
|
|
const passwordHash = await hashPassword(password);
|
|
const user = await prisma.user.create({
|
|
data: { fullName, email, passwordHash },
|
|
select: { id: true, email: true, fullName: true, nickname: true, avatar: true, phone: true, location: true, bio: true, rating: true, showEmail: true, showPhone: true, showLocation: true, createdAt: true },
|
|
});
|
|
|
|
const accessToken = generateAccessToken(user.id);
|
|
const refreshToken = generateRefreshToken(user.id);
|
|
|
|
await prisma.session.create({
|
|
data: {
|
|
userId: user.id,
|
|
refreshToken,
|
|
userAgent: req.headers['user-agent'] || null,
|
|
ipAddress: req.ip || null,
|
|
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
},
|
|
});
|
|
|
|
res.cookie('refreshToken', refreshToken, {
|
|
httpOnly: true,
|
|
secure: process.env['NODE_ENV'] === 'production',
|
|
sameSite: 'lax',
|
|
maxAge: 7 * 24 * 60 * 60 * 1000,
|
|
});
|
|
|
|
res.status(201).json({ user, accessToken });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
router.post('/login', validate(loginSchema), async (req, res, next) => {
|
|
try {
|
|
const { email, password } = req.body;
|
|
|
|
const user = await prisma.user.findUnique({ where: { email } });
|
|
if (!user) throw new AppError(401, 'Invalid email or password');
|
|
if (!user.isActive) throw new AppError(403, 'Account is disabled');
|
|
|
|
const valid = await comparePassword(password, user.passwordHash);
|
|
if (!valid) throw new AppError(401, 'Invalid email or password');
|
|
|
|
const accessToken = generateAccessToken(user.id);
|
|
const refreshToken = generateRefreshToken(user.id);
|
|
|
|
await prisma.session.create({
|
|
data: {
|
|
userId: user.id,
|
|
refreshToken,
|
|
userAgent: req.headers['user-agent'] || null,
|
|
ipAddress: req.ip || null,
|
|
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
},
|
|
});
|
|
|
|
res.cookie('refreshToken', refreshToken, {
|
|
httpOnly: true,
|
|
secure: process.env['NODE_ENV'] === 'production',
|
|
sameSite: 'lax',
|
|
maxAge: 7 * 24 * 60 * 60 * 1000,
|
|
});
|
|
|
|
const { passwordHash: _, ...userData } = user;
|
|
res.json({ user: userData, accessToken });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
router.post('/refresh', async (req, res, next) => {
|
|
try {
|
|
const token = req.cookies?.refreshToken;
|
|
if (!token) throw new AppError(401, 'No refresh token');
|
|
|
|
const payload = verifyRefreshToken(token);
|
|
const session = await prisma.session.findUnique({ where: { refreshToken: token } });
|
|
if (!session || session.expiresAt < new Date()) {
|
|
if (session) await prisma.session.delete({ where: { id: session.id } });
|
|
throw new AppError(401, 'Invalid refresh token');
|
|
}
|
|
|
|
const accessToken = generateAccessToken(payload.userId);
|
|
const newRefreshToken = generateRefreshToken(payload.userId);
|
|
|
|
await prisma.session.update({
|
|
where: { id: session.id },
|
|
data: { refreshToken: newRefreshToken, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) },
|
|
});
|
|
|
|
res.cookie('refreshToken', newRefreshToken, {
|
|
httpOnly: true,
|
|
secure: process.env['NODE_ENV'] === 'production',
|
|
sameSite: 'lax',
|
|
maxAge: 7 * 24 * 60 * 60 * 1000,
|
|
});
|
|
|
|
res.json({ accessToken });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
router.get('/me', authenticate, async (req, res, next) => {
|
|
try {
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: req.userId },
|
|
select: { id: true, email: true, fullName: true, nickname: true, avatar: true, phone: true, location: true, bio: true, rating: true, showEmail: true, showPhone: true, showLocation: true, createdAt: true },
|
|
});
|
|
if (!user) throw new AppError(404, 'User not found');
|
|
res.json({ user });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
router.post('/logout', authenticate, async (req, res, next) => {
|
|
try {
|
|
const token = req.cookies?.refreshToken;
|
|
if (token) {
|
|
await prisma.session.deleteMany({ where: { refreshToken: token } });
|
|
}
|
|
res.clearCookie('refreshToken');
|
|
res.json({ message: 'Logged out' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
export default router;
|