""" Interactive authentication menu for AniList OAuth login/logout and user profile management. Implements Step 5: AniList Authentication Flow """ import webbrowser from typing import Optional from rich.console import Console from rich.panel import Panel from rich.table import Table from ....libs.media_api.types import UserProfile from ...auth.manager import AuthManager from ...utils.feedback import create_feedback_manager, execute_with_feedback from ..session import Context, session from ..state import InternalDirective, State @session.menu def auth(ctx: Context, state: State) -> State | InternalDirective: """ Interactive authentication menu for managing AniList login/logout and viewing user profile. """ icons = ctx.config.general.icons feedback = create_feedback_manager(icons) console = Console() console.clear() # Get current authentication status user_profile = getattr(ctx.media_api, "user_profile", None) auth_manager = AuthManager() # Display current authentication status _display_auth_status(console, user_profile, icons) # Menu options based on authentication status if user_profile: options = [ f"{'šŸ‘¤ ' if icons else ''}View Profile Details", f"{'šŸ”“ ' if icons else ''}Logout", f"{'ā†©ļø ' if icons else ''}Back to Main Menu", ] else: options = [ f"{'šŸ” ' if icons else ''}Login to AniList", f"{'ā“ ' if icons else ''}How to Get Token", f"{'ā†©ļø ' if icons else ''}Back to Main Menu", ] choice = ctx.selector.choose( prompt="Select Authentication Action", choices=options, header="AniList Authentication Menu", ) if not choice: return InternalDirective.BACK # Handle menu choices if "Login to AniList" in choice: return _handle_login(ctx, auth_manager, feedback, icons) elif "Logout" in choice: return _handle_logout(ctx, auth_manager, feedback, icons) elif "View Profile Details" in choice: _display_user_profile_details(console, user_profile, icons) feedback.pause_for_user("Press Enter to continue") return InternalDirective.RELOAD elif "How to Get Token" in choice: _display_token_help(console, icons) feedback.pause_for_user("Press Enter to continue") return InternalDirective.RELOAD else: # Back to Main Menu return InternalDirective.BACK def _display_auth_status( console: Console, user_profile: Optional[UserProfile], icons: bool ): """Display current authentication status in a nice panel.""" if user_profile: status_icon = "🟢" if icons else "[green]ā—[/green]" status_text = f"{status_icon} Authenticated" user_info = f"Logged in as: [bold cyan]{user_profile.name}[/bold cyan]\nUser ID: {user_profile.id}" else: status_icon = "šŸ”“" if icons else "[red]ā—‹[/red]" status_text = f"{status_icon} Not Authenticated" user_info = "Log in to access personalized features like:\n• Your anime lists (Watching, Completed, etc.)\n• Progress tracking\n• List management" panel = Panel( user_info, title=f"Authentication Status: {status_text}", border_style="green" if user_profile else "red", ) console.print(panel) console.print() def _handle_login( ctx: Context, auth_manager: AuthManager, feedback, icons: bool ) -> State | InternalDirective: """Handle the interactive login process.""" def perform_login(): # Open browser to AniList OAuth page oauth_url = "https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token" if feedback.confirm( "Open AniList authorization page in browser?", default=True ): try: webbrowser.open(oauth_url) feedback.info( "Browser opened", "Complete the authorization process in your browser", ) except Exception: feedback.warning( "Could not open browser automatically", f"Please manually visit: {oauth_url}", ) else: feedback.info("Manual authorization", f"Please visit: {oauth_url}") # Get token from user feedback.info( "Token Input", "Paste the token from the browser URL after '#access_token='" ) token = ctx.selector.ask("Enter your AniList Access Token") if not token or not token.strip(): feedback.error("Login cancelled", "No token provided") return None # Authenticate with the API profile = ctx.media_api.authenticate(token.strip()) if not profile: feedback.error( "Authentication failed", "The token may be invalid or expired" ) return None # Save credentials using the auth manager auth_manager.save_user_profile(profile, token.strip()) return profile success, profile = execute_with_feedback( perform_login, feedback, "authenticate", loading_msg="Validating token with AniList", success_msg="Successfully logged in! šŸŽ‰" if icons else "Successfully logged in!", error_msg="Login failed", show_loading=True, ) if success and profile: feedback.success( f"Logged in as {profile.name}" if profile else "Successfully logged in" ) feedback.pause_for_user("Press Enter to continue") return InternalDirective.RELOAD def _handle_logout( ctx: Context, auth_manager: AuthManager, feedback, icons: bool ) -> State | InternalDirective: """Handle the logout process with confirmation.""" if not feedback.confirm( "Are you sure you want to logout?", "This will remove your saved AniList token and log you out", default=False, ): return InternalDirective.RELOAD def perform_logout(): # Clear from auth manager if hasattr(auth_manager, "logout"): auth_manager.logout() else: auth_manager.clear_user_profile() # Clear from API client ctx.media_api.token = None ctx.media_api.user_profile = None if hasattr(ctx.media_api, "http_client"): ctx.media_api.http_client.headers.pop("Authorization", None) return True success, _ = execute_with_feedback( perform_logout, feedback, "logout", loading_msg="Logging out", success_msg="Successfully logged out šŸ‘‹" if icons else "Successfully logged out", error_msg="Logout failed", show_loading=False, ) if success: feedback.pause_for_user("Press Enter to continue") return InternalDirective.CONFIG_EDIT def _display_user_profile_details( console: Console, user_profile: UserProfile, icons: bool ): """Display detailed user profile information.""" if not user_profile: console.print("[red]No user profile available[/red]") return # Create a detailed profile table table = Table(title=f"{'šŸ‘¤ ' if icons else ''}User Profile: {user_profile.name}") table.add_column("Property", style="cyan", no_wrap=True) table.add_column("Value", style="green") table.add_row("Name", user_profile.name) table.add_row("User ID", str(user_profile.id)) if user_profile.avatar_url: table.add_row("Avatar URL", user_profile.avatar_url) if user_profile.banner_url: table.add_row("Banner URL", user_profile.banner_url) console.print() console.print(table) console.print() # Show available features features_panel = Panel( "Available Features:\n" f"{'šŸ“ŗ ' if icons else '• '}Access your anime lists (Watching, Completed, etc.)\n" f"{'āœļø ' if icons else '• '}Update watch progress and scores\n" f"{'āž• ' if icons else '• '}Add/remove anime from your lists\n" f"{'šŸ”„ ' if icons else '• '}Sync progress with AniList\n" f"{'šŸ”” ' if icons else '• '}Access AniList notifications", title="Available with Authentication", border_style="green", ) console.print(features_panel) def _display_token_help(console: Console, icons: bool): """Display help information about getting an AniList token.""" help_text = """ [bold cyan]How to get your AniList Access Token:[/bold cyan] [bold]Step 1:[/bold] Visit the AniList authorization page https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token [bold]Step 2:[/bold] Log in to your AniList account if prompted [bold]Step 3:[/bold] Click "Authorize" to grant FastAnime access [bold]Step 4:[/bold] Copy the token from the browser URL Look for the part after "#access_token=" in the address bar [bold]Step 5:[/bold] Paste the token when prompted in FastAnime [yellow]Note:[/yellow] The token will be stored securely and used for all AniList features. You only need to do this once unless you revoke access or the token expires. [yellow]Privacy:[/yellow] FastAnime only requests minimal permissions needed for list management and does not access sensitive account information. """ panel = Panel( help_text, title=f"{'ā“ ' if icons else ''}AniList Token Help", border_style="blue", ) console.print() console.print(panel)