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,155 @@
|
||||
import { 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 { TenantService } from '../tenant/tenant.service';
|
||||
import { CreateContactSubmissionDto } from './dto/create-contact-submission.dto';
|
||||
import { ListContactSubmissionsDto } from './dto/list-contact-submissions.dto';
|
||||
|
||||
type ContactSubmissionRow = {
|
||||
id: bigint;
|
||||
title: string;
|
||||
name: string;
|
||||
email: string | null;
|
||||
cellNumber: string | null;
|
||||
text: string;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class ContactSubmissionsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly permissions: PermissionsService,
|
||||
private readonly tenant: TenantService,
|
||||
) {}
|
||||
|
||||
async createPublic(host: string, dto: CreateContactSubmissionDto) {
|
||||
const business = await this.tenant.resolveBusinessByDomain(host);
|
||||
|
||||
const created = await this.prisma.contactSubmission.create({
|
||||
data: {
|
||||
businessId: business.id,
|
||||
title: dto.title.trim(),
|
||||
name: dto.name.trim(),
|
||||
email: dto.email?.trim() || null,
|
||||
cellNumber: dto.cellNumber?.trim() || null,
|
||||
text: dto.text.trim(),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
submission: this.serialize(created),
|
||||
message: 'Contact form submitted successfully',
|
||||
};
|
||||
}
|
||||
|
||||
async list(
|
||||
businessIdRaw: string,
|
||||
query: ListContactSubmissionsDto,
|
||||
actor: AuthUser,
|
||||
) {
|
||||
const businessId = BigInt(businessIdRaw);
|
||||
await this.assertPermission(businessId, actor.id, 'business.read');
|
||||
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = Math.min(Math.max(query.pageSize ?? 20, 1), 100);
|
||||
const skip = (page - 1) * pageSize;
|
||||
const qLike = query.q?.trim() ? `%${query.q.trim()}%` : null;
|
||||
|
||||
const where = Prisma.sql`
|
||||
WHERE cs.business_id = ${businessId}
|
||||
${qLike ? Prisma.sql`
|
||||
AND (
|
||||
cs.title ILIKE ${qLike}
|
||||
OR cs.name ILIKE ${qLike}
|
||||
OR cs.email ILIKE ${qLike}
|
||||
OR cs.cell_number ILIKE ${qLike}
|
||||
OR cs.text ILIKE ${qLike}
|
||||
)
|
||||
` : Prisma.empty}
|
||||
`;
|
||||
|
||||
const [items, totalRow] = await Promise.all([
|
||||
this.prisma.$queryRaw<ContactSubmissionRow[]>(Prisma.sql`
|
||||
SELECT
|
||||
cs.id AS "id",
|
||||
cs.title AS "title",
|
||||
cs.name AS "name",
|
||||
cs.email AS "email",
|
||||
cs.cell_number AS "cellNumber",
|
||||
cs.text AS "text",
|
||||
cs.created_at AS "createdAt"
|
||||
FROM contact_submissions cs
|
||||
${where}
|
||||
ORDER BY cs.created_at DESC
|
||||
LIMIT ${pageSize} OFFSET ${skip}
|
||||
`),
|
||||
this.prisma.$queryRaw<{ total: number }[]>(Prisma.sql`
|
||||
SELECT COUNT(*)::int AS "total"
|
||||
FROM contact_submissions cs
|
||||
${where}
|
||||
`),
|
||||
]);
|
||||
|
||||
return {
|
||||
items: items.map((row) => this.serialize(row)),
|
||||
total: totalRow[0]?.total ?? 0,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
async getOne(businessIdRaw: string, submissionIdRaw: string, actor: AuthUser) {
|
||||
const businessId = BigInt(businessIdRaw);
|
||||
const submissionId = BigInt(submissionIdRaw);
|
||||
await this.assertPermission(businessId, actor.id, 'business.read');
|
||||
|
||||
const submission = await this.prisma.contactSubmission.findFirst({
|
||||
where: { id: submissionId, businessId },
|
||||
});
|
||||
|
||||
if (!submission) {
|
||||
throw new NotFoundException('Contact submission not found');
|
||||
}
|
||||
|
||||
return { submission: this.serialize(submission) };
|
||||
}
|
||||
|
||||
private serialize(row: ContactSubmissionRow | {
|
||||
id: bigint;
|
||||
title: string;
|
||||
name: string;
|
||||
email: string | null;
|
||||
cellNumber: string | null;
|
||||
text: string;
|
||||
createdAt: Date;
|
||||
}) {
|
||||
return {
|
||||
id: row.id.toString(),
|
||||
title: row.title,
|
||||
name: row.name,
|
||||
email: row.email,
|
||||
cellNumber: row.cellNumber,
|
||||
text: row.text,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
private async assertPermission(
|
||||
businessId: bigint,
|
||||
userId: bigint,
|
||||
permission: string,
|
||||
) {
|
||||
const allowed = await this.permissions.hasBusinessPermission(
|
||||
userId,
|
||||
businessId,
|
||||
permission,
|
||||
);
|
||||
|
||||
if (!allowed) {
|
||||
throw new ForbiddenException('Insufficient permissions');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user