mirror of
https://git.meshkee.com/Meshkee/backend.git
synced 2026-08-12 06:40:58 +04:30
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user