Initial commit: Meshkee CMS API

NestJS backend with Prisma, Docker Compose for Postgres/Redis, and deploy docs for the production VM.
This commit is contained in:
Ali Reza
2026-07-21 17:52:36 +03:30
commit bb59d5e9ba
254 changed files with 37031 additions and 0 deletions
@@ -0,0 +1,56 @@
import { Body, Controller, Delete, Get, Param, Patch, Query, UseGuards } from '@nestjs/common';
import { AuthUser } from '../auth/auth.types';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { DisableDomainDto } from './dto/disable-domain.dto';
import { ListDomainsDto } from './dto/list-domains.dto';
import { ToggleSslDto } from './dto/toggle-ssl.dto';
import { UpdateDomainAdminDto } from './dto/update-domain-admin.dto';
import { DomainAdminService } from './domain-admin.service';
@Controller('domains')
export class DomainAdminController {
constructor(private readonly service: DomainAdminService) {}
@Get()
@UseGuards(JwtAuthGuard)
list(@Query() query: ListDomainsDto, @CurrentUser() user: AuthUser) {
return this.service.list(query, user);
}
@Patch(':domainId')
@UseGuards(JwtAuthGuard)
update(
@Param('domainId') domainId: string,
@Body() dto: UpdateDomainAdminDto,
@CurrentUser() user: AuthUser,
) {
return this.service.update(domainId, dto, user);
}
@Patch(':domainId/disable')
@UseGuards(JwtAuthGuard)
disable(
@Param('domainId') domainId: string,
@Body() dto: DisableDomainDto,
@CurrentUser() user: AuthUser,
) {
return this.service.disable(domainId, dto, user);
}
@Patch(':domainId/ssl')
@UseGuards(JwtAuthGuard)
toggleSsl(
@Param('domainId') domainId: string,
@Body() dto: ToggleSslDto,
@CurrentUser() user: AuthUser,
) {
return this.service.toggleSsl(domainId, dto, user);
}
@Delete(':domainId')
@UseGuards(JwtAuthGuard)
remove(@Param('domainId') domainId: string, @CurrentUser() user: AuthUser) {
return this.service.remove(domainId, user);
}
}
+11
View File
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
import { DomainAdminController } from './domain-admin.controller';
import { DomainAdminService } from './domain-admin.service';
@Module({
imports: [AuthModule],
controllers: [DomainAdminController],
providers: [DomainAdminService],
})
export class DomainAdminModule {}
+150
View File
@@ -0,0 +1,150 @@
import {
BadRequestException,
ConflictException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { AuthUser } from '../auth/auth.types';
import { PermissionsService } from '../auth/permissions.service';
import { PrismaService } from '../prisma/prisma.service';
import { DisableDomainDto } from './dto/disable-domain.dto';
import { ListDomainsDto } from './dto/list-domains.dto';
import { ToggleSslDto } from './dto/toggle-ssl.dto';
import { UpdateDomainAdminDto } from './dto/update-domain-admin.dto';
type DomainRow = {
id: bigint;
host: string;
businessId: bigint;
businessName: string;
sslEnabled: boolean;
isActive: boolean;
expiresAt: Date | null;
createdAt: Date;
};
@Injectable()
export class DomainAdminService {
constructor(
private readonly prisma: PrismaService,
private readonly permissions: PermissionsService,
) {}
private async assertSuperAdmin(actor: AuthUser) {
if (!(await this.permissions.isSuperAdmin(actor.id))) {
throw new ForbiddenException('Super admin access required');
}
}
async list(query: ListDomainsDto, actor: AuthUser) {
await this.assertSuperAdmin(actor);
const page = query.page ?? 1;
const pageSize = query.pageSize ?? 10;
const skip = (page - 1) * pageSize;
const nameLike = query.name?.trim() ? `%${query.name.trim()}%` : null;
const where = Prisma.sql`
WHERE 1=1
${nameLike ? Prisma.sql`AND d.host ILIKE ${nameLike}` : Prisma.empty}
`;
const [items, totalRow] = await Promise.all([
this.prisma.$queryRaw<DomainRow[]>(Prisma.sql`
SELECT
d.id AS "id",
d.host AS "host",
d.business_id AS "businessId",
b.name AS "businessName",
d.ssl_enabled AS "sslEnabled",
d.is_active AS "isActive",
d.expires_at AS "expiresAt",
d.created_at AS "createdAt"
FROM domains d
JOIN businesses b ON b.id = d.business_id
${where}
ORDER BY d.created_at DESC
LIMIT ${pageSize} OFFSET ${skip}
`),
this.prisma.$queryRaw<{ total: number }[]>(Prisma.sql`
SELECT COUNT(*)::int AS "total"
FROM domains d
${where}
`),
]);
return { items, total: totalRow[0]?.total ?? 0, page, pageSize };
}
async update(domainIdRaw: string, dto: UpdateDomainAdminDto, actor: AuthUser) {
await this.assertSuperAdmin(actor);
const domainId = BigInt(domainIdRaw);
const domain = await this.prisma.domain.findUnique({ where: { id: domainId } });
if (!domain) {
throw new NotFoundException('Domain not found');
}
if (dto.host) {
const host = dto.host.trim();
const existing = await this.prisma.domain.findUnique({ where: { host } });
if (existing && existing.id !== domainId) {
throw new ConflictException('Domain host is already taken');
}
}
return this.prisma.domain.update({
where: { id: domainId },
data: {
host: dto.host?.trim(),
expiresAt: dto.expiresAt !== undefined ? new Date(dto.expiresAt) : undefined,
},
});
}
async disable(domainIdRaw: string, dto: DisableDomainDto, actor: AuthUser) {
await this.assertSuperAdmin(actor);
const domainId = BigInt(domainIdRaw);
const domain = await this.prisma.domain.findUnique({ where: { id: domainId } });
if (!domain) {
throw new NotFoundException('Domain not found');
}
return this.prisma.domain.update({
where: { id: domainId },
data: { isActive: dto.isActive },
});
}
async toggleSsl(domainIdRaw: string, dto: ToggleSslDto, actor: AuthUser) {
await this.assertSuperAdmin(actor);
const domainId = BigInt(domainIdRaw);
const domain = await this.prisma.domain.findUnique({ where: { id: domainId } });
if (!domain) {
throw new NotFoundException('Domain not found');
}
return this.prisma.domain.update({
where: { id: domainId },
data: { sslEnabled: dto.sslEnabled },
});
}
async remove(domainIdRaw: string, actor: AuthUser) {
await this.assertSuperAdmin(actor);
const domainId = BigInt(domainIdRaw);
const domain = await this.prisma.domain.findUnique({ where: { id: domainId } });
if (!domain) {
throw new NotFoundException('Domain not found');
}
await this.prisma.domain.delete({ where: { id: domainId } });
return { message: 'Domain removed' };
}
}
@@ -0,0 +1,6 @@
import { IsBoolean } from 'class-validator';
export class DisableDomainDto {
@IsBoolean()
isActive!: boolean;
}
+21
View File
@@ -0,0 +1,21 @@
import { Type } from 'class-transformer';
import { IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
export class ListDomainsDto {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(5)
@Max(50)
pageSize?: number;
@IsOptional()
@IsString()
name?: string;
}
+6
View File
@@ -0,0 +1,6 @@
import { IsBoolean } from 'class-validator';
export class ToggleSslDto {
@IsBoolean()
sslEnabled!: boolean;
}
@@ -0,0 +1,12 @@
import { IsDateString, IsOptional, IsString, MinLength } from 'class-validator';
export class UpdateDomainAdminDto {
@IsOptional()
@IsString()
@MinLength(1)
host?: string;
@IsOptional()
@IsDateString()
expiresAt?: string;
}