Vitron-Front/app/routes/store.$storeId.product.$id.tsx
Arda Samadi 485f8d460a Security fixes: session hardening, open redirect, SSR path injection
- sessions: require REMIX_SECRET in prod (no "secret" fallback); base Secure
  cookie on NODE_ENV; add maxAge
- login: safeRedirect() blocks open-redirect via redirectTo (loader + action)
- SSR loaders: encodeURIComponent on user path params before internal API
  fetch (product, store product, seller)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:02:11 +03:30

100 lines
3.3 KiB
TypeScript

import { json, LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { ProductDetail as ProductDetailType } from "../../src/api/types";
import { ProductDetailView } from "../components/product/ProductDetailView";
import { ClientOnly } from "../components/ClientOnly";
import { getInternalApiUrl } from "~/utils/api-config";
export const meta: MetaFunction<typeof loader> = ({ data, params }) => {
const product = data?.product;
const siteUrl = data?.siteUrl;
if (!product) {
return [
{ title: "محصول یافت نشد | ویترون" },
{ name: "description", content: "متاسفانه محصول مورد نظر یافت نشد." },
{ name: "robots", content: "noindex, follow" },
];
}
const productTitle = product.title;
const productDescription =
product.description || "مشاهده جزئیات محصول در فروشگاه ویترون";
// Ensure image URL is absolute
const baseImageUrl = product.imageUrls?.[0];
const imageUrl = baseImageUrl
? baseImageUrl.startsWith("http")
? baseImageUrl
: `${siteUrl}${baseImageUrl}`
: `${siteUrl}/og-image.svg`;
return [
{ title: `${productTitle} - ویترون` },
{ name: "description", content: productDescription },
{
name: "keywords",
content: `${productTitle}، خرید آنلاین، فروشگاه آنلاین، ویترون، محصول`,
},
{ name: "robots", content: "index, follow" },
// Open Graph
{ property: "og:title", content: `${productTitle} - ویترون` },
{ property: "og:description", content: productDescription },
{ property: "og:type", content: "product" },
{ property: "og:image", content: imageUrl },
{ property: "og:image:width", content: "1200" },
{ property: "og:image:height", content: "630" },
{ property: "og:image:alt", content: productTitle },
{
property: "og:url",
content: `${siteUrl}/store/${params?.storeId}/product/${product.id}`,
},
{ property: "og:site_name", content: "ویترون" },
{ property: "og:locale", content: "fa_IR" },
// Twitter Card
{ name: "twitter:card", content: "summary_large_image" },
{ name: "twitter:title", content: `${productTitle} - ویترون` },
{ name: "twitter:description", content: productDescription },
{ name: "twitter:image", content: imageUrl },
];
};
interface LoaderData {
product: ProductDetailType;
siteUrl: string;
}
// Server-side loader
export async function loader({ params }: LoaderFunctionArgs) {
const { id } = params;
try {
const response = await fetch(
`${getInternalApiUrl()}/api/product/v1/products/${encodeURIComponent(id ?? "")}/`,
);
if (!response.ok) {
throw new Error(`Failed to fetch product: ${response.status}`);
}
const product = await response.json();
const siteUrl = process.env.VITE_SITE_URL || "https://vitrown.com";
return json<LoaderData>({ product, siteUrl });
} catch (error) {
console.error("Error fetching product:", error);
throw new Response("Error loading product", { status: 500 });
}
}
// Main component
export default function ProductDetail() {
const { product } = useLoaderData<typeof loader>();
return (
<ClientOnly>
{() => <ProductDetailView product={product} isOwner={true} />}
</ClientOnly>
);
}