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,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' };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user