Initial SuperAdmin API (NestJS + Prisma)
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Matches,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
import { ProductType } from '@prisma/client';
|
||||
|
||||
export class CreateTenantDto {
|
||||
@IsEnum(ProductType)
|
||||
product!: ProductType;
|
||||
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(64)
|
||||
@Matches(/^[a-z0-9_-]+$/i, {
|
||||
message: 'slug must be alphanumeric (a-z, 0-9, _-)',
|
||||
})
|
||||
slug!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(120)
|
||||
nameFa!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(120)
|
||||
nameEn?: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(3)
|
||||
@MaxLength(255)
|
||||
domain!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
avatarUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(32)
|
||||
primaryColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(32)
|
||||
secondaryColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(32)
|
||||
backgroundColor?: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
nodeServer!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nodeWs?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
appUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
adminUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
remoteApiToken?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
appUiConfig?: Record<string, unknown>;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateTenantDto } from './create-tenant.dto';
|
||||
|
||||
export class UpdateTenantDto extends PartialType(CreateTenantDto) {}
|
||||
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ProductType } from '@prisma/client';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { CreateTenantDto } from './dto/create-tenant.dto';
|
||||
import { UpdateTenantDto } from './dto/update-tenant.dto';
|
||||
import { TenantsService } from './tenants.service';
|
||||
|
||||
@Controller('tenants')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class TenantsController {
|
||||
constructor(private readonly tenantsService: TenantsService) {}
|
||||
|
||||
@Get()
|
||||
findAll(@Query('product') product?: ProductType) {
|
||||
return this.tenantsService.findAll(product);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.tenantsService.findOne(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(
|
||||
@Body() dto: CreateTenantDto,
|
||||
@CurrentUser() user: { id: string },
|
||||
) {
|
||||
return this.tenantsService.create(dto, user.id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateTenantDto,
|
||||
@CurrentUser() user: { id: string },
|
||||
) {
|
||||
return this.tenantsService.update(id, dto, user.id);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||
return this.tenantsService.remove(id, user.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TenantsController } from './tenants.controller';
|
||||
import { TenantsService } from './tenants.service';
|
||||
|
||||
@Module({
|
||||
controllers: [TenantsController],
|
||||
providers: [TenantsService],
|
||||
exports: [TenantsService],
|
||||
})
|
||||
export class TenantsModule {}
|
||||
@@ -0,0 +1,199 @@
|
||||
import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma, ProductType, Tenant } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { CreateTenantDto } from './dto/create-tenant.dto';
|
||||
import { UpdateTenantDto } from './dto/update-tenant.dto';
|
||||
|
||||
@Injectable()
|
||||
export class TenantsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly audit: AuditService,
|
||||
) {}
|
||||
|
||||
async findAll(product?: ProductType) {
|
||||
return this.prisma.tenant.findMany({
|
||||
where: product ? { product } : undefined,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
product: true,
|
||||
slug: true,
|
||||
nameFa: true,
|
||||
nameEn: true,
|
||||
domain: true,
|
||||
avatarUrl: true,
|
||||
primaryColor: true,
|
||||
secondaryColor: true,
|
||||
backgroundColor: true,
|
||||
nodeServer: true,
|
||||
nodeWs: true,
|
||||
appUrl: true,
|
||||
adminUrl: true,
|
||||
isActive: true,
|
||||
notes: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const tenant = await this.prisma.tenant.findUnique({ where: { id } });
|
||||
if (!tenant) {
|
||||
throw new NotFoundException('نسخه یافت نشد');
|
||||
}
|
||||
return this.sanitize(tenant);
|
||||
}
|
||||
|
||||
async create(dto: CreateTenantDto, userId?: string) {
|
||||
try {
|
||||
const tenant = await this.prisma.tenant.create({
|
||||
data: {
|
||||
product: dto.product,
|
||||
slug: dto.slug.toLowerCase(),
|
||||
nameFa: dto.nameFa,
|
||||
nameEn: dto.nameEn,
|
||||
domain: this.normalizeDomain(dto.domain),
|
||||
avatarUrl: dto.avatarUrl,
|
||||
primaryColor: dto.primaryColor,
|
||||
secondaryColor: dto.secondaryColor,
|
||||
backgroundColor: dto.backgroundColor,
|
||||
nodeServer: dto.nodeServer,
|
||||
nodeWs: dto.nodeWs,
|
||||
appUrl: dto.appUrl,
|
||||
adminUrl: dto.adminUrl,
|
||||
remoteApiToken: dto.remoteApiToken,
|
||||
appUiConfig: dto.appUiConfig
|
||||
? (dto.appUiConfig as Prisma.InputJsonValue)
|
||||
: undefined,
|
||||
isActive: dto.isActive ?? true,
|
||||
notes: dto.notes,
|
||||
},
|
||||
});
|
||||
|
||||
await this.audit.log({
|
||||
action: 'tenant.create',
|
||||
entityType: 'tenant',
|
||||
entityId: tenant.id,
|
||||
tenantId: tenant.id,
|
||||
userId,
|
||||
after: this.sanitize(tenant) as unknown as Prisma.InputJsonValue,
|
||||
});
|
||||
|
||||
return this.sanitize(tenant);
|
||||
} catch (error) {
|
||||
this.handleUnique(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateTenantDto, userId?: string) {
|
||||
const before = await this.prisma.tenant.findUnique({ where: { id } });
|
||||
if (!before) {
|
||||
throw new NotFoundException('نسخه یافت نشد');
|
||||
}
|
||||
|
||||
try {
|
||||
const tenant = await this.prisma.tenant.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.product !== undefined && { product: dto.product }),
|
||||
...(dto.slug !== undefined && { slug: dto.slug.toLowerCase() }),
|
||||
...(dto.nameFa !== undefined && { nameFa: dto.nameFa }),
|
||||
...(dto.nameEn !== undefined && { nameEn: dto.nameEn }),
|
||||
...(dto.domain !== undefined && {
|
||||
domain: this.normalizeDomain(dto.domain),
|
||||
}),
|
||||
...(dto.avatarUrl !== undefined && { avatarUrl: dto.avatarUrl }),
|
||||
...(dto.primaryColor !== undefined && {
|
||||
primaryColor: dto.primaryColor,
|
||||
}),
|
||||
...(dto.secondaryColor !== undefined && {
|
||||
secondaryColor: dto.secondaryColor,
|
||||
}),
|
||||
...(dto.backgroundColor !== undefined && {
|
||||
backgroundColor: dto.backgroundColor,
|
||||
}),
|
||||
...(dto.nodeServer !== undefined && { nodeServer: dto.nodeServer }),
|
||||
...(dto.nodeWs !== undefined && { nodeWs: dto.nodeWs }),
|
||||
...(dto.appUrl !== undefined && { appUrl: dto.appUrl }),
|
||||
...(dto.adminUrl !== undefined && { adminUrl: dto.adminUrl }),
|
||||
...(dto.remoteApiToken !== undefined && {
|
||||
remoteApiToken: dto.remoteApiToken,
|
||||
}),
|
||||
...(dto.appUiConfig !== undefined && {
|
||||
appUiConfig: dto.appUiConfig as Prisma.InputJsonValue,
|
||||
}),
|
||||
...(dto.isActive !== undefined && { isActive: dto.isActive }),
|
||||
...(dto.notes !== undefined && { notes: dto.notes }),
|
||||
},
|
||||
});
|
||||
|
||||
await this.audit.log({
|
||||
action: 'tenant.update',
|
||||
entityType: 'tenant',
|
||||
entityId: tenant.id,
|
||||
tenantId: tenant.id,
|
||||
userId,
|
||||
before: this.sanitize(before) as unknown as Prisma.InputJsonValue,
|
||||
after: this.sanitize(tenant) as unknown as Prisma.InputJsonValue,
|
||||
});
|
||||
|
||||
return this.sanitize(tenant);
|
||||
} catch (error) {
|
||||
this.handleUnique(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async remove(id: string, userId?: string) {
|
||||
const before = await this.prisma.tenant.findUnique({ where: { id } });
|
||||
if (!before) {
|
||||
throw new NotFoundException('نسخه یافت نشد');
|
||||
}
|
||||
|
||||
await this.prisma.tenant.delete({ where: { id } });
|
||||
|
||||
await this.audit.log({
|
||||
action: 'tenant.delete',
|
||||
entityType: 'tenant',
|
||||
entityId: id,
|
||||
tenantId: id,
|
||||
userId,
|
||||
before: this.sanitize(before) as unknown as Prisma.InputJsonValue,
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
private sanitize(tenant: Tenant) {
|
||||
const { remoteApiToken, ...rest } = tenant;
|
||||
return {
|
||||
...rest,
|
||||
hasRemoteApiToken: Boolean(remoteApiToken),
|
||||
};
|
||||
}
|
||||
|
||||
private normalizeDomain(domain: string) {
|
||||
return domain
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^https?:\/\//, '')
|
||||
.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
private handleUnique(error: unknown) {
|
||||
if (
|
||||
error instanceof Prisma.PrismaClientKnownRequestError &&
|
||||
error.code === 'P2002'
|
||||
) {
|
||||
throw new ConflictException('slug یا domain تکراری است');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user