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)) {