diff --git a/src/cli/sync.py b/src/cli/sync.py index dc9ae00..5222787 100644 --- a/src/cli/sync.py +++ b/src/cli/sync.py @@ -715,6 +715,24 @@ def sync( else: # Default: Launch interactive TUI dashboard from .sync_dashboard import run_dashboard_sync + from src.services.microsoft_graph.auth import has_valid_cached_token + + # Check if we need to authenticate before starting the TUI + # This prevents the TUI from appearing to freeze during device flow auth + if not demo: + scopes = [ + "https://graph.microsoft.com/Calendars.Read", + "https://graph.microsoft.com/Mail.ReadWrite", + ] + if not has_valid_cached_token(scopes): + click.echo("Authentication required. Please complete the login flow...") + try: + # This will trigger the device flow auth in the console + get_access_token(scopes) + click.echo("Authentication successful! Starting dashboard...") + except Exception as e: + click.echo(f"Authentication failed: {e}") + return sync_config = { "org": org, @@ -936,6 +954,27 @@ def status(): def interactive(org, vdir, notify, dry_run, demo): """Launch interactive TUI dashboard for sync operations.""" from .sync_dashboard import run_dashboard_sync + from src.services.microsoft_graph.auth import ( + has_valid_cached_token, + get_access_token, + ) + + # Check if we need to authenticate before starting the TUI + # This prevents the TUI from appearing to freeze during device flow auth + if not demo: + scopes = [ + "https://graph.microsoft.com/Calendars.Read", + "https://graph.microsoft.com/Mail.ReadWrite", + ] + if not has_valid_cached_token(scopes): + click.echo("Authentication required. Please complete the login flow...") + try: + # This will trigger the device flow auth in the console + get_access_token(scopes) + click.echo("Authentication successful! Starting dashboard...") + except Exception as e: + click.echo(f"Authentication failed: {e}") + return sync_config = { "org": org, diff --git a/src/services/microsoft_graph/auth.py b/src/services/microsoft_graph/auth.py index cbedd0a..d0616e3 100644 --- a/src/services/microsoft_graph/auth.py +++ b/src/services/microsoft_graph/auth.py @@ -25,6 +25,49 @@ def ensure_directory_exists(path): os.makedirs(path) +def has_valid_cached_token(scopes=None): + """ + Check if we have a valid cached token (without triggering auth flow). + + Args: + scopes: List of scopes to check. If None, uses default scopes. + + Returns: + bool: True if a valid cached token exists, False otherwise. + """ + if scopes is None: + scopes = ["https://graph.microsoft.com/Mail.Read"] + + client_id = os.getenv("AZURE_CLIENT_ID") + tenant_id = os.getenv("AZURE_TENANT_ID") + + if not client_id or not tenant_id: + return False + + cache = msal.SerializableTokenCache() + cache_file = "token_cache.bin" + + if not os.path.exists(cache_file): + return False + + try: + cache.deserialize(open(cache_file, "r").read()) + authority = f"https://login.microsoftonline.com/{tenant_id}" + app = msal.PublicClientApplication( + client_id, authority=authority, token_cache=cache + ) + accounts = app.get_accounts() + + if not accounts: + return False + + # Try silent auth - this will return None if token is expired + token_response = app.acquire_token_silent(scopes, account=accounts[0]) + return token_response is not None and "access_token" in token_response + except Exception: + return False + + def get_access_token(scopes): """ Authenticate with Microsoft Graph API and obtain an access token.