#!/usr/bin/env python3 """ provision-discourse.py -- Complete Discourse SSO signup and API key extraction. Flow: 1. Login to Cloudron panel (establish OIDC session) 2. Click Discourse login -> SSO via OpenID Connect 3. Complete signup page (enter username) 4. Check for API key generation options Usage: docker compose run --rm --entrypoint python3 provision provision-discourse.py """ import os, sys, time, re 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 = os.environ.get("CLOUDRON_BASE", "https://my.knownelement.com") DISCOURSE_URL = os.environ.get("DISCOURSE_URL", "https://community.turnsys.com") STATE_DIR = Path("/app/state") BW_ITEM = "vp-techops Cloudron" EMAIL = "tsgstaff-coo-vptechops@turnsys.com" USERNAME = "vptechops" def dump(page, label): ts = time.strftime("%H%M%S") try: page.screenshot(path=str(STATE_DIR / f"discourse-{label}-{ts}.png"), full_page=True) except Exception: pass body = page.evaluate("() => document.body.innerText.substring(0, 500)") print(f" [{label}] URL: {page.url}") print(f" Body: {body[:200]}") 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() password = bw.get_item_password(BW_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() # Step 1: Cloudron login print("=== STEP 1: Cloudron login ===") page.goto(f"{CLOUDRON_BASE}/login.html", wait_until="networkidle", timeout=15000) page.wait_for_timeout(2000) if "login" in page.url.lower() or "openid" in page.url.lower(): 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(3000) totp = page.query_selector("#inputTotpToken") if totp and totp.is_visible(): code = bw.get_totp(BW_ITEM) totp.click() page.keyboard.type(code) page.locator("#totpTokenSubmitButton").click() page.wait_for_timeout(5000) print(f" Cloudron: {page.url}") # Step 2: Discourse SSO print("=== STEP 2: Discourse SSO ===") page.goto(DISCOURSE_URL, wait_until="domcontentloaded", timeout=30000) page.wait_for_timeout(3000) # Click Log In button login_btn = page.locator(".login-button") if login_btn.count() == 0: login_btn = page.locator('button:has-text("Log In")') if login_btn.count() > 0: login_btn.first.click() page.wait_for_timeout(3000) dump(page, "01-login-modal") # Click OpenID Connect button sso_btn = page.locator('button:has-text("OpenID")') if sso_btn.count() > 0: sso_btn.first.click() page.wait_for_timeout(5000) dump(page, "02-after-sso") # Step 3: Handle signup or login complete print("=== STEP 3: Handle signup/login ===") if "/signup" in page.url: print(" On signup page -- need to create account") # Fill username page.wait_for_timeout(2000) # Look for username input username_input = None for sel in ['#new-account-username', 'input[name="username"]', 'input#new-account-username']: loc = page.locator(sel) if loc.count() > 0 and loc.first.is_visible(): username_input = loc.first break if username_input: username_input.click() page.keyboard.type(USERNAME) page.wait_for_timeout(1000) print(f" Entered username: {USERNAME}") dump(page, "03-username-entered") # Look for create/submit button for btn_text in ["Create Account", "Sign Up", "Register", "Submit", "Continue"]: loc = page.locator(f'button:has-text("{btn_text}"), [role="button"]:has-text("{btn_text}")') if loc.count() > 0 and loc.first.is_visible(): loc.first.click() page.wait_for_timeout(5000) print(f" Clicked: {btn_text}") break dump(page, "04-after-signup") else: print(" Could not find username input") dump(page, "03-no-username-input") elif "/login" in page.url: print(" Back on login page") else: print(" Appears logged in!") # Step 4: Check if we're authenticated now print("=== STEP 4: Verify authentication ===") page.goto(DISCOURSE_URL, wait_until="domcontentloaded", timeout=30000) page.wait_for_timeout(3000) # Check for user avatar or logged-in indicators body = page.evaluate("() => document.body.innerText") has_avatar = page.query_selector("#current-user, .current-user, [data-user-card]") is_logged_in = has_avatar is not None or USERNAME in body if is_logged_in: print(f" Discourse login SUCCESSFUL!") else: print(f" Discourse login status unclear") dump(page, "05-status-check") # Step 5: Check for API key options print("=== STEP 5: Check API key options ===") # Try user API key generation endpoint page.goto(f"{DISCOURSE_URL}/u/{USERNAME}/preferences/account", wait_until="domcontentloaded", timeout=30000) page.wait_for_timeout(3000) dump(page, "06-account-preferences") # Look for API key section body = page.evaluate("() => document.body.innerText") if "api key" in body.lower(): print(" API key section found!") else: print(" No API key section in user preferences") # Try the user API key generation flow page.goto(f"{DISCOURSE_URL}/user-api-key/new", wait_until="domcontentloaded", timeout=30000) page.wait_for_timeout(3000) dump(page, "07-user-api-key") body = page.evaluate("() => document.body.innerText") print(f" User API key page: {body[:200]}") browser.close() print("\n=== DONE ===") if __name__ == "__main__": main()