2026-10-01 11:21:08 +03:30
/**
2026-10-01 20:36:06 +03:30
* 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).
2026-10-01 11:21:08 +03:30
*/
import http from "node:http" ;
2026-10-01 20:36:06 +03:30
import { createRequire } from "node:module" ;
2026-10-01 11:21:08 +03:30
2026-10-01 20:36:06 +03:30
const require = createRequire ( import . meta . url );
const { DEFAULT_SECTIONS , SECTION_KEYS } = require ( "../packages/shared/dist/index.js" );
2026-10-01 11:21:08 +03:30
2026-10-01 20:36:06 +03:30
let settings = {
id : "default" ,
brandName : "منتک" ,
tagline : "هوشافزار منظر" ,
email : "info@montech.ir" ,
phone : "۰۹۱۲ ۳۴۵ ۶۷۸۹" ,
address : "تهران، ایران" ,
timezone : "Asia/Tehran" ,
logoUrl : null ,
socials : [
{ label : "تلگرام" , url : "#" },
{ label : "لینکدین" , url : "#" },
{ label : "اینستاگرام" , url : "#" },
2026-10-01 11:21:08 +03:30
],
};
2026-10-01 20:36:06 +03:30
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 = [
2026-10-01 11:21:08 +03:30
{
2026-10-01 20:36:06 +03:30
id : "b1" ,
2026-10-01 11:21:08 +03:30
slug : "welcome-to-montech" ,
title : "به منتک خوش آمدید" ,
2026-10-01 20:36:06 +03:30
excerpt : "ما نرمافزاری میسازیم که واقعاً کار میکند — و اینجا دربارهاش مینویسیم." ,
content : "منتک یک گروه توسعه نرمافزار است که پلتفرم وب، اپلیکیشن موبایل و زیرساخت ابری میسازد.\n\nدر این بلاگ درباره ساخت محصول، مهندسی و درسآموختههای استودیو مینویسیم." ,
coverUrl : null ,
published : true ,
publishedAt : now ,
seoTitle : "به منتک خوش آمدید — بلاگ" ,
seoDesc : "معرفی منتک و نحوه ساخت نرمافزار در تیم ما." ,
ogImage : null ,
canonical : null ,
createdAt : now ,
updatedAt : now ,
2026-10-01 11:21:08 +03:30
},
];
2026-10-01 20:36:06 +03:30
let gallery = [];
let media = [];
let leads = [];
let seq = 1 ;
const id = ( p ) => ` ${ p }${ Date . now (). toString ( 36 ) }${ seq ++ } ` ;
2026-10-01 11:21:08 +03:30
const port = Number ( process . env . PORT || 3050 );
2026-10-01 20:36:06 +03:30
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 ) => {
2026-10-01 11:21:08 +03:30
res . setHeader ( "Access-Control-Allow-Origin" , "*" );
2026-10-01 20:36:06 +03:30
res . setHeader ( "Access-Control-Allow-Methods" , "GET,POST,PUT,PATCH,DELETE,OPTIONS" );
res . setHeader ( "Access-Control-Allow-Headers" , "Content-Type,Authorization" );
2026-10-01 11:21:08 +03:30
if ( req . method === "OPTIONS" ) {
res . writeHead ( 204 );
res . end ();
return ;
}
2026-10-01 20:36:06 +03:30
const url = new URL ( req . url || "/" , "http://localhost" );
const path = url . pathname ;
const m = req . method ;
2026-10-01 11:21:08 +03:30
const json = ( data , code = 200 ) => {
res . writeHead ( code , { "Content-Type" : "application/json; charset=utf-8" });
res . end ( JSON . stringify ( data ));
};
2026-10-01 20:36:06 +03:30
const seg = path . split ( "/" ). filter ( Boolean );
2026-10-01 11:21:08 +03:30
2026-10-01 20:36:06 +03:30
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 );
2026-10-01 11:21:08 +03:30
}
2026-10-01 20:36:06 +03:30
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 ] || "" ));
2026-10-01 11:21:08 +03:30
return page ? json ( page ) : json ({ message : "Not found" }, 404 );
}
2026-10-01 20:36:06 +03:30
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 );
}
2026-10-01 11:21:08 +03:30
2026-10-01 20:36:06 +03:30
// 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 );
2026-10-01 11:21:08 +03:30
});
server . listen ( port , "127.0.0.1" , () => {
console . log ( `[mock-api] http://127.0.0.1: ${ port } ` );
});