#!/usr/bin/env python3 """ probe-discourse-signup.py -- Dump Discourse signup validation state. Gets an agent to the OIDC-authenticated /signup page, fills a candidate username, clicks Create Account, and dumps every field hint/error plus the page state 8s later. Reveals why "account created" never sticks. Usage: docker compose run --rm --entrypoint python3 \ -e AGENT=vp-secops provision probe-discourse-signup.py """ import os, sys, time from pathlib import Path from playwright.sync_api import sync_playwright sys.path.insert(0, os.path.dirname(__file__)) from bw_helper import BitwardenHelper CLOUDRON_BASE = "https://my.knownelement.com" DISCOURSE_URL = "https://community.turnsys.com" STATE_DIR = Path("/app/state") AGENT = os.environ.get("AGENT", "vp-secops") EMAILS = { "vp-secops": "tsgstaff-coo-vpsecops@turnsys.com", "svp-knel": "tsgstaff-coo-svpknel@turnsys.com", } USERNAME = AGENT.replace("-", "") def field_state(page): return page.evaluate("""() => { const out = []; document.querySelectorAll('input, .tip, .invalid, .good, .bad, .warning, [class*="hint"]').forEach(el => { const id = el.id || ''; const val = (el.value || '').substring(0, 40); const text = (el.textContent || '').trim().substring(0, 100); const cls = (el.getAttribute('class') || '').substring(0, 60); const vis = el.offsetParent !== null; if (text || val || id) out.push(`<${el.tagName.toLowerCase()}> id=${id} class=${cls} value="${val}" vis=${vis} text="${text}"`); }); return out.join('\\n'); }""") def main(): bw = BitwardenHelper( client_id=os.environ["BW_CLIENTID"], client_secret=os.environ["BW_CLIENTSECRET"], password=os.environ["BW_PASSWORD"], server_url=os.environ.get("BW_SERVER", ""), ) bw.login() item = f"{AGENT} Cloudron" email = EMAILS.get(AGENT, f"{AGENT}@turnsys.com") password = bw.get_item_password(item) with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context(viewport={"width": 1280, "height": 1024}) page = context.new_page() # Panel login page.goto(f"{CLOUDRON_BASE}/login.html", wait_until="networkidle", timeout=30000) page.wait_for_timeout(2000) page.wait_for_selector("#inputPassword", timeout=15000) page.click("#inputUsername") page.keyboard.type(email) page.click("#inputPassword") page.keyboard.type(password) page.locator('[role="button"]:has-text("Log in")').first.click() page.wait_for_timeout(4000) totp = page.query_selector("#inputTotpToken") if totp and totp.is_visible(): totp.click() page.keyboard.type(bw.get_totp(item)) page.locator("#totpTokenSubmitButton").click() page.wait_for_timeout(5000) print(f"PANEL: {page.url}") # Discourse: login modal -> OpenID page.goto(DISCOURSE_URL, wait_until="domcontentloaded", timeout=30000) page.wait_for_timeout(3000) page.locator(".login-button").first.click() page.wait_for_timeout(3000) page.locator('button:has-text("OpenID")').first.click() page.wait_for_timeout(5000) # Handle any OIDC interaction for _ in range(4): pw = page.query_selector("#inputPassword") if pw and pw.is_visible(): page.click("#inputUsername"); page.keyboard.type(email) page.click("#inputPassword"); page.keyboard.type(password) page.locator('[role="button"]:has-text("Log in")').first.click() page.wait_for_timeout(4000) continue t = page.query_selector("#inputTotpToken") if t and t.is_visible(): t.click(); page.keyboard.type(bw.get_totp(item)) page.locator("#totpTokenSubmitButton").click() page.wait_for_timeout(5000) continue if "community.turnsys.com" in page.url: break page.wait_for_timeout(2500) print(f"POST-OIDC: {page.url}") if "/signup" not in page.url: print("NOT ON SIGNUP -- dumping body:") print(page.evaluate("() => document.body.innerText.substring(0, 400)")) browser.close() return # Fill username page.wait_for_timeout(2000) inp = page.locator("#new-account-username, input[name='username']").first inp.click() page.keyboard.type(USERNAME) page.wait_for_timeout(2000) print(f"\n=== FIELD STATE after typing {USERNAME} ===") print(field_state(page)) # Click create for btn in ["Create Account", "Sign Up"]: loc = page.locator(f'button:has-text("{btn}")') if loc.count() > 0 and loc.first.is_visible(): loc.first.click() print(f"\nClicked: {btn}") break page.wait_for_timeout(8000) print(f"\n=== 8s LATER: URL={page.url} ===") print(field_state(page)) body = page.evaluate("() => document.body.innerText.substring(0, 400)") print(f"BODY: {body!r}") page.screenshot(path=str(STATE_DIR / f"discourse-signup-{AGENT}-{time.strftime('%H%M%S')}.png"), full_page=True) browser.close() if __name__ == "__main__": main()