125 lines
3.4 KiB
Python
125 lines
3.4 KiB
Python
import logging
|
|
import time
|
|
from typing import Any
|
|
|
|
import httpx
|
|
import typer
|
|
from pydantic import BaseModel
|
|
|
|
from fastapi_cloud_cli.config import Settings
|
|
from fastapi_cloud_cli.utils.api import APIClient
|
|
from fastapi_cloud_cli.utils.auth import (
|
|
AuthConfig,
|
|
get_auth_token,
|
|
is_logged_in,
|
|
is_token_expired,
|
|
write_auth_config,
|
|
)
|
|
from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
|
|
from fastapi_cloud_cli.utils.pydantic_compat import model_validate_json
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AuthorizationData(BaseModel):
|
|
user_code: str
|
|
device_code: str
|
|
verification_uri: str
|
|
verification_uri_complete: str
|
|
interval: int = 5
|
|
|
|
|
|
class TokenResponse(BaseModel):
|
|
access_token: str
|
|
|
|
|
|
def _start_device_authorization(
|
|
client: httpx.Client,
|
|
) -> AuthorizationData:
|
|
settings = Settings.get()
|
|
|
|
response = client.post(
|
|
"/login/device/authorization", data={"client_id": settings.client_id}
|
|
)
|
|
|
|
response.raise_for_status()
|
|
|
|
return model_validate_json(AuthorizationData, response.text)
|
|
|
|
|
|
def _fetch_access_token(client: httpx.Client, device_code: str, interval: int) -> str:
|
|
settings = Settings.get()
|
|
|
|
while True:
|
|
response = client.post(
|
|
"/login/device/token",
|
|
data={
|
|
"device_code": device_code,
|
|
"client_id": settings.client_id,
|
|
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
|
},
|
|
)
|
|
|
|
if response.status_code not in (200, 400):
|
|
response.raise_for_status()
|
|
|
|
if response.status_code == 400:
|
|
data = response.json()
|
|
|
|
if data.get("error") != "authorization_pending":
|
|
response.raise_for_status()
|
|
|
|
if response.status_code == 200:
|
|
break
|
|
|
|
time.sleep(interval)
|
|
|
|
response_data = model_validate_json(TokenResponse, response.text)
|
|
|
|
return response_data.access_token
|
|
|
|
|
|
def login() -> Any:
|
|
"""
|
|
Login to FastAPI Cloud. 🚀
|
|
"""
|
|
token = get_auth_token()
|
|
if token is not None and is_token_expired(token):
|
|
with get_rich_toolkit(minimal=True) as toolkit:
|
|
toolkit.print("Your session has expired. Logging in again...")
|
|
toolkit.print_line()
|
|
|
|
if is_logged_in():
|
|
with get_rich_toolkit(minimal=True) as toolkit:
|
|
toolkit.print("You are already logged in.")
|
|
toolkit.print(
|
|
"Run [bold]fastapi cloud logout[/bold] first if you want to switch accounts."
|
|
)
|
|
return
|
|
|
|
with get_rich_toolkit() as toolkit, APIClient() as client:
|
|
toolkit.print_title("Login to FastAPI Cloud", tag="FastAPI")
|
|
|
|
toolkit.print_line()
|
|
|
|
with toolkit.progress("Starting authorization") as progress:
|
|
with handle_http_errors(progress):
|
|
authorization_data = _start_device_authorization(client)
|
|
|
|
url = authorization_data.verification_uri_complete
|
|
|
|
progress.log(f"Opening [link={url}]{url}[/link]")
|
|
|
|
toolkit.print_line()
|
|
|
|
with toolkit.progress("Waiting for user to authorize...") as progress:
|
|
typer.launch(url)
|
|
|
|
with handle_http_errors(progress):
|
|
access_token = _fetch_access_token(
|
|
client, authorization_data.device_code, authorization_data.interval
|
|
)
|
|
|
|
write_auth_config(AuthConfig(access_token=access_token))
|
|
|
|
progress.log("Now you are logged in! 🚀")
|