181 lines
5.7 KiB
TypeScript
181 lines
5.7 KiB
TypeScript
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<SiteSections> {
|
|
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<string, unknown>).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 {}
|