Add production-ready SuperAdmin API features for tenants, SMS, servers, and plugins.
Includes mobile auth, get_user/get_version, SMS wallet parity endpoints, appearance backfill, and related Prisma migrations. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
IsBoolean,
|
||||
IsIn,
|
||||
IsInt,
|
||||
IsISO8601,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Max,
|
||||
MaxLength,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
const PLAN_MONTHS = [1, 3, 6, 12] as const;
|
||||
|
||||
export class UpdateServerAccountDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isDedicated?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(120)
|
||||
serverName?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(2000)
|
||||
specs?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
priceRials?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsISO8601()
|
||||
chargedAt?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@IsIn(PLAN_MONTHS as unknown as number[])
|
||||
planMonths?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(4000)
|
||||
description?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(30)
|
||||
alertDaysBefore?: number;
|
||||
}
|
||||
|
||||
export class RenewServerDto {
|
||||
@IsOptional()
|
||||
@IsISO8601()
|
||||
chargedAt?: string;
|
||||
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@IsIn(PLAN_MONTHS as unknown as number[])
|
||||
planMonths!: number;
|
||||
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
priceRials!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(500)
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export { PLAN_MONTHS };
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { RenewServerDto, UpdateServerAccountDto } from './dto/server-account.dto';
|
||||
import { ServersService } from './servers.service';
|
||||
|
||||
@Controller('servers')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class ServersController {
|
||||
constructor(private readonly serversService: ServersService) {}
|
||||
|
||||
@Get('tenants')
|
||||
listTenants() {
|
||||
return this.serversService.listTenants();
|
||||
}
|
||||
|
||||
@Get('expiring')
|
||||
expiring(@Query('limit') limit?: string) {
|
||||
return this.serversService.getExpiringSoon(Number(limit) || 5);
|
||||
}
|
||||
|
||||
@Post('check-alerts')
|
||||
checkAlerts() {
|
||||
return this.serversService.runExpiryAlerts().then(() => ({ ok: true }));
|
||||
}
|
||||
|
||||
@Get('tenants/:id')
|
||||
getAccount(@Param('id') id: string) {
|
||||
return this.serversService.getAccount(id);
|
||||
}
|
||||
|
||||
@Patch('tenants/:id')
|
||||
updateAccount(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateServerAccountDto,
|
||||
@CurrentUser() user: { id: string },
|
||||
) {
|
||||
return this.serversService.updateAccount(id, dto, user.id);
|
||||
}
|
||||
|
||||
@Post('tenants/:id/renew')
|
||||
renew(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: RenewServerDto,
|
||||
@CurrentUser() user: { id: string },
|
||||
) {
|
||||
return this.serversService.renew(id, dto, user.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { ServersController } from './servers.controller';
|
||||
import { ServersService } from './servers.service';
|
||||
|
||||
@Module({
|
||||
imports: [NotificationsModule],
|
||||
controllers: [ServersController],
|
||||
providers: [ServersService],
|
||||
exports: [ServersService],
|
||||
})
|
||||
export class ServersModule {}
|
||||
@@ -0,0 +1,505 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
OnModuleDestroy,
|
||||
OnModuleInit,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma, ServerAccount } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { AdminNotifyService } from '../notifications/admin-notify.service';
|
||||
import {
|
||||
PLAN_MONTHS,
|
||||
RenewServerDto,
|
||||
UpdateServerAccountDto,
|
||||
} from './dto/server-account.dto';
|
||||
import {
|
||||
formatIranDate,
|
||||
iranDaysUntil,
|
||||
iranYmd,
|
||||
} from '../common/iran-time.util';
|
||||
|
||||
export type ExpiryLevel = 'dedicated' | 'healthy' | 'warning' | 'critical' | 'unknown';
|
||||
|
||||
@Injectable()
|
||||
export class ServersService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(ServersService.name);
|
||||
private alertTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly audit: AuditService,
|
||||
private readonly notify: AdminNotifyService,
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
// Check once shortly after boot, then every 6 hours
|
||||
setTimeout(() => void this.runExpiryAlerts(), 15_000);
|
||||
this.alertTimer = setInterval(
|
||||
() => void this.runExpiryAlerts(),
|
||||
6 * 60 * 60 * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
onModuleDestroy() {
|
||||
if (this.alertTimer) clearInterval(this.alertTimer);
|
||||
}
|
||||
|
||||
async listTenants() {
|
||||
await this.ensureAccountsForAllTenants();
|
||||
void this.runExpiryAlerts();
|
||||
|
||||
const rows = await this.prisma.tenant.findMany({
|
||||
where: { isActive: true },
|
||||
orderBy: [{ product: 'asc' }, { nameFa: 'asc' }],
|
||||
include: { serverAccount: true },
|
||||
});
|
||||
|
||||
return rows.map((tenant) => {
|
||||
const account = tenant.serverAccount!;
|
||||
const summary = this.toSummary(account);
|
||||
return {
|
||||
id: tenant.id,
|
||||
product: tenant.product,
|
||||
slug: tenant.slug,
|
||||
nameFa: tenant.nameFa,
|
||||
nameEn: tenant.nameEn,
|
||||
domain: tenant.domain,
|
||||
avatarUrl: tenant.avatarUrl,
|
||||
isActive: tenant.isActive,
|
||||
server: summary,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getExpiringSoon(limit = 5) {
|
||||
const rows = await this.listTenants();
|
||||
return rows
|
||||
.filter((r) => !r.server.isDedicated && r.server.expiresAt)
|
||||
.sort((a, b) => {
|
||||
const ae = a.server.expiresAt ? Date.parse(a.server.expiresAt) : Infinity;
|
||||
const be = b.server.expiresAt ? Date.parse(b.server.expiresAt) : Infinity;
|
||||
return ae - be;
|
||||
})
|
||||
.filter((r) => {
|
||||
const level = r.server.expiryLevel;
|
||||
return level === 'critical' || level === 'warning' || level === 'unknown';
|
||||
})
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
async getAccount(tenantId: string) {
|
||||
const tenant = await this.requireTenant(tenantId);
|
||||
const account = await this.ensureAccount(tenant.id);
|
||||
const charges = await this.prisma.serverCharge.findMany({
|
||||
where: { serverAccountId: account.id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 20,
|
||||
});
|
||||
|
||||
return {
|
||||
tenant: {
|
||||
id: tenant.id,
|
||||
product: tenant.product,
|
||||
slug: tenant.slug,
|
||||
nameFa: tenant.nameFa,
|
||||
nameEn: tenant.nameEn,
|
||||
domain: tenant.domain,
|
||||
avatarUrl: tenant.avatarUrl,
|
||||
},
|
||||
account: this.publicAccount(account),
|
||||
summary: this.toSummary(account),
|
||||
charges,
|
||||
planOptions: PLAN_MONTHS.map((m) => ({
|
||||
months: m,
|
||||
label: this.planLabel(m),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async updateAccount(
|
||||
tenantId: string,
|
||||
dto: UpdateServerAccountDto,
|
||||
userId?: string,
|
||||
) {
|
||||
const account = await this.ensureAccount(tenantId);
|
||||
const before = { ...account };
|
||||
|
||||
if (dto.planMonths != null && !PLAN_MONTHS.includes(dto.planMonths as any)) {
|
||||
throw new BadRequestException('مدت شارژ نامعتبر است');
|
||||
}
|
||||
|
||||
let expiresAt = account.expiresAt;
|
||||
let chargedAt = account.chargedAt;
|
||||
let planMonths = account.planMonths;
|
||||
|
||||
if (dto.isDedicated === true) {
|
||||
// Dedicated: keep history but stop managing/alerting on current period fields optionally
|
||||
// We keep expiresAt as-is for record, but alerts skip dedicated.
|
||||
}
|
||||
|
||||
if (dto.chargedAt !== undefined) {
|
||||
chargedAt = dto.chargedAt ? new Date(dto.chargedAt) : null;
|
||||
}
|
||||
if (dto.planMonths !== undefined) {
|
||||
planMonths = dto.planMonths;
|
||||
}
|
||||
|
||||
// Recalc expiry when charge date or plan changes (and not dedicated-only clear)
|
||||
if (
|
||||
dto.isDedicated !== true &&
|
||||
(dto.chargedAt !== undefined || dto.planMonths !== undefined) &&
|
||||
chargedAt &&
|
||||
planMonths
|
||||
) {
|
||||
expiresAt = this.addMonths(chargedAt, planMonths);
|
||||
}
|
||||
|
||||
if (dto.isDedicated === true) {
|
||||
// Clearing managed alert state when switching to dedicated
|
||||
}
|
||||
|
||||
const updated = await this.prisma.serverAccount.update({
|
||||
where: { id: account.id },
|
||||
data: {
|
||||
isDedicated: dto.isDedicated ?? undefined,
|
||||
serverName:
|
||||
dto.serverName === undefined
|
||||
? undefined
|
||||
: dto.serverName?.trim() || null,
|
||||
specs: dto.specs === undefined ? undefined : dto.specs?.trim() || null,
|
||||
priceRials: dto.priceRials === undefined ? undefined : dto.priceRials,
|
||||
chargedAt:
|
||||
dto.chargedAt === undefined
|
||||
? undefined
|
||||
: dto.chargedAt
|
||||
? new Date(dto.chargedAt)
|
||||
: null,
|
||||
planMonths: dto.planMonths === undefined ? undefined : dto.planMonths,
|
||||
expiresAt:
|
||||
dto.chargedAt !== undefined || dto.planMonths !== undefined
|
||||
? expiresAt
|
||||
: undefined,
|
||||
description:
|
||||
dto.description === undefined
|
||||
? undefined
|
||||
: dto.description?.trim() || null,
|
||||
alertDaysBefore: dto.alertDaysBefore ?? undefined,
|
||||
...(dto.isDedicated === true
|
||||
? { lastAlertDate: null }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
// If we toggled off dedicated and have charge+plan, ensure expiresAt
|
||||
if (
|
||||
updated.isDedicated === false &&
|
||||
updated.chargedAt &&
|
||||
updated.planMonths &&
|
||||
!updated.expiresAt
|
||||
) {
|
||||
const fixed = await this.prisma.serverAccount.update({
|
||||
where: { id: updated.id },
|
||||
data: {
|
||||
expiresAt: this.addMonths(updated.chargedAt, updated.planMonths),
|
||||
},
|
||||
});
|
||||
Object.assign(updated, fixed);
|
||||
}
|
||||
|
||||
await this.audit.log({
|
||||
action: 'server.account.update',
|
||||
entityType: 'server_account',
|
||||
entityId: updated.id,
|
||||
tenantId,
|
||||
userId,
|
||||
before: this.publicAccount(before) as unknown as Prisma.InputJsonValue,
|
||||
after: this.publicAccount(updated) as unknown as Prisma.InputJsonValue,
|
||||
});
|
||||
|
||||
const tenant = await this.requireTenant(tenantId);
|
||||
if (before.isDedicated !== updated.isDedicated) {
|
||||
this.notify.notifyAdmins(
|
||||
updated.isDedicated
|
||||
? `سوپرادمین: سرور «${tenant.nameFa}» روی حالت اختصاصی قرار گرفت.`
|
||||
: `سوپرادمین: سرور «${tenant.nameFa}» از حالت اختصاصی خارج شد (مدیریت با ما).`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.getAccount(tenantId);
|
||||
}
|
||||
|
||||
async renew(tenantId: string, dto: RenewServerDto, userId?: string) {
|
||||
if (!PLAN_MONTHS.includes(dto.planMonths as any)) {
|
||||
throw new BadRequestException('مدت شارژ نامعتبر است');
|
||||
}
|
||||
if (dto.priceRials < 0) {
|
||||
throw new BadRequestException('قیمت نامعتبر است');
|
||||
}
|
||||
|
||||
const tenant = await this.requireTenant(tenantId);
|
||||
const account = await this.ensureAccount(tenantId);
|
||||
|
||||
if (account.isDedicated) {
|
||||
throw new BadRequestException(
|
||||
'این نسخه سرور اختصاصی است؛ شارژ مدیریتشده لازم نیست. ابتدا سوئیچ اختصاصی را خاموش کنید.',
|
||||
);
|
||||
}
|
||||
|
||||
const chargedAt = dto.chargedAt ? new Date(dto.chargedAt) : new Date();
|
||||
if (Number.isNaN(chargedAt.getTime())) {
|
||||
throw new BadRequestException('تاریخ شارژ نامعتبر است');
|
||||
}
|
||||
const expiresAt = this.addMonths(chargedAt, dto.planMonths);
|
||||
|
||||
const [updated] = await this.prisma.$transaction([
|
||||
this.prisma.serverAccount.update({
|
||||
where: { id: account.id },
|
||||
data: {
|
||||
chargedAt,
|
||||
planMonths: dto.planMonths,
|
||||
priceRials: dto.priceRials,
|
||||
expiresAt,
|
||||
lastAlertDate: null,
|
||||
},
|
||||
}),
|
||||
this.prisma.serverCharge.create({
|
||||
data: {
|
||||
serverAccountId: account.id,
|
||||
chargedAt,
|
||||
planMonths: dto.planMonths,
|
||||
priceRials: dto.priceRials,
|
||||
expiresAt,
|
||||
note: dto.note?.trim() || null,
|
||||
createdById: userId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
await this.audit.log({
|
||||
action: 'server.renew',
|
||||
entityType: 'server_account',
|
||||
entityId: account.id,
|
||||
tenantId,
|
||||
userId,
|
||||
after: {
|
||||
chargedAt: chargedAt.toISOString(),
|
||||
planMonths: dto.planMonths,
|
||||
priceRials: dto.priceRials,
|
||||
expiresAt: expiresAt.toISOString(),
|
||||
note: dto.note || null,
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
});
|
||||
|
||||
this.notify.notifyAdmins(
|
||||
`سوپرادمین: سرور «${tenant.nameFa}» شارژ/تمدید شد (${this.planLabel(dto.planMonths)}، تا ${formatIranDate(expiresAt)}).`,
|
||||
);
|
||||
|
||||
return this.getAccount(tenantId);
|
||||
}
|
||||
|
||||
async runExpiryAlerts() {
|
||||
try {
|
||||
const accounts = await this.prisma.serverAccount.findMany({
|
||||
where: {
|
||||
isDedicated: false,
|
||||
expiresAt: { not: null },
|
||||
tenant: { isActive: true },
|
||||
},
|
||||
include: {
|
||||
tenant: { select: { id: true, nameFa: true, domain: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const todayKey = iranYmd(new Date());
|
||||
let sent = 0;
|
||||
|
||||
for (const account of accounts) {
|
||||
if (!account.expiresAt) continue;
|
||||
const daysLeft = iranDaysUntil(account.expiresAt);
|
||||
const threshold = account.alertDaysBefore ?? 3;
|
||||
|
||||
if (daysLeft > threshold) continue;
|
||||
|
||||
if (
|
||||
account.lastAlertDate &&
|
||||
iranYmd(account.lastAlertDate) === todayKey
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const when =
|
||||
daysLeft < 0
|
||||
? `${Math.abs(daysLeft)} روز است منقضی شده`
|
||||
: daysLeft === 0
|
||||
? 'امروز منقضی میشود'
|
||||
: `${daysLeft} روز تا پایان اعتبار`;
|
||||
|
||||
this.notify.notifyAdmins(
|
||||
`هشدار سرور: «${account.tenant.nameFa}» (${account.tenant.domain}) — ${when} (انقضا: ${formatIranDate(account.expiresAt)}). لطفاً شارژ/تمدید کنید.`,
|
||||
);
|
||||
|
||||
await this.prisma.serverAccount.update({
|
||||
where: { id: account.id },
|
||||
data: { lastAlertDate: new Date() },
|
||||
});
|
||||
sent += 1;
|
||||
}
|
||||
|
||||
if (sent > 0) {
|
||||
this.logger.log(`Server expiry alerts sent: ${sent}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Expiry alert failed: ${error?.message || error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private toSummary(account: ServerAccount) {
|
||||
if (account.isDedicated) {
|
||||
return {
|
||||
isDedicated: true,
|
||||
serverName: account.serverName,
|
||||
specs: account.specs,
|
||||
priceRials: account.priceRials,
|
||||
chargedAt: account.chargedAt?.toISOString() ?? null,
|
||||
planMonths: account.planMonths,
|
||||
expiresAt: account.expiresAt?.toISOString() ?? null,
|
||||
daysLeft: null as number | null,
|
||||
expiryLevel: 'dedicated' as ExpiryLevel,
|
||||
expiryLabel: 'اختصاصی',
|
||||
planLabel: account.planMonths
|
||||
? this.planLabel(account.planMonths)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (!account.expiresAt) {
|
||||
return {
|
||||
isDedicated: false,
|
||||
serverName: account.serverName,
|
||||
specs: account.specs,
|
||||
priceRials: account.priceRials,
|
||||
chargedAt: account.chargedAt?.toISOString() ?? null,
|
||||
planMonths: account.planMonths,
|
||||
expiresAt: null,
|
||||
daysLeft: null,
|
||||
expiryLevel: 'unknown' as ExpiryLevel,
|
||||
expiryLabel: 'بدون تاریخ اعتبار',
|
||||
planLabel: account.planMonths
|
||||
? this.planLabel(account.planMonths)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
const daysLeft = iranDaysUntil(account.expiresAt);
|
||||
const threshold = account.alertDaysBefore ?? 3;
|
||||
let expiryLevel: ExpiryLevel = 'healthy';
|
||||
let expiryLabel = `${daysLeft} روز مانده`;
|
||||
|
||||
if (daysLeft < 0) {
|
||||
expiryLevel = 'critical';
|
||||
expiryLabel = `${Math.abs(daysLeft)} روز گذشته از انقضا`;
|
||||
} else if (daysLeft <= threshold) {
|
||||
expiryLevel = 'critical';
|
||||
expiryLabel =
|
||||
daysLeft === 0 ? 'امروز منقضی میشود' : `${daysLeft} روز مانده`;
|
||||
} else if (daysLeft <= threshold + 4) {
|
||||
expiryLevel = 'warning';
|
||||
expiryLabel = `${daysLeft} روز مانده`;
|
||||
}
|
||||
|
||||
return {
|
||||
isDedicated: false,
|
||||
serverName: account.serverName,
|
||||
specs: account.specs,
|
||||
priceRials: account.priceRials,
|
||||
chargedAt: account.chargedAt?.toISOString() ?? null,
|
||||
planMonths: account.planMonths,
|
||||
expiresAt: account.expiresAt.toISOString(),
|
||||
daysLeft,
|
||||
expiryLevel,
|
||||
expiryLabel,
|
||||
planLabel: account.planMonths ? this.planLabel(account.planMonths) : null,
|
||||
};
|
||||
}
|
||||
|
||||
private publicAccount(account: ServerAccount) {
|
||||
return {
|
||||
id: account.id,
|
||||
tenantId: account.tenantId,
|
||||
isDedicated: account.isDedicated,
|
||||
serverName: account.serverName,
|
||||
specs: account.specs,
|
||||
priceRials: account.priceRials,
|
||||
chargedAt: account.chargedAt?.toISOString() ?? null,
|
||||
planMonths: account.planMonths,
|
||||
expiresAt: account.expiresAt?.toISOString() ?? null,
|
||||
description: account.description,
|
||||
alertDaysBefore: account.alertDaysBefore,
|
||||
lastAlertDate: account.lastAlertDate?.toISOString() ?? null,
|
||||
updatedAt: account.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
private planLabel(months: number) {
|
||||
switch (months) {
|
||||
case 1:
|
||||
return 'یکماهه';
|
||||
case 3:
|
||||
return 'سهماهه';
|
||||
case 6:
|
||||
return 'ششماهه';
|
||||
case 12:
|
||||
return 'یکساله';
|
||||
default:
|
||||
return `${months} ماهه`;
|
||||
}
|
||||
}
|
||||
|
||||
private addMonths(from: Date, months: number) {
|
||||
const d = new Date(from.getTime());
|
||||
const day = d.getDate();
|
||||
d.setMonth(d.getMonth() + months);
|
||||
// Handle month-end overflow (e.g. Jan 31 + 1 month)
|
||||
if (d.getDate() < day) {
|
||||
d.setDate(0);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
private async ensureAccountsForAllTenants() {
|
||||
const tenants = await this.prisma.tenant.findMany({
|
||||
where: { serverAccount: null },
|
||||
select: { id: true },
|
||||
});
|
||||
for (const tenant of tenants) {
|
||||
await this.ensureAccount(tenant.id);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureAccount(tenantId: string) {
|
||||
await this.requireTenant(tenantId);
|
||||
const existing = await this.prisma.serverAccount.findUnique({
|
||||
where: { tenantId },
|
||||
});
|
||||
if (existing) return existing;
|
||||
|
||||
return this.prisma.serverAccount.create({
|
||||
data: {
|
||||
tenantId,
|
||||
isDedicated: false,
|
||||
alertDaysBefore: 3,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async requireTenant(tenantId: string) {
|
||||
const tenant = await this.prisma.tenant.findUnique({
|
||||
where: { id: tenantId },
|
||||
});
|
||||
if (!tenant) throw new NotFoundException('نسخه یافت نشد');
|
||||
return tenant;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user