import { eq } from 'drizzle-orm'; import { SignJWT } from 'jose'; import bcrypt from 'bcrypt'; import { db } from '../db/index'; import { users } from '../db/schema'; import { config } from '../config'; import { AppError } from '../middleware/error-handler'; const secret = new TextEncoder().encode(config.JWT_SECRET); export async function login(email: string, password: string) { const user = await db.query.users.findFirst({ where: eq(users.email, email), }); if (!user) { throw new AppError(40101, 'Invalid email or password', 401); } // Check account lockout if (user.lockedUntil && user.lockedUntil > new Date()) { const retryAfter = Math.ceil((user.lockedUntil.getTime() - Date.now()) / 1000); throw new AppError(42300, `Account locked. Try again in ${Math.ceil(retryAfter / 60)} minutes`, 423); } // Verify password const isValid = await bcrypt.compare(password, user.passwordHash); if (!isValid) { const attempts = (user.loginAttempts || 0) + 1; const updateData: Record = { loginAttempts: attempts, updatedAt: new Date(), }; if (attempts >= 5) { updateData.lockedUntil = new Date(Date.now() + 15 * 60 * 1000); } await db.update(users) .set(updateData) .where(eq(users.id, user.id)); throw new AppError(40101, 'Invalid email or password', 401); } // Reset login attempts on success await db.update(users) .set({ loginAttempts: 0, lockedUntil: null, updatedAt: new Date() }) .where(eq(users.id, user.id)); // Generate JWT const token = await new SignJWT({ sub: user.id, email: user.email, role: user.role, displayName: user.displayName, }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('7d') .sign(secret); return { token, user: { id: user.id, displayName: user.displayName, email: user.email, role: user.role, }, }; } export async function getUserById(userId: string) { const user = await db.query.users.findFirst({ where: eq(users.id, userId), }); if (!user) { throw new AppError(40401, 'User not found', 404); } return { id: user.id, displayName: user.displayName, email: user.email, role: user.role, }; }