x
This commit is contained in:
1 parent
fb776f778f
commit
8f64d89f6f
13 files changed
+779
-295
No files matched your search
@@ -11,133 +11,18 @@ import {
|
||||
NewsletterSectionPayloadSchema,
|
||||
EditorialTileSectionPayloadSchema,
|
||||
EditorialCarouselSectionPayloadSchema,
|
||||
HeroArticlePayloadSchema,
|
||||
RichTextPayloadSchema,
|
||||
ScentNotesBreakdownPayloadSchema,
|
||||
QuoteCalloutPayloadSchema,
|
||||
ProductCarouselPayloadSchema,
|
||||
HeroArticleSectionPayloadSchema,
|
||||
RichTextSectionPayloadSchema,
|
||||
ScentNotesBreakdownSectionPayloadSchema,
|
||||
QuoteCalloutSectionPayloadSchema,
|
||||
ProductCarouselSectionPayloadSchema,
|
||||
AccordionSectionPayloadSchema,
|
||||
ContactInfoSectionPayloadSchema,
|
||||
ContactFormSectionPayloadSchema
|
||||
} from "@homaroma/types";
|
||||
import { z } from "zod";
|
||||
import { adminOnly } from "../middleware/auth";
|
||||
|
||||
function isLocalizedStringValue(value: unknown): value is string | Record<string, string> {
|
||||
if (typeof value === "string") return true;
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
||||
return Object.values(value).every((entry) => typeof entry === "string");
|
||||
}
|
||||
|
||||
function isLocalizedStringList(value: unknown): boolean {
|
||||
return Array.isArray(value) && value.every((entry) => isLocalizedStringValue(entry));
|
||||
}
|
||||
|
||||
function hasValidString(value: unknown): boolean {
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
}
|
||||
|
||||
function hasValidNumber(value: unknown): boolean {
|
||||
return typeof value === "number" && Number.isFinite(value);
|
||||
}
|
||||
|
||||
function hasValidBoolean(value: unknown): boolean {
|
||||
return typeof value === "boolean";
|
||||
}
|
||||
|
||||
function validateHeroArticlePayload(payload: any): string | null {
|
||||
if (!payload || typeof payload !== "object") return "Payload must be a JSON object";
|
||||
if (!isLocalizedStringValue(payload.title)) return "title: Must be a localized string";
|
||||
const localizedFields = ["subtitle", "author", "category"] as const;
|
||||
for (const field of localizedFields) {
|
||||
if (payload[field] !== undefined && !isLocalizedStringValue(payload[field])) {
|
||||
return `${field}: Must be a localized string`;
|
||||
}
|
||||
}
|
||||
const stringFields = ["publishDate", "date", "imageUrl", "backgroundImageUrl", "overlayColor"] as const;
|
||||
for (const field of stringFields) {
|
||||
if (payload[field] !== undefined && !hasValidString(payload[field])) {
|
||||
return `${field}: Must be a string`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateRichTextPayload(payload: any): string | null {
|
||||
if (!payload || typeof payload !== "object") return "Payload must be a JSON object";
|
||||
if (payload.title !== undefined && !isLocalizedStringValue(payload.title)) return "title: Must be a localized string";
|
||||
if (!isLocalizedStringValue(payload.content)) return "content: Must be a localized string";
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateScentNotesBreakdownPayload(payload: any): string | null {
|
||||
if (!payload || typeof payload !== "object") return "Payload must be a JSON object";
|
||||
if (payload.title !== undefined && !isLocalizedStringValue(payload.title)) return "title: Must be a localized string";
|
||||
if (payload.description !== undefined && !isLocalizedStringValue(payload.description)) return "description: Must be a localized string";
|
||||
const listFields = ["topNotes", "heartNotes", "middleNotes", "baseNotes"] as const;
|
||||
for (const field of listFields) {
|
||||
const value = payload[field];
|
||||
if (value !== undefined && !(isLocalizedStringValue(value) || isLocalizedStringList(value))) {
|
||||
return `${field}: Must be a localized string or localized string list`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateQuoteCalloutPayload(payload: any): string | null {
|
||||
if (!payload || typeof payload !== "object") return "Payload must be a JSON object";
|
||||
if (!isLocalizedStringValue(payload.quote)) return "quote: Must be a localized string";
|
||||
if (payload.author !== undefined && !isLocalizedStringValue(payload.author)) return "author: Must be a localized string";
|
||||
if (payload.role !== undefined && !isLocalizedStringValue(payload.role)) return "role: Must be a localized string";
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateProductCarouselPayload(payload: any): string | null {
|
||||
if (!payload || typeof payload !== "object") return "Payload must be a JSON object";
|
||||
if (payload.title !== undefined && !isLocalizedStringValue(payload.title)) return "title: Must be a localized string";
|
||||
if (payload.description !== undefined && !isLocalizedStringValue(payload.description)) return "description: Must be a localized string";
|
||||
if (payload.collectionId !== undefined && !hasValidString(payload.collectionId)) return "collectionId: Must be a string";
|
||||
if (payload.limit !== undefined && (!hasValidNumber(payload.limit) || payload.limit <= 0)) return "limit: Must be a positive number";
|
||||
if (payload.showPrice !== undefined && !hasValidBoolean(payload.showPrice)) return "showPrice: Must be a boolean";
|
||||
if (payload.showAddToCart !== undefined && !hasValidBoolean(payload.showAddToCart)) return "showAddToCart: Must be a boolean";
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateAccordionPayload(payload: any): string | null {
|
||||
if (!payload || typeof payload !== "object") return "Payload must be a JSON object";
|
||||
if (payload.title !== undefined && !isLocalizedStringValue(payload.title)) return "title: Must be a localized string";
|
||||
if (!Array.isArray(payload.categories) || payload.categories.length === 0) return "categories: Must be a non-empty array";
|
||||
for (const [catIndex, category] of payload.categories.entries()) {
|
||||
if (!category || typeof category !== "object") return `categories.${catIndex}: Must be an object`;
|
||||
if (!isLocalizedStringValue(category.name)) return `categories.${catIndex}.name: Must be a localized string`;
|
||||
if (category.icon !== undefined && !hasValidString(category.icon)) return `categories.${catIndex}.icon: Must be a string`;
|
||||
if (!Array.isArray(category.questions) || category.questions.length === 0) {
|
||||
return `categories.${catIndex}.questions: Must be a non-empty array`;
|
||||
}
|
||||
for (const [qIndex, question] of category.questions.entries()) {
|
||||
if (!question || typeof question !== "object") return `categories.${catIndex}.questions.${qIndex}: Must be an object`;
|
||||
if (!isLocalizedStringValue(question.q)) return `categories.${catIndex}.questions.${qIndex}.q: Must be a localized string`;
|
||||
if (!isLocalizedStringValue(question.a)) return `categories.${catIndex}.questions.${qIndex}.a: Must be a localized string`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateContactInfoPayload(payload: any): string | null {
|
||||
if (!payload || typeof payload !== "object") return "Payload must be a JSON object";
|
||||
if (payload.title !== undefined && !isLocalizedStringValue(payload.title)) return "title: Must be a localized string";
|
||||
for (const field of ["address", "email", "line", "hours"] as const) {
|
||||
if (!isLocalizedStringValue(payload[field])) return `${field}: Must be a localized string`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateContactFormPayload(payload: any): string | null {
|
||||
if (!payload || typeof payload !== "object") return "Payload must be a JSON object";
|
||||
if (payload.title !== undefined && !isLocalizedStringValue(payload.title)) return "title: Must be a localized string";
|
||||
if (payload.subtitle !== undefined && !isLocalizedStringValue(payload.subtitle)) return "subtitle: Must be a localized string";
|
||||
return null;
|
||||
}
|
||||
|
||||
const RESERVED_SLUGS = [
|
||||
"shop", "category", "products", "cart", "checkout", "account", "search",
|
||||
"login", "admin", "wishlist", "track", "coupons", "scent-guide",
|
||||
@@ -215,7 +100,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
|
||||
|
||||
if (payload.type === 'featured') {
|
||||
products = await prisma.product.findMany({
|
||||
where: { categoryId: 1 },
|
||||
where: { categoryId: Number(payload.categoryId) || 1 },
|
||||
include: productInclude,
|
||||
take: limit
|
||||
});
|
||||
@@ -273,7 +158,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
|
||||
|
||||
if (payload.type === 'featured' || payload.collectionId === 'featured') {
|
||||
products = await prisma.product.findMany({
|
||||
where: { categoryId: 1 },
|
||||
where: { categoryId: Number(payload.categoryId) || 1 },
|
||||
include: productInclude,
|
||||
take: limit
|
||||
});
|
||||
@@ -517,10 +402,10 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
|
||||
const pageId = Number(slug);
|
||||
const { type, order, payload, isActive } = body as any;
|
||||
|
||||
const validationError = validateSectionPayload(type, payload);
|
||||
if (validationError) {
|
||||
const validationResult = validateSectionPayload(type, payload);
|
||||
if (!validationResult.success) {
|
||||
set.status = 400;
|
||||
return { error: validationError };
|
||||
return { error: "Validation failed", details: validationResult.issues };
|
||||
}
|
||||
|
||||
let finalOrder = order;
|
||||
@@ -569,10 +454,10 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
|
||||
}
|
||||
validationType = existingSection.type;
|
||||
}
|
||||
const validationError = validateSectionPayload(validationType, payload);
|
||||
if (validationError) {
|
||||
const validationResult = validateSectionPayload(validationType, payload);
|
||||
if (!validationResult.success) {
|
||||
set.status = 400;
|
||||
return { error: validationError };
|
||||
return { error: "Validation failed", details: validationResult.issues };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -637,100 +522,38 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
|
||||
}
|
||||
});
|
||||
|
||||
function validateSectionPayload(type: string, payload: any): string | null {
|
||||
function validateSectionPayload(type: string, payload: any): { success: boolean; issues?: z.ZodIssue[] } {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return "Payload must be a JSON object";
|
||||
return { success: false, issues: [{ code: 'custom', message: "Payload must be a JSON object", path: [] }] as z.ZodIssue[] };
|
||||
}
|
||||
|
||||
let schema: z.ZodTypeAny;
|
||||
|
||||
switch (type.toUpperCase()) {
|
||||
case 'HERO': {
|
||||
const res = HeroSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'CATEGORIES': {
|
||||
const res = CategoriesSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'PRODUCTS': {
|
||||
const res = ProductsSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'BANNER': {
|
||||
const res = BannerSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'TEXT': {
|
||||
const res = TextSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'ANNOUNCEMENT_BAR': {
|
||||
const res = AnnouncementBarSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'NEWSLETTER': {
|
||||
const res = NewsletterSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'EDITORIAL_TILE': {
|
||||
const res = EditorialTileSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'EDITORIAL_CAROUSEL': {
|
||||
const res = EditorialCarouselSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'HERO_ARTICLE': {
|
||||
const error = validateHeroArticlePayload(payload);
|
||||
if (error) return error;
|
||||
break;
|
||||
}
|
||||
case 'RICH_TEXT': {
|
||||
const error = validateRichTextPayload(payload);
|
||||
if (error) return error;
|
||||
break;
|
||||
}
|
||||
case 'SCENT_NOTES_BREAKDOWN': {
|
||||
const error = validateScentNotesBreakdownPayload(payload);
|
||||
if (error) return error;
|
||||
break;
|
||||
}
|
||||
case 'QUOTE_CALLOUT': {
|
||||
const error = validateQuoteCalloutPayload(payload);
|
||||
if (error) return error;
|
||||
break;
|
||||
}
|
||||
case 'PRODUCT_CAROUSEL': {
|
||||
const error = validateProductCarouselPayload(payload);
|
||||
if (error) return error;
|
||||
break;
|
||||
}
|
||||
case 'ACCORDION': {
|
||||
const error = validateAccordionPayload(payload);
|
||||
if (error) return error;
|
||||
break;
|
||||
}
|
||||
case 'CONTACT_INFO': {
|
||||
const error = validateContactInfoPayload(payload);
|
||||
if (error) return error;
|
||||
break;
|
||||
}
|
||||
case 'CONTACT_FORM': {
|
||||
const error = validateContactFormPayload(payload);
|
||||
if (error) return error;
|
||||
break;
|
||||
}
|
||||
case 'HERO': schema = HeroSectionPayloadSchema; break;
|
||||
case 'CATEGORIES': schema = CategoriesSectionPayloadSchema; break;
|
||||
case 'PRODUCTS': schema = ProductsSectionPayloadSchema; break;
|
||||
case 'BANNER': schema = BannerSectionPayloadSchema; break;
|
||||
case 'TEXT': schema = TextSectionPayloadSchema; break;
|
||||
case 'ANNOUNCEMENT_BAR': schema = AnnouncementBarSectionPayloadSchema; break;
|
||||
case 'NEWSLETTER': schema = NewsletterSectionPayloadSchema; break;
|
||||
case 'EDITORIAL_TILE': schema = EditorialTileSectionPayloadSchema; break;
|
||||
case 'EDITORIAL_CAROUSEL': schema = EditorialCarouselSectionPayloadSchema; break;
|
||||
case 'HERO_ARTICLE': schema = HeroArticleSectionPayloadSchema; break;
|
||||
case 'RICH_TEXT': schema = RichTextSectionPayloadSchema; break;
|
||||
case 'SCENT_NOTES_BREAKDOWN': schema = ScentNotesBreakdownSectionPayloadSchema; break;
|
||||
case 'QUOTE_CALLOUT': schema = QuoteCalloutSectionPayloadSchema; break;
|
||||
case 'PRODUCT_CAROUSEL': schema = ProductCarouselSectionPayloadSchema; break;
|
||||
case 'ACCORDION': schema = AccordionSectionPayloadSchema; break;
|
||||
case 'CONTACT_INFO': schema = ContactInfoSectionPayloadSchema; break;
|
||||
case 'CONTACT_FORM': schema = ContactFormSectionPayloadSchema; break;
|
||||
default:
|
||||
return `Unknown section type: ${type}`;
|
||||
return { success: false, issues: [{ code: 'custom', message: `Unknown section type: ${type}`, path: [] }] as z.ZodIssue[] };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
const res = schema.safeParse(payload);
|
||||
if (!res.success) {
|
||||
return { success: false, issues: res.error.issues };
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
Reference in new issue
Block a user