import prisma from '../utils/db'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import { AppError } from '../utils/errors'; export class AuthService { public async register(data: any) { const existing = await prisma.user.findUnique({ where: { email: data.email } }); if (existing) throw new AppError('Email already in use', 400); const passwordHash = await bcrypt.hash(data.password, 10); const user = await prisma.user.create({ data: { email: data.email, passwordHash, role: data.role || 'PARTNER_USER', organizationId: data.organizationId || null, onboardingStatus: data.role === 'ADMIN' ? 'APPROVED' : 'PENDING_ONBOARDING' } }); const { passwordHash: _, ...userWithoutPassword } = user; return userWithoutPassword; } public async invitePartner(email: string, organizationId?: string) { const existing = await prisma.user.findUnique({ where: { email } }); if (existing) throw new AppError('Email already in use', 400); const crypto = require('crypto'); const inviteToken = crypto.randomBytes(32).toString('hex'); const inviteTokenExp = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours const user = await prisma.user.create({ data: { email, passwordHash: '', // Set on accept role: 'PARTNER_USER', organizationId: organizationId || null, inviteToken, inviteTokenExp, onboardingStatus: 'PENDING_ONBOARDING', mfaEnabled: true, } }); // In a real app, send email here return { inviteToken }; } public async validateInvite(token: string) { const user = await prisma.user.findUnique({ where: { inviteToken: token } }); if (!user || !user.inviteTokenExp || user.inviteTokenExp < new Date()) { throw new AppError('Invalid or expired invite token', 400); } return { email: user.email }; } public async acceptInvite(token: string, passwordString: string) { const user = await prisma.user.findUnique({ where: { inviteToken: token } }); if (!user || !user.inviteTokenExp || user.inviteTokenExp < new Date()) { throw new AppError('Invalid or expired invite token', 400); } const passwordHash = await bcrypt.hash(passwordString, 10); const updatedUser = await prisma.user.update({ where: { id: user.id }, data: { passwordHash, inviteToken: null, inviteTokenExp: null, } }); const secret = process.env.JWT_SECRET || 'secret'; const accessToken = jwt.sign({ userId: updatedUser.id, role: updatedUser.role }, secret, { expiresIn: '15m' }); const refreshToken = jwt.sign({ userId: updatedUser.id }, secret, { expiresIn: '7d' }); const { passwordHash: _, ...userWithoutPassword } = updatedUser; return { user: userWithoutPassword, accessToken, refreshToken }; } public async login(email: string, passwordString: string) { const user = await prisma.user.findUnique({ where: { email } }); if (!user) { throw new AppError('Invalid credentials', 401); } const isMatch = await bcrypt.compare(passwordString, user.passwordHash); if (!isMatch) { throw new AppError('Invalid credentials', 401); } const secret = process.env.JWT_SECRET || 'secret'; const accessToken = jwt.sign({ userId: user.id, role: user.role }, secret, { expiresIn: '15m' }); const refreshToken = jwt.sign({ userId: user.id }, secret, { expiresIn: '7d' }); const { passwordHash: _, ...userWithoutPassword } = user; return { user: userWithoutPassword, accessToken, refreshToken }; } public async refresh(refreshToken: string) { const secret = process.env.JWT_SECRET || 'secret'; try { const decoded = jwt.verify(refreshToken, secret) as any; const user = await prisma.user.findUnique({ where: { id: decoded.userId } }); if (!user) throw new AppError('Invalid refresh token', 401); const newAccessToken = jwt.sign({ userId: user.id, role: user.role }, secret, { expiresIn: '15m' }); return { accessToken: newAccessToken }; } catch(err) { throw new AppError('Invalid or expired refresh token', 401); } } public async listPartners() { return await prisma.user.findMany({ where: { role: 'PARTNER_USER' }, select: { id: true, email: true, onboardingStatus: true, mfaEnabled: true, createdAt: true, updatedAt: true, organizationId: true, }, orderBy: { createdAt: 'desc' }, }); } }