feat: line (#13)
* add wedhook line * text message * add router get employee, request, quotation * move code * do not crash application when not set line token This feature is opt-in * dot not crash when not set line client id Main auth method is keycloak * change dotenv * fix: wrong env * refactor: change to get instead of post * refactor: remove body for employee get endpoint * feat: add work relation include * feat: include customer relation in employee * feat: add line file controller * add detail flex message and get date requestWork * chore: update deps lock * fix: error line token * fix: redirect head instead if response with body * feat: add response relation * fix: route casing * add userId in customerBranch verifyOTP * delete consile log * add is registered endpoint placeholder * feat: quotation list * fix: wrong endpoint name * feat: include relation in get by id request data * add where userId line * refactor: adjust parameter for liff app * delete code * refactor: remove post quotation endpoint * refactor: add where userId line for quotation * feat: add pending only parameter * refactor: more condition for inProgressOnly * refactor: update condition * feat: add line quotation attachment endpoint * feat: include product in request work line endpoint * refactor: pending only now cover more condition * feat: include invoice with payment relation * chore: update api docs tag * chore: clean * feat: check for registered user * fix: wrong file location * feat: add email client for sending an otp * chore: move some deps to dev deps * add otpCode otpExpires * add send-otp and verify-otp --------- Co-authored-by: Kanjana <kanjana@chamomind.com> Co-authored-by: chamomind <chamomind@localhost> Co-authored-by: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
This commit is contained in:
parent
8709a7dcc8
commit
12bf2182dc
15 changed files with 10449 additions and 4 deletions
1230
src/controllers/09-line-controller.ts
Normal file
1230
src/controllers/09-line-controller.ts
Normal file
File diff suppressed because it is too large
Load diff
125
src/controllers/09-verification-controller.ts
Normal file
125
src/controllers/09-verification-controller.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import { Body, Controller, Get, Post, Request, Route, Security, Tags } from "tsoa";
|
||||
import prisma from "../db";
|
||||
import nodemailer from "nodemailer";
|
||||
import { notFoundError } from "../utils/error";
|
||||
import { RequestWithLineUser } from "../interfaces/user";
|
||||
|
||||
type sendEmail = {
|
||||
email: string;
|
||||
} & ({ legalPersonNo: string } | { citizenId: string });
|
||||
|
||||
type VerificationPayload = {
|
||||
email: string;
|
||||
otp: string;
|
||||
} & ({ legalPersonNo: string } | { citizenId: string });
|
||||
|
||||
@Route("/api/v1/verification")
|
||||
@Tags("Verification")
|
||||
export class verificationController extends Controller {
|
||||
@Get()
|
||||
@Security("line")
|
||||
async isRegistered(@Request() req: RequestWithLineUser) {
|
||||
return !!(await prisma.customerBranch.findFirst({ where: { userId: req.user.sub } }));
|
||||
}
|
||||
|
||||
@Post("/send-otp")
|
||||
public async sendOTP(@Body() body: sendEmail) {
|
||||
const generateOTP = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
const expiresTime = new Date(Date.now() + 5 * 60 * 1000);
|
||||
|
||||
const dataCustomerBranch = await prisma.customerBranch.findFirst({
|
||||
where: {
|
||||
email: body.email,
|
||||
...("citizenId" in body
|
||||
? {
|
||||
citizenId: body.citizenId,
|
||||
customer: {
|
||||
customerType: "PERS",
|
||||
},
|
||||
}
|
||||
: {
|
||||
legalPersonNo: body.legalPersonNo,
|
||||
customer: {
|
||||
customerType: "CORP",
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!dataCustomerBranch) throw notFoundError("Customer Branch");
|
||||
|
||||
const fromData = nodemailer.createTransport({
|
||||
host: "smtp.gmail.com",
|
||||
port: 587,
|
||||
secure: false, // true for port 465, false for other ports
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
});
|
||||
|
||||
await fromData.sendMail({
|
||||
from: process.env.SMTP_USER,
|
||||
to: body.email,
|
||||
subject: "Your OTP Code",
|
||||
text: `Your OTP Code ${generateOTP}`,
|
||||
html: `<p>Your OTP code is: <strong>${generateOTP}</strong></p>`,
|
||||
});
|
||||
|
||||
await prisma.customerBranch.update({
|
||||
where: {
|
||||
id: dataCustomerBranch.id,
|
||||
},
|
||||
data: {
|
||||
otpCode: generateOTP,
|
||||
otpExpires: expiresTime,
|
||||
},
|
||||
});
|
||||
return { message: "OTP sent successfully" };
|
||||
}
|
||||
|
||||
@Post("/verify-otp")
|
||||
@Security("line")
|
||||
public async verifyOTP(@Request() req: RequestWithLineUser, @Body() body: VerificationPayload) {
|
||||
const customerBranch = await prisma.customerBranch.findFirst({
|
||||
where: {
|
||||
email: body.email,
|
||||
...("citizenId" in body
|
||||
? {
|
||||
citizenId: body.citizenId,
|
||||
customer: {
|
||||
customerType: "PERS",
|
||||
},
|
||||
}
|
||||
: {
|
||||
legalPersonNo: body.legalPersonNo,
|
||||
customer: {
|
||||
customerType: "CORP",
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!customerBranch) throw notFoundError("Customer Branch");
|
||||
|
||||
if (
|
||||
customerBranch.otpCode &&
|
||||
customerBranch.otpCode === body.otp &&
|
||||
customerBranch.otpExpires &&
|
||||
customerBranch.otpExpires >= new Date()
|
||||
) {
|
||||
const dataCustomer = await prisma.customerBranch.update({
|
||||
where: {
|
||||
id: customerBranch.id,
|
||||
},
|
||||
data: {
|
||||
userId: req.user.sub,
|
||||
},
|
||||
});
|
||||
|
||||
return dataCustomer;
|
||||
} else {
|
||||
return "OTP ไม่ถูกต้อง";
|
||||
}
|
||||
}
|
||||
}
|
||||
183
src/controllers/09-web-hook-controller.ts
Normal file
183
src/controllers/09-web-hook-controller.ts
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
import { Body, Controller, Get, Post, Query, Response, Route, Tags } from "tsoa";
|
||||
import prisma from "../db";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatus from "../interfaces/http-status";
|
||||
import { notFoundError } from "../utils/error";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
interface WebhookPayload {
|
||||
destination?: string;
|
||||
events: Array<{
|
||||
mode?: string;
|
||||
deliveryContext?: Record<string, any>;
|
||||
webhookEventId?: string;
|
||||
type: string;
|
||||
replyToken: string;
|
||||
source: {
|
||||
userId: string;
|
||||
type: string;
|
||||
};
|
||||
timestamp: number;
|
||||
message: {
|
||||
id?: string;
|
||||
type: string;
|
||||
text?: string;
|
||||
quoteToken?: string;
|
||||
stickerId?: string;
|
||||
packageId?: string;
|
||||
stickerResourceType?: string;
|
||||
keywords?: string[];
|
||||
emojis?: string[] | { productId: string; emojiId: string; index: number; length: number }[];
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
interface accessToken {
|
||||
code: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
@Route("api/v1/webhook")
|
||||
@Tags("Webhook")
|
||||
export class WebHookController extends Controller {
|
||||
async #getLineToken() {
|
||||
if (!process.env.LINE_MESSAGING_API_TOKEN) {
|
||||
console.warn("Line Webhook Activated but LINE_MESSAGING_API_TOKEN not set.");
|
||||
throw new HttpError(HttpStatus.NOT_IMPLEMENTED, "NOT IMPLEMENTED", "notImplemented");
|
||||
}
|
||||
|
||||
return process.env.LINE_MESSAGING_API_TOKEN;
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Response(200, "Webhook received successfully")
|
||||
public async receiveWebhook(@Body() payload: WebhookPayload) {
|
||||
const token = await this.#getLineToken();
|
||||
|
||||
if (!payload || !payload.events || !Array.isArray(payload.events)) {
|
||||
this.setStatus(400);
|
||||
return { message: "Invalid payload structure" };
|
||||
}
|
||||
|
||||
if (payload.events.length > 0) {
|
||||
const userIdLine = payload.events[0]?.source?.userId;
|
||||
const dataNow = dayjs().tz("Asia/Bangkok").startOf("day");
|
||||
|
||||
// const dataUser = await prisma.customerBranch.findFirst({
|
||||
// where:{
|
||||
// userId:userIdLine
|
||||
// }
|
||||
// })
|
||||
|
||||
const dataEmployee = await prisma.employeePassport.findMany({
|
||||
select: {
|
||||
firstName: true,
|
||||
firstNameEN: true,
|
||||
lastName: true,
|
||||
lastNameEN: true,
|
||||
employeeId: true,
|
||||
expireDate: true,
|
||||
employee: {
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
customerBranch: {
|
||||
select: {
|
||||
firstName: true,
|
||||
firstNameEN: true,
|
||||
lastName: true,
|
||||
lastNameEN: true,
|
||||
customerName: true,
|
||||
customer: {
|
||||
select: {
|
||||
customerType: true,
|
||||
registeredBranch: {
|
||||
select: {
|
||||
telephoneNo: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
expireDate: {
|
||||
lt: dataNow.add(30, "day").toDate(),
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
expireDate: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
if (payload?.events[0]?.message) {
|
||||
const message = payload.events[0].message.text;
|
||||
|
||||
if (message === "เมนูหลัก > ข้อความ") {
|
||||
const dataUser = userIdLine;
|
||||
const textHead = "JWS ALERT:";
|
||||
let textData = "";
|
||||
|
||||
if (dataEmployee.length > 0) {
|
||||
const customerName =
|
||||
dataEmployee[0]?.employee?.customerBranch?.customerName ?? "ไม่ระบุ";
|
||||
const telephoneNo =
|
||||
dataEmployee[0]?.employee?.customerBranch?.customer.registeredBranch.telephoneNo ??
|
||||
"ไม่ระบุ";
|
||||
|
||||
const textEmployer = `เรียน คุณ${customerName}`;
|
||||
const textAlert = "ขอแจ้งให้ทราบว่าหนังสือเดินทางของลูกจ้าง";
|
||||
const textAlert2 = "และจำเป็นต้องดำเนินการต่ออายุในเร็ว ๆ นี้";
|
||||
const textExpDate =
|
||||
"🔹 กรุณาตรวจสอบและดำเนินการต่ออายุภายในวันที่กำหนด เพื่อป้องกันปัญหาด้านเอกสารหรือการเดินทางที่อาจเกิดขึ้น";
|
||||
const textAlert3 = "หากดำเนินการเรียบร้อยแล้ว กรุณาแจ้งให้ฝ่ายที่เกี่ยวข้องทราบ 🙏";
|
||||
let textFooter = `📞 สอบถามข้อมูลเพิ่มเติม: ${telephoneNo}`;
|
||||
|
||||
const textEmployees = dataEmployee
|
||||
.map((item, index) => {
|
||||
const dateFormat =
|
||||
dayjs(item.expireDate).format("DD/MM/") + (dayjs(item.expireDate).year() + 543);
|
||||
const diffDate = dayjs(item.expireDate).diff(dayjs(), "day");
|
||||
|
||||
return `${index + 1}. คุณ${item.firstName} ${item.lastName} วันหมดอายุเอกสาร : ${dateFormat} ใกล้หมดอายุอีก ${diffDate} วัน\n https://taii-cmm.case-collection.com/api/v1/line/employee/${item.employeeId}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
textData = `${textHead}\n\n${textEmployer}\n\n${textAlert}\n${textEmployees}\n${textAlert2}\n\n${textExpDate}\n\n${textAlert3}\n\n${textFooter}`;
|
||||
} else {
|
||||
textData = `${textHead}\n\nขออภัย ไม่พบข้อมูลหนังสือเดินทางที่มีกำหนดหมดอายุภายใน 30 วันข้างหน้า 🙏`;
|
||||
}
|
||||
|
||||
const data = {
|
||||
to: dataUser,
|
||||
messages: [
|
||||
{
|
||||
type: "text",
|
||||
text: textData,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await fetch("https://api.line.me/v2/bot/message/push", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { message: "Webhook received successfully" };
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue