/** * Lightweight in-memory mock of the Nest API for UI preview when Postgres isn't configured. * Serves the public and admin endpoints used by the landing and the admin panel. * Requires `npm run build:shared` first (reads defaults from packages/shared/dist). */ import http from "node:http"; import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const { DEFAULT_SECTIONS, SECTION_KEYS } = require("../packages/shared/dist/index.js"); let settings = { id: "default", brandName: "منتک", tagline: "هوش‌افزار منظر", email: "info@montech.ir", phone: "۰۹۱۲ ۳۴۵ ۶۷۸۹", address: "تهران، ایران", timezone: "Asia/Tehran", logoUrl: null, socials: [ { label: "تلگرام", url: "#" }, { label: "لینکدین", url: "#" }, { label: "اینستاگرام", url: "#" }, ], }; const sections = Object.fromEntries(SECTION_KEYS.map((k) => [k, structuredClone(DEFAULT_SECTIONS[k])])); const pages = [ { id: "p1", key: "home", title: "خانه", seo: { title: "منتک | هوش‌افزار منظر - پلتفرم راهکارهای نرم‌افزاری و هوش مصنوعی", description: "با کمک هوش مصنوعی منتک، بهترین نرم‌افزارها و خدمات متناسب با کسب‌وکار شما پیشنهاد می‌شود.", canonical: "http://localhost:3000/", ogImage: null, noIndex: false } }, { id: "p2", key: "blog", title: "بلاگ", seo: { title: "بلاگ — منتک", description: "یادداشت‌هایی درباره محصول، مهندسی و ساخت نرم‌افزار.", canonical: "http://localhost:3000/blog", ogImage: null, noIndex: false } }, { id: "p3", key: "gallery", title: "گالری", seo: { title: "گالری — منتک", description: "تصاویر منتخب از پروژه‌ها و فضای استودیو منتک.", canonical: "http://localhost:3000/gallery", ogImage: null, noIndex: false } }, ]; const now = new Date().toISOString(); let blog = [ { id: "b1", slug: "welcome-to-montech", title: "به منتک خوش آمدید", excerpt: "ما نرم‌افزاری می‌سازیم که واقعاً کار می‌کند — و اینجا درباره‌اش می‌نویسیم.", content: "منتک یک گروه توسعه نرم‌افزار است که پلتفرم وب، اپلیکیشن موبایل و زیرساخت ابری می‌سازد.\n\nدر این بلاگ درباره ساخت محصول، مهندسی و درس‌آموخته‌های استودیو می‌نویسیم.", coverUrl: null, published: true, publishedAt: now, seoTitle: "به منتک خوش آمدید — بلاگ", seoDesc: "معرفی منتک و نحوه ساخت نرم‌افزار در تیم ما.", ogImage: null, canonical: null, createdAt: now, updatedAt: now, }, ]; let gallery = []; let media = []; let leads = []; let seq = 1; const id = (p) => `${p}${Date.now().toString(36)}${seq++}`; const port = Number(process.env.PORT || 3050); function readBody(req) { return new Promise((resolve) => { let raw = ""; req.on("data", (c) => (raw += c)); req.on("end", () => { try { resolve(raw ? JSON.parse(raw) : {}); } catch { resolve({}); } }); }); } const server = http.createServer(async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization"); if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; } const url = new URL(req.url || "/", "http://localhost"); const path = url.pathname; const m = req.method; const json = (data, code = 200) => { res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" }); res.end(JSON.stringify(data)); }; const seg = path.split("/").filter(Boolean); if (path === "/api/health") return json({ ok: true, mock: true }); // public if (m === "GET" && path === "/api/public/site") return json({ settings, sections, pages }); if (m === "GET" && path === "/api/public/blog") return json(blog.filter((p) => p.published)); if (m === "GET" && path.startsWith("/api/public/blog/")) { const post = blog.find((p) => p.slug === decodeURIComponent(seg[3] || "") && p.published); return post ? json(post) : json({ message: "Not found" }, 404); } if (m === "GET" && path === "/api/public/gallery") return json(gallery.filter((g) => g.published !== false)); if (m === "GET" && path.startsWith("/api/public/pages/")) { const page = pages.find((p) => p.key === decodeURIComponent(seg[3] || "")); return page ? json(page) : json({ message: "Not found" }, 404); } if (m === "POST" && path === "/api/public/leads") { const body = await readBody(req); const phone = String(body.phone || "").replace(/[۰-۹]/g, (d) => String(d.charCodeAt(0) - 0x06f0)).replace(/[\s\-()]/g, ""); if (!body.name || String(body.name).trim().length < 2 || !/^\+?\d{7,15}$/.test(phone) || !body.businessType) { return json({ message: ["validation failed"] }, 400); } const lead = { id: id("l"), name: String(body.name).trim(), phone, businessType: body.businessType, source: body.source || "consult", aiQuery: body.aiQuery || null, status: "new", note: null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; leads.unshift(lead); return json({ id: lead.id, createdAt: lead.createdAt }, 201); } // auth if (m === "POST" && path === "/api/auth/login") { return json({ accessToken: "mock-token", admin: { id: "a1", mobile: "09121234567", name: "مدیر منتک" } }); } if (path.startsWith("/api/admin") || path === "/api/auth/me") { if (req.headers.authorization !== "Bearer mock-token") return json({ message: "Unauthorized" }, 401); } if (m === "GET" && path === "/api/auth/me") return json({ id: "a1", mobile: "09121234567", name: "مدیر منتک" }); // admin if (path === "/api/admin/settings") { if (m === "GET") return json(settings); if (m === "PUT") { settings = { ...settings, ...(await readBody(req)) }; sections.footer = { ...sections.footer, email: settings.email, phone: settings.phone, address: settings.address }; return json(settings); } } if (m === "GET" && path === "/api/admin/sections") { return json(SECTION_KEYS.map((key) => ({ key, data: sections[key], updatedAt: now, isDefault: false }))); } if (seg[1] === "admin" && seg[2] === "sections" && seg[3]) { const key = seg[3]; if (!SECTION_KEYS.includes(key)) return json({ message: "Unknown section" }, 400); if (m === "GET") return json({ key, data: sections[key], updatedAt: now, isDefault: false, defaults: DEFAULT_SECTIONS[key] }); if (m === "PUT") { const body = await readBody(req); sections[key] = body.data; return json({ key, data: sections[key] }); } } if (path === "/api/admin/leads" && m === "GET") { const status = url.searchParams.get("status"); const page = Number(url.searchParams.get("page") || 1); const pageSize = Number(url.searchParams.get("pageSize") || 20); const filtered = status ? leads.filter((l) => l.status === status) : leads; return json({ items: filtered.slice((page - 1) * pageSize, page * pageSize), total: filtered.length, page, pageSize, newCount: leads.filter((l) => l.status === "new").length }); } if (seg[1] === "admin" && seg[2] === "leads" && seg[3]) { const idx = leads.findIndex((l) => l.id === seg[3]); if (idx < 0) return json({ message: "Not found" }, 404); if (m === "PATCH") { leads[idx] = { ...leads[idx], ...(await readBody(req)), updatedAt: new Date().toISOString() }; return json(leads[idx]); } if (m === "DELETE") return json(leads.splice(idx, 1)[0]); } if (path === "/api/admin/pages" && m === "GET") return json(pages); if (seg[1] === "admin" && seg[2] === "pages" && seg[4] === "seo" && m === "PUT") { const page = pages.find((p) => p.key === seg[3]); if (!page) return json({ message: "Not found" }, 404); page.seo = { ...page.seo, ...(await readBody(req)) }; return json(page.seo); } if (path === "/api/admin/blog") { if (m === "GET") return json(blog); if (m === "POST") { const body = await readBody(req); const post = { id: id("b"), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), publishedAt: body.published ? new Date().toISOString() : null, ...body }; blog.unshift(post); return json(post, 201); } } if (seg[1] === "admin" && seg[2] === "blog" && seg[3]) { const idx = blog.findIndex((p) => p.id === seg[3]); if (idx < 0) return json({ message: "Not found" }, 404); if (m === "GET") return json(blog[idx]); if (m === "PUT") { blog[idx] = { ...blog[idx], ...(await readBody(req)), updatedAt: new Date().toISOString() }; return json(blog[idx]); } if (m === "DELETE") return json(blog.splice(idx, 1)[0]); } if (path === "/api/admin/gallery") { if (m === "GET") return json(gallery); if (m === "POST") { const item = { id: id("g"), published: true, sortOrder: 0, ...(await readBody(req)) }; gallery.push(item); return json(item, 201); } } if (seg[1] === "admin" && seg[2] === "gallery" && seg[3]) { const idx = gallery.findIndex((g) => g.id === seg[3]); if (idx < 0) return json({ message: "Not found" }, 404); if (m === "PUT") { gallery[idx] = { ...gallery[idx], ...(await readBody(req)) }; return json(gallery[idx]); } if (m === "DELETE") return json(gallery.splice(idx, 1)[0]); } if (path === "/api/admin/media" && m === "GET") return json(media); if (path === "/api/admin/media/upload" && m === "POST") { req.resume(); const item = { id: id("m"), filename: "mock-upload.png", url: "/brand/logo.svg", mimeType: "image/svg+xml", size: 1024, createdAt: new Date().toISOString() }; media.unshift(item); return json(item, 201); } if (seg[1] === "admin" && seg[2] === "media" && seg[3] && m === "DELETE") { media = media.filter((x) => x.id !== seg[3]); return json({ ok: true }); } json({ message: `Mock API — no route for ${m} ${path}` }, 404); }); server.listen(port, "127.0.0.1", () => { console.log(`[mock-api] http://127.0.0.1:${port}`); });