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