195 lines
7.8 KiB
JavaScript
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
|