Some checks are pending
CI - Production Readiness / Verify (push) Waiting to run
190 lines
5.1 KiB
JavaScript
190 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const BASE_URL = [
|
|
process.env.OPS_BASE_URL,
|
|
process.env.APP_URL,
|
|
process.env.NEXT_PUBLIC_APP_URL
|
|
].map((value) => value?.trim()).find(Boolean);
|
|
|
|
const TEST_EMAIL = process.env.OPS_SESSION_CHECK_EMAIL?.trim();
|
|
const TEST_PASSWORD = process.env.OPS_SESSION_CHECK_PASSWORD?.trim();
|
|
const EXPECTED_TTL_SECONDS = resolveSessionTtl(process.env.SESSION_TTL_SECONDS);
|
|
const SESSION_COOKIE_NAME = "wa_inbox_session";
|
|
|
|
if (!BASE_URL) {
|
|
console.error("[ops-session-check] Missing OPS_BASE_URL / APP_URL / NEXT_PUBLIC_APP_URL");
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!TEST_EMAIL || !TEST_PASSWORD) {
|
|
console.error("[ops-session-check] Missing OPS_SESSION_CHECK_EMAIL / OPS_SESSION_CHECK_PASSWORD. Test skipped.");
|
|
process.exit(1);
|
|
}
|
|
|
|
function resolveSessionTtl(value) {
|
|
if (!value) {
|
|
return 60 * 60 * 24 * 7;
|
|
}
|
|
const parsed = Number(value.trim());
|
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
return 60 * 60 * 24 * 7;
|
|
}
|
|
return Math.floor(parsed);
|
|
}
|
|
|
|
function readCookieLines(headers) {
|
|
const direct = [];
|
|
if (typeof headers.getSetCookie === "function") {
|
|
const lines = headers.getSetCookie();
|
|
if (Array.isArray(lines)) {
|
|
return lines;
|
|
}
|
|
}
|
|
|
|
headers.forEach((value, key) => {
|
|
if (key.toLowerCase() === "set-cookie") {
|
|
direct.push(value);
|
|
}
|
|
});
|
|
|
|
return direct;
|
|
}
|
|
|
|
function getCookieValue(cookieHeader, name) {
|
|
const prefix = `${name}=`;
|
|
const found = cookieHeader.find((entry) => entry.startsWith(prefix));
|
|
if (!found) {
|
|
return null;
|
|
}
|
|
|
|
const [value] = found.split(";")[0].split("=", 2).slice(1);
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
return `${found.split(";")[0].slice(prefix.length)}`;
|
|
}
|
|
|
|
function getCookieTtlSeconds(cookieHeader, name) {
|
|
const prefix = `${name}=`;
|
|
const found = cookieHeader.find((entry) => entry.startsWith(prefix));
|
|
if (!found) {
|
|
return null;
|
|
}
|
|
|
|
const parts = found.split(";").map((part) => part.trim());
|
|
const attrs = new Map();
|
|
for (const part of parts.slice(1)) {
|
|
const [rawKey, rawValue] = part.split("=");
|
|
attrs.set(rawKey.toLowerCase(), rawValue ?? "");
|
|
}
|
|
|
|
const maxAgeRaw = attrs.get("max-age");
|
|
if (maxAgeRaw) {
|
|
const parsed = Number(maxAgeRaw);
|
|
if (Number.isFinite(parsed)) {
|
|
return Math.floor(parsed);
|
|
}
|
|
}
|
|
|
|
const expiresRaw = attrs.get("expires");
|
|
if (expiresRaw) {
|
|
const parsedDate = new Date(expiresRaw);
|
|
if (!Number.isNaN(parsedDate.getTime())) {
|
|
return Math.floor((parsedDate.getTime() - Date.now()) / 1000);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async function requestJson(url, options = {}) {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 12_000);
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
...options,
|
|
signal: controller.signal
|
|
});
|
|
clearTimeout(timeout);
|
|
return response;
|
|
} catch (error) {
|
|
clearTimeout(timeout);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const loginHeaders = {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
"User-Agent": "ops-session-check"
|
|
};
|
|
const loginBody = new URLSearchParams({
|
|
email: TEST_EMAIL,
|
|
password: TEST_PASSWORD,
|
|
next: "/super-admin"
|
|
}).toString();
|
|
|
|
const loginResponse = await requestJson(`${BASE_URL}/auth/login`, {
|
|
method: "POST",
|
|
redirect: "manual",
|
|
headers: loginHeaders,
|
|
body: loginBody
|
|
});
|
|
|
|
if (loginResponse.status !== 307 && loginResponse.status !== 302) {
|
|
console.error(`[ops-session-check] login status unexpected: ${loginResponse.status}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const setCookieLines = readCookieLines(loginResponse.headers);
|
|
const sessionCookieValue = getCookieValue(setCookieLines, SESSION_COOKIE_NAME);
|
|
if (!sessionCookieValue) {
|
|
console.error("[ops-session-check] session cookie not issued by /auth/login");
|
|
process.exit(1);
|
|
}
|
|
|
|
const ttl = getCookieTtlSeconds(setCookieLines, SESSION_COOKIE_NAME);
|
|
if (ttl === null) {
|
|
console.error("[ops-session-check] session cookie ttl missing");
|
|
process.exit(1);
|
|
}
|
|
|
|
if (Math.abs(ttl - EXPECTED_TTL_SECONDS) > 600) {
|
|
console.warn(
|
|
`[ops-session-check] session ttl unexpected: ${ttl}s (expected ${EXPECTED_TTL_SECONDS}s ±600s)`
|
|
);
|
|
} else {
|
|
console.log(`[ops-session-check] session ttl check OK: ${ttl}s`);
|
|
}
|
|
|
|
const protected = await requestJson(`${BASE_URL}/super-admin`, {
|
|
method: "GET",
|
|
redirect: "manual",
|
|
headers: {
|
|
Cookie: `${SESSION_COOKIE_NAME}=${sessionCookieValue}`
|
|
}
|
|
});
|
|
|
|
if (protected.status === 200) {
|
|
console.log("[ops-session-check] protected path access OK (HTTP 200).");
|
|
process.exit(0);
|
|
}
|
|
|
|
if (protected.status >= 300 && protected.status < 400) {
|
|
const location = protected.headers.get("location") || "";
|
|
if (location.includes("/login")) {
|
|
console.error("[ops-session-check] protected path redirected to login; session not accepted.");
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
console.error(`[ops-session-check] protected path check failed with status ${protected.status}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error("[ops-session-check] failed:", error instanceof Error ? error.message : String(error));
|
|
process.exit(1);
|
|
});
|