Creating a CRM Super Admin
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEmail,
|
||||
IsIn,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
import { USER_ROLES, type UserRole } from '@mento/shared';
|
||||
|
||||
export class PlatformLoginDto {
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(6)
|
||||
password!: string;
|
||||
}
|
||||
|
||||
export class UpdatePlatformStatusDto {
|
||||
@IsBoolean()
|
||||
isActive!: boolean;
|
||||
}
|
||||
|
||||
export class CreatePlatformOrganizationDto {
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(120)
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
businessField?: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(120)
|
||||
ownerFullName!: string;
|
||||
|
||||
@IsEmail()
|
||||
ownerEmail!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
@MaxLength(120)
|
||||
ownerPassword!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(40)
|
||||
ownerMobile?: string;
|
||||
}
|
||||
|
||||
export class UpdatePlatformOrganizationDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(120)
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(64)
|
||||
slug?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateIf((_, v) => v !== null && v !== undefined)
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
businessField?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export class UpdatePlatformMembershipRoleDto {
|
||||
@IsIn(USER_ROLES)
|
||||
role!: UserRole;
|
||||
}
|
||||
|
||||
export class ResetPlatformUserPasswordDto {
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
@MaxLength(120)
|
||||
password!: string;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { PlatformLoginDto } from './dto';
|
||||
import { PlatformAuthGuard } from './platform-auth.guard';
|
||||
import { PlatformAuthService } from './platform-auth.service';
|
||||
import type { PlatformActor } from './platform-jwt.strategy';
|
||||
|
||||
@Controller('platform/auth')
|
||||
export class PlatformAuthController {
|
||||
constructor(private readonly auth: PlatformAuthService) {}
|
||||
|
||||
@Post('login')
|
||||
login(@Body() dto: PlatformLoginDto) {
|
||||
return this.auth.login(dto);
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
@UseGuards(PlatformAuthGuard)
|
||||
me(@CurrentUser() actor: PlatformActor) {
|
||||
return this.auth.me(actor.platformAdminId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class PlatformAuthGuard extends AuthGuard('platform-jwt') {}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import type { PlatformAdminDto, PlatformLoginResponse } from '@mento/shared';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { PlatformLoginDto } from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class PlatformAuthService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly jwt: JwtService,
|
||||
) {}
|
||||
|
||||
async login(dto: PlatformLoginDto): Promise<PlatformLoginResponse> {
|
||||
const email = dto.email.trim().toLowerCase();
|
||||
const admin = await this.prisma.platformAdmin.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
if (!admin || admin.isActive === false) {
|
||||
throw new UnauthorizedException('اطلاعات ورود نادرست است');
|
||||
}
|
||||
const ok = await bcrypt.compare(dto.password, admin.passwordHash);
|
||||
if (!ok) {
|
||||
throw new UnauthorizedException('اطلاعات ورود نادرست است');
|
||||
}
|
||||
await this.prisma.platformAdmin.update({
|
||||
where: { id: admin.id },
|
||||
data: { lastLoginAt: new Date() },
|
||||
});
|
||||
return {
|
||||
accessToken: this.jwt.sign(
|
||||
{ sub: admin.id, typ: 'platform-admin' },
|
||||
{ expiresIn: '12h' },
|
||||
),
|
||||
admin: this.toDto(admin),
|
||||
};
|
||||
}
|
||||
|
||||
async me(platformAdminId: string): Promise<PlatformAdminDto> {
|
||||
const admin = await this.prisma.platformAdmin.findUnique({
|
||||
where: { id: platformAdminId },
|
||||
});
|
||||
if (!admin || admin.isActive === false) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
return this.toDto(admin);
|
||||
}
|
||||
|
||||
private toDto(admin: {
|
||||
id: string;
|
||||
email: string;
|
||||
fullName: string;
|
||||
}): PlatformAdminDto {
|
||||
return {
|
||||
id: admin.id,
|
||||
email: admin.email,
|
||||
fullName: admin.fullName,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
export type PlatformJwtPayload = {
|
||||
sub: string;
|
||||
typ?: string;
|
||||
};
|
||||
|
||||
export type PlatformActor = {
|
||||
platformAdminId: string;
|
||||
email: string;
|
||||
fullName: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PlatformJwtStrategy extends PassportStrategy(
|
||||
Strategy,
|
||||
'platform-jwt',
|
||||
) {
|
||||
constructor(
|
||||
config: ConfigService,
|
||||
private readonly prisma: PrismaService,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: config.get<string>('JWT_SECRET') ?? 'dev-secret',
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: PlatformJwtPayload): Promise<PlatformActor> {
|
||||
if (payload.typ !== 'platform-admin' || !payload.sub) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
const admin = await this.prisma.platformAdmin.findUnique({
|
||||
where: { id: payload.sub },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
fullName: true,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
if (!admin || admin.isActive === false) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
return {
|
||||
platformAdminId: admin.id,
|
||||
email: admin.email,
|
||||
fullName: admin.fullName,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
CreatePlatformOrganizationDto,
|
||||
ResetPlatformUserPasswordDto,
|
||||
UpdatePlatformMembershipRoleDto,
|
||||
UpdatePlatformOrganizationDto,
|
||||
UpdatePlatformStatusDto,
|
||||
} from './dto';
|
||||
import { PlatformAuthGuard } from './platform-auth.guard';
|
||||
import { PlatformService } from './platform.service';
|
||||
|
||||
@Controller('platform')
|
||||
@UseGuards(PlatformAuthGuard)
|
||||
export class PlatformController {
|
||||
constructor(private readonly platform: PlatformService) {}
|
||||
|
||||
@Get('stats')
|
||||
stats() {
|
||||
return this.platform.stats();
|
||||
}
|
||||
|
||||
@Get('organizations')
|
||||
listOrganizations(@Query('q') q?: string) {
|
||||
return this.platform.listOrganizations(q);
|
||||
}
|
||||
|
||||
@Post('organizations')
|
||||
createOrganization(@Body() dto: CreatePlatformOrganizationDto) {
|
||||
return this.platform.createOrganization(dto);
|
||||
}
|
||||
|
||||
@Get('organizations/:id')
|
||||
getOrganization(@Param('id') id: string) {
|
||||
return this.platform.getOrganization(id);
|
||||
}
|
||||
|
||||
@Patch('organizations/:id')
|
||||
updateOrganization(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdatePlatformOrganizationDto,
|
||||
) {
|
||||
return this.platform.updateOrganization(id, dto);
|
||||
}
|
||||
|
||||
@Get('users')
|
||||
listUsers(@Query('q') q?: string) {
|
||||
return this.platform.listUsers(q);
|
||||
}
|
||||
|
||||
@Get('users/:id')
|
||||
getUser(@Param('id') id: string) {
|
||||
return this.platform.getUser(id);
|
||||
}
|
||||
|
||||
@Patch('users/:id')
|
||||
setUserActive(@Param('id') id: string, @Body() dto: UpdatePlatformStatusDto) {
|
||||
return this.platform.setUserActive(id, dto.isActive);
|
||||
}
|
||||
|
||||
@Patch('users/:id/password')
|
||||
resetUserPassword(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: ResetPlatformUserPasswordDto,
|
||||
) {
|
||||
return this.platform.resetUserPassword(id, dto);
|
||||
}
|
||||
|
||||
@Patch('memberships/:id')
|
||||
setMembershipActive(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdatePlatformStatusDto,
|
||||
) {
|
||||
return this.platform.setMembershipActive(id, dto.isActive);
|
||||
}
|
||||
|
||||
@Patch('memberships/:id/role')
|
||||
updateMembershipRole(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdatePlatformMembershipRoleDto,
|
||||
) {
|
||||
return this.platform.updateMembershipRole(id, dto.role);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { MembershipsModule } from '../memberships/memberships.module';
|
||||
import { OrgRolesModule } from '../org-roles/org-roles.module';
|
||||
import { PipelinesModule } from '../pipelines/pipelines.module';
|
||||
import { PlatformAuthController } from './platform-auth.controller';
|
||||
import { PlatformAuthGuard } from './platform-auth.guard';
|
||||
import { PlatformAuthService } from './platform-auth.service';
|
||||
import { PlatformJwtStrategy } from './platform-jwt.strategy';
|
||||
import { PlatformController } from './platform.controller';
|
||||
import { PlatformService } from './platform.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
AuthModule,
|
||||
MembershipsModule,
|
||||
OrgRolesModule,
|
||||
PipelinesModule,
|
||||
PassportModule,
|
||||
],
|
||||
controllers: [PlatformAuthController, PlatformController],
|
||||
providers: [
|
||||
PlatformJwtStrategy,
|
||||
PlatformAuthGuard,
|
||||
PlatformAuthService,
|
||||
PlatformService,
|
||||
],
|
||||
})
|
||||
export class PlatformModule {}
|
||||
@@ -0,0 +1,530 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import type {
|
||||
PlatformOrganizationDto,
|
||||
PlatformStatsDto,
|
||||
PlatformUserDto,
|
||||
UserRole,
|
||||
} from '@mento/shared';
|
||||
import { isBusinessField } from '@mento/shared';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { normalizeMobile } from '../auth/auth.service';
|
||||
import { MembershipsService } from '../memberships/memberships.service';
|
||||
import { OrgRolesService } from '../org-roles/org-roles.service';
|
||||
import { PipelinesService } from '../pipelines/pipelines.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import type {
|
||||
CreatePlatformOrganizationDto,
|
||||
ResetPlatformUserPasswordDto,
|
||||
UpdatePlatformOrganizationDto,
|
||||
} from './dto';
|
||||
|
||||
function slugify(input: string): string {
|
||||
return (
|
||||
input
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\u0600-\u06FF]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 48) || `org-${Date.now()}`
|
||||
);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PlatformService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly memberships: MembershipsService,
|
||||
private readonly orgRoles: OrgRolesService,
|
||||
private readonly pipelines: PipelinesService,
|
||||
) {}
|
||||
|
||||
async stats(): Promise<PlatformStatsDto> {
|
||||
const [
|
||||
organizationCount,
|
||||
activeOrganizationCount,
|
||||
userCount,
|
||||
activeUserCount,
|
||||
membershipCount,
|
||||
] = await this.prisma.$transaction([
|
||||
this.prisma.organization.count(),
|
||||
this.prisma.organization.count({ where: { isActive: true } }),
|
||||
this.prisma.user.count(),
|
||||
this.prisma.user.count({ where: { isActive: true } }),
|
||||
this.prisma.membership.count(),
|
||||
]);
|
||||
return {
|
||||
organizationCount,
|
||||
activeOrganizationCount,
|
||||
userCount,
|
||||
activeUserCount,
|
||||
membershipCount,
|
||||
};
|
||||
}
|
||||
|
||||
async listOrganizations(query?: string): Promise<PlatformOrganizationDto[]> {
|
||||
const q = query?.trim();
|
||||
const rows = await this.prisma.organization.findMany({
|
||||
where: q
|
||||
? {
|
||||
OR: [
|
||||
{ name: { contains: q, mode: 'insensitive' } },
|
||||
{ slug: { contains: q, mode: 'insensitive' } },
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
businessField: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
memberships: true,
|
||||
people: true,
|
||||
deals: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return rows.map((row) => this.orgToDto(row));
|
||||
}
|
||||
|
||||
async getOrganization(id: string): Promise<PlatformOrganizationDto> {
|
||||
const row = await this.prisma.organization.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
businessField: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
memberships: true,
|
||||
people: true,
|
||||
deals: true,
|
||||
},
|
||||
},
|
||||
memberships: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
fullName: true,
|
||||
email: true,
|
||||
mobile: true,
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!row) throw new NotFoundException('محیط پیدا نشد');
|
||||
return {
|
||||
...this.orgToDto(row),
|
||||
members: row.memberships.map((m) => ({
|
||||
membershipId: m.id,
|
||||
userId: m.user.id,
|
||||
fullName: m.user.fullName,
|
||||
email: m.user.email,
|
||||
mobile: m.user.mobile,
|
||||
role: m.role,
|
||||
isActive: m.isActive,
|
||||
userIsActive: m.user.isActive,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async createOrganization(
|
||||
dto: CreatePlatformOrganizationDto,
|
||||
): Promise<PlatformOrganizationDto> {
|
||||
const name = dto.name.trim();
|
||||
if (name.length < 2) {
|
||||
throw new BadRequestException('نام محیط معتبر نیست');
|
||||
}
|
||||
const ownerFullName = dto.ownerFullName.trim();
|
||||
if (ownerFullName.length < 2) {
|
||||
throw new BadRequestException('نام مالک معتبر نیست');
|
||||
}
|
||||
const ownerEmail = dto.ownerEmail.trim().toLowerCase();
|
||||
const ownerPassword = dto.ownerPassword.trim();
|
||||
if (ownerPassword.length < 8) {
|
||||
throw new BadRequestException('رمز عبور حداقل ۸ کاراکتر لازم است');
|
||||
}
|
||||
|
||||
const businessField =
|
||||
dto.businessField && isBusinessField(dto.businessField)
|
||||
? dto.businessField
|
||||
: 'personal';
|
||||
|
||||
let ownerMobile: string | null = null;
|
||||
if (dto.ownerMobile?.trim()) {
|
||||
ownerMobile = normalizeMobile(dto.ownerMobile);
|
||||
if (!ownerMobile) {
|
||||
throw new BadRequestException('شماره موبایل مالک معتبر نیست');
|
||||
}
|
||||
}
|
||||
|
||||
const existingEmail = await this.prisma.user.findFirst({
|
||||
where: { email: ownerEmail },
|
||||
select: { id: true },
|
||||
});
|
||||
if (existingEmail) {
|
||||
throw new ConflictException('این ایمیل قبلاً ثبت شده است');
|
||||
}
|
||||
if (ownerMobile) {
|
||||
const existingMobile = await this.prisma.user.findFirst({
|
||||
where: { mobile: ownerMobile },
|
||||
select: { id: true },
|
||||
});
|
||||
if (existingMobile) {
|
||||
throw new ConflictException('این شماره موبایل قبلاً ثبت شده است');
|
||||
}
|
||||
}
|
||||
|
||||
const baseSlug = slugify(name);
|
||||
let slug = baseSlug;
|
||||
let i = 1;
|
||||
while (await this.prisma.organization.findUnique({ where: { slug } })) {
|
||||
slug = `${baseSlug}-${i++}`;
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(ownerPassword, 10);
|
||||
const now = new Date();
|
||||
|
||||
try {
|
||||
const organization = await this.prisma.organization.create({
|
||||
data: {
|
||||
name,
|
||||
slug,
|
||||
businessField,
|
||||
users: {
|
||||
create: {
|
||||
email: ownerEmail,
|
||||
mobile: ownerMobile,
|
||||
fullName: ownerFullName,
|
||||
passwordHash,
|
||||
role: 'owner',
|
||||
emailVerifiedAt: now,
|
||||
mobileVerifiedAt: ownerMobile ? now : null,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: { users: true },
|
||||
});
|
||||
|
||||
const owner = organization.users[0];
|
||||
if (!owner) {
|
||||
throw new BadRequestException('ساخت حساب مالک ناموفق بود');
|
||||
}
|
||||
|
||||
await this.memberships.createMembership({
|
||||
userId: owner.id,
|
||||
organizationId: organization.id,
|
||||
role: 'owner',
|
||||
});
|
||||
await this.orgRoles.ensureDefaultRoles(organization.id);
|
||||
await this.pipelines.ensureDefaultPipeline(organization.id);
|
||||
|
||||
return this.getOrganization(organization.id);
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
||||
throw new ConflictException('ایمیل یا موبایل تکراری است');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async updateOrganization(
|
||||
id: string,
|
||||
dto: UpdatePlatformOrganizationDto,
|
||||
): Promise<PlatformOrganizationDto> {
|
||||
const existing = await this.prisma.organization.findUnique({
|
||||
where: { id },
|
||||
select: { id: true, slug: true },
|
||||
});
|
||||
if (!existing) throw new NotFoundException('محیط پیدا نشد');
|
||||
|
||||
const data: Prisma.OrganizationUpdateInput = {};
|
||||
|
||||
if (dto.name !== undefined) {
|
||||
const name = dto.name.trim();
|
||||
if (name.length < 2) {
|
||||
throw new BadRequestException('نام محیط معتبر نیست');
|
||||
}
|
||||
data.name = name;
|
||||
}
|
||||
|
||||
if (dto.slug !== undefined) {
|
||||
const slug = slugify(dto.slug);
|
||||
if (slug.length < 2) {
|
||||
throw new BadRequestException('اسلاگ معتبر نیست');
|
||||
}
|
||||
if (slug !== existing.slug) {
|
||||
const clash = await this.prisma.organization.findUnique({
|
||||
where: { slug },
|
||||
select: { id: true },
|
||||
});
|
||||
if (clash) {
|
||||
throw new ConflictException('این اسلاگ قبلاً استفاده شده است');
|
||||
}
|
||||
data.slug = slug;
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.businessField !== undefined) {
|
||||
if (dto.businessField === null || dto.businessField === '') {
|
||||
data.businessField = null;
|
||||
} else if (!isBusinessField(dto.businessField)) {
|
||||
throw new BadRequestException('زمینه فعالیت معتبر نیست');
|
||||
} else {
|
||||
data.businessField = dto.businessField;
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.isActive !== undefined) {
|
||||
data.isActive = dto.isActive;
|
||||
}
|
||||
|
||||
if (Object.keys(data).length === 0) {
|
||||
throw new BadRequestException('هیچ فیلدی برای بهروزرسانی ارسال نشده');
|
||||
}
|
||||
|
||||
await this.prisma.organization.update({ where: { id }, data });
|
||||
return this.getOrganization(id);
|
||||
}
|
||||
|
||||
async setOrganizationActive(
|
||||
id: string,
|
||||
isActive: boolean,
|
||||
): Promise<PlatformOrganizationDto> {
|
||||
return this.updateOrganization(id, { isActive });
|
||||
}
|
||||
|
||||
async listUsers(query?: string): Promise<PlatformUserDto[]> {
|
||||
const q = query?.trim();
|
||||
const rows = await this.prisma.user.findMany({
|
||||
where: q
|
||||
? {
|
||||
OR: [
|
||||
{ fullName: { contains: q, mode: 'insensitive' } },
|
||||
{ email: { contains: q, mode: 'insensitive' } },
|
||||
{ mobile: { contains: q } },
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 300,
|
||||
include: {
|
||||
memberships: {
|
||||
include: {
|
||||
organization: {
|
||||
select: { id: true, name: true, isActive: true },
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
return rows.map((row) => this.userToDto(row));
|
||||
}
|
||||
|
||||
async getUser(id: string): Promise<PlatformUserDto> {
|
||||
const row = await this.prisma.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
memberships: {
|
||||
include: {
|
||||
organization: {
|
||||
select: { id: true, name: true, isActive: true },
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!row) throw new NotFoundException('کاربر پیدا نشد');
|
||||
return this.userToDto(row);
|
||||
}
|
||||
|
||||
async setUserActive(id: string, isActive: boolean): Promise<PlatformUserDto> {
|
||||
const existing = await this.prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!existing) throw new NotFoundException('کاربر پیدا نشد');
|
||||
await this.prisma.user.update({
|
||||
where: { id },
|
||||
data: { isActive },
|
||||
});
|
||||
return this.getUser(id);
|
||||
}
|
||||
|
||||
async resetUserPassword(
|
||||
id: string,
|
||||
dto: ResetPlatformUserPasswordDto,
|
||||
): Promise<{ ok: true }> {
|
||||
const existing = await this.prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!existing) throw new NotFoundException('کاربر پیدا نشد');
|
||||
const password = dto.password.trim();
|
||||
if (password.length < 8) {
|
||||
throw new BadRequestException('رمز عبور حداقل ۸ کاراکتر لازم است');
|
||||
}
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
await this.prisma.user.update({
|
||||
where: { id },
|
||||
data: { passwordHash },
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async setMembershipActive(
|
||||
id: string,
|
||||
isActive: boolean,
|
||||
): Promise<PlatformUserDto> {
|
||||
const row = await this.prisma.membership.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
organizationId: true,
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
if (!row) throw new NotFoundException('عضویت پیدا نشد');
|
||||
if (!isActive && row.role === 'owner') {
|
||||
const activeOwners = await this.memberships.countActiveOwners(
|
||||
row.organizationId,
|
||||
row.userId,
|
||||
);
|
||||
if (activeOwners < 1) {
|
||||
throw new BadRequestException('حداقل یک مالک فعال برای این محیط لازم است');
|
||||
}
|
||||
}
|
||||
await this.prisma.membership.update({
|
||||
where: { id },
|
||||
data: { isActive },
|
||||
});
|
||||
return this.getUser(row.userId);
|
||||
}
|
||||
|
||||
async updateMembershipRole(
|
||||
id: string,
|
||||
role: UserRole,
|
||||
): Promise<PlatformUserDto> {
|
||||
const row = await this.prisma.membership.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
user: { select: { id: true, organizationId: true } },
|
||||
},
|
||||
});
|
||||
if (!row) throw new NotFoundException('عضویت پیدا نشد');
|
||||
|
||||
if (row.role === 'owner' && role !== 'owner') {
|
||||
const owners = await this.memberships.countActiveOwners(
|
||||
row.organizationId,
|
||||
row.userId,
|
||||
);
|
||||
if (owners < 1) {
|
||||
throw new BadRequestException(
|
||||
'حداقل یک مالک برای سازمان لازم است؛ ابتدا مالک دیگری تعیین کنید',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await this.orgRoles.ensureDefaultRoles(row.organizationId);
|
||||
const orgRole = await this.prisma.orgRole.findFirst({
|
||||
where: { organizationId: row.organizationId, systemKey: role },
|
||||
});
|
||||
|
||||
await this.prisma.membership.update({
|
||||
where: { id },
|
||||
data: {
|
||||
role,
|
||||
orgRoleId: orgRole?.id ?? row.orgRoleId,
|
||||
},
|
||||
});
|
||||
|
||||
if (row.user.organizationId === row.organizationId) {
|
||||
await this.memberships.syncUserLastOrg(
|
||||
row.userId,
|
||||
row.organizationId,
|
||||
role,
|
||||
orgRole?.id ?? row.orgRoleId,
|
||||
);
|
||||
}
|
||||
|
||||
return this.getUser(row.userId);
|
||||
}
|
||||
|
||||
private orgToDto(row: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
businessField: string | null;
|
||||
isActive: boolean;
|
||||
createdAt: Date;
|
||||
_count: { memberships: number; people: number; deals: number };
|
||||
}): PlatformOrganizationDto {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
slug: row.slug,
|
||||
businessField: row.businessField,
|
||||
isActive: row.isActive,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
memberCount: row._count.memberships,
|
||||
personCount: row._count.people,
|
||||
dealCount: row._count.deals,
|
||||
};
|
||||
}
|
||||
|
||||
private userToDto(row: {
|
||||
id: string;
|
||||
email: string;
|
||||
mobile: string | null;
|
||||
fullName: string;
|
||||
isActive: boolean;
|
||||
lastLoginAt: Date | null;
|
||||
createdAt: Date;
|
||||
memberships: Array<{
|
||||
id: string;
|
||||
role: PlatformUserDto['memberships'][number]['role'];
|
||||
isActive: boolean;
|
||||
organization: { id: string; name: string; isActive: boolean };
|
||||
}>;
|
||||
}): PlatformUserDto {
|
||||
return {
|
||||
id: row.id,
|
||||
email: row.email,
|
||||
mobile: row.mobile,
|
||||
fullName: row.fullName,
|
||||
isActive: row.isActive,
|
||||
lastLoginAt: row.lastLoginAt?.toISOString() ?? null,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
memberships: row.memberships.map((m) => ({
|
||||
membershipId: m.id,
|
||||
organizationId: m.organization.id,
|
||||
organizationName: m.organization.name,
|
||||
organizationIsActive: m.organization.isActive,
|
||||
role: m.role,
|
||||
isActive: m.isActive,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user