commit ff6d5a3c0c51dc40e6e7a92274af9dabbbcacd12 Author: Codex Date: Sun Sep 6 00:14:09 2026 +0330 Initial ASPHP website diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7c2cc3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.next/ +node_modules/ +.env +.env.* +!.env.example +*.log +.DS_Store diff --git a/.source/asphp-home-www.html b/.source/asphp-home-www.html new file mode 100644 index 0000000..9c245ea --- /dev/null +++ b/.source/asphp-home-www.html @@ -0,0 +1,2943 @@ + + + + + + + + + + + + + + + شرکت آریا شبکه پیشرو | فروش تجهیزات سرور hp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
آریا شبکه پیشرو
+
LOADING
+
+
+
+
+ +
+
+
+
+
+ + + +
  • + +
    + ورود + +
    +
    +
  • + + +
    + +
    + +
    +
    +
    + + +
    + +
    + + +
    + + +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + + +
    +
    + +
    +
    + + + +
    +
    +
    +
    +
    + + + + +
    +
    + + + + + + +
    +
    + +
    +
    +
    +
    +
    +

    سرور (Server) چیست؟

    +

    در ویکی پدیا نیز معنی سرور، سرویس دهنده ذکر شده است. به بیان ساده معنی سرور یا در پاسخ سوال server به چه معناست باید گفت، کامپیوتری همیشه روشن و قدرتمند است که کاربران در زمان‌های متفاوت، از آن انتظار پاسخ به درخواست‌های خود را دارند.

    +

    متن تستی

    + + +

    درباره سرور بیشتر بدانید

    + +
    + +
    + +
    +
    + سرور چیست؟ + image +
    +
    +
    +
    +
    +
    + + + + +
    +
    + + + + + + +
    +
    + +
    +
    +
    +

    خدمات آریا شبکه پیشرو

    +

    Services

    +
    +
    + +
    +
    + + فروش تجهیزات شبکه + +
    + +

    فروش دیگر تجهیزات شبکه

    + +
    +

    باتوجه به اینکه شرکت آریا شبکه پیشرو یک شرکت مادر در زمینه تجهیزات شبکه محسوب می شود، می توانید تجهیزات مرتبط نظیر، رم، CPU و هارد و … را به صورت یکجا و با اعتماد کامل از این شرکت تهیه کنید.

    +
    +
    +
    +
    +
    + + فروش سوئیچ + +
    + +

    Cisco فروش سوئیچ‌های

    + +
    +

    آریا شبکه پیشرو به عنوان یکی از معتبرترین شرکت ها در زمینه فروش سوئیچ های سیسکو در صنف تجهیزات شبکه، شناخته شده است. مشتریان ما می توانند با اطمینان این محصول را از ما تهیه نمایند.

    +
    +
    +
    +
    +
    + + فروش سرور های hp + +
    + +

    HP فروش سرورهای

    + +
    +

    یکی از عوامل اولیه تجهیزات شبکه، خرید یک سرور مناسب است، شرکت آریا شبکه پیشرو با ارائه یکی از بهترین برندهای سرور جهان یعنی سرورهای HP خرید شما را تضمین می کند.

    +
    +
    +
    + +
    +
    +
    + + + + +
    +
    + +
    +
    +

    آریا شبکه پیشرو

    + +

    شرکتی جوان خلاق ، با انگیزه و دانش بنیان

    + +

    تماس با ما

    + +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    +

    ویژگـــی مــــا

    +

    انضباط، مشتری مداری، ارائه محصولات با کیفیت و تضمین شده می باشد.

    +
    +
    +
    +
    +
    +
    +
    + image +
    +

    پس از مشاوره قبل از خرید طبق نیاز کسب وکارتان سفارش خود را ثبت کنید و پیش فاکتور دریافت کنید.

    +
    +
    +
    + image +
    +

    قبل از هر اقدام به خرید با کارشناسان ما تماس بگیرید و از مشاوره ی آنها بهرمند شوید.

    +
    +
    +
    + image +
    +

    با واحد فروش ما تماس بگیرید و تجهیزات شبکه را با بهترین قیمت و کیفیت تهیه کنید.

    +
    +
    +
    + image +
    +

    بعد از خرید پرداخت فاکتور ثبت شده، سفارش شما را وارد مرحله آماده سازی و ارسال خواهد کرد.

    +
    +
    +
    + image +
    +

    سفارشات طبق زمان و مکان تعیین شده به مشتریان تحویل می گردد.

    +
    +
    +
    + image +
    +

    چرخه ی مشتری مداری ما همیشه برقرار است با پشتیبانی پس از خرید تجهیزات.

    +
    +
    +
    +
    +
    +
    +
    + + + + +
    +
    + +
    +
    +
    +

    طرح یک پروژه

    +

    Project a Design

    +
    +
    +
    +
    +

    + 01 + طراحی و تجهیز +

    +
    +

    برای انجام یک پروژه ی راه اندازی شبکه موفق ابتدا نیاز به طراحی یک طرح کلی و برآورد تجهیزات است. پس با تماس با یک تیم مشاوره خوب ابتدا نقشه راه تهیه کنید و سپس به مرحله اجرا بروید.

    +
    +
    +
    +

    + 02 + اجرا توسط تیم حرفه ای +

    +
    +

    یک نقشه خوب نیازمند اجرا توسط یک تیم حرفه ای و کارآمد است، نقشه ی حساب شده، تجهیزات عالی و با کیفیت را یک تیم اجرایی حرفه ای کامل می کند. پروژه های بی نظیر را افراد یک تیم حرفه ای به اجرا می گذارند.

    +
    +
    +
    +

    + 03 + پشتیبانی بعد از اجرا +

    +
    +

    یک شبکه و تجهیزات آن همیشه نیازمند یک تیم پشتیبانی منظم و دقیق است تا دوام و پایداری و امنیت اطلاعات یک کسب و کار را تضمین کند. پشتیبانی از تجهیزات یکی از مهمترین آیتم ها هنگام خرید است.

    +
    +
    +
    +
    + + + + +
    +
    + +
    +
    + +
    +
    + + + + +
    +
    + +
    +
    +
    +

    نظرات مشتریان

    +

    Customers comments

    +
    + + + +
    +
    + + + + +
    +
    + +
    +
    +
    +
    + +

    برای مشاوره و خرید در زمینه ی تجهیزات شبکه با مشاوران ما در آریا شبکه پیشرو تماس بگیرید، کارشناسان ما بهترین راهکارها را با مناسب ترین قیمت ها در اخــتیار شما قرار خواهند داد.

    +
    +
    +
    +
    .با کارشناسان ما تماس بگیرید
    + +

    (۰۲۱)۷۷۱۳۳۹۳۴

    +
    + +
    +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    + +

    مقــــالــات

    +

    ARTICLES

    +
    + + + +
    +
    + + + + +
    +
    + + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + + + + + + + + + + + + + + diff --git a/.source/asphp-home.html b/.source/asphp-home.html new file mode 100644 index 0000000..9c245ea --- /dev/null +++ b/.source/asphp-home.html @@ -0,0 +1,2943 @@ + + + + + + + + + + + + + + + شرکت آریا شبکه پیشرو | فروش تجهیزات سرور hp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + +
    آریا شبکه پیشرو
    +
    LOADING
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + + +
  • + +
    + ورود + +
    +
    +
  • + + +
    + +
    + +
    +
    +
    + + +
    + +
    + + +
    + + +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + + +
    +
    + +
    +
    + + + +
    +
    +
    +
    +
    + + + + +
    +
    + + + + + + +
    +
    + +
    +
    +
    +
    +
    +

    سرور (Server) چیست؟

    +

    در ویکی پدیا نیز معنی سرور، سرویس دهنده ذکر شده است. به بیان ساده معنی سرور یا در پاسخ سوال server به چه معناست باید گفت، کامپیوتری همیشه روشن و قدرتمند است که کاربران در زمان‌های متفاوت، از آن انتظار پاسخ به درخواست‌های خود را دارند.

    +

    متن تستی

    + + +

    درباره سرور بیشتر بدانید

    + +
    + +
    + +
    +
    + سرور چیست؟ + image +
    +
    +
    +
    +
    +
    + + + + +
    +
    + + + + + + +
    +
    + +
    +
    +
    +

    خدمات آریا شبکه پیشرو

    +

    Services

    +
    +
    + +
    +
    + + فروش تجهیزات شبکه + +
    + +

    فروش دیگر تجهیزات شبکه

    + +
    +

    باتوجه به اینکه شرکت آریا شبکه پیشرو یک شرکت مادر در زمینه تجهیزات شبکه محسوب می شود، می توانید تجهیزات مرتبط نظیر، رم، CPU و هارد و … را به صورت یکجا و با اعتماد کامل از این شرکت تهیه کنید.

    +
    +
    +
    +
    +
    + + فروش سوئیچ + +
    + +

    Cisco فروش سوئیچ‌های

    + +
    +

    آریا شبکه پیشرو به عنوان یکی از معتبرترین شرکت ها در زمینه فروش سوئیچ های سیسکو در صنف تجهیزات شبکه، شناخته شده است. مشتریان ما می توانند با اطمینان این محصول را از ما تهیه نمایند.

    +
    +
    +
    +
    +
    + + فروش سرور های hp + +
    + +

    HP فروش سرورهای

    + +
    +

    یکی از عوامل اولیه تجهیزات شبکه، خرید یک سرور مناسب است، شرکت آریا شبکه پیشرو با ارائه یکی از بهترین برندهای سرور جهان یعنی سرورهای HP خرید شما را تضمین می کند.

    +
    +
    +
    + +
    +
    +
    + + + + +
    +
    + +
    +
    +

    آریا شبکه پیشرو

    + +

    شرکتی جوان خلاق ، با انگیزه و دانش بنیان

    + +

    تماس با ما

    + +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    +

    ویژگـــی مــــا

    +

    انضباط، مشتری مداری، ارائه محصولات با کیفیت و تضمین شده می باشد.

    +
    +
    +
    +
    +
    +
    +
    + image +
    +

    پس از مشاوره قبل از خرید طبق نیاز کسب وکارتان سفارش خود را ثبت کنید و پیش فاکتور دریافت کنید.

    +
    +
    +
    + image +
    +

    قبل از هر اقدام به خرید با کارشناسان ما تماس بگیرید و از مشاوره ی آنها بهرمند شوید.

    +
    +
    +
    + image +
    +

    با واحد فروش ما تماس بگیرید و تجهیزات شبکه را با بهترین قیمت و کیفیت تهیه کنید.

    +
    +
    +
    + image +
    +

    بعد از خرید پرداخت فاکتور ثبت شده، سفارش شما را وارد مرحله آماده سازی و ارسال خواهد کرد.

    +
    +
    +
    + image +
    +

    سفارشات طبق زمان و مکان تعیین شده به مشتریان تحویل می گردد.

    +
    +
    +
    + image +
    +

    چرخه ی مشتری مداری ما همیشه برقرار است با پشتیبانی پس از خرید تجهیزات.

    +
    +
    +
    +
    +
    +
    +
    + + + + +
    +
    + +
    +
    +
    +

    طرح یک پروژه

    +

    Project a Design

    +
    +
    +
    +
    +

    + 01 + طراحی و تجهیز +

    +
    +

    برای انجام یک پروژه ی راه اندازی شبکه موفق ابتدا نیاز به طراحی یک طرح کلی و برآورد تجهیزات است. پس با تماس با یک تیم مشاوره خوب ابتدا نقشه راه تهیه کنید و سپس به مرحله اجرا بروید.

    +
    +
    +
    +

    + 02 + اجرا توسط تیم حرفه ای +

    +
    +

    یک نقشه خوب نیازمند اجرا توسط یک تیم حرفه ای و کارآمد است، نقشه ی حساب شده، تجهیزات عالی و با کیفیت را یک تیم اجرایی حرفه ای کامل می کند. پروژه های بی نظیر را افراد یک تیم حرفه ای به اجرا می گذارند.

    +
    +
    +
    +

    + 03 + پشتیبانی بعد از اجرا +

    +
    +

    یک شبکه و تجهیزات آن همیشه نیازمند یک تیم پشتیبانی منظم و دقیق است تا دوام و پایداری و امنیت اطلاعات یک کسب و کار را تضمین کند. پشتیبانی از تجهیزات یکی از مهمترین آیتم ها هنگام خرید است.

    +
    +
    +
    +
    + + + + +
    +
    + +
    +
    + +
    +
    + + + + +
    +
    + +
    +
    +
    +

    نظرات مشتریان

    +

    Customers comments

    +
    + + + +
    +
    + + + + +
    +
    + +
    +
    +
    +
    + +

    برای مشاوره و خرید در زمینه ی تجهیزات شبکه با مشاوران ما در آریا شبکه پیشرو تماس بگیرید، کارشناسان ما بهترین راهکارها را با مناسب ترین قیمت ها در اخــتیار شما قرار خواهند داد.

    +
    +
    +
    +
    .با کارشناسان ما تماس بگیرید
    + +

    (۰۲۱)۷۷۱۳۳۹۳۴

    +
    + +
    +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    + +

    مقــــالــات

    +

    ARTICLES

    +
    + + + +
    +
    + + + + +
    +
    + + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + + + + + + + + + + + + + + diff --git a/.source/meshkee-website-openapi.json b/.source/meshkee-website-openapi.json new file mode 100644 index 0000000..0d07f54 --- /dev/null +++ b/.source/meshkee-website-openapi.json @@ -0,0 +1,3812 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Meshkee Website API", + "version": "1.0.0", + "description": "Global storefront API for every Meshkee business website.\n\n**Not domain-specific.** Replace `{domain}` with the website apex (e.g. `sanihome.ir`).\n\n**Base URL:** `https://api.meshkee.com/api/v1` (or `https://api.{domain}/api/v1` if that alias is configured).\n\n**Tenant rule:** public content uses `/tenants/{domain}/...`. After login, cart/orders/favorites use `/businesses/{businessId}/...` with Bearer JWT.\n\n**Technical specs (AI agents):** Detail endpoints may return `technicalValues` with **values only** (`fieldId` + text/option ids — **no labels**). To render label→value technical details, always call the matching `.../technical-info` endpoint and join `form.fields[].id` to `values[].fieldId`. There is no public `product-category-variation-fields` route.\n\n**Docs:** https://api.meshkee.com/docs/website" + }, + "servers": [ + { + "url": "https://api.meshkee.com/api/v1", + "description": "Production (central) — use this for all websites" + }, + { + "url": "https://api.{domain}/api/v1", + "description": "Optional per-site alias (same backend). {domain} = website apex", + "variables": { + "domain": { + "default": "example.com" + } + } + } + ], + "tags": [ + { + "name": "Tenant" + }, + { + "name": "SEO" + }, + { + "name": "Homepage" + }, + { + "name": "Categories" + }, + { + "name": "Products", + "description": "Catalog products. For a specs/technical-details table on the product page, call `GET .../products/{slug}/technical-info` (or by-id). Do not rely on detail-only payloads for field labels." + }, + { + "name": "User Products", + "description": "Customer marketplace listings. Detail returns `technicalValues` without labels; use `GET .../user-products/{slug}/technical-info` for form field labels + values." + }, + { + "name": "Store" + }, + { + "name": "Torob", + "description": "Product API v3 for Torob. Called by Torob (not storefront JS). Nginx on the shop apex proxies POST /torob_api/v3/products. Only tenants with the store module enabled; otherwise 404." + }, + { + "name": "Blogs" + }, + { + "name": "Portfolios" + }, + { + "name": "Comments" + }, + { + "name": "Expert Reviews" + }, + { + "name": "Contact" + }, + { + "name": "Auth" + }, + { + "name": "Addresses" + }, + { + "name": "Cities" + }, + { + "name": "Cart" + }, + { + "name": "Orders" + }, + { + "name": "Favorites" + }, + { + "name": "Partner SMS" + }, + { + "name": "Payments", + "description": "Online e-payment gateways (website checkout)" + } + ], + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + }, + "apiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-Api-Key", + "description": "Partner SMS API key (server-to-server only). Issued per allowlisted domain." + } + }, + "parameters": { + "domain": { + "name": "domain", + "in": "path", + "required": true, + "description": "Website apex host only (e.g. sanihome.ir). No www/api/customer/business prefix.", + "schema": { + "type": "string", + "example": "example.com" + } + }, + "businessId": { + "name": "businessId", + "in": "path", + "required": true, + "description": "From GET /tenants/{domain} → id", + "schema": { + "type": "string" + } + } + } + }, + "paths": { + "/tenants/{domain}": { + "get": { + "tags": [ + "Tenant" + ], + "summary": "Resolve website domain → business", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "responses": { + "200": { + "description": "Business branding", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "nameFa": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "primaryColor": { + "type": "string", + "nullable": true + }, + "defaultLocale": { + "type": "string", + "enum": [ + "en", + "fa" + ] + }, + "logoUrl": { + "type": "string", + "nullable": true + }, + "logoDarkUrl": { + "type": "string", + "nullable": true, + "description": "Optional logo for dark backgrounds; falls back to logoUrl when omitted." + }, + "faviconUrl": { + "type": "string", + "nullable": true, + "description": "Tab icon URL. Dedicated favicon when uploaded; otherwise same as logoUrl." + }, + "specialProductsSource": { + "type": "string", + "enum": [ + "product", + "store_item" + ], + "description": "Where homepage special-product carousels read from. Defaults to store_item when the store module is enabled, otherwise product." + } + } + } + } + } + } + } + } + }, + "/tenants/{domain}/sitemap.xml": { + "get": { + "tags": [ + "SEO" + ], + "summary": "XML sitemap for search engines", + "description": "Sitemap **index** for this tenant. Points to `/sitemap-main.xml` (static pages + product categories) plus module child sitemaps when enabled: `/sitemap-products.xml`, `/sitemap-blogs.xml`, `/sitemap-portfolios.xml`, `/sitemap-videos.xml`, `/sitemap-instructions.xml`, `/sitemap-workshops.xml`, `/sitemap-user-products.xml` (when `customer_products` is enabled). Proxied from `https://{domain}/sitemap.xml`. Cached in Redis; refreshed after CMS writes and manifest sync.", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "responses": { + "200": { + "description": "Sitemap XML (urlset)", + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "Cache-Control": { + "schema": { + "type": "string", + "example": "public, max-age=900" + } + } + } + }, + "404": { + "description": "Unknown domain" + } + } + } + }, + "/tenants/{domain}/sitemap-main.xml": { + "get": { + "tags": [ + "SEO" + ], + "summary": "Main sitemap urlset (static pages + product categories)", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "responses": { + "200": { + "description": "Sitemap XML (urlset)", + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/tenants/{domain}/sitemap-products.xml": { + "get": { + "tags": [ + "SEO" + ], + "summary": "Products-only sitemap urlset", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "responses": { + "200": { + "description": "Sitemap XML (urlset) of published products", + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/tenants/{domain}/sitemap-blogs.xml": { + "get": { + "tags": [ + "SEO" + ], + "summary": "Blogs-only sitemap urlset", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "responses": { + "200": { + "description": "Sitemap XML (urlset) of published blogs", + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/tenants/{domain}/sitemap-portfolios.xml": { + "get": { + "tags": [ + "SEO" + ], + "summary": "Portfolios-only sitemap urlset", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "responses": { + "200": { + "description": "Sitemap XML (urlset) of published portfolios", + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/tenants/{domain}/sitemap-videos.xml": { + "get": { + "tags": [ + "SEO" + ], + "summary": "Videos-only sitemap urlset", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "responses": { + "200": { + "description": "Sitemap XML (urlset) of published videos", + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/tenants/{domain}/sitemap-instructions.xml": { + "get": { + "tags": [ + "SEO" + ], + "summary": "Instructions-only sitemap urlset", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "responses": { + "200": { + "description": "Sitemap XML (urlset) of published instructions", + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/tenants/{domain}/sitemap-workshops.xml": { + "get": { + "tags": [ + "SEO" + ], + "summary": "Workshops-only sitemap urlset", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "responses": { + "200": { + "description": "Sitemap XML (urlset) of published workshops", + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/tenants/{domain}/sitemap-user-products.xml": { + "get": { + "tags": [ + "SEO" + ], + "summary": "User products sitemap urlset", + "description": "Published customer marketplace listings. Default paths: `/user-products/{slug}`. Included in the index when the `customer_products` module is enabled. Override path via sitemap-config `templates.userProduct`.", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "responses": { + "200": { + "description": "Sitemap XML (urlset) of published user products", + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/tenants/{domain}/robots.txt": { + "get": { + "tags": [ + "SEO" + ], + "summary": "robots.txt with sitemap reference", + "description": "Plain-text robots file pointing crawlers to `https://{domain}/sitemap.xml`. Proxied from `https://{domain}/robots.txt` on the storefront.", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "responses": { + "200": { + "description": "robots.txt", + "content": { + "text/plain": { + "schema": { + "type": "string", + "example": "User-agent: *\\nAllow: /\\n\\nSitemap: https://example.com/sitemap.xml\\n" + } + } + }, + "headers": { + "Cache-Control": { + "schema": { + "type": "string", + "example": "public, max-age=900" + } + } + } + }, + "404": { + "description": "Unknown domain" + } + } + } + }, + "/tenants/{domain}/website/business-info": { + "get": { + "tags": [ + "Homepage" + ], + "summary": "About, contacts, addresses, social", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "responses": { + "200": { + "description": "Business public profile", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "nameFa": { "type": "string" }, + "about": { "type": "string" }, + "vision": { "type": "string" }, + "logoUrl": { "type": "string", "nullable": true }, + "logoDarkUrl": { + "type": "string", + "nullable": true, + "description": "Logo for dark backgrounds; null when not set." + }, + "faviconUrl": { + "type": "string", + "nullable": true, + "description": "Dedicated favicon when uploaded; otherwise same as logoUrl." + }, + "emails": { + "type": "array", + "items": { "type": "string" } + }, + "phoneNumbers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["landline", "cell"] }, + "number": { "type": "string" } + } + } + }, + "socialMedia": { "type": "object" }, + "addresses": { "type": "array", "items": { "type": "object" } } + } + } + } + } + } + } + } + }, + "/tenants/{domain}/website/favicon": { + "get": { + "tags": [ + "Homepage" + ], + "summary": "Favicon and logo URLs for site chrome", + "description": "Use `faviconUrl` for `` or Next.js `metadata.icons`. When no dedicated favicon is uploaded, `faviconUrl` equals `logoUrl`. Prefer this endpoint over tenant resolve when you only need branding assets.", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "responses": { + "200": { + "description": "Branding asset URLs", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "faviconUrl": { + "type": "string", + "nullable": true, + "description": "Use for browser tab icon. Falls back to logoUrl when no dedicated favicon exists." + }, + "logoUrl": { + "type": "string", + "nullable": true, + "description": "Light-theme logo." + }, + "logoDarkUrl": { + "type": "string", + "nullable": true, + "description": "Dark-theme logo; null when not set." + }, + "hasDedicatedFavicon": { + "type": "boolean", + "description": "True when the business uploaded a separate favicon (not auto-derived from logo)." + } + }, + "required": ["faviconUrl", "logoUrl", "logoDarkUrl", "hasDedicatedFavicon"] + } + } + } + }, + "404": { + "description": "Unknown domain" + } + } + } + }, + "/tenants/{domain}/analytics/views": { + "post": { + "tags": ["Analytics"], + "summary": "Record a storefront page view", + "description": "Optional explicit view counter. Most page views are recorded automatically when public list/detail APIs are called (blogs, products, portfolios, videos, store-items, homepage sliders). Use this for custom pages. No auth required. Events retained ~6 months; lifetime counters kept forever.", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["kind"], + "properties": { + "kind": { + "type": "string", + "enum": [ + "home", + "portfolio_list", + "portfolio_detail", + "product_list", + "product_detail", + "store_item_list", + "store_item_detail", + "blog_list", + "blog_detail", + "video_list", + "video_detail", + "website", + "product", + "portfolio", + "blog" + ], + "description": "Canonical kinds preferred. Legacy aliases: website→home, product→product_detail, portfolio→portfolio_detail, blog→blog_detail." + }, + "entityId": { + "type": "string", + "description": "Required for *_detail kinds (product, portfolio, blog, video, store item)." + }, + "path": { + "type": "string", + "description": "Optional page path (e.g. `/`, `/blog/my-post`)." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "View recorded", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { "type": "boolean", "enum": [true] } + } + } + } + } + }, + "404": { + "description": "Unknown domain or entity" + } + } + } + }, + "/tenants/{domain}/website/sliders": { + "get": { + "tags": [ + "Homepage" + ], + "summary": "Homepage sliders + slides", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "responses": { + "200": { + "description": "{ items: Slider[] }" + } + } + } + }, + "/tenants/{domain}/website/static-images": { + "get": { + "tags": [ + "Homepage" + ], + "summary": "Named static image slots (single or list)", + "description": "Each slot has a stable `key` used in website placeholders and a `pageKey` (`home`, `products`, `about`, …). Filter with `?pageKey=home`. `kind` is `single` (one image) or `list` (ordered images). `itemCount` is the expected number of images when fixed (e.g. 2 for a two-banner row); omit/null means an unbounded list. Every image may include optional `titleFa`, `titleEn`, `subtext`, and `linkUrl`. Empty `images` means show the site fallback.", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "pageKey", + "in": "query", + "required": false, + "schema": { + "type": "string", + "example": "home" + }, + "description": "If set, only slots for that website page are returned." + } + ], + "responses": { + "200": { + "description": "{ items: StaticImageSlot[] } — key, pageKey, kind, label, aspectRatio, recommendedWidth, itemCount, images[{ id, url, titleFa, titleEn, subtext, linkUrl, width, height, sortOrder }]" + } + } + } + }, + "/tenants/{domain}/website/static-images/{key}": { + "get": { + "tags": [ + "Homepage" + ], + "summary": "One static image slot by key", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "example": "home-hero" + } + } + ], + "responses": { + "200": { + "description": "StaticImageSlot" + }, + "404": { + "description": "Unknown slot key for this domain" + } + } + } + }, + "/tenants/{domain}/website/category-groups": { + "get": { + "tags": [ + "Homepage" + ], + "summary": "Homepage category groups", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "responses": { + "200": { + "description": "{ items: CategoryGroup[] } — each group has id, key, title, items[]" + } + } + } + }, + "/tenants/{domain}/website/brand-groups": { + "get": { + "tags": [ + "Homepage" + ], + "summary": "Homepage brand groups", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "responses": { + "200": { + "description": "{ items: BrandGroup[] } — each group has id, key, title, items[]" + } + } + } + }, + "/tenants/{domain}/store-specials": { + "get": { + "tags": [ + "Homepage", + "Store" + ], + "summary": "Active store specials", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "responses": { + "200": { + "description": "{ source: product|store_item, items: StoreSpecial[] } — source is the business special-products setting; each special has id, key, title, items[]" + } + } + } + }, + "/tenants/{domain}/categories": { + "get": { + "tags": [ + "Categories" + ], + "summary": "Public categories", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "entityType", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "product", + "blog", + "portfolio", + "video" + ], + "default": "product" + } + } + ], + "responses": { + "200": { + "description": "{ items: Category[] }" + } + } + } + }, + "/tenants/{domain}/categories/by-id/{categoryId}": { + "get": { + "tags": [ + "Categories" + ], + "summary": "Category by id (preferred for /products/category/{id}/{nameFaSlug} pages)", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "categoryId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "entityType", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "product", + "blog", + "portfolio", + "video" + ], + "default": "product" + } + } + ], + "responses": { + "200": { + "description": "{ category } — use category.id as categoryId when listing products" + }, + "404": { + "description": "Category not found" + } + } + } + }, + "/tenants/{domain}/categories/by-slug/{slug}": { + "get": { + "tags": [ + "Categories" + ], + "summary": "Category by CMS slug (legacy / lookup helper)", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "entityType", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "product", + "blog", + "portfolio", + "video" + ], + "default": "product" + } + } + ], + "responses": { + "200": { + "description": "{ category } — use category.id as categoryId when listing products" + }, + "404": { + "description": "Category not found" + } + } + } + }, + "/tenants/{domain}/products": { + "get": { + "tags": [ + "Products" + ], + "summary": "List published products", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer", + "default": 12 + } + }, + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "categoryId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "brandId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "inStore", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "{ items, total, page, pageSize }" + } + } + } + }, + "/tenants/{domain}/products/by-id/{productId}": { + "get": { + "tags": [ + "Products" + ], + "summary": "Product by id (preferred for /products/{id}/{nameFaSlug} pages)", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "productId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ product, relatedProducts } — same shape as product-by-slug" + } + } + } + }, + "/tenants/{domain}/products/by-id/{productId}/variations": { + "get": { + "tags": [ + "Products" + ], + "summary": "Product variations by id", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "productId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Variation tree for the product" + } + } + } + }, + "/tenants/{domain}/products/by-id/{productId}/technical-info": { + "get": { + "tags": [ + "Products" + ], + "summary": "Product technical info by id", + "description": "Returns category technical form schema (field labels) plus this product’s submitted values. **Use this for the product specs / technical-details UI** — join `form.fields[].id` ↔ `values[].fieldId`. Product detail alone does not include field labels.", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "productId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ form: { id, categoryId, fields: [{ id, label, key, type, isRequired, sortOrder, options: [{ id, label, ... }] }] } | null, values: [{ fieldId, textValue?, optionId?, optionIds? }] }. Render label→value; hide section if form/fields/values empty." + } + } + } + }, + "/tenants/{domain}/products/{slug}": { + "get": { + "tags": [ + "Products" + ], + "summary": "Product by slug", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ product, relatedProducts } — relatedProducts uses the product list-card shape (incl. store). Ranked by relativity (same category, then same brand) with availability beside it (in-stock first within each group). Up to 8 items; excludes the current product." + } + } + } + }, + "/tenants/{domain}/products/{slug}/variations": { + "get": { + "tags": [ + "Products" + ], + "summary": "Product variation options", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ variations }" + } + } + } + }, + "/tenants/{domain}/products/{slug}/technical-info": { + "get": { + "tags": [ + "Products" + ], + "summary": "Product technical specs (labels + values)", + "description": "Returns category technical form schema (field labels) plus this product’s submitted values. **Required for specs UI.** Join `form.fields[].id` ↔ `values[].fieldId`. Do not invent a separate category-fields public endpoint.", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ form: { id, categoryId, fields: [{ id, label, key, type, isRequired, sortOrder, options }] } | null, values: [{ fieldId, textValue?, optionId?, optionIds? }] }" + } + } + } + }, + "/tenants/{domain}/user-products": { + "get": { + "tags": [ + "User Products" + ], + "summary": "List published customer / stock listings", + "description": "Public marketplace-style listings created by customers (user products). Only `status=published`. Search with `name` or `q` (title/description). Filter by category, city, country, condition, or promoted.", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer", + "default": 12 + } + }, + { + "name": "name", + "in": "query", + "description": "Search title/description (alias of q)", + "schema": { + "type": "string" + } + }, + { + "name": "q", + "in": "query", + "description": "Search title/description (alias of name)", + "schema": { + "type": "string" + } + }, + { + "name": "categoryId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "cityId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "countryId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "condition", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "new", + "stock", + "needs_repair", + "scrap" + ] + } + }, + { + "name": "promoted", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "{ items: UserProductListItem[], total, page, pageSize }. Each item includes id, slug, titleFa/titleEn, price, priceCurrency, condition, city/country names, imageUrl, category*, promoted, publishedAt." + } + } + } + }, + "/tenants/{domain}/user-products/{slug}": { + "get": { + "tags": [ + "User Products" + ], + "summary": "User product details by slug", + "description": "Full published listing: location IDs, gallery images (`images`, `galleryMediaIds`), delivery/technical notes, and `technicalValues`.\n\n**Important:** `technicalValues` items are **values only** (`fieldId` + `textValue` / `optionId` / `optionIds`). They do **not** include field labels (`label`, `fieldName`, etc.). For a label→value technical-details table, call `GET /tenants/{domain}/user-products/{slug}/technical-info` and join on `fieldId`.", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ product } with gallery (`images`: [{ mediaId, url }]), featuredMediaId, technicalValues (values only — no labels), countryId, cityId, countrySlug" + }, + "404": { + "description": "Not found or not published" + } + } + } + }, + "/tenants/{domain}/user-products/{slug}/technical-info": { + "get": { + "tags": [ + "User Products" + ], + "summary": "User product technical form + values (labels)", + "description": "**Use this for the listing specs / technical-details UI.** Returns the category technical form (field `id`, `label`, `key`, `type`, `sortOrder`, `options`) plus the listing’s submitted `values`.\n\nJoin `form.fields[].id` ↔ `values[].fieldId` and render label→value. Sort by `sortOrder`. Hide the section if `form`/fields/`values` are empty.\n\nThere is **no** public `product-category-variation-fields` (or similar) endpoint — use this path only.", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ form: { id, categoryId, fields: [{ id, label, key, type, isRequired, sortOrder, options: [{ id, label, ... }] }] } | null, values: [{ fieldId, textValue?, optionId?, optionIds? }] }" + } + } + } + }, + "/tenants/{domain}/store-items": { + "get": { + "tags": [ + "Store" + ], + "summary": "List sellable variants", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "categoryId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "brandId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "productId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "name", + "in": "query", + "description": "Instant search on product title / nameFa. Results are ordered in-stock first (unlimited or qty > 0), then by updatedAt desc.", + "schema": { + "type": "string" + } + }, + { + "name": "inStock", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isFestival", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "minPrice", + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "maxPrice", + "in": "query", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "{ items, total, page, pageSize }" + } + } + } + }, + "/tenants/{domain}/store-items/by-product/{productId}": { + "get": { + "tags": [ + "Store" + ], + "summary": "Variants for one product", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "productId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ storeItem }" + } + } + } + }, + "/tenants/{domain}/store-items/{variantId}": { + "get": { + "tags": [ + "Store" + ], + "summary": "One variant", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "variantId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ variant }" + } + } + } + }, + "/tenants/{domain}/torob_api/v3/products": { + "post": { + "tags": [ + "Torob" + ], + "summary": "Torob Product API v3 (store module only)", + "description": "Torob POSTs here (or to `https://{domain}/torob_api/v3/products` which nginx proxies). JWT in `X-Torob-Token` (EdDSA, aud = shop host). Returns 404 when the tenant does not have the **store** module. Page size is 100.", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "X-Torob-Token", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-Torob-Token-Version", + "in": "header", + "schema": { + "type": "string", + "example": "1" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "required": [ + "page", + "sort" + ], + "properties": { + "page": { + "type": "integer", + "minimum": 1 + }, + "sort": { + "type": "string", + "enum": [ + "date_added_desc", + "date_updated_desc" + ] + } + } + }, + { + "type": "object", + "required": [ + "page_urls" + ], + "properties": { + "page_urls": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + }, + { + "type": "object", + "required": [ + "page_uniques" + ], + "properties": { + "page_uniques": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "{ api_version: torob_api_v3, current_page, total, max_pages, products[] }" + }, + "400": { + "description": "{ error: string }" + }, + "401": { + "description": "Invalid or missing Torob JWT" + }, + "404": { + "description": "Unknown domain, store module off, or Torob switch off in store settings" + } + } + } + }, + "/tenants/{domain}/blogs": { + "get": { + "tags": [ + "Blogs" + ], + "summary": "List published blogs", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer", + "default": 12 + } + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "news", + "article", + "blog" + ] + } + }, + { + "name": "categoryId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "title", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "Filter by exact tag in metadata.tags", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ items, total, page, pageSize }" + } + } + } + }, + "/tenants/{domain}/blogs/by-id/{blogId}": { + "get": { + "tags": [ + "Blogs" + ], + "summary": "Blog by id (preferred for /blog/{id}/{titleSlug} pages)", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "blogId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ blog }" + } + } + } + }, + "/tenants/{domain}/blogs/{slug}": { + "get": { + "tags": [ + "Blogs" + ], + "summary": "Blog by slug", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ blog }" + } + } + } + }, + "/tenants/{domain}/blogs/{blogId}/comments": { + "get": { + "tags": [ + "Blogs", + "Comments" + ], + "summary": "Approved blog comments", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "blogId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ items }" + } + } + }, + "post": { + "tags": [ + "Blogs", + "Comments" + ], + "summary": "Submit blog comment", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "blogId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "authorName", + "text" + ], + "properties": { + "authorName": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "text": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "{ comment, message }" + } + } + } + }, + "/tenants/{domain}/videos": { + "get": { + "tags": [ + "Videos" + ], + "summary": "List published videos", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer", + "default": 12 + } + }, + { + "name": "provider", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "youtube", + "aparat" + ] + } + }, + { + "name": "categoryId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "title", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "Filter by exact tag in metadata.tags", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ items, total, page, pageSize }" + } + } + } + }, + "/tenants/{domain}/videos/{slug}": { + "get": { + "tags": [ + "Videos" + ], + "summary": "Video by slug", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ video }" + } + } + } + }, + "/tenants/{domain}/videos/{videoId}/comments": { + "get": { + "tags": [ + "Videos", + "Comments" + ], + "summary": "Approved video comments", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "videoId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ items }" + } + } + }, + "post": { + "tags": [ + "Videos", + "Comments" + ], + "summary": "Submit video comment", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "videoId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "authorName", + "text" + ], + "properties": { + "authorName": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "text": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "{ comment, message }" + } + } + } + }, + "/tenants/{domain}/portfolios": { + "get": { + "tags": [ + "Portfolios" + ], + "summary": "List published portfolios", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer", + "default": 12 + } + }, + { + "name": "categoryId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "title", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "Filter by exact tag in metadata.tags", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ items, total, page, pageSize }. Each item includes id, title/titleFa/titleEn, slug, abstract, projectUrl (nullable website link), status, categoryId/categoryName, tags, titleImageUrl, gallery, sortOrder, publishedAt, createdAt, updatedAt. Ordered by sortOrder desc, then publishedAt/createdAt desc." + } + } + } + }, + "/tenants/{domain}/portfolios/by-id/{portfolioId}": { + "get": { + "tags": [ + "Portfolios" + ], + "summary": "Portfolio by id (preferred for /portfolios/{id}/{titleFaSlug} pages)", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "portfolioId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ portfolio } — same fields as list items, plus mainTextHtml, comments, and commentCount. Includes nullable projectUrl (external website link)." + } + } + } + }, + "/tenants/{domain}/portfolios/{slug}": { + "get": { + "tags": [ + "Portfolios" + ], + "summary": "Portfolio by slug", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ portfolio } — same fields as list items, plus mainTextHtml, comments, and commentCount. Includes nullable projectUrl (external website link)." + } + } + } + }, + "/tenants/{domain}/portfolios/{portfolioId}/comments": { + "get": { + "tags": [ + "Portfolios", + "Comments" + ], + "summary": "Approved portfolio comments", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "portfolioId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ items }" + } + } + }, + "post": { + "tags": [ + "Portfolios", + "Comments" + ], + "summary": "Submit portfolio comment", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "portfolioId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "authorName", + "text" + ], + "properties": { + "authorName": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "text": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "{ comment, message }" + } + } + } + }, + "/tenants/{domain}/comments": { + "get": { + "tags": [ + "Comments" + ], + "summary": "List approved comments for any entity", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "entityType", + "in": "query", + "required": true, + "schema": { + "type": "string", + "enum": [ + "product", + "blog", + "portfolio", + "video" + ] + } + }, + { + "name": "entityId", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ items }" + } + } + }, + "post": { + "tags": [ + "Comments" + ], + "summary": "Submit comment (product/blog/portfolio)", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "entityType", + "entityId", + "authorName", + "text" + ], + "properties": { + "entityType": { + "type": "string", + "enum": [ + "product", + "blog", + "portfolio", + "video" + ] + }, + "entityId": { + "type": "string" + }, + "authorName": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "text": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "{ comment, message }" + } + } + } + }, + "/tenants/{domain}/expert-reviews": { + "get": { + "tags": [ + "Expert Reviews" + ], + "summary": "Approved expert reviews for a product", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "productId", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ items }" + } + } + }, + "post": { + "tags": [ + "Expert Reviews" + ], + "summary": "Submit expert review", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "productId", + "authorName", + "rate", + "positivePoints", + "negativePoints", + "text" + ], + "properties": { + "productId": { + "type": "string" + }, + "authorName": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "rate": { + "type": "integer", + "minimum": 1, + "maximum": 10 + }, + "positivePoints": { + "type": "array", + "items": { + "type": "string" + } + }, + "negativePoints": { + "type": "array", + "items": { + "type": "string" + } + }, + "text": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "{ review, message }" + } + } + } + }, + "/tenants/{domain}/contact-submissions": { + "post": { + "tags": [ + "Contact" + ], + "summary": "Contact form", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "title", + "name", + "text" + ], + "properties": { + "title": { + "type": "string" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "cellNumber": { + "type": "string" + }, + "text": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "{ submission, message }" + } + } + } + }, + "/auth/register": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Register customer on a website", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "cellNumber", + "password", + "firstName", + "lastName", + "domain" + ], + "properties": { + "cellNumber": { + "type": "string", + "description": "E.164 e.g. +98912..." + }, + "password": { + "type": "string", + "minLength": 8 + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "domain": { + "type": "string", + "description": "Same website apex as {domain}" + }, + "acknowledgeExistingAccount": { + "type": "boolean", + "description": "If true, link an existing Meshkee account from another website without matching its password. Profile stays unchanged; password is replaced with the new signup password. Response may include passwordUpdated: true." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "{ user, accessToken, refreshToken, registeredBusiness }" + } + } + } + }, + "/auth/login": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Login", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "cellNumber", + "password" + ], + "properties": { + "cellNumber": { + "type": "string" + }, + "password": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "{ user, accessToken, refreshToken }" + } + } + } + }, + "/auth/login-otp": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Passwordless login with SMS OTP", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "cellNumber", + "code" + ], + "properties": { + "cellNumber": { + "type": "string" + }, + "code": { + "type": "string", + "minLength": 6, + "maxLength": 6 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "{ user, accessToken, refreshToken }" + } + } + } + }, + "/auth/reset-password": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Reset password with SMS OTP", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "cellNumber", + "code", + "newPassword" + ], + "properties": { + "cellNumber": { + "type": "string" + }, + "code": { + "type": "string", + "minLength": 6, + "maxLength": 6 + }, + "newPassword": { + "type": "string", + "minLength": 8 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "{ message }" + } + } + } + }, + "/auth/refresh": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Refresh tokens", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "refreshToken" + ], + "properties": { + "refreshToken": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "{ user, accessToken, refreshToken }" + } + } + } + }, + "/auth/me": { + "get": { + "tags": [ + "Auth" + ], + "summary": "Current user", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "{ user }" + } + } + } + }, + "/auth/profile": { + "patch": { + "tags": [ + "Auth" + ], + "summary": "Update profile", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "{ message, user }" + } + } + } + }, + "/auth/change-password": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Change password", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "currentPassword", + "newPassword" + ], + "properties": { + "currentPassword": { + "type": "string" + }, + "newPassword": { + "type": "string", + "minLength": 8 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "{ message }" + } + } + } + }, + "/auth/send-otp": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Send OTP SMS", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "cellNumber" + ], + "properties": { + "cellNumber": { + "type": "string" + }, + "domain": { + "type": "string", + "description": "Tenant host/apex used to brand the OTP SMS with the business Farsi name" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "{ enabled, message, expiresInSeconds? }" + } + } + } + }, + "/auth/verify-otp": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Verify OTP", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "cellNumber", + "code" + ], + "properties": { + "cellNumber": { + "type": "string" + }, + "code": { + "type": "string", + "minLength": 6, + "maxLength": 6 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "{ enabled, verified, message }" + } + } + } + }, + "/auth/addresses": { + "get": { + "tags": [ + "Addresses" + ], + "summary": "List my shipping addresses", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "{ items }" + } + } + }, + "post": { + "tags": [ + "Addresses" + ], + "summary": "Create address", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "province", + "city", + "address" + ], + "properties": { + "label": { + "type": "string" + }, + "province": { + "type": "string" + }, + "city": { + "type": "string" + }, + "address": { + "type": "string" + }, + "postalCode": { + "type": "string" + }, + "landline": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "{ address }" + } + } + } + }, + "/auth/addresses/{addressId}": { + "patch": { + "tags": [ + "Addresses" + ], + "summary": "Update address", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "addressId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ address }" + } + } + }, + "delete": { + "tags": [ + "Addresses" + ], + "summary": "Delete address", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "addressId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ message }" + } + } + } + }, + "/cities": { + "get": { + "tags": [ + "Cities" + ], + "summary": "Location tree (countries / provinces / cities)", + "parameters": [ + { + "name": "level", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "country", + "province", + "city" + ] + } + }, + { + "name": "parentId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "parentSlug", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ items }" + } + } + } + }, + "/cities/{cityId}": { + "get": { + "tags": [ + "Cities" + ], + "summary": "Get one location node", + "parameters": [ + { + "name": "cityId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ city }" + } + } + } + }, + "/businesses/{businessId}/cart": { + "get": { + "tags": [ + "Cart" + ], + "summary": "Get cart", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/businessId" + } + ], + "responses": { + "200": { + "description": "{ cart }" + } + } + }, + "delete": { + "tags": [ + "Cart" + ], + "summary": "Clear cart", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/businessId" + } + ], + "responses": { + "200": { + "description": "{ message, cart }" + } + } + } + }, + "/businesses/{businessId}/cart/items": { + "post": { + "tags": [ + "Cart" + ], + "summary": "Add variant to cart", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/businessId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "storeItemVariantId" + ], + "properties": { + "storeItemVariantId": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1, + "default": 1 + } + } + } + } + } + }, + "responses": { + "201": { + "description": "{ message, cart }" + } + } + } + }, + "/businesses/{businessId}/cart/items/{itemId}": { + "patch": { + "tags": [ + "Cart" + ], + "summary": "Update cart line quantity", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/businessId" + }, + { + "name": "itemId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "quantity" + ], + "properties": { + "quantity": { + "type": "integer", + "minimum": 1 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "{ message, cart }" + } + } + }, + "delete": { + "tags": [ + "Cart" + ], + "summary": "Remove cart line", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/businessId" + }, + { + "name": "itemId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ message, cart }" + } + } + } + }, + "/businesses/{businessId}/cart/checkout": { + "post": { + "tags": [ + "Cart" + ], + "summary": "Checkout → create order", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/businessId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "payment" + ], + "properties": { + "addressId": { + "type": "string" + }, + "shippingAddress": { + "type": "object", + "properties": { + "province": { + "type": "string" + }, + "city": { + "type": "string" + }, + "address": { + "type": "string" + }, + "postalCode": { + "type": "string" + }, + "landline": { + "type": "string" + } + } + }, + "customerNotes": { + "type": "string" + }, + "payment": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "pos", + "cash", + "transfer", + "e_payment_gate" + ] + }, + "posType": { + "type": "string" + }, + "transferAccount": { + "type": "string" + }, + "transferRefNumber": { + "type": "string" + }, + "gatewayType": { + "type": "string", + "enum": [ + "mellat", + "sep", + "snappay", + "digipay", + "zarinpal" + ], + "description": "Required when type is e_payment_gate (defaults to store defaultGateway if omitted)." + }, + "notes": { + "type": "string" + } + } + }, + "returnUrl": { + "type": "string", + "format": "uri", + "description": "Required for e_payment_gate. Absolute URL the bank callback redirects the shopper to (status, orderId, transactionId query params appended)." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Cash/transfer/pos: { message, order }. E-payment: { message, order, payment: { gatewayType, transactionId, redirect: { method, url, fields } } }" + } + } + } + }, + "/businesses/{businessId}/orders": { + "get": { + "tags": [ + "Orders" + ], + "summary": "My orders", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/businessId" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "status", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "pending", + "confirmed", + "processing", + "shipped", + "delivered", + "cancelled" + ] + } + } + ], + "responses": { + "200": { + "description": "{ items, total, page, pageSize }" + } + } + } + }, + "/businesses/{businessId}/orders/{orderId}": { + "get": { + "tags": [ + "Orders" + ], + "summary": "My order detail", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/businessId" + }, + { + "name": "orderId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ order }" + } + } + } + }, + "/businesses/{businessId}/favorites": { + "get": { + "tags": [ + "Favorites" + ], + "summary": "List favorites", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/businessId" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "{ items, total, page, pageSize }" + } + } + }, + "post": { + "tags": [ + "Favorites" + ], + "summary": "Add favorite", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/businessId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "productId" + ], + "properties": { + "productId": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "{ favorite, message }" + } + } + } + }, + "/businesses/{businessId}/favorites/{productId}": { + "delete": { + "tags": [ + "Favorites" + ], + "summary": "Remove favorite", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/businessId" + }, + { + "name": "productId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ message }" + } + } + } + }, + "/public/sms/send": { + "post": { + "tags": [ + "Partner SMS" + ], + "summary": "Send SMS via Meshkee (partner gateway)", + "description": "Server-to-server only. For external/partner backends (e.g. Balout) that need to send SMS through Meshkee → Gama. Not for browser/storefront JS. Requires an allowlisted `domain` + matching `X-Api-Key`. See /docs/website/SMS.md.", + "security": [ + { + "apiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "domain", + "to", + "message" + ], + "properties": { + "domain": { + "type": "string", + "example": "baloutpastry.com", + "description": "Allowlisted partner apex (www. is stripped)" + }, + "to": { + "type": "string", + "example": "09127004945", + "description": "Iranian mobile: 09…, 9…, +989…, or 989…" + }, + "message": { + "type": "string", + "maxLength": 700, + "example": "سفارش شما به شماره ی ۱۲۱۱۳۲۲ اماده می باشد." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Accepted by Gama", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "serverId": { + "type": "string", + "example": "1136923081051406337" + } + } + } + } + } + }, + "400": { + "description": "Invalid phone or message" + }, + "401": { + "description": "Missing/invalid X-Api-Key or domain" + }, + "429": { + "description": "Rate limited (30/partner/min or 5/destination/min)" + }, + "503": { + "description": "SMS disabled or provider unreachable" + } + } + } + }, + "/tenants/{domain}/instructions": { + "get": { + "tags": [ + "Instructions" + ], + "summary": "List published instructions", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer", + "default": 12 + } + }, + { + "name": "provider", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "youtube", + "aparat" + ] + } + }, + { + "name": "categoryId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "title", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ items, total, page, pageSize }" + } + } + } + }, + "/tenants/{domain}/instructions/{slug}": { + "get": { + "tags": [ + "Instructions" + ], + "summary": "Instruction by slug", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ instruction }" + } + } + } + }, + "/tenants/{domain}/instructions/{instructionId}/comments": { + "get": { + "tags": [ + "Instructions", + "Comments" + ], + "summary": "Approved instruction comments", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "instructionId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "{ items }" + } + } + }, + "post": { + "tags": [ + "Instructions", + "Comments" + ], + "summary": "Submit instruction comment", + "parameters": [ + { + "$ref": "#/components/parameters/domain" + }, + { + "name": "instructionId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "authorName", + "text" + ], + "properties": { + "authorName": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "text": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "{ comment, message }" + } + } + } + }, + "/tenants/{domain}/workshops": { + "get": { + "tags": ["Workshops"], + "summary": "List published workshops", + "parameters": [ + { "$ref": "#/components/parameters/domain" }, + { "name": "page", "in": "query", "schema": { "type": "integer" } }, + { "name": "pageSize", "in": "query", "schema": { "type": "integer", "default": 12 } }, + { "name": "categoryId", "in": "query", "schema": { "type": "string" } }, + { "name": "title", "in": "query", "schema": { "type": "string" } } + ], + "responses": { + "200": { "description": "{ items, total, page, pageSize }" } + } + } + }, + "/tenants/{domain}/workshops/{slug}": { + "get": { + "tags": ["Workshops"], + "summary": "Workshop by slug", + "parameters": [ + { "$ref": "#/components/parameters/domain" }, + { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } } + ], + "responses": { + "200": { "description": "{ workshop } — includes eventDate, eventTime, duration" } + } + } + }, + "/tenants/{domain}/workshops/{workshopId}/register": { + "post": { + "tags": ["Workshops"], + "summary": "Register logged-in website customer for a workshop", + "security": [{ "bearerAuth": [] }], + "parameters": [ + { "$ref": "#/components/parameters/domain" }, + { "name": "workshopId", "in": "path", "required": true, "schema": { "type": "string" } } + ], + "responses": { + "201": { "description": "{ registration: { id, status: pending|approved|rejected, ... }, message }" }, + "409": { "description": "Already registered" } + } + }, + "delete": { + "tags": ["Workshops"], + "summary": "Cancel workshop registration", + "security": [{ "bearerAuth": [] }], + "parameters": [ + { "$ref": "#/components/parameters/domain" }, + { "name": "workshopId", "in": "path", "required": true, "schema": { "type": "string" } } + ], + "responses": { + "200": { "description": "{ message }" } + } + } + }, + "/businesses/{businessId}/payments/methods": { + "get": { + "tags": [ + "Payments" + ], + "summary": "List enabled online payment gateways (public)", + "parameters": [ + { + "$ref": "#/components/parameters/businessId" + } + ], + "responses": { + "200": { + "description": "{ enabled, defaultGateway, gateways: [{ id, label, labelFa }] } — no secrets" + } + } + } + }, + "/businesses/{businessId}/payments/{gateway}/callback": { + "post": { + "tags": [ + "Payments" + ], + "summary": "Bank payment callback — legacy direct API URL (prefer store-domain path)", + "description": "Still supported. Prefer https://{store}/meshkee/payments/{gateway}/callback so gateway domain checks match the merchant terminal.", + "parameters": [ + { + "$ref": "#/components/parameters/businessId" + }, + { + "name": "gateway", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "mellat", + "sep", + "snappay", + "digipay", + "zarinpal" + ] + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Gateway-specific fields (Mellat: ResCode, SaleOrderId, SaleReferenceId, RefId, …)" + } + } + } + }, + "responses": { + "303": { + "description": "Redirect to checkout returnUrl with status=success|failed" + } + } + }, + "get": { + "tags": [ + "Payments" + ], + "summary": "Bank payment callback GET — legacy direct API URL", + "parameters": [ + { + "$ref": "#/components/parameters/businessId" + }, + { + "name": "gateway", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "mellat", + "sep", + "snappay", + "digipay", + "zarinpal" + ] + } + } + ], + "responses": { + "303": { + "description": "Redirect to checkout returnUrl with status=success|failed" + } + } + } + }, + "/tenants/{host}/payments/{gateway}/callback": { + "post": { + "tags": [ + "Payments" + ], + "summary": "Bank payment callback via store host (nginx proxies /meshkee/payments/... here)", + "description": "Public. Resolves business from host. Used after storefront nginx proxies https://{store}/meshkee/payments/{gateway}/callback.", + "parameters": [ + { + "name": "host", + "in": "path", + "required": true, + "schema": { + "type": "string", + "example": "YOUR_WEBSITE_DOMAIN" + } + }, + { + "name": "gateway", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "mellat", + "sep", + "snappay", + "digipay", + "zarinpal" + ] + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "responses": { + "303": { + "description": "Redirect to checkout returnUrl with status=success|failed" + } + } + }, + "get": { + "tags": [ + "Payments" + ], + "summary": "Bank payment callback GET via store host (ZarinPal)", + "parameters": [ + { + "name": "host", + "in": "path", + "required": true, + "schema": { + "type": "string", + "example": "YOUR_WEBSITE_DOMAIN" + } + }, + { + "name": "gateway", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "mellat", + "sep", + "snappay", + "digipay", + "zarinpal" + ] + } + } + ], + "responses": { + "303": { + "description": "Redirect to checkout returnUrl with status=success|failed" + } + } + } + } + } +} diff --git a/app/ContactForm.js b/app/ContactForm.js new file mode 100644 index 0000000..261e70e --- /dev/null +++ b/app/ContactForm.js @@ -0,0 +1,61 @@ +"use client"; + +import { useState } from "react"; + +export default function ContactForm() { + const [status, setStatus] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + async function handleSubmit(event) { + event.preventDefault(); + setStatus(""); + setIsSubmitting(true); + + const form = event.currentTarget; + const formData = new FormData(form); + const payload = { + name: formData.get("name"), + cellNumber: formData.get("cellNumber"), + title: formData.get("title"), + text: formData.get("text") + }; + + try { + const response = await fetch("/api/contact", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }); + if (!response.ok) throw new Error("submit failed"); + form.reset(); + setStatus("پیام شما با موفقیت ثبت شد. کارشناسان ما با شما تماس می گیرند."); + } catch { + setStatus("ارسال پیام با مشکل روبرو شد. لطفا دوباره تلاش کنید یا با شماره های شرکت تماس بگیرید."); + } finally { + setIsSubmitting(false); + } + } + + return ( +
    + + + +

    موقعیت شرکت

    عرض جغرافیایی: ${contact.latitude} | طول جغرافیایی: ${contact.longitude}

    `); +} + +function fallbackStoreItems() { + return products.map((p, index) => ({ ...p, id: `local-${index + 1}`, description: "برای قیمت، موجودی و کانفیگ اختصاصی این محصول با کارشناسان فروش تماس بگیرید." })); +} + +function storePage() { + const cards = fallbackStoreItems().map((p) => `
    ${p.title}
    ${p.category}

    ${p.title}

    تماس با فروشگاه
    `).join(""); + return layout("فروشگاه تجهیزات سرور", `
    STORE ITEMS

    فروشگاه تجهیزات سرور

    این صفحه به API Store Items متصل شده و در پیش نمایش از محصولات نمونه استفاده می کند.

    فعلا آیتمی از API فروشگاه برنگشت؛ نمایش محصولات نمونه فعال است.

    ${cards}
    `); +} + +function storeDetail(id) { + const item = fallbackStoreItems().find((p) => p.id === id) || fallbackStoreItems()[0]; + const related = fallbackStoreItems().filter((p) => p.id !== item.id).map((p) => `
    ${p.title}
    ${p.category}

    ${p.title}

    تماس با فروشگاه
    `).join(""); + return layout(item.title, `
    ${item.category}

    ${item.title}

    ${item.description}

    تماس با فروشگاه
    توضیحات

    ${item.description}

    اطلاعات فنی

    دسته بندی${item.category}

    وضعیت قیمتتماس با فروشگاه

    نظرات
    ${sectionTitle("محصولات مشابه", "RELATED")}
    ${related}
    `); +} + +function blogPage() { + const cards = articles.map((a, index) => `
    ${a.title}

    ${a.title}

    ${a.abstract}

    `).join(""); + return layout("مقالات آریا شبکه پیشرو", `
    BLOG

    مقالات آریا شبکه پیشرو

    این صفحه به API Blog متصل شده و در پیش نمایش از مقالات نمونه استفاده می کند.

    فعلا مقاله ای از API برنگشت؛ نمایش مقالات نمونه فعال است.

    ${cards}
    `); +} + +function blogDetail(slug) { + const index = Math.max(0, Number(slug.replace("local-blog-", "")) - 1); + const article = articles[index] || articles[0]; + return layout(article.title, `
    ${article.title}

    ${article.title}

    ${article.abstract}

    ${article.abstract}

    `); +} + +createServer((req, res) => { + const url = new URL(req.url || "/", `http://${hostname}:${port}`); + + if (url.pathname.startsWith("/assets/")) { + const relative = normalize(url.pathname.replace(/^\/assets\//, "")); + const file = join(root, "public/assets", relative); + res.setHeader("Content-Type", mime[extname(file)] || "application/octet-stream"); + createReadStream(file).on("error", () => { + res.writeHead(404); + res.end("Not found"); + }).pipe(res); + return; + } + + if (url.pathname === "/meshkee/sitemap-config.json") { + res.setHeader("Content-Type", "application/json; charset=utf-8"); + res.end(JSON.stringify(sitemapConfig(), null, 2)); + } else if (url.pathname === "/sitemap.xml" || url.pathname === "/robots.txt") { + res.writeHead(404); + res.end("Not served by this preview. Production nginx proxies this to Meshkee."); + } else { + res.setHeader("Content-Type", "text/html; charset=utf-8"); + if (url.pathname === "/" || url.pathname === "/index.html") res.end(home()); + else if (url.pathname === "/about-us" || url.pathname === "/aboutus") res.end(about()); + else if (url.pathname === "/contact-us" || url.pathname === "/contactus") res.end(contactPage()); + else if (url.pathname === "/store") res.end(storePage()); + else if (url.pathname.startsWith("/store/")) res.end(storeDetail(url.pathname.split("/").pop())); + else if (url.pathname === "/blog") res.end(blogPage()); + else if (url.pathname.startsWith("/blog/")) res.end(blogDetail(url.pathname.split("/").pop())); + else { + res.writeHead(404); + res.end(layout("یافت نشد", `

    404

    صفحه مورد نظر پیدا نشد.

    `)); + } + } +}).listen(port, hostname, () => { + console.log(`ASPHP preview ready: http://${hostname}:${port}`); +}); diff --git a/public/assets/about-office.jpg b/public/assets/about-office.jpg new file mode 100644 index 0000000..c50ed03 Binary files /dev/null and b/public/assets/about-office.jpg differ diff --git a/public/assets/banner-1.jpg b/public/assets/banner-1.jpg new file mode 100644 index 0000000..8a8854f Binary files /dev/null and b/public/assets/banner-1.jpg differ diff --git a/public/assets/banner-2.jpg b/public/assets/banner-2.jpg new file mode 100644 index 0000000..85bd1e7 Binary files /dev/null and b/public/assets/banner-2.jpg differ diff --git a/public/assets/banner-3.jpg b/public/assets/banner-3.jpg new file mode 100644 index 0000000..c87d0e8 Binary files /dev/null and b/public/assets/banner-3.jpg differ diff --git a/public/assets/banner-4.jpg b/public/assets/banner-4.jpg new file mode 100644 index 0000000..ff9242d Binary files /dev/null and b/public/assets/banner-4.jpg differ diff --git a/public/assets/bg-footer.jpg b/public/assets/bg-footer.jpg new file mode 100644 index 0000000..b04b965 Binary files /dev/null and b/public/assets/bg-footer.jpg differ diff --git a/public/assets/favicon.png b/public/assets/favicon.png new file mode 100644 index 0000000..5e6d1d4 Binary files /dev/null and b/public/assets/favicon.png differ diff --git a/public/assets/feature-call.png b/public/assets/feature-call.png new file mode 100644 index 0000000..e30d41e Binary files /dev/null and b/public/assets/feature-call.png differ diff --git a/public/assets/feature-consult.png b/public/assets/feature-consult.png new file mode 100644 index 0000000..78d49a8 Binary files /dev/null and b/public/assets/feature-consult.png differ diff --git a/public/assets/feature-delivery.png b/public/assets/feature-delivery.png new file mode 100644 index 0000000..57124f9 Binary files /dev/null and b/public/assets/feature-delivery.png differ diff --git a/public/assets/feature-order.png b/public/assets/feature-order.png new file mode 100644 index 0000000..617587b Binary files /dev/null and b/public/assets/feature-order.png differ diff --git a/public/assets/feature-payment.png b/public/assets/feature-payment.png new file mode 100644 index 0000000..57cdae6 Binary files /dev/null and b/public/assets/feature-payment.png differ diff --git a/public/assets/feature-support.png b/public/assets/feature-support.png new file mode 100644 index 0000000..19bd9a6 Binary files /dev/null and b/public/assets/feature-support.png differ diff --git a/public/assets/header.jpg b/public/assets/header.jpg new file mode 100644 index 0000000..8e74bf8 Binary files /dev/null and b/public/assets/header.jpg differ diff --git a/public/assets/iran-yekan-bold.ttf b/public/assets/iran-yekan-bold.ttf new file mode 100644 index 0000000..6d98764 Binary files /dev/null and b/public/assets/iran-yekan-bold.ttf differ diff --git a/public/assets/iran-yekan-light.ttf b/public/assets/iran-yekan-light.ttf new file mode 100644 index 0000000..b6114ee Binary files /dev/null and b/public/assets/iran-yekan-light.ttf differ diff --git a/public/assets/iran-yekan-medium.ttf b/public/assets/iran-yekan-medium.ttf new file mode 100644 index 0000000..cc9d9b4 Binary files /dev/null and b/public/assets/iran-yekan-medium.ttf differ diff --git a/public/assets/iran-yekan-regular.ttf b/public/assets/iran-yekan-regular.ttf new file mode 100644 index 0000000..c662408 Binary files /dev/null and b/public/assets/iran-yekan-regular.ttf differ diff --git a/public/assets/logo.png b/public/assets/logo.png new file mode 100644 index 0000000..5e6d1d4 Binary files /dev/null and b/public/assets/logo.png differ diff --git a/public/assets/montserrat-bold.ttf b/public/assets/montserrat-bold.ttf new file mode 100644 index 0000000..9a425b9 Binary files /dev/null and b/public/assets/montserrat-bold.ttf differ diff --git a/public/assets/montserrat-regular.ttf b/public/assets/montserrat-regular.ttf new file mode 100644 index 0000000..2a2b2aa Binary files /dev/null and b/public/assets/montserrat-regular.ttf differ diff --git a/public/assets/product-server-g8.jpg b/public/assets/product-server-g8.jpg new file mode 100644 index 0000000..bf6445f Binary files /dev/null and b/public/assets/product-server-g8.jpg differ diff --git a/public/assets/product-server-g9.jpg b/public/assets/product-server-g9.jpg new file mode 100644 index 0000000..9ed3326 Binary files /dev/null and b/public/assets/product-server-g9.jpg differ diff --git a/public/assets/server-info-1.jpg b/public/assets/server-info-1.jpg new file mode 100644 index 0000000..8ae7004 Binary files /dev/null and b/public/assets/server-info-1.jpg differ diff --git a/public/assets/server-info-2.jpg b/public/assets/server-info-2.jpg new file mode 100644 index 0000000..de0e4fc Binary files /dev/null and b/public/assets/server-info-2.jpg differ diff --git a/public/assets/service-cisco.png b/public/assets/service-cisco.png new file mode 100644 index 0000000..57f5249 Binary files /dev/null and b/public/assets/service-cisco.png differ diff --git a/public/assets/service-hp.png b/public/assets/service-hp.png new file mode 100644 index 0000000..28b2556 Binary files /dev/null and b/public/assets/service-hp.png differ diff --git a/public/assets/service-network.png b/public/assets/service-network.png new file mode 100644 index 0000000..25be8b7 Binary files /dev/null and b/public/assets/service-network.png differ diff --git a/public/assets/source-frame-aria.jpg b/public/assets/source-frame-aria.jpg new file mode 100644 index 0000000..2bf9133 Binary files /dev/null and b/public/assets/source-frame-aria.jpg differ diff --git a/public/assets/source-frame-hard.jpg b/public/assets/source-frame-hard.jpg new file mode 100644 index 0000000..567b17b Binary files /dev/null and b/public/assets/source-frame-hard.jpg differ diff --git a/public/assets/source-frame-server.jpg b/public/assets/source-frame-server.jpg new file mode 100644 index 0000000..d3ff4ab Binary files /dev/null and b/public/assets/source-frame-server.jpg differ diff --git a/server.mjs b/server.mjs new file mode 100644 index 0000000..5eafb85 --- /dev/null +++ b/server.mjs @@ -0,0 +1,15 @@ +import { createServer } from "node:http"; +import next from "next"; + +const hostname = process.env.HOSTNAME || "127.0.0.1"; +const port = Number(process.env.PORT || 3000); +const app = next({ dev: true, hostname, port }); +const handle = app.getRequestHandler(); + +await app.prepare(); + +createServer((req, res) => { + handle(req, res); +}).listen(port, hostname, () => { + console.log(`Ready on http://${hostname}:${port}`); +});