mirror of
https://git.meshkee.com/Meshkee/backend.git
synced 2026-08-12 06:40:58 +04:30
NestJS backend with Prisma, Docker Compose for Postgres/Redis, and deploy docs for the production VM.
156 lines
4.3 KiB
TypeScript
156 lines
4.3 KiB
TypeScript
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');
|
|
}
|
|
}
|
|
}
|