From 182d0349fae05a15b4f868712dae128ae0a6809b Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 12 Jun 2026 23:21:14 -0400 Subject: [PATCH] x --- apps/backend/package.json | 3 +- apps/backend/src/index.ts | 3 +- apps/backend/src/lib/lineClient.ts | 13 ++++++++ apps/backend/src/routes/line.ts | 49 ++++++++++++++++++++++++++++++ bun.lock | 5 ++- 5 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 apps/backend/src/lib/lineClient.ts create mode 100644 apps/backend/src/routes/line.ts diff --git a/apps/backend/package.json b/apps/backend/package.json index 888856f..3a3ce81 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -12,11 +12,12 @@ "author": "", "license": "ISC", "dependencies": { - "@homaroma/types": "*", "@elysiajs/cors": "^1.2.2", "@elysiajs/jwt": "^1.4.1", "@elysiajs/static": "^1.4.7", "@elysiajs/swagger": "^1.2.1", + "@homaroma/types": "*", + "@line/bot-sdk": "^11.0.1", "@prisma/adapter-pg": "^7.6.0", "@prisma/client": "^7.6.0", "bcryptjs": "^3.0.3", diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 0c94639..bc6431d 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -23,6 +23,7 @@ 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"; 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_scent"; @@ -88,7 +89,7 @@ const app = new Elysia() return file; }) .group("/api", (app) => - app.use(authRoute).use(categoriesRoute).use(productsRoute).use(uploadRoute).use(heroRoute).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) + app.use(authRoute).use(categoriesRoute).use(productsRoute).use(uploadRoute).use(heroRoute).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) ) .listen({ port: PORT, hostname: "0.0.0.0" }); diff --git a/apps/backend/src/lib/lineClient.ts b/apps/backend/src/lib/lineClient.ts new file mode 100644 index 0000000..68986a8 --- /dev/null +++ b/apps/backend/src/lib/lineClient.ts @@ -0,0 +1,13 @@ +import * as line from '@line/bot-sdk'; + +const channelAccessToken = process.env.LINE_CHANNEL_ACCESS_TOKEN || ''; +const channelSecret = process.env.LINE_CHANNEL_SECRET || ''; + +export const lineConfig = { + channelAccessToken, + channelSecret, +}; + +export const lineClient = new line.messagingApi.MessagingApiClient({ + channelAccessToken, +}); diff --git a/apps/backend/src/routes/line.ts b/apps/backend/src/routes/line.ts new file mode 100644 index 0000000..8eb6a2b --- /dev/null +++ b/apps/backend/src/routes/line.ts @@ -0,0 +1,49 @@ +import { Elysia } from 'elysia'; +import * as line from '@line/bot-sdk'; +import { lineConfig, lineClient } from '../lib/lineClient'; +import crypto from 'crypto'; + +export const lineRoutes = new Elysia({ prefix: '/line' }) + .post('/webhook', async ({ request, body, set }) => { + // 1. Verify Signature + const signature = request.headers.get('x-line-signature'); + if (!signature) { + set.status = 400; + return { error: 'Missing Signature' }; + } + + // get raw body string + const rawBody = await request.clone().text(); + + const hash = crypto + .createHmac('SHA256', lineConfig.channelSecret) + .update(rawBody) + .digest('base64'); + + if (hash !== signature) { + set.status = 401; + return { error: 'Invalid Signature' }; + } + + try { + const events: line.WebhookEvent[] = (body as any).events || []; + + for (const event of events) { + if (event.type === 'message' && event.message.type === 'text') { + // Simple echo bot for now + await lineClient.replyMessage({ + replyToken: event.replyToken, + messages: [{ + type: 'text', + text: `คุณส่งข้อความว่า: ${event.message.text}` + }] + }); + } + } + return { success: true }; + } catch (err) { + console.error('[LINE Webhook] Error:', err); + set.status = 500; + return { error: 'Webhook processing failed' }; + } + }); diff --git a/bun.lock b/bun.lock index fe80666..b921bfb 100644 --- a/bun.lock +++ b/bun.lock @@ -46,6 +46,7 @@ "@elysiajs/static": "^1.4.7", "@elysiajs/swagger": "^1.2.1", "@homaroma/types": "*", + "@line/bot-sdk": "^11.0.1", "@prisma/adapter-pg": "^7.6.0", "@prisma/client": "^7.6.0", "bcryptjs": "^3.0.3", @@ -398,7 +399,7 @@ "@liff/window-postmessage": ["@liff/window-postmessage@2.29.0", "", { "dependencies": { "@liff/consts": "2.29.0", "@liff/logger": "2.29.0", "@liff/util": "2.29.0" }, "peerDependencies": { "tslib": "^2.3.0" } }, "sha512-rIsGf3xC/3JAb/1Nyoqrcss5UUUECz7v36MhruGeaVvTfzXqz1sviaUnU+rNHxij9CJEqMpA1C8+YDKenF6wMw=="], - "@line/bot-sdk": ["@line/bot-sdk@10.8.0", "", { "dependencies": { "@types/node": "^24.0.0" }, "optionalDependencies": { "axios": "^1.7.4" } }, "sha512-1Mdpw/WPS3QP6fGiwPRTL27k8MgmMe7rVKCKsItP/kPzogX7a/X1ipQqk/F6RkorP9DNyJIx590hnVUkfBQf3w=="], + "@line/bot-sdk": ["@line/bot-sdk@11.0.1", "", { "dependencies": { "@types/node": "^24.0.0" } }, "sha512-De9gBX2JfZs78nDSyzfetHJw0R6hncPVVy3tzPMwYZzgwK06C/tcijGy7Vq28K8uYKGVTSthbtMIoYKNhXISzw=="], "@line/liff": ["@line/liff@2.29.0", "", { "dependencies": { "@liff/activity": "2.29.0", "@liff/analytics": "2.29.0", "@liff/close-window": "2.29.0", "@liff/consts": "2.29.0", "@liff/core": "2.29.0", "@liff/create-shortcut-on-home-screen": "2.29.0", "@liff/extensions": "2.29.0", "@liff/get-app-language": "2.29.0", "@liff/get-friendship": "2.29.0", "@liff/get-language": "2.29.0", "@liff/get-line-version": "2.29.0", "@liff/get-origins": "2.29.0", "@liff/get-os": "2.29.0", "@liff/get-profile": "2.29.0", "@liff/get-version": "2.29.0", "@liff/hooks": "2.29.0", "@liff/i18n": "2.29.0", "@liff/iap": "2.29.0", "@liff/init": "2.29.0", "@liff/is-api-available": "2.29.0", "@liff/is-in-client": "2.29.0", "@liff/is-logged-in": "2.29.0", "@liff/is-sub-window": "2.29.0", "@liff/liff-types": "2.29.0", "@liff/login": "2.29.0", "@liff/logout": "2.29.0", "@liff/native-bridge": "2.29.0", "@liff/open-window": "2.29.0", "@liff/permanent-link": "2.29.0", "@liff/permission": "2.29.0", "@liff/ready": "2.29.0", "@liff/request-friendship": "2.29.0", "@liff/scan-code-v2": "2.29.0", "@liff/send-messages": "2.29.0", "@liff/server-api": "2.29.0", "@liff/share-target-picker": "2.29.0", "@liff/store": "2.29.0", "@liff/sub-window": "2.29.0", "@liff/use": "2.29.0", "@liff/util": "2.29.0", "tslib": "^2.3.0", "whatwg-fetch": "^3.0.0" } }, "sha512-50xZ7D5FiOM/42d00GEZh2TJ9bHDkbml+5yjUceC5xF19jYQVMrGn70Qbc0GYkVmnZtKp2q9hXQEzk25/arGrA=="], @@ -1656,6 +1657,8 @@ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "@liff/send-messages/@line/bot-sdk": ["@line/bot-sdk@10.8.0", "", { "dependencies": { "@types/node": "^24.0.0" }, "optionalDependencies": { "axios": "^1.7.4" } }, "sha512-1Mdpw/WPS3QP6fGiwPRTL27k8MgmMe7rVKCKsItP/kPzogX7a/X1ipQqk/F6RkorP9DNyJIx590hnVUkfBQf3w=="], + "@modelcontextprotocol/sdk/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],