import { BadRequestException, Body, Controller, ForbiddenException, Get, Injectable, Module, NotFoundException, Param, Patch, Put, ServiceUnavailableException, } from "@nestjs/common"; import { isSectionKey, SECTION_KEYS, sectionSchemas, type SiteSections, toIssues, } from "@imen/shared"; import { AdminAuth, type AuthUser, CurrentUser } from "../common/auth.js"; import { AuditService, RevalidateService } from "../common/common.module.js"; import { PrismaService } from "../prisma/prisma.service.js"; @Injectable() export class SectionsService { constructor( private readonly prisma: PrismaService, private readonly audit: AuditService, private readonly revalidate: RevalidateService, ) {} /** All sections, parsed through their schemas so missing optional fields get defaults */ async getAll(): Promise { const rows = await this.prisma.siteSection.findMany(); const byKey = new Map(rows.map((r) => [r.key, r.data])); const missing = SECTION_KEYS.filter((k) => !byKey.has(k)); if (missing.length) { throw new ServiceUnavailableException( `محتوای سایت مقداردهی نشده است (${missing.join(", ")}) — pnpm db:seed را اجرا کنید`, ); } return Object.fromEntries( SECTION_KEYS.map((k) => [k, sectionSchemas[k].parse(byKey.get(k))]), ) as SiteSections; } async get(key: string) { if (!isSectionKey(key)) throw new NotFoundException("بخش پیدا نشد"); const row = await this.prisma.siteSection.findUnique({ where: { key } }); if (!row) throw new NotFoundException("بخش پیدا نشد"); return { key, data: sectionSchemas[key].parse(row.data), updatedAt: row.updatedAt }; } async update(key: string, body: unknown, user: AuthUser) { if (!isSectionKey(key)) throw new NotFoundException("بخش پیدا نشد"); // site-wide settings (contact info, SEO defaults) are limited to admins if (key === "settings" && user.role !== "ADMIN") { throw new ForbiddenException("فقط مدیر کل می‌تواند تنظیمات سایت را تغییر دهد"); } const parsed = sectionSchemas[key].safeParse(body); if (!parsed.success) { throw new BadRequestException({ statusCode: 400, message: "اطلاعات واردشده معتبر نیست", issues: toIssues(parsed.error), }); } const row = await this.prisma.siteSection.upsert({ where: { key }, create: { key, data: parsed.data, updatedById: user.id }, update: { data: parsed.data, updatedById: user.id }, }); this.audit.log(user.id, "update", "section", key, key); this.revalidate.trigger(); return { key, data: parsed.data, updatedAt: row.updatedAt }; } async listAdmin() { const rows = await this.prisma.siteSection.findMany(); const byKey = new Map(rows.map((r) => [r.key, r])); return SECTION_KEYS.map((key) => { const row = byKey.get(key); let data: any = null; if (row) { const parsed = sectionSchemas[key].safeParse(row.data); data = parsed.success ? parsed.data : row.data; } return { key, updatedAt: row?.updatedAt?.toISOString() ?? null, enabled: data && typeof data === "object" && "enabled" in data ? (data.enabled as boolean) !== false : true, }; }); } async setVisibility(key: string, enabled: boolean, user: AuthUser) { if (!isSectionKey(key)) throw new NotFoundException("بخش پیدا نشد"); if (key === "settings") { throw new BadRequestException("تنظیمات سایت قابلیت تغییر وضعیت نمایش ندارد"); } const row = await this.prisma.siteSection.findUnique({ where: { key } }); if (!row) throw new NotFoundException("بخش پیدا نشد"); const currentData = row.data && typeof row.data === "object" ? { ...(row.data as object) } : {}; (currentData as Record).enabled = enabled; const parsed = sectionSchemas[key].safeParse(currentData); if (!parsed.success) { throw new BadRequestException({ statusCode: 400, message: "اطلاعات بخش معتبر نیست", issues: toIssues(parsed.error), }); } const updated = await this.prisma.siteSection.update({ where: { key }, data: { data: parsed.data, updatedById: user.id }, }); this.audit.log( user.id, "update", "section", key, `تغییر وضعیت نمایش به ${enabled ? "نمایش" : "عدم نمایش"}`, ); this.revalidate.trigger(); return { key, enabled: ((parsed.data as any).enabled as boolean) !== false, updatedAt: updated.updatedAt.toISOString(), }; } } @AdminAuth() @Controller("admin/sections") export class SectionsController { constructor(private readonly sections: SectionsService) {} @Get() list() { return this.sections.listAdmin(); } @Get(":key") get(@Param("key") key: string) { return this.sections.get(key); } @Put(":key") update(@Param("key") key: string, @Body() body: unknown, @CurrentUser() user: AuthUser) { return this.sections.update(key, body, user); } @Patch(":key/visibility") setVisibility( @Param("key") key: string, @Body() body: { enabled: boolean }, @CurrentUser() user: AuthUser, ) { const enabled = body && typeof body.enabled === "boolean" ? body.enabled : Boolean(body?.enabled); return this.sections.setVisibility(key, enabled, user); } } @Module({ controllers: [SectionsController], providers: [SectionsService], exports: [SectionsService], }) export class SectionsModule {}