"""Umami initial setup scenario. Automates the first-time setup for a fresh Umami installation. This scenario: 1. Navigates to the Umami login page 2. Logs in with default credentials (admin / umami) 3. Changes the admin password 4. Optionally adds the first website to track """ import secrets import string from typing import Any from playwright.async_api import Page from app.playwright_scenarios import register_scenario from app.playwright_scenarios.base import BaseScenario, ScenarioOptions, ScenarioResult def generate_secure_password(length: int = 24) -> str: """Generate a cryptographically secure password. Args: length: Password length (default: 24) Returns: A secure random password with mixed characters """ alphabet = string.ascii_letters + string.digits + "!@#$%^&*" password = [ secrets.choice(string.ascii_lowercase), secrets.choice(string.ascii_uppercase), secrets.choice(string.digits), secrets.choice("!@#$%^&*"), ] password.extend(secrets.choice(alphabet) for _ in range(length - 4)) password_list = list(password) secrets.SystemRandom().shuffle(password_list) return "".join(password_list) @register_scenario class UmamiInitialSetup(BaseScenario): """Automate Umami first-time setup. This scenario handles the initial configuration after Umami is deployed. Umami ships with default credentials (admin / umami). This scenario logs in with those defaults, changes the password, and optionally adds the first website to track. Required inputs: base_url: The Umami instance URL (e.g., https://analytics.example.com) Optional inputs: admin_password: New password for admin (auto-generated if not provided) website_name: Name of the first website to add website_url: URL of the first website to track Result data: setup_completed: Whether initial setup was completed admin_password: The new admin password - STORE SECURELY password_changed: Whether the default password was changed website_added: Whether a website was added already_configured: True if default password no longer works """ @property def name(self) -> str: return "umami_initial_setup" @property def required_inputs(self) -> list[str]: return ["base_url"] @property def optional_inputs(self) -> list[str]: return ["admin_password", "website_name", "website_url"] @property def description(self) -> str: return "Automate Umami first-time password change and website setup" async def execute( self, page: Page, inputs: dict[str, Any], options: ScenarioOptions, ) -> ScenarioResult: """Execute the Umami initial setup. Args: page: Playwright Page object inputs: Scenario inputs (base_url) options: Scenario options Returns: ScenarioResult with setup status and credentials """ base_url = inputs["base_url"].rstrip("/") new_password = inputs.get("admin_password") or generate_secure_password() website_name = inputs.get("website_name") website_url = inputs.get("website_url") screenshots = [] result_data = { "setup_completed": False, "admin_password": new_password, "password_changed": False, "website_added": False, "already_configured": False, } try: # Navigate to Umami login page login_url = f"{base_url}/login" await page.goto(login_url, wait_until="networkidle") # Look for login form username_input = page.locator( 'input[name="username"], ' 'input[id="username"], ' 'input[placeholder*="username" i]' ) await username_input.wait_for(state="visible", timeout=10000) # Try default credentials: admin / umami await username_input.fill("admin") password_input = page.locator( 'input[name="password"], ' 'input[type="password"]' ).first await password_input.fill("umami") # Click login login_button = page.locator( 'button:has-text("Login"), ' 'button:has-text("Sign in"), ' 'button[type="submit"]' ).first await login_button.click() # Wait for navigation await page.wait_for_timeout(3000) # Check if login succeeded current_url = page.url if "/login" in current_url: # Default password may have already been changed error_el = page.locator( '.error, [class*="error"], [class*="alert"]' ) if await error_el.count() > 0: result_data["already_configured"] = True result_data["setup_completed"] = True return ScenarioResult( success=True, data=result_data, screenshots=screenshots, error=None, ) # Logged in successfully with default password - change it # Navigate to profile/settings to change password settings_url = f"{base_url}/settings/profile" await page.goto(settings_url, wait_until="networkidle") # Look for password change form current_password_input = page.locator( 'input[name="currentPassword"], ' 'input[name="current_password"], ' 'input[placeholder*="current" i]' ).first if await current_password_input.count() > 0: await current_password_input.wait_for(state="visible", timeout=10000) await current_password_input.fill("umami") new_password_input = page.locator( 'input[name="newPassword"], ' 'input[name="new_password"], ' 'input[placeholder*="new" i]' ).first await new_password_input.fill(new_password) confirm_password_input = page.locator( 'input[name="confirmPassword"], ' 'input[name="confirm_password"], ' 'input[placeholder*="confirm" i]' ).first if await confirm_password_input.count() > 0: await confirm_password_input.fill(new_password) # Save password save_button = page.locator( 'button:has-text("Save"), ' 'button:has-text("Change"), ' 'button:has-text("Update"), ' 'button[type="submit"]' ).first await save_button.click() await page.wait_for_timeout(2000) # Check for success success_el = page.locator( '[class*="success"], ' ':has-text("saved"), ' ':has-text("updated")' ) if await success_el.count() > 0: result_data["password_changed"] = True else: # Assume success if no error visible error_el = page.locator('[class*="error"]') if await error_el.count() == 0: result_data["password_changed"] = True # Optionally add first website if website_name and website_url: websites_url = f"{base_url}/settings/websites" await page.goto(websites_url, wait_until="networkidle") # Click Add Website button add_button = page.locator( 'button:has-text("Add website"), ' 'button:has-text("Add"), ' 'a:has-text("Add website")' ).first if await add_button.count() > 0: await add_button.click() await page.wait_for_timeout(1000) # Fill website name name_input = page.locator( 'input[name="name"], ' 'input[placeholder*="name" i]' ).first if await name_input.count() > 0: await name_input.fill(website_name) # Fill website URL/domain url_input = page.locator( 'input[name="domain"], ' 'input[name="url"], ' 'input[placeholder*="domain" i], ' 'input[placeholder*="url" i]' ).first if await url_input.count() > 0: await url_input.fill(website_url) # Save save_button = page.locator( 'button:has-text("Save"), ' 'button:has-text("Create"), ' 'button[type="submit"]' ).first await save_button.click() await page.wait_for_timeout(2000) result_data["website_added"] = True result_data["setup_completed"] = True # Take final screenshot if options.screenshot_on_success and options.artifacts_dir: final_path = options.artifacts_dir / "umami_setup_complete.png" await page.screenshot(path=str(final_path)) screenshots.append(str(final_path)) return ScenarioResult( success=True, data=result_data, screenshots=screenshots, error=None, ) except Exception as e: if options.screenshot_on_failure and options.artifacts_dir: error_path = options.artifacts_dir / "umami_setup_error.png" await page.screenshot(path=str(error_path)) screenshots.append(str(error_path)) return ScenarioResult( success=False, data=result_data, screenshots=screenshots, error=f"Umami setup failed: {str(e)}", )