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:
2026-08-29 21:06:30 +03:30
co-authored by Cursor
commit 29a040c21d
195 changed files with 39430 additions and 0 deletions
+33
View File
@@ -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];
}
+51
View File
@@ -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,
);
}
}
+12
View File
@@ -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 {}
+113
View File
@@ -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(),
};
}
}