Compare commits
2 Commits
cb7f99dc09
...
a8f468a497
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8f468a497 | ||
|
|
b18b8a85e0 |
40
Dockerfile
40
Dockerfile
@@ -1,15 +1,53 @@
|
|||||||
FROM oven/bun:latest
|
FROM oven/bun:latest
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Install Playwright system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libnss3 \
|
||||||
|
libnspr4 \
|
||||||
|
libatk1.0-0 \
|
||||||
|
libatk-bridge2.0-0 \
|
||||||
|
libcups2 \
|
||||||
|
libdrm2 \
|
||||||
|
libxkbcommon0 \
|
||||||
|
libxcomposite1 \
|
||||||
|
libxdamage1 \
|
||||||
|
libxfixes3 \
|
||||||
|
libxrandr2 \
|
||||||
|
libgbm1 \
|
||||||
|
libasound2 \
|
||||||
|
libpango-1.0-0 \
|
||||||
|
libcairo2 \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy dependency files
|
||||||
COPY package.json bun.lock ./
|
COPY package.json bun.lock ./
|
||||||
|
|
||||||
|
# Install dependencies (including Playwright)
|
||||||
RUN bun install --production
|
RUN bun install --production
|
||||||
|
|
||||||
|
# Install Playwright browsers
|
||||||
|
RUN bunx playwright install chromium --with-deps || true
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Make startup script executable
|
||||||
|
RUN chmod +x startup.sh
|
||||||
|
|
||||||
|
# Create non-root user for security
|
||||||
|
RUN adduser --disabled-password --gecos '' appuser && \
|
||||||
|
chown -R appuser:appuser /usr/src/app
|
||||||
|
|
||||||
|
USER appuser
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
CMD ["bun", "start"]
|
# Use startup script
|
||||||
|
CMD ["/bin/sh", "startup.sh"]
|
||||||
|
|||||||
17
bun.lock
17
bun.lock
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "dsas-cca-backend-bun",
|
"name": "dsas-cca-backend-bun",
|
||||||
@@ -15,10 +16,12 @@
|
|||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.49.0",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
|
"typescript-language-server": "^5.1.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5",
|
"typescript": "^5.9.3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -65,6 +68,8 @@
|
|||||||
|
|
||||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.1", "", { "os": "win32", "cpu": "x64" }, "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw=="],
|
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.1", "", { "os": "win32", "cpu": "x64" }, "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw=="],
|
||||||
|
|
||||||
|
"@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="],
|
"@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@22.15.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw=="],
|
"@types/node": ["@types/node@22.15.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw=="],
|
||||||
@@ -147,6 +152,8 @@
|
|||||||
|
|
||||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||||
|
|
||||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
@@ -203,6 +210,10 @@
|
|||||||
|
|
||||||
"path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
|
"path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
|
||||||
|
|
||||||
|
"playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="],
|
||||||
|
|
||||||
|
"playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
|
||||||
|
|
||||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||||
|
|
||||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||||
@@ -247,7 +258,9 @@
|
|||||||
|
|
||||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"typescript-language-server": ["typescript-language-server@5.1.3", "", { "bin": { "typescript-language-server": "lib/cli.mjs" } }, "sha512-r+pAcYtWdN8tKlYZPwiiHNA2QPjXnI02NrW5Sf2cVM3TRtuQ3V9EKKwOxqwaQ0krsaEXk/CbN90I5erBuf84Vg=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
|
|||||||
@@ -5,21 +5,29 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: dsas-cca-backend
|
container_name: dsas-cca-backend
|
||||||
ports:
|
ports:
|
||||||
- "${PORT}:${PORT}"
|
- "${PORT:-3000}:${PORT:-3000}"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- ./services/cookies.json:/usr/src/app/services/cookies.json
|
||||||
networks:
|
networks:
|
||||||
- cca_network
|
- cca_network
|
||||||
|
mem_limit: 1g
|
||||||
|
cpus: 1.0
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: "redis:8.0-alpine"
|
image: "redis:8.0-alpine"
|
||||||
container_name: dsas-cca-redis
|
container_name: dsas-cca-redis
|
||||||
command: redis-server --requirepass "dsas-cca"
|
command: redis-server --requirepass "dsas-cca"
|
||||||
volumes:
|
volumes:
|
||||||
- ./redis_data:/data
|
- redis_data:/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- cca_network
|
- cca_network
|
||||||
@@ -28,6 +36,10 @@ services:
|
|||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
mem_limit: 256m
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis_data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
cca_network:
|
cca_network:
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
// engage-api/get-activity.ts
|
// engage-api/get-activity.ts
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { readFile,writeFile,unlink } from 'fs/promises';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
import {
|
||||||
|
loginWithPlaywright,
|
||||||
|
loadCachedCookies,
|
||||||
|
saveCookiesToCache,
|
||||||
|
clearCookieCache,
|
||||||
|
getCachedCookieString
|
||||||
|
} from '../services/playwright-auth';
|
||||||
|
|
||||||
// Define interfaces for our data structures
|
// Define interfaces for our data structures
|
||||||
interface ActivityResponse {
|
interface ActivityResponse {
|
||||||
d: string;
|
d: string;
|
||||||
isError ? : boolean;
|
isError?: boolean;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,71 +20,19 @@ interface ActivityResponse {
|
|||||||
class AuthenticationError extends Error {
|
class AuthenticationError extends Error {
|
||||||
status: number;
|
status: number;
|
||||||
|
|
||||||
constructor(message: string = "Authentication failed, cookie may be invalid.", status ? : number) {
|
constructor(message: string = "Authentication failed, cookie may be invalid.", status?: number) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "AuthenticationError";
|
this.name = "AuthenticationError";
|
||||||
this.status = status || 0;
|
this.status = status || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In Bun, we can use import.meta.dir instead of the Node.js __dirname approach
|
/**
|
||||||
const COOKIE_FILE_PATH = resolve(import.meta.dir, 'nkcs-engage.cookie.txt');
|
* Test cookie validity by calling API
|
||||||
let _inMemoryCookie: string | null = null;
|
*/
|
||||||
|
async function testCookieValidityWithApi(cookieString: string): Promise<boolean> {
|
||||||
// Cookie Cache Helper Functions
|
|
||||||
async function loadCachedCookie(): Promise < string | null > {
|
|
||||||
if (_inMemoryCookie) {
|
|
||||||
logger.debug("Using in-memory cached cookie.");
|
|
||||||
return _inMemoryCookie;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const cookieFromFile = await readFile(COOKIE_FILE_PATH, 'utf8');
|
|
||||||
if (cookieFromFile) {
|
|
||||||
_inMemoryCookie = cookieFromFile;
|
|
||||||
logger.debug("Loaded cookie from file cache.");
|
|
||||||
return _inMemoryCookie;
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err.code === 'ENOENT') {
|
|
||||||
logger.debug("Cookie cache file not found. No cached cookie loaded.");
|
|
||||||
} else {
|
|
||||||
logger.warn("Error loading cookie from file:", err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveCookieToCache(cookieString: string): Promise < void > {
|
|
||||||
if (!cookieString) {
|
|
||||||
logger.warn("Attempted to save an empty or null cookie. Aborting save.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_inMemoryCookie = cookieString;
|
|
||||||
try {
|
|
||||||
await writeFile(COOKIE_FILE_PATH, cookieString, 'utf8');
|
|
||||||
logger.debug("Cookie saved to file cache.");
|
|
||||||
} catch (err: any) {
|
|
||||||
logger.error("Error saving cookie to file:", err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clearCookieCache(): Promise < void > {
|
|
||||||
_inMemoryCookie = null;
|
|
||||||
try {
|
|
||||||
await unlink(COOKIE_FILE_PATH);
|
|
||||||
logger.debug("Cookie cache file deleted.");
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err.code !== 'ENOENT') {
|
|
||||||
logger.error("Error deleting cookie file:", err.message);
|
|
||||||
} else {
|
|
||||||
logger.debug("Cookie cache file did not exist, no need to delete.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testCookieValidity(cookieString: string): Promise < boolean > {
|
|
||||||
if (!cookieString) return false;
|
if (!cookieString) return false;
|
||||||
logger.debug("Testing cookie validity...");
|
logger.debug('Testing cookie validity via API...');
|
||||||
|
|
||||||
const MAX_RETRIES = 3;
|
const MAX_RETRIES = 3;
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
@@ -98,123 +51,69 @@ async function testCookieValidity(cookieString: string): Promise < boolean > {
|
|||||||
};
|
};
|
||||||
|
|
||||||
logger.debug(`Attempt ${attempt}/${MAX_RETRIES}`);
|
logger.debug(`Attempt ${attempt}/${MAX_RETRIES}`);
|
||||||
await axios.post(url, payload, {
|
const response = await axios.post(url, payload, {
|
||||||
headers,
|
headers,
|
||||||
timeout: 20000
|
timeout: 20000
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug("Cookie test successful (API responded 2xx). Cookie is valid.");
|
// Check for 4xx errors (auth failures)
|
||||||
|
if (response.status >= 400 && response.status < 500) {
|
||||||
|
logger.warn(`Cookie test returned ${response.status}, likely invalid`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Cookie test successful (API responded 2xx). Cookie is valid.');
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.warn(`Cookie validity test failed (attempt ${attempt}/${MAX_RETRIES}).`);
|
logger.warn(`Cookie validity test failed (attempt ${attempt}/${MAX_RETRIES}).`);
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
logger.warn(`Cookie test API response status: ${error.response.status}.`);
|
// 4xx = auth failure (immediate fail)
|
||||||
|
if (error.response.status >= 400 && error.response.status < 500) {
|
||||||
|
logger.warn(`Cookie test API response status: ${error.response.status} (auth error)`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 5xx = server error (retry with delay)
|
||||||
|
logger.warn(`Cookie test API response status: ${error.response.status} (server error, retrying...)`);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`Network/other error: ${error.message}`);
|
// No response (000 status, network error, timeout)
|
||||||
|
logger.warn(`Network/timeout error: ${error.message} (retrying...)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attempt >= MAX_RETRIES) {
|
if (attempt < MAX_RETRIES) {
|
||||||
logger.warn("Max retries reached. Cookie is likely invalid or expired.");
|
await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.warn('Max retries reached. Cookie is likely invalid or expired.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Core API Interaction Functions
|
/**
|
||||||
async function getSessionId(): Promise < string | null > {
|
* Get complete cookies using Playwright
|
||||||
const url = 'https://engage.nkcswx.cn/Login.aspx';
|
*/
|
||||||
try {
|
async function getCompleteCookies(userName: string, userPwd: string): Promise<string> {
|
||||||
const response = await axios.get(url, {
|
logger.info('Attempting to get complete cookie string using Playwright login...');
|
||||||
headers: {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Bun DSAS-CCA get-activity Module)'
|
const cookies = await loginWithPlaywright(userName, userPwd);
|
||||||
}
|
|
||||||
});
|
if (!cookies || cookies.length === 0) {
|
||||||
const setCookieHeader = response.headers['set-cookie'];
|
throw new Error("Login failed: Could not obtain cookies.");
|
||||||
if (setCookieHeader && setCookieHeader.length > 0) {
|
|
||||||
const sessionIdCookie = setCookieHeader.find(cookie => cookie.trim().startsWith('ASP.NET_SessionId='));
|
|
||||||
if (sessionIdCookie) {
|
|
||||||
logger.debug('ASP.NET_SessionId created');
|
|
||||||
return sessionIdCookie.split(';')[0] || null; // Ensure a fallback to `null` if splitting fails
|
|
||||||
}
|
|
||||||
return null; // Explicitly return `null` if no cookie is found
|
|
||||||
}
|
|
||||||
logger.error("No ASP.NET_SessionId cookie found in Set-Cookie header.");
|
|
||||||
return null;
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error(`Error in getSessionId: ${error.response ? `${error.response.status} - ${error.response.statusText}` : error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cookieString = cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
||||||
|
return cookieString;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMSAUTH(sessionId: string, userName: string, userPwd: string, templateFilePath: string): Promise < string | null > {
|
/**
|
||||||
const url = 'https://engage.nkcswx.cn/Login.aspx';
|
* Get activity details from API
|
||||||
try {
|
*/
|
||||||
let templateData = await readFile(templateFilePath, 'utf8');
|
|
||||||
const postData = templateData
|
|
||||||
.replace('{{USERNAME}}', userName)
|
|
||||||
.replace('{{PASSWORD}}', userPwd);
|
|
||||||
const headers = {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Cookie': sessionId,
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Bun DSAS-CCA get-activity Module)',
|
|
||||||
'Referer': 'https://engage.nkcswx.cn/Login.aspx'
|
|
||||||
};
|
|
||||||
logger.debug('Getting .ASPXFORMSAUTH');
|
|
||||||
const response = await axios.post(url, postData, {
|
|
||||||
headers,
|
|
||||||
maxRedirects: 0,
|
|
||||||
validateStatus: (status) => status >= 200 && status < 400
|
|
||||||
});
|
|
||||||
const setCookieHeader = response.headers['set-cookie'];
|
|
||||||
let formsAuthCookieValue = null;
|
|
||||||
if (setCookieHeader && setCookieHeader.length > 0) {
|
|
||||||
const aspxAuthCookies = setCookieHeader.filter(cookie => cookie.trim().startsWith('.ASPXFORMSAUTH='));
|
|
||||||
if (aspxAuthCookies.length > 0) {
|
|
||||||
for (let i = aspxAuthCookies.length - 1; i >= 0; i--) {
|
|
||||||
const cookieCandidateParts = aspxAuthCookies[i].split(';');
|
|
||||||
if (cookieCandidateParts.length > 0 && cookieCandidateParts[0] !== undefined) { // Explicit check
|
|
||||||
const firstPart = cookieCandidateParts[0].trim();
|
|
||||||
if (firstPart.length > '.ASPXFORMSAUTH='.length && firstPart.substring('.ASPXFORMSAUTH='.length).length > 0) {
|
|
||||||
formsAuthCookieValue = firstPart;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (formsAuthCookieValue) {
|
|
||||||
logger.debug('.ASPXFORMSAUTH cookie obtained.');
|
|
||||||
return formsAuthCookieValue;
|
|
||||||
} else {
|
|
||||||
logger.error("No valid .ASPXFORMSAUTH cookie found. Headers:", setCookieHeader || "none");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.code === 'ENOENT') logger.error(`Error: Template file '${templateFilePath}' not found.`);
|
|
||||||
else logger.error(`Error in getMSAUTH: ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCompleteCookies(userName: string, userPwd: string, templateFilePath: string): Promise < string > {
|
|
||||||
logger.debug('Attempting to get complete cookie string (login process).');
|
|
||||||
const sessionId = await getSessionId();
|
|
||||||
if (!sessionId) throw new Error("Login failed: Could not obtain ASP.NET_SessionId.");
|
|
||||||
|
|
||||||
const msAuth = await getMSAUTH(sessionId, userName, userPwd, templateFilePath);
|
|
||||||
if (!msAuth) throw new Error("Login failed: Could not obtain .ASPXFORMSAUTH cookie.");
|
|
||||||
|
|
||||||
return `${sessionId}; ${msAuth}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getActivityDetailsRaw(
|
async function getActivityDetailsRaw(
|
||||||
activityId: string,
|
activityId: string,
|
||||||
cookies: string,
|
cookies: string,
|
||||||
maxRetries: number = 3,
|
maxRetries: number = 3,
|
||||||
timeoutMilliseconds: number = 20000
|
timeoutMilliseconds: number = 20000
|
||||||
): Promise < string | null > {
|
): Promise<string | null> {
|
||||||
const url = 'https://engage.nkcswx.cn/Services/ActivitiesService.asmx/GetActivityDetails';
|
const url = 'https://engage.nkcswx.cn/Services/ActivitiesService.asmx/GetActivityDetails';
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Type': 'application/json; charset=UTF-8',
|
'Content-Type': 'application/json; charset=UTF-8',
|
||||||
@@ -270,7 +169,6 @@ async function getActivityDetailsRaw(
|
|||||||
* @param activityId - The ID of the activity to fetch.
|
* @param activityId - The ID of the activity to fetch.
|
||||||
* @param userName - URL-encoded username.
|
* @param userName - URL-encoded username.
|
||||||
* @param userPwd - URL-encoded password.
|
* @param userPwd - URL-encoded password.
|
||||||
* @param templateFileName - Name of the login template file.
|
|
||||||
* @param forceLogin - If true, bypasses cached cookie and forces a new login.
|
* @param forceLogin - If true, bypasses cached cookie and forces a new login.
|
||||||
* @returns The parsed JSON object of activity details, or null on failure.
|
* @returns The parsed JSON object of activity details, or null on failure.
|
||||||
*/
|
*/
|
||||||
@@ -278,10 +176,9 @@ export async function fetchActivityData(
|
|||||||
activityId: string,
|
activityId: string,
|
||||||
userName: string,
|
userName: string,
|
||||||
userPwd: string,
|
userPwd: string,
|
||||||
templateFileName: string = "login_template.txt",
|
|
||||||
forceLogin: boolean = false
|
forceLogin: boolean = false
|
||||||
): Promise < any | null > {
|
): Promise<any | null> {
|
||||||
let currentCookie = forceLogin ? null : await loadCachedCookie();
|
let currentCookie = forceLogin ? null : await getCachedCookieString();
|
||||||
|
|
||||||
if (forceLogin && currentCookie) {
|
if (forceLogin && currentCookie) {
|
||||||
await clearCookieCache();
|
await clearCookieCache();
|
||||||
@@ -289,21 +186,25 @@ export async function fetchActivityData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentCookie) {
|
if (currentCookie) {
|
||||||
const isValid = await testCookieValidity(currentCookie);
|
const isValid = await testCookieValidityWithApi(currentCookie);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
logger.info("Cached cookie test failed or cookie expired. Clearing cache.");
|
logger.info('Cached cookie test failed or cookie expired. Clearing cache.');
|
||||||
await clearCookieCache();
|
await clearCookieCache();
|
||||||
currentCookie = null;
|
currentCookie = null;
|
||||||
} else {
|
} else {
|
||||||
logger.info("Using valid cached cookie.");
|
logger.info('Using valid cached cookie.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentCookie) {
|
if (!currentCookie) {
|
||||||
logger.info(forceLogin ? "Forcing new login." : "No valid cached cookie found or cache bypassed. Attempting login...");
|
logger.info(forceLogin ? 'Forcing new login.' : 'No valid cached cookie found or cache bypassed. Attempting login...');
|
||||||
try {
|
try {
|
||||||
currentCookie = await getCompleteCookies(userName, userPwd, resolve(import.meta.dir, templateFileName));
|
currentCookie = await getCompleteCookies(userName, userPwd);
|
||||||
await saveCookieToCache(currentCookie);
|
|
||||||
|
const cookies = await loadCachedCookies();
|
||||||
|
if (cookies) {
|
||||||
|
await saveCookiesToCache(cookies);
|
||||||
|
}
|
||||||
} catch (loginError) {
|
} catch (loginError) {
|
||||||
logger.error(`Login process failed: ${(loginError as Error).message}`);
|
logger.error(`Login process failed: ${(loginError as Error).message}`);
|
||||||
return null;
|
return null;
|
||||||
@@ -311,7 +212,7 @@ export async function fetchActivityData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!currentCookie) {
|
if (!currentCookie) {
|
||||||
logger.error("Critical: No cookie available after login attempt. Cannot fetch activity data.");
|
logger.error('Critical: No cookie available after login attempt. Cannot fetch activity data.');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,11 +230,15 @@ export async function fetchActivityData(
|
|||||||
await clearCookieCache();
|
await clearCookieCache();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info("Attempting re-login due to authentication failure...");
|
logger.info('Attempting re-login due to authentication failure...');
|
||||||
currentCookie = await getCompleteCookies(userName, userPwd, resolve(import.meta.dir, templateFileName));
|
currentCookie = await getCompleteCookies(userName, userPwd);
|
||||||
await saveCookieToCache(currentCookie);
|
|
||||||
|
const cookies = await loadCachedCookies();
|
||||||
|
if (cookies) {
|
||||||
|
await saveCookiesToCache(cookies);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("Re-login successful. Retrying request for activity details once...");
|
logger.info('Re-login successful. Retrying request for activity details once...');
|
||||||
const rawActivityDetailsStringRetry = await getActivityDetailsRaw(activityId, currentCookie);
|
const rawActivityDetailsStringRetry = await getActivityDetailsRaw(activityId, currentCookie);
|
||||||
if (rawActivityDetailsStringRetry) {
|
if (rawActivityDetailsStringRetry) {
|
||||||
const parsedOuterRetry = JSON.parse(rawActivityDetailsStringRetry);
|
const parsedOuterRetry = JSON.parse(rawActivityDetailsStringRetry);
|
||||||
@@ -351,6 +256,3 @@ export async function fetchActivityData(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally
|
|
||||||
//export { clearCookieCache,testCookieValidity };
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -3,7 +3,8 @@ API_PASSWORD=
|
|||||||
PORT=3000
|
PORT=3000
|
||||||
FIXED_STAFF_ACTIVITY_ID=7095
|
FIXED_STAFF_ACTIVITY_ID=7095
|
||||||
ALLOWED_ORIGINS=*
|
ALLOWED_ORIGINS=*
|
||||||
S3_ENDPOINT=
|
S3_ENDPOINT=
|
||||||
|
S3_PUBLIC_URL=
|
||||||
S3_BUCKET_NAME=
|
S3_BUCKET_NAME=
|
||||||
S3_ACCESS_KEY_ID=
|
S3_ACCESS_KEY_ID=
|
||||||
S3_SECRET_ACCESS_KEY=
|
S3_SECRET_ACCESS_KEY=
|
||||||
|
|||||||
13
package.json
13
package.json
@@ -3,13 +3,20 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun run index.ts",
|
"start": "bun run index.ts",
|
||||||
"dev": "bun run --watch index.ts"
|
"dev": "bun run --watch index.ts",
|
||||||
|
"playwright:install": "bunx playwright install",
|
||||||
|
"cookie:get": "bun run test/get-cookies.ts",
|
||||||
|
"test": "bun test",
|
||||||
|
"test:auth": "bun test test/auth.spec.ts",
|
||||||
|
"test:ui": "bunx playwright test --ui"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest"
|
"@playwright/test": "^1.49.0",
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"typescript-language-server": "^5.1.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5"
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
|
|||||||
90
playwright-report/index.html
Normal file
90
playwright-report/index.html
Normal file
File diff suppressed because one or more lines are too long
24
playwright.config.ts
Normal file
24
playwright.config.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { defineConfig, type PlaywrightTestConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './test',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: 'list',
|
||||||
|
use: {
|
||||||
|
baseURL: 'https://engage.nkcswx.cn',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
headless: true,
|
||||||
|
},
|
||||||
|
timeout: 60000,
|
||||||
|
expect: {
|
||||||
|
timeout: 5000,
|
||||||
|
},
|
||||||
|
webServer: {
|
||||||
|
command: 'echo "No web server needed"',
|
||||||
|
port: 0,
|
||||||
|
reuseExistingServer: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
32
services/cookies.json
Normal file
32
services/cookies.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "ASP.NET_SessionId",
|
||||||
|
"value": "fjurweoenv1wdcvhcyvhreqh",
|
||||||
|
"domain": "engage.nkcswx.cn",
|
||||||
|
"path": "/",
|
||||||
|
"expires": -1,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": true,
|
||||||
|
"sameSite": "None"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "usernameCookie",
|
||||||
|
"value": "1hDdyhfXMJP9S2CpwOgJZDKOXrEEXLB%23EXOogV55NRskzh6XKDU2rym1YrGfnoklj",
|
||||||
|
"domain": "engage.nkcswx.cn",
|
||||||
|
"path": "/",
|
||||||
|
"expires": 1778095681.649044,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": false,
|
||||||
|
"sameSite": "Lax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ".ASPXFORMSAUTH",
|
||||||
|
"value": "8E4B9D03FE08C5A2C6EB323B35110D6290CCF3408B68940F9783D1F9D37FA326A38457C956652C8A55D68218AA681485AB8C2533E4E7C85B2BF3413847009C18918281566DF940EA26761F8C0424B93AE79F519DDD0585DF19907E1F9231F5C020039960F5BFC53B7D429B1F3F83B5655F83D796",
|
||||||
|
"domain": "engage.nkcswx.cn",
|
||||||
|
"path": "/",
|
||||||
|
"expires": -1,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": false,
|
||||||
|
"sameSite": "Lax"
|
||||||
|
}
|
||||||
|
]
|
||||||
188
services/playwright-auth.ts
Normal file
188
services/playwright-auth.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import { chromium, type BrowserContext, type Cookie } from 'playwright';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
|
const LOGIN_URL = 'https://engage.nkcswx.cn/Login.aspx';
|
||||||
|
const COOKIE_FILE_PATH = resolve(import.meta.dir, 'cookies.json');
|
||||||
|
|
||||||
|
let _inMemoryCookies: Cookie[] | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login using Playwright and extract cookies
|
||||||
|
*/
|
||||||
|
export async function loginWithPlaywright(username: string, password: string): Promise<Cookie[]> {
|
||||||
|
logger.info('Starting Playwright login process...');
|
||||||
|
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const context = await browser.newContext({
|
||||||
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
logger.info(`Navigating to login page: ${LOGIN_URL}`);
|
||||||
|
await page.goto(LOGIN_URL, {
|
||||||
|
waitUntil: 'networkidle',
|
||||||
|
timeout: 30000
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('Login page loaded. Filling form...');
|
||||||
|
|
||||||
|
const usernameField = page.locator('input[name="ctl00$PageContent$loginControl$txtUN"]');
|
||||||
|
await usernameField.fill(decodeURIComponent(username));
|
||||||
|
|
||||||
|
const passwordField = page.locator('input[name="ctl00$PageContent$loginControl$txtPwd"]');
|
||||||
|
await passwordField.fill(decodeURIComponent(password));
|
||||||
|
|
||||||
|
const rememberMe = page.locator('input[name="ctl00$PageContent$loginControl$cbRememberMe"]');
|
||||||
|
await rememberMe.check().catch(() => {
|
||||||
|
logger.debug('Could not check remember me checkbox (optional)');
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginButton = page.locator('input[name="ctl00$PageContent$loginControl$btnLogin"]');
|
||||||
|
logger.info('Clicking login button...');
|
||||||
|
await loginButton.click();
|
||||||
|
|
||||||
|
await page.waitForLoadState('networkidle', { timeout: 30000 });
|
||||||
|
|
||||||
|
const isLoggedIn = await checkLoginSuccess(page);
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
const errorMessage = await page.locator('.error, .errorMessage, [class*="error"]').first().textContent();
|
||||||
|
throw new Error(`Login failed. Possible error: ${errorMessage || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Login successful! Extracting cookies...');
|
||||||
|
|
||||||
|
const cookies = await context.cookies();
|
||||||
|
logger.info(`Extracted ${cookies.length} cookies`);
|
||||||
|
|
||||||
|
await saveCookiesToCache(cookies);
|
||||||
|
logImportantCookies(cookies);
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
return cookies;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error during Playwright login:', error);
|
||||||
|
await browser.close();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if login was successful
|
||||||
|
*/
|
||||||
|
async function checkLoginSuccess(page: any): Promise<boolean> {
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
const currentUrl = page.url();
|
||||||
|
const notOnLoginPage = !currentUrl.includes('Login.aspx');
|
||||||
|
|
||||||
|
const hasLogoutLink = await page.locator('text=Logout, text=退出,text=Sign Out').count() > 0;
|
||||||
|
const hasWelcomeText = await page.locator('text=Welcome, text=欢迎').count() > 0;
|
||||||
|
|
||||||
|
return notOnLoginPage || hasLogoutLink || hasWelcomeText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log important cookies for debugging
|
||||||
|
*/
|
||||||
|
function logImportantCookies(cookies: Cookie[]): void {
|
||||||
|
const importantCookieNames = [
|
||||||
|
'ASP.NET_SessionId',
|
||||||
|
'.ASPXFORMSAUTH',
|
||||||
|
];
|
||||||
|
|
||||||
|
logger.debug('Important cookies:');
|
||||||
|
cookies.forEach(cookie => {
|
||||||
|
if (importantCookieNames.some(name => cookie.name.includes(name))) {
|
||||||
|
logger.debug(` ${cookie.name}: ${cookie.value.substring(0, 50)}${cookie.value.length > 50 ? '...' : ''}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load cookies from cache file
|
||||||
|
*/
|
||||||
|
export async function loadCachedCookies(): Promise<Cookie[] | null> {
|
||||||
|
if (_inMemoryCookies) {
|
||||||
|
logger.debug('Using in-memory cached cookies.');
|
||||||
|
return _inMemoryCookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(COOKIE_FILE_PATH)) {
|
||||||
|
logger.debug('Cookie cache file not found. No cached cookies loaded.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cookies = JSON.parse(fs.readFileSync(COOKIE_FILE_PATH, 'utf-8')) as Cookie[];
|
||||||
|
_inMemoryCookies = cookies;
|
||||||
|
logger.debug(`Loaded ${cookies.length} cookies from file cache.`);
|
||||||
|
return cookies;
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.warn('Error loading cookies from file:', error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save cookies to cache file
|
||||||
|
*/
|
||||||
|
export async function saveCookiesToCache(cookies: Cookie[]): Promise<void> {
|
||||||
|
if (!cookies || cookies.length === 0) {
|
||||||
|
logger.warn('Attempted to save empty or null cookies. Aborting save.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_inMemoryCookies = cookies;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.promises.writeFile(COOKIE_FILE_PATH, JSON.stringify(cookies, null, 2), 'utf-8');
|
||||||
|
logger.debug('Cookies saved to file cache.');
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Error saving cookies to file:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cookie cache
|
||||||
|
*/
|
||||||
|
export async function clearCookieCache(): Promise<void> {
|
||||||
|
_inMemoryCookies = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.promises.unlink(COOKIE_FILE_PATH);
|
||||||
|
logger.debug('Cookie cache file deleted.');
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code !== 'ENOENT') {
|
||||||
|
logger.error('Error deleting cookie file:', error.message);
|
||||||
|
} else {
|
||||||
|
logger.debug('Cookie cache file did not exist, no need to delete.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert cookies array to cookie string for axios
|
||||||
|
*/
|
||||||
|
export function cookiesToString(cookies: Cookie[]): string {
|
||||||
|
return cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cookie string from cache
|
||||||
|
*/
|
||||||
|
export async function getCachedCookieString(): Promise<string | null> {
|
||||||
|
const cookies = await loadCachedCookies();
|
||||||
|
if (!cookies || cookies.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return cookiesToString(cookies);
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ const S3_REGION = process.env.S3_REGION;
|
|||||||
const S3_ACCESS_KEY_ID = process.env.S3_ACCESS_KEY_ID;
|
const S3_ACCESS_KEY_ID = process.env.S3_ACCESS_KEY_ID;
|
||||||
const S3_SECRET_ACCESS_KEY = process.env.S3_SECRET_ACCESS_KEY;
|
const S3_SECRET_ACCESS_KEY = process.env.S3_SECRET_ACCESS_KEY;
|
||||||
const BUCKET_NAME = process.env.S3_BUCKET_NAME;
|
const BUCKET_NAME = process.env.S3_BUCKET_NAME;
|
||||||
|
const S3_PUBLIC_URL = process.env.S3_PUBLIC_URL;
|
||||||
const PUBLIC_URL_FILE_PREFIX = (process.env.S3_PUBLIC_URL_PREFIX || 'files').replace(/\/$/, '');
|
const PUBLIC_URL_FILE_PREFIX = (process.env.S3_PUBLIC_URL_PREFIX || 'files').replace(/\/$/, '');
|
||||||
|
|
||||||
// Initialize S3 client
|
// Initialize S3 client
|
||||||
@@ -195,6 +196,7 @@ export async function deleteS3Objects(objectKeysArray: string[]): Promise<boolea
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs the public S3 URL for an object key.
|
* Constructs the public S3 URL for an object key.
|
||||||
|
* Uses S3_PUBLIC_URL if set (reverse proxy scenario), otherwise uses S3_ENDPOINT.
|
||||||
* @param objectKey - The key of the object in S3
|
* @param objectKey - The key of the object in S3
|
||||||
* @returns The full public URL
|
* @returns The full public URL
|
||||||
*/
|
*/
|
||||||
@@ -202,8 +204,8 @@ export function constructS3Url(objectKey: string): string {
|
|||||||
if (!S3_ENDPOINT || !BUCKET_NAME) {
|
if (!S3_ENDPOINT || !BUCKET_NAME) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
// Ensure S3_ENDPOINT does not end with a slash
|
// Use S3_PUBLIC_URL if set (reverse proxy), otherwise use S3_ENDPOINT
|
||||||
const s3Base = S3_ENDPOINT.replace(/\/$/, '');
|
const s3Base = (S3_PUBLIC_URL || S3_ENDPOINT).replace(/\/$/, '');
|
||||||
// Ensure BUCKET_NAME does not start or end with a slash
|
// Ensure BUCKET_NAME does not start or end with a slash
|
||||||
const bucket = BUCKET_NAME.replace(/^\//, '').replace(/\/$/, '');
|
const bucket = BUCKET_NAME.replace(/^\//, '').replace(/\/$/, '');
|
||||||
// Ensure objectKey does not start with a slash
|
// Ensure objectKey does not start with a slash
|
||||||
|
|||||||
25
startup.sh
Executable file
25
startup.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 Starting DSAS CCA Backend..."
|
||||||
|
|
||||||
|
# Check if cookies exist and are valid
|
||||||
|
if [ -f /usr/src/app/services/cookies.json ]; then
|
||||||
|
echo "📁 Cookies file found. Checking validity..."
|
||||||
|
|
||||||
|
# Try to fetch a simple activity to test cookies
|
||||||
|
# If it fails, we'll get fresh cookies
|
||||||
|
if ! timeout 10 bun run test/test-cookies-validity.ts 2>/dev/null; then
|
||||||
|
echo "⚠️ Cookies are invalid or expired. Getting fresh cookies..."
|
||||||
|
bun run test/get-cookies.ts
|
||||||
|
else
|
||||||
|
echo "✅ Cookies are valid. Using cached cookies."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "📁 No cookies file found. Getting fresh cookies..."
|
||||||
|
bun run test/get-cookies.ts
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
echo "🎯 Starting application..."
|
||||||
|
exec bun run index.ts
|
||||||
118
test/auth.spec.ts
Normal file
118
test/auth.spec.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { test, expect } from 'bun:test';
|
||||||
|
import { chromium, type Cookie } from 'playwright';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { resolve, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const COOKIE_FILE_PATH = resolve(__dirname, '../services/cookies.json');
|
||||||
|
|
||||||
|
const testUsername = process.env.API_USERNAME || 'test@test.com';
|
||||||
|
const testPassword = process.env.API_PASSWORD || 'test123';
|
||||||
|
|
||||||
|
test('should login and extract cookies successfully', async () => {
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const context = await browser.newContext({
|
||||||
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
await page.goto('https://engage.nkcswx.cn/Login.aspx', {
|
||||||
|
waitUntil: 'networkidle',
|
||||||
|
timeout: 60000
|
||||||
|
});
|
||||||
|
|
||||||
|
const usernameField = page.locator('input[name="ctl00$PageContent$loginControl$txtUN"]');
|
||||||
|
await usernameField.fill(decodeURIComponent(testUsername));
|
||||||
|
|
||||||
|
const passwordField = page.locator('input[name="ctl00$PageContent$loginControl$txtPwd"]');
|
||||||
|
await passwordField.fill(decodeURIComponent(testPassword));
|
||||||
|
|
||||||
|
const loginButton = page.locator('input[name="ctl00$PageContent$loginControl$btnLogin"]');
|
||||||
|
await loginButton.click();
|
||||||
|
|
||||||
|
await page.waitForLoadState('networkidle', { timeout: 60000 });
|
||||||
|
|
||||||
|
const cookies = await context.cookies();
|
||||||
|
|
||||||
|
expect(cookies).toBeDefined();
|
||||||
|
expect(cookies.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const hasSessionCookie = cookies.some(c => c.name === 'ASP.NET_SessionId');
|
||||||
|
expect(hasSessionCookie).toBe(true);
|
||||||
|
|
||||||
|
fs.writeFileSync(COOKIE_FILE_PATH, JSON.stringify(cookies, null, 2));
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
}, 120000);
|
||||||
|
|
||||||
|
test('should load cookies from file if exists', () => {
|
||||||
|
if (!fs.existsSync(COOKIE_FILE_PATH)) {
|
||||||
|
throw new Error('Cookie file does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = JSON.parse(fs.readFileSync(COOKIE_FILE_PATH, 'utf-8')) as Cookie[];
|
||||||
|
expect(cookies.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should test cookie validity', async () => {
|
||||||
|
if (!fs.existsSync(COOKIE_FILE_PATH)) {
|
||||||
|
throw new Error('Cookie file does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = JSON.parse(fs.readFileSync(COOKIE_FILE_PATH, 'utf-8')) as Cookie[];
|
||||||
|
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const context = await browser.newContext();
|
||||||
|
await context.addCookies(cookies);
|
||||||
|
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
await page.goto('https://engage.nkcswx.cn/', {
|
||||||
|
waitUntil: 'networkidle',
|
||||||
|
timeout: 30000
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = page.url();
|
||||||
|
const isRedirectedToLogin = url.includes('/Login.aspx');
|
||||||
|
|
||||||
|
expect(isRedirectedToLogin).toBe(false);
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
test('should convert cookies to string format', () => {
|
||||||
|
if (!fs.existsSync(COOKIE_FILE_PATH)) {
|
||||||
|
throw new Error('Cookie file does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = JSON.parse(fs.readFileSync(COOKIE_FILE_PATH, 'utf-8')) as Cookie[];
|
||||||
|
|
||||||
|
const cookieString = cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
||||||
|
expect(cookieString).toBeDefined();
|
||||||
|
expect(cookieString.length).toBeGreaterThan(0);
|
||||||
|
expect(cookieString).toContain('ASP.NET_SessionId=');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should clear cookie cache', () => {
|
||||||
|
if (fs.existsSync(COOKIE_FILE_PATH)) {
|
||||||
|
fs.unlinkSync(COOKIE_FILE_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = fs.existsSync(COOKIE_FILE_PATH);
|
||||||
|
expect(exists).toBe(false);
|
||||||
|
});
|
||||||
24
test/get-cookies.ts
Normal file
24
test/get-cookies.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { loginWithPlaywright, saveCookiesToCache } from '../services/playwright-auth';
|
||||||
|
|
||||||
|
const username = process.env.API_USERNAME;
|
||||||
|
const password = process.env.API_PASSWORD;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
console.error('❌ API_USERNAME and API_PASSWORD environment variables are required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔑 Starting cookie extraction...\n');
|
||||||
|
|
||||||
|
loginWithPlaywright(username, password)
|
||||||
|
.then(cookies => {
|
||||||
|
console.log(`\n✅ Extracted ${cookies.length} cookies`);
|
||||||
|
console.log('📁 Cookies saved to: ./services/cookies.json');
|
||||||
|
|
||||||
|
saveCookiesToCache(cookies);
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('\n❌ Cookie extraction failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
35
test/test-cookies-validity.ts
Normal file
35
test/test-cookies-validity.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const COOKIE_FILE = './services/cookies.json';
|
||||||
|
|
||||||
|
async function testCookies() {
|
||||||
|
try {
|
||||||
|
const fs = await import('fs');
|
||||||
|
if (!fs.existsSync(COOKIE_FILE)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = JSON.parse(fs.readFileSync(COOKIE_FILE, 'utf-8'));
|
||||||
|
const cookieString = cookies.map((c: any) => `${c.name}=${c.value}`).join('; ');
|
||||||
|
|
||||||
|
const url = 'https://engage.nkcswx.cn/Services/ActivitiesService.asmx/GetActivityDetails';
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json; charset=UTF-8',
|
||||||
|
'Cookie': cookieString,
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Bun DSAS-CCA)',
|
||||||
|
};
|
||||||
|
const payload = { "activityID": "3350" };
|
||||||
|
|
||||||
|
await axios.post(url, payload, {
|
||||||
|
headers,
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await testCookies();
|
||||||
|
process.exit(isValid ? 0 : 1);
|
||||||
Reference in New Issue
Block a user