166 lines
8.9 KiB
JavaScript
166 lines
8.9 KiB
JavaScript
"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
|