ultisuite-client/scripts/authentik-dom-dump-auth.mjs
R3D347HR4Y 9ea2d3325d
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(auth): enhance authentication flows with embedded support and UI improvements
- Updated login and signup components to utilize AuthCard for better user experience during redirection.
- Introduced AuthentikEmbedDialog for seamless integration of Authentik's identity portal within the application.
- Enhanced password recovery and signup flows with dynamic theme handling and improved loading states.
- Refactored existing components to streamline authentication processes and improve maintainability.
2026-06-21 00:12:45 +02:00

170 lines
5.5 KiB
JavaScript

// Headless dump of an AUTHENTICATED Authentik page (user-settings SPA, etc.).
//
// The user-settings interface (/auth/if/user/) redirects to login when
// unauthenticated, so a plain dump is useless. This variant logs in first.
//
// Two auth modes:
// 1) Recovery link (no password needed). Generate one on the host with:
// docker exec deploy-authentik-server-1 ak create_recovery_key 1 akadmin
// then pass the returned "/auth/recovery/use-token/<token>/" path.
// 2) Identification + password (set AK_USER / AK_PASS env vars).
//
// Usage:
// AK_RECOVERY="/auth/recovery/use-token/XXer/" \
// node scripts/authentik-dom-dump-auth.mjs "<targetUrl>" "<outName>" "<light|dark>"
// # or
// AK_USER=akadmin AK_PASS=... \
// node scripts/authentik-dom-dump-auth.mjs "<targetUrl>" "<outName>" "<light|dark>"
import { chromium } from "@playwright/test"
import { writeFileSync } from "node:fs"
const target = process.argv[2] ?? "http://localhost/auth/if/user/#/settings"
const outName = process.argv[3] ?? "user-settings"
const theme = process.argv[4] ?? "light"
const origin = process.env.AK_ORIGIN ?? "http://localhost"
const recovery = process.env.AK_RECOVERY ?? ""
const akUser = process.env.AK_USER ?? "akadmin"
const akPass = process.env.AK_PASS ?? ""
const outDir = "/tmp/authentik-dom"
await import("node:fs").then((fs) => fs.mkdirSync(outDir, { recursive: true }))
const browser = await chromium.launch()
const ctx = await browser.newContext({
viewport: { width: 1280, height: 900 },
colorScheme: theme === "dark" ? "dark" : "light",
ignoreHTTPSErrors: true,
})
const page = await ctx.newPage()
// Authentik advertises api.base as https://localhost, but nginx is http-only here.
await page.addInitScript(() => {
Object.defineProperty(window, "authentik", {
configurable: true,
set(v) {
try {
if (v && v.api) {
v.api.base = location.origin + "/auth/"
v.api.relBase = "/auth/"
}
} catch {}
Object.defineProperty(window, "authentik", {
value: v,
writable: true,
configurable: true,
})
},
get() {
return undefined
},
})
})
async function loginWithRecovery() {
const url = recovery.startsWith("http") ? recovery : origin + recovery
await page.goto(url, { waitUntil: "networkidle", timeout: 30000 })
// Recovery flow may present a "use this token" confirmation or land straight
// on the user interface. Give it time and click any primary continue button.
await page.waitForTimeout(2500)
for (let i = 0; i < 4; i++) {
const clicked = await page.evaluate(() => {
function findBtn(root) {
const btn = root.querySelector?.("button.pf-m-primary, button[type=submit]")
if (btn) return btn
const all = root.querySelectorAll?.("*") ?? []
for (const el of all) {
if (el.shadowRoot) {
const f = findBtn(el.shadowRoot)
if (f) return f
}
}
return null
}
const b = findBtn(document)
if (b) {
b.click()
return true
}
return false
})
if (!clicked) break
await page.waitForTimeout(1500)
}
}
async function loginWithPassword() {
await page.goto(`${origin}/auth/if/flow/default-authentication-flow/`, {
waitUntil: "networkidle",
timeout: 30000,
})
await page.waitForTimeout(2000)
// Fill identification, submit, then password, submit — reaching across shadow roots.
const typeAndSubmit = async (value) => {
await page.evaluate((val) => {
function findInput(root) {
const i = root.querySelector?.("input[name=uidField], input[name=password], input")
if (i) return i
for (const el of root.querySelectorAll?.("*") ?? []) {
if (el.shadowRoot) {
const f = findInput(el.shadowRoot)
if (f) return f
}
}
return null
}
const input = findInput(document)
if (input) {
input.value = val
input.dispatchEvent(new Event("input", { bubbles: true }))
}
}, value)
await page.keyboard.press("Enter")
await page.waitForTimeout(2500)
}
await typeAndSubmit(akUser)
await typeAndSubmit(akPass)
}
if (recovery) {
await loginWithRecovery()
} else {
await loginWithPassword()
}
// Now navigate to the authenticated target.
await page.goto(target, { waitUntil: "networkidle", timeout: 30000 })
await page.waitForTimeout(3500)
// Recursively serialize light + shadow DOM into an indented outline.
const outline = await page.evaluate(() => {
function attrs(el) {
return [...el.attributes]
.filter((a) => !["style"].includes(a.name))
.map((a) => (a.value ? `${a.name}="${a.value}"` : a.name))
.join(" ")
}
function walk(node, depth, lines) {
const pad = " ".repeat(depth)
if (node.nodeType === Node.ELEMENT_NODE) {
const tag = node.tagName.toLowerCase()
const a = attrs(node)
lines.push(`${pad}<${tag}${a ? " " + a : ""}>`)
if (node.shadowRoot) {
lines.push(`${pad} #shadow-root`)
for (const c of node.shadowRoot.children) walk(c, depth + 2, lines)
}
for (const c of node.children) walk(c, depth + 1, lines)
}
return lines
}
const lines = []
walk(document.documentElement, 0, lines)
return lines.slice(0, 2000).join("\n")
})
writeFileSync(`${outDir}/${outName}-${theme}.dom.txt`, outline)
await page.screenshot({ path: `${outDir}/${outName}-${theme}.png`, fullPage: true })
console.log(`wrote ${outDir}/${outName}-${theme}.dom.txt (+ .png) — final url: ${page.url()}`)
await browser.close()