diff --git a/auto-bid-admin/src/components/dashboard/working-page.tsx b/auto-bid-admin/src/components/dashboard/working-page.tsx
index 223cda2..50f9d43 100644
--- a/auto-bid-admin/src/components/dashboard/working-page.tsx
+++ b/auto-bid-admin/src/components/dashboard/working-page.tsx
@@ -6,7 +6,7 @@ import { Socket } from "socket.io-client";
import { getImagesWorking } from "../../apis/bid";
import { useStatusToolStore } from "../../lib/zustand/use-status-tool-store";
import { IBid, IWebBid } from "../../system/type";
-import { cn, stringToColor } from "../../utils";
+import { cn, extractDomainSmart, stringToColor } from "../../utils";
import ShowImageModal from "./show-image-modal";
import { isTimeReached, subtractSeconds } from "../../lib/table/ultils";
export interface IWorkingPageProps {
@@ -129,7 +129,7 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
src={imageSrc}
/>
-
+
{isIBid(data) ? data.name : "Tracking page"}
@@ -207,11 +207,11 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
- {isIBid(data) ? data.web_bid.origin_url : data.origin_url}
+ {isIBid(data) ? extractDomainSmart(data.web_bid.origin_url) : extractDomainSmart(data.origin_url)}
diff --git a/auto-bid-admin/src/pages/bids.tsx b/auto-bid-admin/src/pages/bids.tsx
index 8edd0d2..9966fd7 100644
--- a/auto-bid-admin/src/pages/bids.tsx
+++ b/auto-bid-admin/src/pages/bids.tsx
@@ -18,13 +18,13 @@ import {
ShowHistoriesBidPicklesApiModal,
ShowHistoriesModal,
} from "../components/bid";
+import constants, { haveHistories } from "../constant";
import Table from "../lib/table/table";
import { IColumn, TRefTableFn } from "../lib/table/type";
import { useConfirmStore } from "../lib/zustand/use-confirm";
import { mappingStatusColors } from "../system/constants";
import { IBid } from "../system/type";
-import { formatTime } from "../utils";
-import constants, { haveHistories } from "../constant";
+import { extractDomainSmart, formatTime } from "../utils";
export default function Bids() {
const refTableFn: TRefTableFn = useRef({});
@@ -57,7 +57,7 @@ export default function Bids() {
title: "Web",
typeFilter: "text",
renderRow(row) {
- return {row.web_bid.origin_url};
+ return {extractDomainSmart(row.web_bid.origin_url)};
},
},
{
diff --git a/auto-bid-admin/src/utils/index.ts b/auto-bid-admin/src/utils/index.ts
index 086316e..7cb4a9d 100644
--- a/auto-bid-admin/src/utils/index.ts
+++ b/auto-bid-admin/src/utils/index.ts
@@ -157,14 +157,18 @@ export function findEarlyLoginTime(webBid: IWebBid): string | null {
const now = new Date();
// 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;
// Bước 2: Tìm bid có close_time gần hiện tại nhất
const closestBid = validChildren.reduce((closest, current) => {
- const closestDiff = Math.abs(new Date(closest.close_time!).getTime() - now.getTime());
- const currentDiff = Math.abs(new Date(current.close_time!).getTime() - now.getTime());
+ const closestDiff = Math.abs(
+ 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;
});
@@ -172,7 +176,28 @@ export function findEarlyLoginTime(webBid: IWebBid): string | null {
// Bước 3: Tính toán thời gian login sớm
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();
}
+
+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;
+ }
+}
diff --git a/auto-bid-tool/.gitignore b/auto-bid-tool/.gitignore
index 92344c3..e0b5f67 100644
--- a/auto-bid-tool/.gitignore
+++ b/auto-bid-tool/.gitignore
@@ -5,6 +5,7 @@
/public
/system/error-images
/system/profiles
+/system/local-data
# Logs
logs
diff --git a/auto-bid-tool/index.js b/auto-bid-tool/index.js
index 7c6a79f..87345dc 100644
--- a/auto-bid-tool/index.js
+++ b/auto-bid-tool/index.js
@@ -269,6 +269,20 @@ const clearLazyTab = async () => {
// product tabs
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) {
try {
if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua
diff --git a/auto-bid-tool/models/api-bid.js b/auto-bid-tool/models/api-bid.js
index 96f32fa..d389470 100644
--- a/auto-bid-tool/models/api-bid.js
+++ b/auto-bid-tool/models/api-bid.js
@@ -5,6 +5,7 @@ import browser from "../system/browser.js";
import CONSTANTS from "../system/constants.js";
import {
findEarlyLoginTime,
+ getPathLocalData,
getPathProfile,
isTimeReached,
sanitizeFileName,
@@ -141,4 +142,102 @@ export class ApiBid extends Bid {
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;
+ }
+ }
}
diff --git a/auto-bid-tool/models/langtons.com.au/langtons-api-bid.js b/auto-bid-tool/models/langtons.com.au/langtons-api-bid.js
index dc79ea4..84c6771 100644
--- a/auto-bid-tool/models/langtons.com.au/langtons-api-bid.js
+++ b/auto-bid-tool/models/langtons.com.au/langtons-api-bid.js
@@ -1,6 +1,10 @@
import fs from "fs";
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 _ from "lodash";
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() {
const page = this.page_context;
@@ -60,6 +227,13 @@ export class LangtonsApiBid extends ApiBid {
return;
}
+ // // check valid prev code
+ // if (this.isCodeValid()) {
+ // const result = await this.submitPrevCode();
+
+ // if (result) return;
+ // }
+
if (fs.existsSync(filePath)) {
console.log(`🗑 [${this.id}] Deleting existing file: ${filePath}`);
fs.unlinkSync(filePath);
@@ -133,6 +307,9 @@ export class LangtonsApiBid extends ApiBid {
code,
});
+ // save code to local
+ // await this.saveCodeToLocal({ name, code });
+
// ⌨ Enter verification code
console.log(`✍ [${this.id}] Entering verification code...`);
await page.type("#code", code, { delay: 120 });
@@ -160,6 +337,9 @@ export class LangtonsApiBid extends ApiBid {
// await page.goto(this.url);
console.log(`✅ [${this.id}] Navigation successful!`);
+
+ // clear code
+ // this.clearCodeFromLocal();
} catch (error) {
console.error(
`❌ [${this.id}] Error during login process:`,
diff --git a/auto-bid-tool/system/constants.js b/auto-bid-tool/system/constants.js
index 27ff2b7..b97e5ec 100644
--- a/auto-bid-tool/system/constants.js
+++ b/auto-bid-tool/system/constants.js
@@ -1,17 +1,18 @@
-import * as path from 'path';
-import { fileURLToPath } from 'url'; // ✅ Cần import từ 'url'
+import * as path from "path";
+import { fileURLToPath } from "url"; // ✅ Cần import từ 'url'
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const CONSTANTS = {
- PROFILE_PATH: path.join(__dirname, 'profiles'),
- ERROR_IMAGES_PATH: path.join(__dirname, 'error-images'),
- TYPE_IMAGE: {
- ERRORS: 'errors',
- SUCCESS: 'success',
- WORK: 'work',
- },
+ PROFILE_PATH: path.join(__dirname, "profiles"),
+ LOCAL_DATA_PATH: path.join(__dirname, "local-data"),
+ ERROR_IMAGES_PATH: path.join(__dirname, "error-images"),
+ TYPE_IMAGE: {
+ ERRORS: "errors",
+ SUCCESS: "success",
+ WORK: "work",
+ },
};
export default CONSTANTS;
diff --git a/auto-bid-tool/system/utils.js b/auto-bid-tool/system/utils.js
index 9ffed77..080e903 100644
--- a/auto-bid-tool/system/utils.js
+++ b/auto-bid-tool/system/utils.js
@@ -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 = []) {
return Object.entries(obj).reduce((acc, [key, value]) => {
if (value || excludeKeys.includes(key)) {