132 lines
4.5 KiB
TypeScript
132 lines
4.5 KiB
TypeScript
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' },
|
|
});
|
|
}
|
|
}
|