import { Elysia } from "elysia"; import { cors } from "@elysiajs/cors"; import { swagger } from "@elysiajs/swagger"; import { jwt } from "@elysiajs/jwt"; import { staticPlugin } from "@elysiajs/static"; import { categoriesRoute } from "./routes/categories"; import { productsRoute } from "./routes/products"; import { authRoute } from "./routes/auth"; import { uploadRoute } from "./routes/upload"; import { ordersRoute } from "./routes/orders"; import { settingsRoute } from "./routes/settings"; import { articlesRoute } from "./routes/articles"; import { tiktokRoute } from "./routes/tiktok"; import { rawMaterialsRoute } from "./routes/raw-materials"; import { shopeeRoute } from "./routes/shopee"; import { tiktokOrderRoute } from "./routes/tiktokOrder"; import { lazadaOrderRoute } from "./routes/lazadaOrder"; import { customersRoute } from "./routes/customers"; import { couponsRoute } from "./routes/coupons"; import { reviewsRoute } from "./routes/reviews"; import { promotionsRoute } from "./routes/promotions"; import { pagesRoute } from "./routes/pages"; import { paymentsRoute } from "./routes/payments"; import { profileRoute } from "./routes/profile"; import { lineRoutes } from "./routes/line"; import { menusRoute } from "./routes/menus"; import { newsletterRoute } from "./routes/newsletter"; import { mediaRoute } from "./routes/media"; import { contactsRoute } from "./routes/contacts"; import { productAttributesRoute } from "./routes/product-attributes"; import { backupRoute } from "./routes/backup"; import { businessAnalyticsRoute } from "./routes/businessAnalytics"; import { toolsRoute } from "./routes/tools"; import { crmRoute } from "./routes/crm"; import { brandKnowledgeRoute } from "./routes/brandKnowledge"; import { categorySectionsRoute } from "./routes/categorySections"; import { llmPromptsRoute } from "./routes/llmPrompts"; import { frameSequencesRoute } from "./routes/frame-sequences"; const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3051; const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:5173"; const JWT_SECRET = process.env.JWT_SECRET || "homaroma_secret_2026_brand"; const isDevelopment = process.env.NODE_ENV !== "production"; const isPrivateLanHostname = (hostname: string) => { return ( hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1" || /^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname) || /^192\.168\.\d{1,3}\.\d{1,3}$/.test(hostname) || /^172\.(1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3}$/.test(hostname) ); }; const allowLanDevOrigin = (request: Request) => { const origin = request.headers.get("origin"); if (!origin) return false; try { return isPrivateLanHostname(new URL(origin).hostname); } catch { return false; } }; const app = new Elysia() .onRequest(({ request }) => { console.log(`\n➡️ [${request.method}] ${request.url}`); }) .onAfterHandle(({ request, set }) => { console.log(`✅ [${request.method}] ${request.url} → ${set.status ?? 200}`); }) .onError(({ request, error, set }) => { console.error(`❌ [${request.method}] ${request.url} → ${set.status ?? 500}`, error?.message ?? error); }) .onParse(async ({ request }) => { console.log('[onParse] Incoming request URL:', request.url); if (request.url.includes('/api/line/webhook')) { const text = await request.text(); console.log('[onParse] Intercepted LINE webhook raw body length:', text.length); return text; } }) .use( cors({ origin: [ ...CORS_ORIGIN.split(",").map((o) => o.trim()), ...(isDevelopment ? [allowLanDevOrigin] : []), "https://admin.homaroma.shop", "https://spa.homaroma.shop", // LIFF (SPA) "https://homaroma.shop", // Main Website "http://localhost:5174", // Admin "http://localhost:4173", "http://localhost:3001", // SSR Next.js "https://react5173.kitizone.com", "https://elysia3051.kitizone.com" ], methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], credentials: true, }) ) .use( jwt({ name: "jwt", secret: JWT_SECRET, exp: "24h", }) ) .use( swagger({ documentation: { info: { title: "HomAroma API", version: "1.0.0", description: "API for HomAroma & Essential Oil Products", }, tags: [ { name: "categories", description: "Product categories" }, { name: "products", description: "Products and variants" }, { name: "auth", description: "Authentication for Admin" }, ], }, }) ) .use(staticPlugin({ assets: "public", prefix: "", // use empty string for root })) .get("/", () => ({ name: "HomAroma API", version: "1.0.0", status: "running", docs: `/swagger`, })) // Explicit route for uploads to guarantee it works even if staticPlugin fails .get("/uploads/:filename", ({ params: { filename }, set }) => { const file = Bun.file(`public/uploads/${filename}`); if (file.size === 0) { set.status = 404; return "File not found"; } return file; }) .get("/frame-sequences/:sequence/:filename", ({ params: { sequence, filename }, set }) => { const file = Bun.file(`public/frame-sequences/${sequence}/${filename}`); if (file.size === 0) { set.status = 404; return "File not found"; } return file; }) .group("/api", (app) => app.use(authRoute).use(categoriesRoute).use(categorySectionsRoute).use(productsRoute).use(productAttributesRoute).use(uploadRoute).use(ordersRoute).use(settingsRoute).use(articlesRoute).use(tiktokRoute).use(rawMaterialsRoute).use(shopeeRoute).use(tiktokOrderRoute).use(lazadaOrderRoute).use(customersRoute).use(couponsRoute).use(reviewsRoute).use(promotionsRoute).use(pagesRoute).use(paymentsRoute).use(profileRoute).use(lineRoutes).use(menusRoute).use(newsletterRoute).use(mediaRoute).use(contactsRoute).use(backupRoute).use(businessAnalyticsRoute).use(toolsRoute).use(crmRoute).use(brandKnowledgeRoute).use(llmPromptsRoute).use(frameSequencesRoute) ) .listen({ port: PORT, hostname: "0.0.0.0" }); console.log( `🌿 HomAroma API is running at http://${app.server?.hostname}:${app.server?.port}` ); console.log( `📖 Swagger docs: http://${app.server?.hostname}:${app.server?.port}/swagger` );