Creating import&export excel
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayUnique,
|
||||
IsArray,
|
||||
IsIn,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { ROLE_PERMISSION_ITEMS, type RolePermissionKey } from '@mento/shared';
|
||||
import { ROLE_COLORS } from './role-defaults';
|
||||
|
||||
const PERMISSION_KEYS = ROLE_PERMISSION_ITEMS.map((item) => item.key);
|
||||
|
||||
export class CreateOrgRoleDto {
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(80)
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(280)
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn([...ROLE_COLORS])
|
||||
color?: (typeof ROLE_COLORS)[number];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayUnique()
|
||||
@IsIn(PERMISSION_KEYS, { each: true })
|
||||
permissions?: RolePermissionKey[];
|
||||
}
|
||||
|
||||
export class UpdateOrgRoleDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(80)
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(280)
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn([...ROLE_COLORS])
|
||||
color?: (typeof ROLE_COLORS)[number];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayUnique()
|
||||
@IsIn(PERMISSION_KEYS, { each: true })
|
||||
permissions?: RolePermissionKey[];
|
||||
}
|
||||
|
||||
export class RolePermissionPatchItemDto {
|
||||
@IsString()
|
||||
id!: string;
|
||||
|
||||
@IsArray()
|
||||
@ArrayUnique()
|
||||
@IsIn(PERMISSION_KEYS, { each: true })
|
||||
permissions!: RolePermissionKey[];
|
||||
}
|
||||
|
||||
export class BulkUpdatePermissionsDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => RolePermissionPatchItemDto)
|
||||
items!: RolePermissionPatchItemDto[];
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common';
|
||||
import type { UserRole } from '@mento/shared';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { Roles } from '../auth/roles.decorator';
|
||||
import { RolesGuard } from '../auth/roles.guard';
|
||||
import { BulkUpdatePermissionsDto, CreateOrgRoleDto, UpdateOrgRoleDto } from './dto';
|
||||
import { OrgRolesService } from './org-roles.service';
|
||||
|
||||
@Controller('org-roles')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
export class OrgRolesController {
|
||||
constructor(private readonly orgRoles: OrgRolesService) {}
|
||||
|
||||
@Get()
|
||||
list(@CurrentUser() user: { organizationId: string }) {
|
||||
return this.orgRoles.list(user.organizationId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Roles('owner', 'admin')
|
||||
create(
|
||||
@CurrentUser() user: { organizationId: string; role: UserRole },
|
||||
@Body() dto: CreateOrgRoleDto,
|
||||
) {
|
||||
return this.orgRoles.create(user.organizationId, user.role, dto);
|
||||
}
|
||||
|
||||
@Patch('permissions')
|
||||
@Roles('owner', 'admin')
|
||||
bulkPermissions(
|
||||
@CurrentUser() user: { organizationId: string; role: UserRole },
|
||||
@Body() dto: BulkUpdatePermissionsDto,
|
||||
) {
|
||||
return this.orgRoles.bulkUpdatePermissions(user.organizationId, user.role, dto.items);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Roles('owner', 'admin')
|
||||
update(
|
||||
@CurrentUser() user: { organizationId: string; role: UserRole },
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateOrgRoleDto,
|
||||
) {
|
||||
return this.orgRoles.update(user.organizationId, user.role, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Roles('owner', 'admin')
|
||||
remove(
|
||||
@CurrentUser() user: { organizationId: string; role: UserRole },
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
return this.orgRoles.remove(user.organizationId, user.role, id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { OrgRolesController } from './org-roles.controller';
|
||||
import { OrgRolesService } from './org-roles.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule],
|
||||
controllers: [OrgRolesController],
|
||||
providers: [OrgRolesService],
|
||||
exports: [OrgRolesService],
|
||||
})
|
||||
export class OrgRolesModule {}
|
||||
@@ -0,0 +1,264 @@
|
||||
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;
|
||||
users: Array<{
|
||||
id: string;
|
||||
fullName: string;
|
||||
avatarStorageKey: string | null;
|
||||
}>;
|
||||
};
|
||||
|
||||
@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: {
|
||||
users: {
|
||||
select: { id: true, fullName: true, avatarStorageKey: true },
|
||||
orderBy: { fullName: 'asc' },
|
||||
},
|
||||
},
|
||||
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: {
|
||||
users: {
|
||||
select: { id: true, fullName: true, avatarStorageKey: true },
|
||||
orderBy: { fullName: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
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: {
|
||||
users: {
|
||||
select: { id: true, fullName: true, avatarStorageKey: true },
|
||||
orderBy: { fullName: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
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.user.findMany({
|
||||
where: { organizationId, orgRoleId: null },
|
||||
select: { id: true, role: true },
|
||||
});
|
||||
for (const user of unassigned) {
|
||||
const role = byKey.get(user.role);
|
||||
if (!role) continue;
|
||||
await this.prisma.user.update({
|
||||
where: { id: user.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.users.length,
|
||||
members: row.users.map((user) => ({
|
||||
id: user.id,
|
||||
fullName: user.fullName,
|
||||
hasAvatar: Boolean(user.avatarStorageKey),
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { RolePermissionKey, UserRole } from '@mento/shared';
|
||||
import { ROLE_PERMISSION_ITEMS } from '@mento/shared';
|
||||
|
||||
export const ALL_ROLE_PERMISSIONS: RolePermissionKey[] = ROLE_PERMISSION_ITEMS.map(
|
||||
(item) => item.key,
|
||||
);
|
||||
|
||||
export const ROLE_COLORS = ['teal', 'violet', 'rose', 'sky', 'amber', 'slate', 'pink'] as const;
|
||||
export type RoleColor = (typeof ROLE_COLORS)[number];
|
||||
|
||||
export function defaultColorForSystemKey(key: UserRole): RoleColor {
|
||||
if (key === 'owner') return 'violet';
|
||||
if (key === 'admin') return 'rose';
|
||||
if (key === 'support') return 'sky';
|
||||
return 'teal';
|
||||
}
|
||||
|
||||
export const SYSTEM_ROLE_SEEDS: Array<{
|
||||
systemKey: UserRole;
|
||||
name: string;
|
||||
description: string;
|
||||
color: RoleColor;
|
||||
permissions: RolePermissionKey[];
|
||||
}> = [
|
||||
{
|
||||
systemKey: 'owner',
|
||||
name: 'مدیر سیستم',
|
||||
description: 'دسترسی کامل به تمام بخشهای سامانه.',
|
||||
color: 'violet',
|
||||
permissions: [...ALL_ROLE_PERMISSIONS],
|
||||
},
|
||||
{
|
||||
systemKey: 'admin',
|
||||
name: 'مدیر',
|
||||
description: 'مدیریت کاربران، تنظیمات و همه بخشهای سازمان.',
|
||||
color: 'rose',
|
||||
permissions: [...ALL_ROLE_PERMISSIONS],
|
||||
},
|
||||
{
|
||||
systemKey: 'sales',
|
||||
name: 'کارشناس فروش',
|
||||
description: 'امکان دسترسی به بخشهای فروش و مشتریان برای انجام فعالیتهای روزمره.',
|
||||
color: 'teal',
|
||||
permissions: [...ALL_ROLE_PERMISSIONS],
|
||||
},
|
||||
{
|
||||
systemKey: 'support',
|
||||
name: 'کارشناس پشتیبانی',
|
||||
description: 'پاسخگویی به مشتریان و پیگیری تیکتها و فعالیتها.',
|
||||
color: 'sky',
|
||||
permissions: ['customers', 'activities', 'notes', 'reports'],
|
||||
},
|
||||
];
|
||||
|
||||
export function isRolePermissionKey(value: string): value is RolePermissionKey {
|
||||
return ALL_ROLE_PERMISSIONS.includes(value as RolePermissionKey);
|
||||
}
|
||||
|
||||
export function sanitizePermissions(input: unknown): RolePermissionKey[] {
|
||||
if (!Array.isArray(input)) return [];
|
||||
return [...new Set(input.filter((item): item is RolePermissionKey => isRolePermissionKey(String(item))))];
|
||||
}
|
||||
|
||||
export function systemKeyToUserRole(key: string | null | undefined): UserRole {
|
||||
if (key === 'owner' || key === 'admin' || key === 'sales' || key === 'support') {
|
||||
return key;
|
||||
}
|
||||
return 'sales';
|
||||
}
|
||||
Reference in New Issue
Block a user