feat(adm): split chat and billing settings and add a master login
Super admin can filter dashboard counts, edit or delete admins, and sign into a user's CRM account with one shared password. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -54,6 +54,31 @@ export class ResetPlatformAdminPasswordDto {
|
||||
password!: string;
|
||||
}
|
||||
|
||||
export class UpdatePlatformAdminDto {
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(120)
|
||||
fullName!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
@MaxLength(40)
|
||||
mobile!: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateIf((_, value) => value !== null && value !== '')
|
||||
@IsEmail()
|
||||
email?: string | null;
|
||||
}
|
||||
|
||||
export class SetMasterPasswordDto {
|
||||
@ValidateIf((_, value) => value !== null)
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
@MaxLength(120)
|
||||
password!: string | null;
|
||||
}
|
||||
|
||||
export class UpdatePlatformStatusDto {
|
||||
@IsBoolean()
|
||||
isActive!: boolean;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
@@ -16,6 +18,8 @@ import {
|
||||
CreatePlatformAdminDto,
|
||||
CreatePlatformOrganizationDto,
|
||||
ResetPlatformAdminPasswordDto,
|
||||
SetMasterPasswordDto,
|
||||
UpdatePlatformAdminDto,
|
||||
ResetPlatformUserPasswordDto,
|
||||
UpdateOrgModulesDto,
|
||||
UpdatePlatformMembershipRoleDto,
|
||||
@@ -73,6 +77,26 @@ export class PlatformController {
|
||||
return this.platform.resetAdminPassword(id, dto);
|
||||
}
|
||||
|
||||
@Patch('admins/:id/profile')
|
||||
updateAdmin(@Param('id') id: string, @Body() dto: UpdatePlatformAdminDto) {
|
||||
return this.platform.updateAdmin(id, dto);
|
||||
}
|
||||
|
||||
@Delete('admins/:id')
|
||||
deleteAdmin(@Param('id') id: string, @CurrentUser() actor: PlatformActor) {
|
||||
return this.platform.deleteAdmin(id, actor.platformAdminId);
|
||||
}
|
||||
|
||||
@Get('master-password')
|
||||
masterPasswordStatus() {
|
||||
return this.platform.masterPasswordStatus();
|
||||
}
|
||||
|
||||
@Put('master-password')
|
||||
setMasterPassword(@Body() dto: SetMasterPasswordDto) {
|
||||
return this.platform.setMasterPassword(dto.password);
|
||||
}
|
||||
|
||||
@Get('organizations')
|
||||
listOrganizations(@Query('q') q?: string) {
|
||||
return this.platform.listOrganizations(q);
|
||||
|
||||
@@ -111,15 +111,23 @@ export class PlatformService {
|
||||
where: { role: 'owner', isActive: true },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
take: 1,
|
||||
select: { user: { select: { billingAccount: true } } },
|
||||
select: {
|
||||
userId: true,
|
||||
user: { select: { fullName: true, billingAccount: true } },
|
||||
},
|
||||
},
|
||||
conversationSettings: { select: { organizationId: true } },
|
||||
},
|
||||
});
|
||||
return rows.map((row) => {
|
||||
const account = row.memberships[0]?.user.billingAccount;
|
||||
const owner = row.memberships[0];
|
||||
const account = owner?.user.billingAccount;
|
||||
return {
|
||||
...this.orgToDto(row),
|
||||
subscription: account ? toSummary(account) : null,
|
||||
ownerUserId: owner?.userId ?? null,
|
||||
ownerName: owner?.user.fullName ?? null,
|
||||
chatConfigured: Boolean(row.conversationSettings),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -698,6 +706,104 @@ export class PlatformService {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async updateAdmin(
|
||||
id: string,
|
||||
dto: { fullName: string; mobile: string; email?: string | null },
|
||||
): Promise<PlatformAdminDto> {
|
||||
const existing = await this.prisma.platformAdmin.findUnique({
|
||||
where: { id },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!existing) throw new NotFoundException('ادمین پیدا نشد');
|
||||
const fullName = dto.fullName.trim();
|
||||
if (fullName.length < 2) {
|
||||
throw new BadRequestException('نام معتبر نیست');
|
||||
}
|
||||
const mobile = normalizeMobile(dto.mobile);
|
||||
if (!mobile) {
|
||||
throw new BadRequestException('شماره موبایل معتبر نیست');
|
||||
}
|
||||
const email = dto.email?.trim().toLowerCase() || null;
|
||||
const mobileTaken = await this.prisma.platformAdmin.findFirst({
|
||||
where: { mobile, id: { not: id } },
|
||||
select: { id: true },
|
||||
});
|
||||
if (mobileTaken) {
|
||||
throw new ConflictException('این شماره موبایل قبلاً ثبت شده است');
|
||||
}
|
||||
if (email) {
|
||||
const emailTaken = await this.prisma.platformAdmin.findFirst({
|
||||
where: { email, id: { not: id } },
|
||||
select: { id: true },
|
||||
});
|
||||
if (emailTaken) {
|
||||
throw new ConflictException('این ایمیل قبلاً ثبت شده است');
|
||||
}
|
||||
}
|
||||
try {
|
||||
const updated = await this.prisma.platformAdmin.update({
|
||||
where: { id },
|
||||
data: { fullName, mobile, email },
|
||||
});
|
||||
return this.adminToDto(updated);
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
||||
throw new ConflictException('موبایل یا ایمیل تکراری است');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAdmin(id: string, actorId: string): Promise<{ ok: true }> {
|
||||
const existing = await this.prisma.platformAdmin.findUnique({
|
||||
where: { id },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!existing) throw new NotFoundException('ادمین پیدا نشد');
|
||||
if (id === actorId) {
|
||||
throw new BadRequestException('نمیتوانید حساب خودتان را حذف کنید');
|
||||
}
|
||||
const others = await this.prisma.platformAdmin.count({
|
||||
where: { id: { not: id } },
|
||||
});
|
||||
if (others < 1) {
|
||||
throw new BadRequestException('حداقل یک سوپرادمین باید بماند');
|
||||
}
|
||||
await this.prisma.platformAdmin.delete({ where: { id } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async masterPasswordStatus(): Promise<{ enabled: boolean; updatedAt: string | null }> {
|
||||
const row = await this.prisma.platformMasterPassword.findUnique({
|
||||
where: { id: 'default' },
|
||||
select: { updatedAt: true },
|
||||
});
|
||||
return {
|
||||
enabled: Boolean(row),
|
||||
updatedAt: row?.updatedAt.toISOString() ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async setMasterPassword(
|
||||
password: string | null,
|
||||
): Promise<{ enabled: boolean; updatedAt: string | null }> {
|
||||
const trimmed = password?.trim() ?? '';
|
||||
if (!trimmed) {
|
||||
await this.prisma.platformMasterPassword.deleteMany({ where: { id: 'default' } });
|
||||
return { enabled: false, updatedAt: null };
|
||||
}
|
||||
if (trimmed.length < 8) {
|
||||
throw new BadRequestException('رمز کلی حداقل ۸ کاراکتر لازم است');
|
||||
}
|
||||
const passwordHash = await bcrypt.hash(trimmed, 10);
|
||||
const row = await this.prisma.platformMasterPassword.upsert({
|
||||
where: { id: 'default' },
|
||||
create: { id: 'default', passwordHash },
|
||||
update: { passwordHash },
|
||||
});
|
||||
return { enabled: true, updatedAt: row.updatedAt.toISOString() };
|
||||
}
|
||||
|
||||
private adminToDto(row: {
|
||||
id: string;
|
||||
mobile: string | null;
|
||||
|
||||
Reference in New Issue
Block a user