Files
CRM_Montzar/apps/api/src/org-roles/org-roles.service.ts
T

261 lines
7.8 KiB
TypeScript

import {
BadRequestException,
ConflictException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import type { OrgRoleDto, RolePermissionKey, UserRole } from '@mento/shared';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import {
ALL_ROLE_PERMISSIONS,
defaultColorForSystemKey,
sanitizePermissions,
SYSTEM_ROLE_SEEDS,
systemKeyToUserRole,
} from './role-defaults';
type RoleRow = {
id: string;
name: string;
description: string;
color: string;
permissions: string[];
systemKey: string | null;
memberships: Array<{
user: {
id: string;
fullName: string;
avatarStorageKey: string | null;
};
}>;
};
const ROLE_MEMBERS_INCLUDE = {
memberships: {
include: {
user: { select: { id: true, fullName: true, avatarStorageKey: true } },
},
orderBy: { createdAt: 'asc' as const },
},
};
@Injectable()
export class OrgRolesService {
constructor(private readonly prisma: PrismaService) {}
async list(organizationId: string): Promise<OrgRoleDto[]> {
await this.ensureDefaultRoles(organizationId);
const rows = await this.prisma.orgRole.findMany({
where: { organizationId },
include: ROLE_MEMBERS_INCLUDE,
orderBy: [{ systemKey: 'asc' }, { createdAt: 'asc' }],
});
return rows.map((row) => this.toDto(row));
}
async create(
organizationId: string,
actorRole: UserRole,
dto: {
name: string;
description?: string;
color?: string;
permissions?: RolePermissionKey[];
},
): Promise<OrgRoleDto> {
this.assertManager(actorRole);
const name = dto.name.trim();
if (name.length < 2) {
throw new BadRequestException('نام نقش باید حداقل دو حرف باشد');
}
try {
const row = await this.prisma.orgRole.create({
data: {
organizationId,
name,
description: (dto.description ?? '').trim(),
color: dto.color ?? 'teal',
permissions: dto.permissions?.length ? dto.permissions : [...ALL_ROLE_PERMISSIONS],
},
include: ROLE_MEMBERS_INCLUDE,
});
return this.toDto(row);
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
throw new ConflictException('نقشی با این نام از قبل وجود دارد');
}
throw e;
}
}
async update(
organizationId: string,
actorRole: UserRole,
id: string,
dto: {
name?: string;
description?: string;
color?: string;
permissions?: RolePermissionKey[];
},
): Promise<OrgRoleDto> {
this.assertManager(actorRole);
const existing = await this.prisma.orgRole.findFirst({
where: { id, organizationId },
});
if (!existing) throw new NotFoundException('نقش پیدا نشد');
const data: Prisma.OrgRoleUpdateInput = {};
if (dto.name !== undefined) {
const name = dto.name.trim();
if (name.length < 2) {
throw new BadRequestException('نام نقش باید حداقل دو حرف باشد');
}
data.name = name;
}
if (dto.description !== undefined) data.description = dto.description.trim();
if (dto.color !== undefined) data.color = dto.color;
if (dto.permissions !== undefined) data.permissions = sanitizePermissions(dto.permissions);
try {
const row = await this.prisma.orgRole.update({
where: { id: existing.id },
data,
include: ROLE_MEMBERS_INCLUDE,
});
return this.toDto(row);
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
throw new ConflictException('نقشی با این نام از قبل وجود دارد');
}
throw e;
}
}
async bulkUpdatePermissions(
organizationId: string,
actorRole: UserRole,
items: Array<{ id: string; permissions: RolePermissionKey[] }>,
): Promise<OrgRoleDto[]> {
this.assertManager(actorRole);
await this.ensureDefaultRoles(organizationId);
await this.prisma.$transaction(
items.map((item) =>
this.prisma.orgRole.updateMany({
where: { id: item.id, organizationId },
data: { permissions: sanitizePermissions(item.permissions) },
}),
),
);
return this.list(organizationId);
}
async remove(organizationId: string, actorRole: UserRole, id: string): Promise<{ ok: true }> {
this.assertManager(actorRole);
const existing = await this.prisma.orgRole.findFirst({
where: { id, organizationId },
});
if (!existing) throw new NotFoundException('نقش پیدا نشد');
if (existing.systemKey) {
throw new BadRequestException('نقش‌های سیستمی قابل حذف نیستند');
}
await this.prisma.orgRole.delete({ where: { id: existing.id } });
return { ok: true };
}
async ensureDefaultRoles(organizationId: string) {
const existing = await this.prisma.orgRole.findMany({
where: { organizationId },
});
const byKey = new Map(existing.filter((r) => r.systemKey).map((r) => [r.systemKey!, r]));
for (const seed of SYSTEM_ROLE_SEEDS) {
if (byKey.has(seed.systemKey)) continue;
const created = await this.prisma.orgRole.create({
data: {
organizationId,
name: seed.name,
description: seed.description,
color: seed.color,
permissions: seed.permissions,
systemKey: seed.systemKey,
},
});
byKey.set(seed.systemKey, created);
}
const unassigned = await this.prisma.membership.findMany({
where: { organizationId, orgRoleId: null },
select: { id: true, role: true },
});
for (const membership of unassigned) {
const role = byKey.get(membership.role);
if (!role) continue;
await this.prisma.membership.update({
where: { id: membership.id },
data: { orgRoleId: role.id },
});
}
}
async resolveInviteRole(
organizationId: string,
dto: { orgRoleId?: string; role?: Exclude<UserRole, 'owner'> },
): Promise<{ orgRoleId: string; role: UserRole }> {
await this.ensureDefaultRoles(organizationId);
if (dto.orgRoleId) {
const orgRole = await this.prisma.orgRole.findFirst({
where: { id: dto.orgRoleId, organizationId },
});
if (!orgRole) throw new BadRequestException('نقش انتخاب‌شده معتبر نیست');
return {
orgRoleId: orgRole.id,
role: systemKeyToUserRole(orgRole.systemKey),
};
}
const role = dto.role ?? 'sales';
const orgRole = await this.prisma.orgRole.findFirst({
where: { organizationId, systemKey: role },
});
if (!orgRole) {
throw new BadRequestException('نقش پیش‌فرض پیدا نشد');
}
return { orgRoleId: orgRole.id, role };
}
defaultColorFor(role: UserRole) {
return defaultColorForSystemKey(role);
}
private assertManager(actorRole: UserRole) {
if (actorRole !== 'owner' && actorRole !== 'admin') {
throw new ForbiddenException('فقط مالک یا مدیر می‌تواند نقش‌ها را مدیریت کند');
}
}
private toDto(row: RoleRow): OrgRoleDto {
const systemKey =
row.systemKey === 'owner' ||
row.systemKey === 'admin' ||
row.systemKey === 'sales' ||
row.systemKey === 'support'
? row.systemKey
: null;
return {
id: row.id,
name: row.name,
description: row.description,
color: row.color,
permissions: sanitizePermissions(row.permissions),
systemKey,
userCount: row.memberships.length,
members: row.memberships.map((membership) => ({
id: membership.user.id,
fullName: membership.user.fullName,
hasAvatar: Boolean(membership.user.avatarStorageKey),
})),
};
}
}