Initial Mento CRM: portal OTP, reports, SMS, and deploy for crm.montzar.ir.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
IsEmail,
|
||||
IsIn,
|
||||
IsString,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
|
||||
const INVITE_ROLES = ['admin', 'sales', 'support'] as const;
|
||||
const ALL_ROLES = ['owner', 'admin', 'sales', 'support'] as const;
|
||||
|
||||
export class InviteUserDto {
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(120)
|
||||
fullName!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
@MaxLength(72)
|
||||
password!: string;
|
||||
|
||||
@IsIn(INVITE_ROLES)
|
||||
role!: (typeof INVITE_ROLES)[number];
|
||||
}
|
||||
|
||||
export class UpdateUserRoleDto {
|
||||
@IsIn(ALL_ROLES)
|
||||
role!: (typeof ALL_ROLES)[number];
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
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 { InviteUserDto, UpdateUserRoleDto } from './dto';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@Controller('users')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Get()
|
||||
list(@CurrentUser() user: { organizationId: string }) {
|
||||
return this.usersService.list(user.organizationId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Roles('owner', 'admin')
|
||||
invite(
|
||||
@CurrentUser() user: { organizationId: string; role: UserRole },
|
||||
@Body() dto: InviteUserDto,
|
||||
) {
|
||||
return this.usersService.invite(user.organizationId, user.role, dto);
|
||||
}
|
||||
|
||||
@Patch(':id/role')
|
||||
@Roles('owner', 'admin')
|
||||
updateRole(
|
||||
@CurrentUser() user: { organizationId: string; userId: string; role: UserRole },
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateUserRoleDto,
|
||||
) {
|
||||
return this.usersService.updateRole(
|
||||
user.organizationId,
|
||||
{ userId: user.userId, role: user.role },
|
||||
id,
|
||||
dto.role,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import type { InviteUserRequest, OrgUserDto, UserRole } from '@mento/shared';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(organizationId: string): Promise<OrgUserDto[]> {
|
||||
const rows = await this.prisma.user.findMany({
|
||||
where: { organizationId },
|
||||
orderBy: [{ role: 'asc' }, { fullName: 'asc' }],
|
||||
});
|
||||
return rows.map((u) => this.toDto(u));
|
||||
}
|
||||
|
||||
async invite(
|
||||
organizationId: string,
|
||||
actorRole: UserRole,
|
||||
dto: InviteUserRequest,
|
||||
): Promise<OrgUserDto> {
|
||||
if (actorRole !== 'owner' && actorRole !== 'admin') {
|
||||
throw new ForbiddenException('فقط مالک یا مدیر میتواند کاربر اضافه کند');
|
||||
}
|
||||
|
||||
const email = dto.email.trim().toLowerCase();
|
||||
const existing = await this.prisma.user.findFirst({
|
||||
where: { organizationId, email },
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException('این ایمیل در سازمان شما وجود دارد');
|
||||
}
|
||||
|
||||
const global = await this.prisma.user.findFirst({ where: { email } });
|
||||
if (global) {
|
||||
throw new ConflictException('این ایمیل قبلاً در سیستم ثبت شده است');
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(dto.password, 10);
|
||||
const row = await this.prisma.user.create({
|
||||
data: {
|
||||
organizationId,
|
||||
email,
|
||||
fullName: dto.fullName.trim(),
|
||||
passwordHash,
|
||||
role: dto.role,
|
||||
},
|
||||
});
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
async updateRole(
|
||||
organizationId: string,
|
||||
actor: { userId: string; role: UserRole },
|
||||
targetId: string,
|
||||
nextRole: UserRole,
|
||||
): Promise<OrgUserDto> {
|
||||
if (actor.role !== 'owner' && actor.role !== 'admin') {
|
||||
throw new ForbiddenException('فقط مالک یا مدیر میتواند نقش را تغییر دهد');
|
||||
}
|
||||
|
||||
const target = await this.prisma.user.findFirst({
|
||||
where: { id: targetId, organizationId },
|
||||
});
|
||||
if (!target) throw new NotFoundException('کاربر پیدا نشد');
|
||||
|
||||
if (actor.role === 'admin') {
|
||||
if (target.role === 'owner' || nextRole === 'owner') {
|
||||
throw new ForbiddenException('مدیر نمیتواند نقش مالک را تغییر دهد');
|
||||
}
|
||||
}
|
||||
|
||||
if (target.role === 'owner' && nextRole !== 'owner') {
|
||||
const owners = await this.prisma.user.count({
|
||||
where: { organizationId, role: 'owner' },
|
||||
});
|
||||
if (owners <= 1) {
|
||||
throw new BadRequestException(
|
||||
'حداقل یک مالک برای سازمان لازم است؛ ابتدا مالک دیگری تعیین کنید',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const row = await this.prisma.user.update({
|
||||
where: { id: target.id },
|
||||
data: { role: nextRole },
|
||||
});
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
private toDto(row: {
|
||||
id: string;
|
||||
email: string;
|
||||
fullName: string;
|
||||
role: UserRole;
|
||||
createdAt: Date;
|
||||
}): OrgUserDto {
|
||||
return {
|
||||
id: row.id,
|
||||
email: row.email,
|
||||
fullName: row.fullName,
|
||||
role: row.role,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user