update clear lazy tab

This commit is contained in:
Admin 2025-05-10 09:04:15 +07:00
parent 0ca1e6846c
commit 46a012b673
9 changed files with 348 additions and 21 deletions

View File

@ -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}
/>
<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">
{isIBid(data) ? data.name : "Tracking page"}
</Text>
@ -207,11 +207,11 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
<Badge
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"
>
{isIBid(data) ? data.web_bid.origin_url : data.origin_url}
{isIBid(data) ? extractDomainSmart(data.web_bid.origin_url) : extractDomainSmart(data.origin_url)}
</Badge>
</Box>
</Box>

View File

@ -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<IBid> = useRef({});
@ -57,7 +57,7 @@ export default function Bids() {
title: "Web",
typeFilter: "text",
renderRow(row) {
return <span>{row.web_bid.origin_url}</span>;
return <span>{extractDomainSmart(row.web_bid.origin_url)}</span>;
},
},
{

View File

@ -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;
}
}

View File

@ -5,6 +5,7 @@
/public
/system/error-images
/system/profiles
/system/local-data
# Logs
logs

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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:`,

View File

@ -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;

View File

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