Deploy to stating #24
			
				
			
		
		
		
	| 
						 | 
					@ -6,7 +6,7 @@ import { Socket } from "socket.io-client";
 | 
				
			||||||
import { getImagesWorking } from "../../apis/bid";
 | 
					import { getImagesWorking } from "../../apis/bid";
 | 
				
			||||||
import { useStatusToolStore } from "../../lib/zustand/use-status-tool-store";
 | 
					import { useStatusToolStore } from "../../lib/zustand/use-status-tool-store";
 | 
				
			||||||
import { IBid, IWebBid } from "../../system/type";
 | 
					import { IBid, IWebBid } from "../../system/type";
 | 
				
			||||||
import { cn, stringToColor } from "../../utils";
 | 
					import { cn, extractDomainSmart, stringToColor } from "../../utils";
 | 
				
			||||||
import ShowImageModal from "./show-image-modal";
 | 
					import ShowImageModal from "./show-image-modal";
 | 
				
			||||||
import { isTimeReached, subtractSeconds } from "../../lib/table/ultils";
 | 
					import { isTimeReached, subtractSeconds } from "../../lib/table/ultils";
 | 
				
			||||||
export interface IWorkingPageProps {
 | 
					export interface IWorkingPageProps {
 | 
				
			||||||
| 
						 | 
					@ -129,7 +129,7 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
				
			||||||
          src={imageSrc}
 | 
					          src={imageSrc}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Box className="absolute bg-black/60 inset-0 flex items-center justify-center text-white font-bold flex-col gap-3 p-4 rounded-lg transition duration-300 hover:bg-opacity-70">
 | 
					        <Box className="absolute bg-black/60 inset-0 flex items-center justify-center text-white font-bold flex-col gap-2 p-4 rounded-lg transition duration-300 hover:bg-opacity-70">
 | 
				
			||||||
          <Text className="text-lg tracking-wide text-center font-bold">
 | 
					          <Text className="text-lg tracking-wide text-center font-bold">
 | 
				
			||||||
            {isIBid(data) ? data.name : "Tracking page"}
 | 
					            {isIBid(data) ? data.name : "Tracking page"}
 | 
				
			||||||
          </Text>
 | 
					          </Text>
 | 
				
			||||||
| 
						 | 
					@ -207,11 +207,11 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <Badge
 | 
					          <Badge
 | 
				
			||||||
            color={stringToColor(
 | 
					            color={stringToColor(
 | 
				
			||||||
              isIBid(data) ? data.web_bid.origin_url : data.origin_url
 | 
					              isIBid(data) ? extractDomainSmart(data.web_bid.origin_url) : extractDomainSmart(data.origin_url)
 | 
				
			||||||
            )}
 | 
					            )}
 | 
				
			||||||
            size="xs"
 | 
					            size="xs"
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            {isIBid(data) ? data.web_bid.origin_url : data.origin_url}
 | 
					            {isIBid(data) ? extractDomainSmart(data.web_bid.origin_url) : extractDomainSmart(data.origin_url)}
 | 
				
			||||||
          </Badge>
 | 
					          </Badge>
 | 
				
			||||||
        </Box>
 | 
					        </Box>
 | 
				
			||||||
      </Box>
 | 
					      </Box>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,13 +18,13 @@ import {
 | 
				
			||||||
  ShowHistoriesBidPicklesApiModal,
 | 
					  ShowHistoriesBidPicklesApiModal,
 | 
				
			||||||
  ShowHistoriesModal,
 | 
					  ShowHistoriesModal,
 | 
				
			||||||
} from "../components/bid";
 | 
					} from "../components/bid";
 | 
				
			||||||
 | 
					import constants, { haveHistories } from "../constant";
 | 
				
			||||||
import Table from "../lib/table/table";
 | 
					import Table from "../lib/table/table";
 | 
				
			||||||
import { IColumn, TRefTableFn } from "../lib/table/type";
 | 
					import { IColumn, TRefTableFn } from "../lib/table/type";
 | 
				
			||||||
import { useConfirmStore } from "../lib/zustand/use-confirm";
 | 
					import { useConfirmStore } from "../lib/zustand/use-confirm";
 | 
				
