archetecture security fix
This commit is contained in:
+166
@@ -0,0 +1,166 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.listEmployees = listEmployees;
|
||||
exports.inviteEmployee = inviteEmployee;
|
||||
exports.updateEmployeeRole = updateEmployeeRole;
|
||||
exports.deactivateEmployee = deactivateEmployee;
|
||||
exports.reactivateEmployee = reactivateEmployee;
|
||||
exports.removeEmployee = removeEmployee;
|
||||
const crypto_1 = __importDefault(require("crypto"));
|
||||
const prisma_1 = require("../lib/prisma");
|
||||
const notificationService_1 = require("./notificationService");
|
||||
const notificationLocalizationService_1 = require("./notificationLocalizationService");
|
||||
const INVITE_TOKEN_TTL_MINUTES = 60 * 24 * 7;
|
||||
function ensureDashboardBasePath(baseUrl) {
|
||||
try {
|
||||
const url = new URL(baseUrl);
|
||||
const pathname = url.pathname.replace(/\/$/, '');
|
||||
if (!pathname.endsWith('/dashboard'))
|
||||
url.pathname = `${pathname}/dashboard`;
|
||||
return url.toString().replace(/\/$/, '');
|
||||
}
|
||||
catch {
|
||||
const trimmed = baseUrl.replace(/\/$/, '');
|
||||
return trimmed.endsWith('/dashboard') ? trimmed : `${trimmed}/dashboard`;
|
||||
}
|
||||
}
|
||||
function buildInviteEmail(resetUrl, companyName, firstName, role, locale) {
|
||||
const greetingName = firstName.trim() || 'there';
|
||||
if (locale === 'fr') {
|
||||
return {
|
||||
subject: `Vous avez ete invite chez ${companyName}`,
|
||||
html: `
|
||||
<p>Bonjour ${greetingName},</p>
|
||||
<p>Vous avez ete invite a rejoindre ${companyName} sur RentalDriveGo en tant que ${role.toLowerCase()}.</p>
|
||||
<p><a href="${resetUrl}" style="background:#1d4ed8;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;display:inline-block;font-weight:600;">Definir votre mot de passe</a></p>
|
||||
<p>Ce lien d'invitation expire dans 7 jours.</p>
|
||||
<p>RentalDriveGo</p>
|
||||
`,
|
||||
text: `Bonjour ${greetingName},\n\nVous avez ete invite a rejoindre ${companyName} sur RentalDriveGo en tant que ${role.toLowerCase()}.\n\nDefinissez votre mot de passe ici : ${resetUrl}\n\nCe lien d'invitation expire dans 7 jours.\n\nRentalDriveGo`,
|
||||
};
|
||||
}
|
||||
if (locale === 'ar') {
|
||||
return {
|
||||
subject: `تمت دعوتك إلى ${companyName}`,
|
||||
html: `
|
||||
<div dir="rtl" style="text-align:right">
|
||||
<p>مرحباً ${greetingName}،</p>
|
||||
<p>تمت دعوتك للانضمام إلى ${companyName} على RentalDriveGo بصفتك ${role.toLowerCase()}.</p>
|
||||
<p><a href="${resetUrl}" style="background:#1d4ed8;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;display:inline-block;font-weight:600;">عيّن كلمة المرور</a></p>
|
||||
<p>تنتهي صلاحية رابط الدعوة هذا خلال 7 أيام.</p>
|
||||
<p>RentalDriveGo</p>
|
||||
</div>
|
||||
`,
|
||||
text: `مرحباً ${greetingName}،\n\nتمت دعوتك للانضمام إلى ${companyName} على RentalDriveGo بصفتك ${role.toLowerCase()}.\n\nعيّن كلمة المرور هنا: ${resetUrl}\n\nتنتهي صلاحية رابط الدعوة هذا خلال 7 أيام.\n\nRentalDriveGo`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
subject: `You have been invited to ${companyName}`,
|
||||
html: `
|
||||
<p>Hi ${greetingName},</p>
|
||||
<p>You were invited to join ${companyName} on RentalDriveGo as a ${role.toLowerCase()}.</p>
|
||||
<p><a href="${resetUrl}" style="background:#1d4ed8;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;display:inline-block;font-weight:600;">Set your password</a></p>
|
||||
<p>This invitation link expires in 7 days.</p>
|
||||
<p>RentalDriveGo</p>
|
||||
`,
|
||||
text: `Hi ${greetingName},\n\nYou were invited to join ${companyName} on RentalDriveGo as a ${role.toLowerCase()}.\n\nSet your password here: ${resetUrl}\n\nThis invitation link expires in 7 days.\n\nRentalDriveGo`,
|
||||
};
|
||||
}
|
||||
async function listEmployees(companyId) {
|
||||
const employees = await prisma_1.prisma.employee.findMany({
|
||||
where: { companyId },
|
||||
orderBy: [{ role: 'asc' }, { createdAt: 'asc' }],
|
||||
});
|
||||
return employees.map((employee) => ({
|
||||
...employee,
|
||||
lastActiveAt: null,
|
||||
invitationStatus: employee.passwordHash ? 'accepted' : 'pending',
|
||||
}));
|
||||
}
|
||||
async function inviteEmployee(companyId, inviterId, payload) {
|
||||
if (payload.role === 'OWNER') {
|
||||
throw Object.assign(new Error('Cannot invite a member with the OWNER role'), { statusCode: 400, code: 'invalid_role' });
|
||||
}
|
||||
const existing = await prisma_1.prisma.employee.findFirst({ where: { companyId, email: payload.email } });
|
||||
if (existing) {
|
||||
throw Object.assign(new Error('An employee with this email already exists in your team'), { statusCode: 409, code: 'employee_already_exists' });
|
||||
}
|
||||
const company = await prisma_1.prisma.company.findUniqueOrThrow({
|
||||
where: { id: companyId },
|
||||
include: { brand: { select: { defaultLocale: true, displayName: true } } },
|
||||
});
|
||||
const rawToken = crypto_1.default.randomBytes(32).toString('hex');
|
||||
const expiresAt = new Date(Date.now() + INVITE_TOKEN_TTL_MINUTES * 60 * 1000);
|
||||
const locale = (0, notificationLocalizationService_1.coerceNotificationLocale)(company.brand?.defaultLocale);
|
||||
const employee = await prisma_1.prisma.employee.create({
|
||||
data: {
|
||||
companyId,
|
||||
clerkUserId: `local_member_${crypto_1.default.randomUUID()}`,
|
||||
firstName: payload.firstName,
|
||||
lastName: payload.lastName,
|
||||
email: payload.email,
|
||||
role: payload.role,
|
||||
isActive: true,
|
||||
preferredLanguage: locale,
|
||||
passwordResetToken: rawToken,
|
||||
passwordResetExpiresAt: expiresAt,
|
||||
},
|
||||
});
|
||||
const dashboardUrl = ensureDashboardBasePath(process.env.DASHBOARD_URL ?? process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard');
|
||||
const resetUrl = `${dashboardUrl}/reset-password?token=${rawToken}`;
|
||||
const message = buildInviteEmail(resetUrl, company.brand?.displayName ?? company.name, payload.firstName, payload.role, locale);
|
||||
await (0, notificationService_1.sendTransactionalEmail)({
|
||||
to: payload.email,
|
||||
subject: message.subject,
|
||||
html: message.html,
|
||||
text: message.text,
|
||||
});
|
||||
return {
|
||||
employee: {
|
||||
...employee,
|
||||
lastActiveAt: null,
|
||||
invitationStatus: 'pending',
|
||||
},
|
||||
invitationId: employee.id,
|
||||
invitedBy: inviterId,
|
||||
};
|
||||
}
|
||||
async function updateEmployeeRole(companyId, requesterId, requesterRole, employeeId, payload) {
|
||||
const target = await prisma_1.prisma.employee.findFirstOrThrow({ where: { id: employeeId, companyId } });
|
||||
if (requesterRole !== 'OWNER')
|
||||
throw Object.assign(new Error('Only the account owner can change team member roles'), { statusCode: 403, code: 'forbidden' });
|
||||
if (target.role === 'OWNER')
|
||||
throw Object.assign(new Error('Cannot change the role of the account owner'), { statusCode: 400 });
|
||||
if (payload.role === 'OWNER')
|
||||
throw Object.assign(new Error('Cannot assign the OWNER role via this endpoint'), { statusCode: 400 });
|
||||
if (target.id === requesterId)
|
||||
throw Object.assign(new Error('You cannot change your own role'), { statusCode: 400 });
|
||||
return prisma_1.prisma.employee.update({ where: { id: employeeId }, data: { role: payload.role } });
|
||||
}
|
||||
async function deactivateEmployee(companyId, requesterRole, employeeId) {
|
||||
if (requesterRole !== 'OWNER')
|
||||
throw Object.assign(new Error('Only the account owner can deactivate team members'), { statusCode: 403 });
|
||||
const target = await prisma_1.prisma.employee.findFirstOrThrow({ where: { id: employeeId, companyId } });
|
||||
if (target.role === 'OWNER')
|
||||
throw Object.assign(new Error('Cannot deactivate the account owner'), { statusCode: 400 });
|
||||
return prisma_1.prisma.employee.update({ where: { id: employeeId }, data: { isActive: false } });
|
||||
}
|
||||
async function reactivateEmployee(companyId, requesterRole, employeeId) {
|
||||
if (requesterRole !== 'OWNER')
|
||||
throw Object.assign(new Error('Only the account owner can reactivate team members'), { statusCode: 403 });
|
||||
await prisma_1.prisma.employee.findFirstOrThrow({ where: { id: employeeId, companyId } });
|
||||
return prisma_1.prisma.employee.update({ where: { id: employeeId }, data: { isActive: true } });
|
||||
}
|
||||
async function removeEmployee(companyId, requesterRole, employeeId) {
|
||||
if (requesterRole !== 'OWNER')
|
||||
throw Object.assign(new Error('Only the account owner can remove team members'), { statusCode: 403 });
|
||||
const target = await prisma_1.prisma.employee.findFirstOrThrow({ where: { id: employeeId, companyId } });
|
||||
if (target.role === 'OWNER')
|
||||
throw Object.assign(new Error('Cannot remove the account owner'), { statusCode: 400 });
|
||||
await prisma_1.prisma.employee.delete({ where: { id: employeeId } });
|
||||
return { success: true };
|
||||
}
|
||||
//# sourceMappingURL=teamService.js.map
|
||||
Reference in New Issue
Block a user