Lb1/services/gateway/src/routes/auth.routes.js

195 lines
7.8 KiB
JavaScript

import { prisma } from '@luxe/database';
import { hashPassword, verifyPassword, generateAccessToken, generateRefreshToken, verifyToken, successResponse, errorResponse, registerSchema, loginSchema, } from '@luxe/shared';
export async function authRoutes(fastify) {
// 1. POST /api/v1/auth/register
fastify.post('/register', async (request, reply) => {
const parseResult = registerSchema.safeParse(request.body);
if (!parseResult.success) {
return reply.status(400).send(errorResponse('Validation error', parseResult.error.errors.map(e => ({ field: e.path.join('.'), message: e.message }))));
}
const { email, password, username, role, phone } = parseResult.data;
const existingUser = await prisma.user.findFirst({
where: { OR: [{ email }, { username }] },
});
if (existingUser) {
return reply.status(400).send(errorResponse('User with email or username already exists'));
}
const passwordHash = await hashPassword(password);
const user = await prisma.user.create({
data: {
email,
username,
passwordHash,
role: role,
phone,
},
});
// Create Wallet for new user
await prisma.wallet.create({
data: {
userId: user.id,
balance: 0.0,
credits: 10.0, // Complimentary bonus credits
},
});
// If Advertiser role, create AdvertiserProfile shell
if (role === 'ADVERTISER' || role === 'AGENCY') {
await prisma.advertiserProfile.create({
data: {
userId: user.id,
type: role,
},
});
}
const tokenPayload = { userId: user.id, email: user.email, role: user.role };
const accessToken = generateAccessToken(tokenPayload);
const refreshToken = generateRefreshToken(tokenPayload);
// Save Session in DB
await prisma.session.create({
data: {
userId: user.id,
refreshToken,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
},
});
reply.setCookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/api/v1/auth',
});
return reply.status(201).send(successResponse({
accessToken,
user: {
id: user.id,
email: user.email,
username: user.username,
role: user.role,
},
}, 'User registered successfully'));
});
// 2. POST /api/v1/auth/login
fastify.post('/login', async (request, reply) => {
const parseResult = loginSchema.safeParse(request.body);
if (!parseResult.success) {
return reply.status(400).send(errorResponse('Invalid payload'));
}
const { email, password } = parseResult.data;
const user = await prisma.user.findUnique({ where: { email } });
if (!user) {
return reply.status(401).send(errorResponse('Invalid email or password'));
}
if (user.status === 'BANNED' || user.status === 'SUSPENDED') {
return reply.status(403).send(errorResponse(`Account is ${user.status.toLowerCase()}`));
}
const isValid = await verifyPassword(user.passwordHash, password);
if (!isValid) {
await prisma.user.update({
where: { id: user.id },
data: { failedLoginCount: { increment: 1 } },
});
return reply.status(401).send(errorResponse('Invalid email or password'));
}
// Reset failed attempts & update last login
await prisma.user.update({
where: { id: user.id },
data: { failedLoginCount: 0, lastLoginAt: new Date() },
});
const tokenPayload = { userId: user.id, email: user.email, role: user.role };
const accessToken = generateAccessToken(tokenPayload);
const refreshToken = generateRefreshToken(tokenPayload);
await prisma.session.create({
data: {
userId: user.id,
refreshToken,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
},
});
reply.setCookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/api/v1/auth',
});
return reply.send(successResponse({
accessToken,
user: {
id: user.id,
email: user.email,
username: user.username,
role: user.role,
avatar: user.avatar,
},
}, 'Logged in successfully'));
});
// 3. POST /api/v1/auth/refresh-token
fastify.post('/refresh-token', async (request, reply) => {
const refreshToken = request.cookies.refreshToken;
if (!refreshToken) {
return reply.status(401).send(errorResponse('Refresh token required'));
}
try {
const decoded = verifyToken(refreshToken);
const session = await prisma.session.findUnique({ where: { refreshToken } });
if (!session || session.isRevoked || session.expiresAt < new Date()) {
return reply.status(401).send(errorResponse('Invalid or expired session'));
}
const newAccessToken = generateAccessToken({
userId: decoded.userId,
email: decoded.email,
role: decoded.role,
});
return reply.send(successResponse({ accessToken: newAccessToken }));
}
catch (err) {
return reply.status(401).send(errorResponse('Invalid refresh token'));
}
});
// 4. POST /api/v1/auth/logout
fastify.post('/logout', async (request, reply) => {
const refreshToken = request.cookies.refreshToken;
if (refreshToken) {
await prisma.session.updateMany({
where: { refreshToken },
data: { isRevoked: true },
});
}
reply.clearCookie('refreshToken', { path: '/api/v1/auth' });
return reply.send(successResponse(null, 'Logged out successfully'));
});
// 5. GET /api/v1/auth/me
fastify.get('/me', async (request, reply) => {
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return reply.status(401).send(errorResponse('Bearer token required'));
}
try {
const token = authHeader.split(' ')[1];
const decoded = verifyToken(token);
const user = await prisma.user.findUnique({
where: { id: decoded.userId },
select: {
id: true,
email: true,
username: true,
role: true,
avatar: true,
bio: true,
language: true,
currency: true,
emailVerifiedAt: true,
phoneVerifiedAt: true,
wallet: { select: { balance: true, credits: true } },
},
});
if (!user) {
return reply.status(404).send(errorResponse('User not found'));
}
return reply.send(successResponse(user));
}
catch (err) {
return reply.status(401).send(errorResponse('Invalid or expired token'));
}
});
}
//# sourceMappingURL=auth.routes.js.map