			||||||
import { mappingStatusColors } from "../system/constants";
 | 
					import { mappingStatusColors } from "../system/constants";
 | 
				
			||||||
import { IBid } from "../system/type";
 | 
					import { IBid } from "../system/type";
 | 
				
			||||||
import { formatTime } from "../utils";
 | 
					import { extractDomainSmart, formatTime } from "../utils";
 | 
				
			||||||
import constants, { haveHistories } from "../constant";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function Bids() {
 | 
					export default function Bids() {
 | 
				
			||||||
  const refTableFn: TRefTableFn<IBid> = useRef({});
 | 
					  const refTableFn: TRefTableFn<IBid> = useRef({});
 | 
				
			||||||
| 
						 | 
					@ -57,7 +57,7 @@ export default function Bids() {
 | 
				
			||||||
      title: "Web",
 | 
					      title: "Web",
 | 
				
			||||||
      typeFilter: "text",
 | 
					      typeFilter: "text",
 | 
				
			||||||
      renderRow(row) {
 | 
					      renderRow(row) {
 | 
				
			||||||
        return <span>{row.web_bid.origin_url}</span>;
 | 
					        return <span>{extractDomainSmart(row.web_bid.origin_url)}</span>;
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -157,14 +157,18 @@ export function findEarlyLoginTime(webBid: IWebBid): string | null {
 | 
				
			||||||
  const now = new Date();
 | 
					  const now = new Date();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Bước 1: Lọc ra những bid có close_time hợp lệ
 | 
					  // Bước 1: Lọc ra những bid có close_time hợp lệ
 | 
				
			||||||
  const validChildren = webBid.children.filter(child => child.close_time);
 | 
					  const validChildren = webBid.children.filter((child) => child.close_time);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (validChildren.length === 0) return null;
 | 
					  if (validChildren.length === 0) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Bước 2: Tìm bid có close_time gần hiện tại nhất
 | 
					  // Bước 2: Tìm bid có close_time gần hiện tại nhất
 | 
				
			||||||
  const closestBid = validChildren.reduce((closest, current) => {
 | 
					  const closestBid = validChildren.reduce((closest, current) => {
 | 
				
			||||||
    const closestDiff = Math.abs(new Date(closest.close_time!).getTime() - now.getTime());
 | 
					    const closestDiff = Math.abs(
 | 
				
			||||||
    const currentDiff = Math.abs(new Date(current.close_time!).getTime() - now.getTime());
 | 
					      new Date(closest.close_time!).getTime() - now.getTime()
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const currentDiff = Math.abs(
 | 
				
			||||||
 | 
					      new Date(current.close_time!).getTime() - now.getTime()
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
    return currentDiff < closestDiff ? current : closest;
 | 
					    return currentDiff < closestDiff ? current : closest;
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -172,7 +176,28 @@ export function findEarlyLoginTime(webBid: IWebBid): string | null {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Bước 3: Tính toán thời gian login sớm
 | 
					  // Bước 3: Tính toán thời gian login sớm
 | 
				
			||||||
  const closeTime = new Date(closestBid.close_time);
 | 
					  const closeTime = new Date(closestBid.close_time);
 | 
				
			||||||
  closeTime.setSeconds(closeTime.getSeconds() - (webBid.early_tracking_seconds || 0));
 | 
					  closeTime.setSeconds(
 | 
				
			||||||
 | 
					    closeTime.getSeconds() - (webBid.early_tracking_seconds || 0)
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return closeTime.toISOString();
 | 
					  return closeTime.toISOString();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function extractDomainSmart(url: string) {
 | 
				
			||||||
 | 
					  const PUBLIC_SUFFIXES = ["com.au", "co.uk", "com.vn", "org.au", "gov.uk"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const hostname = new URL(url).hostname.replace(/^www\./, ""); // remove "www."
 | 
				
			||||||
 | 
					    const parts = hostname.split(".");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (let i = 0; i < PUBLIC_SUFFIXES.length; i++) {
 | 
				
			||||||
 | 
					      if (hostname.endsWith(PUBLIC_SUFFIXES[i])) {
 | 
				
			||||||
 | 
					        return parts[parts.length - PUBLIC_SUFFIXES[i].split(".").length - 1];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return parts[parts.length - 2];
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    return url;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@
 | 
				
			||||||
/public
 | 
					/public
 | 
				
			||||||
/system/error-images
 | 
					/system/error-images
 | 
				
			||||||
/system/profiles
 | 
					/system/profiles
 | 
				
			||||||
 | 
					/system/local-data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Logs
 | 
					# Logs
 | 
				
			||||||
logs
 | 
					logs
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -269,6 +269,20 @@ const clearLazyTab = async () => {
 | 
				
			||||||
    // product tabs
 | 
					    // product tabs
 | 
				
			||||||
    const productTabs = _.flatMap(MANAGER_BIDS, "children");
 | 
					    const productTabs = _.flatMap(MANAGER_BIDS, "children");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const item of [...productTabs, ...MANAGER_BIDS]) {
 | 
				
			||||||
 | 
					      if (!item.page_context) continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        if (!(await item.page_context.title())) {
 | 
				
			||||||
 | 
					          // await vì title() là async
 | 
				
			||||||
 | 
					          await safeClosePage(item);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.warn("⚠️ Error checking page_context.title()", e.message);
 | 
				
			||||||
 | 
					        await safeClosePage(item);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const page of pages) {
 | 
					    for (const page of pages) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua
 | 
					        if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@ import browser from "../system/browser.js";
 | 
				
			||||||
import CONSTANTS from "../system/constants.js";
 | 
					import CONSTANTS from "../system/constants.js";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  findEarlyLoginTime,
 | 
					  findEarlyLoginTime,
 | 
				
			||||||
 | 
					  getPathLocalData,
 | 
				
			||||||
  getPathProfile,
 | 
					  getPathProfile,
 | 
				
			||||||
  isTimeReached,
 | 
					  isTimeReached,
 | 
				
			||||||
  sanitizeFileName,
 | 
					  sanitizeFileName,
 | 
				
			||||||
| 
						 | 
					@ -141,4 +142,102 @@ export class ApiBid extends Bid {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return earlyLoginTime && isTimeReached(earlyLoginTime);
 | 
					    return earlyLoginTime && isTimeReached(earlyLoginTime);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async saveCodeToLocal({ name, code }) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const filePath = getPathLocalData(this.origin_url); // file path
 | 
				
			||||||
 | 
					      const dirPath = path.dirname(filePath); // lấy thư mục cha
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // kiểm tra folder chứa file đã tồn tại chưa
 | 
				
			||||||
 | 
					      if (!fs.existsSync(dirPath)) {
 | 
				
			||||||
 | 
					        fs.mkdirSync(dirPath, { recursive: true });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // ghi file
 | 
				
			||||||
 | 
					      fs.writeFileSync(
 | 
				
			||||||
 | 
					        filePath,
 | 
				
			||||||
 | 
					        JSON.stringify({ name, code, time: Date.now() }, null, 2) // format JSON đẹp
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.log(
 | 
				
			||||||
 | 
					        `%cerror [${this.id}] models/api-bid.js line:149`,
 | 
				
			||||||
 | 
					        "color: red; display: block; width: 100%;",
 | 
				
			||||||
 | 
					        error
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async clearCodeFromLocal() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const filePath = getPathLocalData(this.origin_url); // file path
 | 
				
			||||||
 | 
					      const dirPath = path.dirname(filePath); // lấy thư mục cha
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // kiểm tra folder chứa file đã tồn tại chưa
 | 
				
			||||||
 | 
					      if (!fs.existsSync(dirPath)) {
 | 
				
			||||||
 | 
					        fs.mkdirSync(dirPath, { recursive: true });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // ghi file
 | 
				
			||||||
 | 
					      fs.writeFileSync(
 | 
				
			||||||
 | 
					        filePath,
 | 
				
			||||||
 | 
					        JSON.stringify({}, null, 2) // format JSON đẹp
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.log(
 | 
				
			||||||
 | 
					        `%cerror [${this.id}] models/api-bid.js line:187`,
 | 
				
			||||||
 | 
					        "color: red; display: block; width: 100%;",
 | 
				
			||||||
 | 
					        error
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async loadCodeFromLocal() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const filePath = getPathLocalData(this.origin_url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!fs.existsSync(filePath)) {
 | 
				
			||||||
 | 
					        console.warn(
 | 
				
			||||||
 | 
					          `%cwarn [${this.id}] models/api-bid.js`,
 | 
				
			||||||
 | 
					          "color: orange; display: block; width: 100%;",
 | 
				
			||||||
 | 
					          `File not found: ${filePath}`
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return null; // hoặc {} tùy bạn muốn trả gì
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const fileContent = fs.readFileSync(filePath, "utf-8");
 | 
				
			||||||
 | 
					      const data = JSON.parse(fileContent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return data; // { name, code, time }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error(
 | 
				
			||||||
 | 
					        `%cerror [${this.id}] models/api-bid.js`,
 | 
				
			||||||
 | 
					        "color: red; display: block; width: 100%;",
 | 
				
			||||||
 | 
					        error
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async isCodeValid(minutes = 8) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const data = await this.loadCodeFromLocal();
 | 
				
			||||||
 | 
					      if (!data || !data.time) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const now = Date.now();
 | 
				
			||||||
 | 
					      const timeDiff = now - data.time; // tính chênh lệch thời gian (ms)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const validDuration = minutes * 60 * 1000; // phút -> mili giây
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return timeDiff <= validDuration;
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error(
 | 
				
			||||||
 | 
					        `%cerror [${this.id}] models/api-bid.js`,
 | 
				
			||||||
 | 
					        "color: red; display: block; width: 100%;",
 | 
				
			||||||
 | 
					        error
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,10 @@
 | 
				
			||||||
import fs from "fs";
 | 
					import fs from "fs";
 | 
				
			||||||
import configs from "../../system/config.js";
 | 
					import configs from "../../system/config.js";
 | 
				
			||||||
import { getPathProfile, safeClosePage } from "../../system/utils.js";
 | 
					import {
 | 
				
			||||||
 | 
					  getPathLocalData,
 | 
				
			||||||
 | 
					  getPathProfile,
 | 
				
			||||||
 | 
					  safeClosePage,
 | 
				
			||||||
 | 
					} from "../../system/utils.js";
 | 
				
			||||||
import { ApiBid } from "../api-bid.js";
 | 
					import { ApiBid } from "../api-bid.js";
 | 
				
			||||||
import _ from "lodash";
 | 
					import _ from "lodash";
 | 
				
			||||||
import { updateStatusByPrice } from "../../system/apis/bid.js";
 | 
					import { updateStatusByPrice } from "../../system/apis/bid.js";
 | 
				
			||||||
| 
						 | 
					@ -42,6 +46,169 @@ export class LangtonsApiBid extends ApiBid {
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async callSubmitPrevCodeApi(code, csrfToken) {
 | 
				
			||||||
 | 
					    if (!this.page_context) return;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const result = await this.page_context.evaluate(
 | 
				
			||||||
 | 
					        async (code, csrfToken) => {
 | 
				
			||||||
 | 
					          const formData = new FormData();
 | 
				
			||||||
 | 
					          formData.append("dwfrm_profile_login_code", code);
 | 
				
			||||||
 | 
					          formData.append("csrf_token", csrfToken);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          try {
 | 
				
			||||||
 | 
					            const response = await fetch(
 | 
				
			||||||
 | 
					              "https://www.langtons.com.au/on/demandware.store/Sites-langtons-Site/en_AU/Login-VerifyOtpForLogin",
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                method: "POST",
 | 
				
			||||||
 | 
					                body: formData,
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const data = await response.json();
 | 
				
			||||||
 | 
					            return { success: true, data };
 | 
				
			||||||
 | 
					          } catch (error) {
 | 
				
			||||||
 | 
					            return { success: false, error: error.toString() };
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        code,
 | 
				
			||||||
 | 
					        csrfToken
 | 
				
			||||||
 | 
					      ); // truyền biến vào page context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (result.success) {
 | 
				
			||||||
 | 
					        console.log(`[${this.id}] callInsideApi API response:`, result.data);
 | 
				
			||||||
 | 
					        return result.data;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        console.error(`[${this.id}] callInsideApi API error:`, result.error);
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error(`[${this.id}] Puppeteer evaluate error:`, error);
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async callSubmitAccountApi(csrfToken) {
 | 
				
			||||||
 | 
					    if (!this.page_context) return;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const result = await this.page_context.evaluate(
 | 
				
			||||||
 | 
					        async ({ username, password }, csrfToken) => {
 | 
				
			||||||
 | 
					          const formData = new FormData();
 | 
				
			||||||
 | 
					          formData.append("loginEmail", username);
 | 
				
			||||||
 | 
					          formData.append("loginPassword", password);
 | 
				
			||||||
 | 
					          formData.append("rememberMe", true);
 | 
				
			||||||
 | 
					          formData.append("csrf_token", csrfToken);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          try {
 | 
				
			||||||
 | 
					            const response = await fetch(
 | 
				
			||||||
 | 
					              "https://www.langtons.com.au/on/demandware.store/Sites-langtons-Site/en_AU/Account-Login?rurl=5",
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                method: "POST",
 | 
				
			||||||
 | 
					                body: formData,
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const data = await response.json();
 | 
				
			||||||
 | 
					            return { success: true, data };
 | 
				
			||||||
 | 
					          } catch (error) {
 | 
				
			||||||
 | 
					            return { success: false, error: error.toString() };
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        { usernam: this.username, password: this.password },
 | 
				
			||||||
 | 
					        csrfToken
 | 
				
			||||||
 | 
					      ); // truyền biến vào page context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (result.success) {
 | 
				
			||||||
 | 
					        console.log(
 | 
				
			||||||
 | 
					          `[${this.id}] callSubmitAccountApi API response:`,
 | 
				
			||||||
 | 
					          result.data
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return result.data;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        console.error(
 | 
				
			||||||
 | 
					          `[${this.id}] callSubmitAccountApi API error:`,
 | 
				
			||||||
 | 
					          result.error
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error(`[${this.id}] Puppeteer evaluate error:`, error);
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getCsrfToken() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const csrfToken = await this.page_context.evaluate(() => {
 | 
				
			||||||
 | 
					        const csrfInput = document.querySelector(
 | 
				
			||||||
 | 
					          'input[name*="csrf"], input[name="_token"]'
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return csrfInput ? csrfInput.value : null;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (csrfToken) {
 | 
				
			||||||
 | 
					        console.log(`✅ [${this.id}] CSRF token: ${csrfToken}`);
 | 
				
			||||||
 | 
					        return csrfToken;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        console.warn(`⚠️ [${this.id}] No CSRF token found.`);
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error(`❌ [${this.id}] Error getting CSRF token:`, error);
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async submitPrevCode() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const prevCodeData = await this.loadCodeFromLocal();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!prevCodeData) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.page_context.goto(
 | 
				
			||||||
 | 
					        "https://www.langtons.com.au/account/mfalogin?rurl=5"
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.page_context.waitForNavigation({
 | 
				
			||||||
 | 
					        timeout: 8000,
 | 
				
			||||||
 | 
					        waitUntil: "domcontentloaded",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!(await page.type("#code", code, { delay: 120 }))) {
 | 
				
			||||||
 | 
					        await this.clearCodeFromLocal();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await this.page_context.goto(configs.WEB_URLS.LANGTONS.LOGIN_URL);
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const csrfToken = await this.getCsrfToken();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!csrfToken) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const responsePrevCode = await this.callSubmitPrevCodeApi(
 | 
				
			||||||
 | 
					        prevCodeData.code,
 | 
				
			||||||
 | 
					        csrfToken
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!responsePrevCode || !responsePrevCode.success) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const responseAccount = await this.callSubmitAccountApi(csrfToken);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!responseAccount || !responseAccount.success) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // submit tiếp api login....
 | 
				
			||||||
 | 
					      await this.page_context.goto(this.url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error(`❌ [${this.id}] Error submitPrevCode:`, error);
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleLogin() {
 | 
					  async handleLogin() {
 | 
				
			||||||
    const page = this.page_context;
 | 
					    const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -60,6 +227,13 @@ export class LangtonsApiBid extends ApiBid {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // // check valid prev code
 | 
				
			||||||
 | 
					    // if (this.isCodeValid()) {
 | 
				
			||||||
 | 
					    //   const result = await this.submitPrevCode();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //   if (result) return;
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (fs.existsSync(filePath)) {
 | 
					    if (fs.existsSync(filePath)) {
 | 
				
			||||||
      console.log(`🗑 [${this.id}] Deleting existing file: ${filePath}`);
 | 
					      console.log(`🗑 [${this.id}] Deleting existing file: ${filePath}`);
 | 
				
			||||||
      fs.unlinkSync(filePath);
 | 
					      fs.unlinkSync(filePath);
 | 
				
			||||||
| 
						 | 
					@ -133,6 +307,9 @@ export class LangtonsApiBid extends ApiBid {
 | 
				
			||||||
        code,
 | 
					        code,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // save code to local
 | 
				
			||||||
 | 
					      // await this.saveCodeToLocal({ name, code });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // ⌨ Enter verification code
 | 
					      // ⌨ Enter verification code
 | 
				
			||||||
      console.log(`✍ [${this.id}] Entering verification code...`);
 | 
					      console.log(`✍ [${this.id}] Entering verification code...`);
 | 
				
			||||||
      await page.type("#code", code, { delay: 120 });
 | 
					      await page.type("#code", code, { delay: 120 });
 | 
				
			||||||
| 
						 | 
					@ -160,6 +337,9 @@ export class LangtonsApiBid extends ApiBid {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // await page.goto(this.url);
 | 
					      // await page.goto(this.url);
 | 
				
			||||||
      console.log(`✅ [${this.id}] Navigation successful!`);
 | 
					      console.log(`✅ [${this.id}] Navigation successful!`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // clear code
 | 
				
			||||||
 | 
					      // this.clearCodeFromLocal();
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error(
 | 
					      console.error(
 | 
				
			||||||
        `❌ [${this.id}] Error during login process:`,
 | 
					        `❌ [${this.id}] Error during login process:`,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,16 +1,17 @@
 | 
				
			||||||
import * as path from 'path';
 | 
					import * as path from "path";
 | 
				
			||||||
import { fileURLToPath } from 'url'; // ✅ Cần import từ 'url'
 | 
					import { fileURLToPath } from "url"; // ✅ Cần import từ 'url'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const __filename = fileURLToPath(import.meta.url);
 | 
					const __filename = fileURLToPath(import.meta.url);
 | 
				
			||||||
const __dirname = path.dirname(__filename);
 | 
					const __dirname = path.dirname(__filename);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const CONSTANTS = {
 | 
					const CONSTANTS = {
 | 
				
			||||||
    PROFILE_PATH: path.join(__dirname, 'profiles'),
 | 
					  PROFILE_PATH: path.join(__dirname, "profiles"),
 | 
				
			||||||
    ERROR_IMAGES_PATH: path.join(__dirname, 'error-images'),
 | 
					  LOCAL_DATA_PATH: path.join(__dirname, "local-data"),
 | 
				
			||||||
 | 
					  ERROR_IMAGES_PATH: path.join(__dirname, "error-images"),
 | 
				
			||||||
  TYPE_IMAGE: {
 | 
					  TYPE_IMAGE: {
 | 
				
			||||||
        ERRORS: 'errors',
 | 
					    ERRORS: "errors",
 | 
				
			||||||
        SUCCESS: 'success',
 | 
					    SUCCESS: "success",
 | 
				
			||||||
        WORK: 'work',
 | 
					    WORK: "work",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -133,6 +133,13 @@ export const getPathProfile = (origin_url) => {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getPathLocalData = (origin_url) => {
 | 
				
			||||||
 | 
					  return path.join(
 | 
				
			||||||
 | 
					    CONSTANTS.LOCAL_DATA_PATH,
 | 
				
			||||||
 | 
					    sanitizeFileName(origin_url) + ".json"
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function removeFalsyValues(obj, excludeKeys = []) {
 | 
					export function removeFalsyValues(obj, excludeKeys = []) {
 | 
				
			||||||
  return Object.entries(obj).reduce((acc, [key, value]) => {
 | 
					  return Object.entries(obj).reduce((acc, [key, value]) => {
 | 
				
			||||||
    if (value || excludeKeys.includes(key)) {
 | 
					    if (value || excludeKeys.includes(key)) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue