Initial marketplace implementation
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>
This commit is contained in:
148
server/src/routes/auth.ts
Normal file
148
server/src/routes/auth.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user