176 lines
5.2 KiB
Go
176 lines
5.2 KiB
Go
package bkosmi
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/chromedp/chromedp"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// loginWithChromedp uses browser automation to log in and extract the JWT token.
|
|
// This is the proven implementation that successfully authenticates users.
|
|
func loginWithChromedp(email, password string, log *logrus.Entry) (string, error) {
|
|
log.Info("Starting browser automation for authentication...")
|
|
|
|
// Create context with timeout
|
|
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
|
defer cancel()
|
|
|
|
// Set up chromedp options (headless mode)
|
|
opts := []chromedp.ExecAllocatorOption{
|
|
chromedp.NoFirstRun,
|
|
chromedp.NoDefaultBrowserCheck,
|
|
chromedp.DisableGPU,
|
|
chromedp.NoSandbox,
|
|
chromedp.Headless,
|
|
}
|
|
|
|
// Create allocator context
|
|
allocCtx, cancel := chromedp.NewExecAllocator(ctx, opts...)
|
|
defer cancel()
|
|
|
|
// Create browser context with no logging to suppress cookie errors
|
|
ctx, cancel = chromedp.NewContext(allocCtx, chromedp.WithLogf(func(string, ...interface{}) {}))
|
|
defer cancel()
|
|
|
|
var token string
|
|
|
|
// Run the automation tasks
|
|
err := chromedp.Run(ctx,
|
|
// Navigate to Kosmi
|
|
chromedp.Navigate("https://app.kosmi.io"),
|
|
chromedp.WaitReady("body"),
|
|
chromedp.Sleep(3*time.Second),
|
|
|
|
// Find and click Login button
|
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
|
log.Debug("Looking for Login button...")
|
|
var found bool
|
|
if err := chromedp.Evaluate(`
|
|
(() => {
|
|
const buttons = Array.from(document.querySelectorAll('button'));
|
|
const btn = buttons.find(el => {
|
|
const text = el.textContent.trim();
|
|
return text === 'Login' || text === 'Log in';
|
|
});
|
|
if (btn) {
|
|
btn.click();
|
|
return true;
|
|
}
|
|
return false;
|
|
})()
|
|
`, &found).Do(ctx); err != nil {
|
|
return err
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("Login button not found")
|
|
}
|
|
log.Debug("✓ Clicked Login button")
|
|
return nil
|
|
}),
|
|
|
|
// Wait and click "Login with Email"
|
|
chromedp.Sleep(3*time.Second),
|
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
|
log.Debug("Looking for 'Login with Email' button...")
|
|
var found bool
|
|
if err := chromedp.Evaluate(`
|
|
(() => {
|
|
const btn = Array.from(document.querySelectorAll('button')).find(el =>
|
|
el.textContent.includes('Email')
|
|
);
|
|
if (btn) {
|
|
btn.click();
|
|
return true;
|
|
}
|
|
return false;
|
|
})()
|
|
`, &found).Do(ctx); err != nil {
|
|
return err
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("'Login with Email' button not found")
|
|
}
|
|
log.Debug("✓ Clicked 'Login with Email' button")
|
|
return nil
|
|
}),
|
|
|
|
// Wait for form and fill credentials
|
|
chromedp.Sleep(3*time.Second),
|
|
chromedp.WaitVisible(`input[type="password"]`, chromedp.ByQuery),
|
|
chromedp.Click(`input[placeholder*="Email"], input[placeholder*="Username"]`, chromedp.ByQuery),
|
|
chromedp.Sleep(200*time.Millisecond),
|
|
chromedp.SendKeys(`input[placeholder*="Email"], input[placeholder*="Username"]`, email, chromedp.ByQuery),
|
|
chromedp.Sleep(500*time.Millisecond),
|
|
chromedp.Click(`input[type="password"]`, chromedp.ByQuery),
|
|
chromedp.Sleep(200*time.Millisecond),
|
|
chromedp.SendKeys(`input[type="password"]`, password+"\n", chromedp.ByQuery),
|
|
chromedp.Sleep(500*time.Millisecond),
|
|
|
|
// Wait for login to complete - check for modal to close
|
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
|
log.Debug("Waiting for login to complete...")
|
|
// Wait for the login modal to disappear (indicates successful login)
|
|
maxAttempts := 30 // 15 seconds total
|
|
for i := 0; i < maxAttempts; i++ {
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Check if login modal is gone (successful login)
|
|
var modalGone bool
|
|
chromedp.Evaluate(`
|
|
(() => {
|
|
// Check if the email/password form is still visible
|
|
const emailInput = document.querySelector('input[placeholder*="Email"], input[placeholder*="Username"]');
|
|
const passwordInput = document.querySelector('input[type="password"]');
|
|
return !emailInput && !passwordInput;
|
|
})()
|
|
`, &modalGone).Do(ctx)
|
|
|
|
if modalGone {
|
|
log.Debug("✓ Login modal closed")
|
|
// Modal is gone, wait a bit more for token to be set
|
|
time.Sleep(2 * time.Second)
|
|
chromedp.Evaluate(`localStorage.getItem('token')`, &token).Do(ctx)
|
|
if token != "" {
|
|
log.Info("✅ Authentication successful")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Check for error messages
|
|
var errorText string
|
|
chromedp.Evaluate(`
|
|
(() => {
|
|
const errorEl = document.querySelector('[role="alert"], .error, .error-message');
|
|
return errorEl ? errorEl.textContent.trim() : '';
|
|
})()
|
|
`, &errorText).Do(ctx)
|
|
|
|
if errorText != "" && errorText != "null" {
|
|
return fmt.Errorf("login failed: %s", errorText)
|
|
}
|
|
}
|
|
|
|
// Timeout - get whatever token is there
|
|
chromedp.Evaluate(`localStorage.getItem('token')`, &token).Do(ctx)
|
|
if token == "" {
|
|
return fmt.Errorf("login timeout: no token found")
|
|
}
|
|
log.Warn("⚠ Login timeout but token was found")
|
|
return nil
|
|
}),
|
|
)
|
|
|
|
if err != nil {
|
|
return "", fmt.Errorf("browser automation failed: %w", err)
|
|
}
|
|
|
|
if token == "" {
|
|
return "", fmt.Errorf("failed to extract token from localStorage")
|
|
}
|
|
|
|
return token, nil
|
|
}
|