Skip to content

Commit 1aa4e3e

Browse files
committed
♻️ Refactor authentication
1 parent e79be2b commit 1aa4e3e

File tree

8 files changed

+77
-63
lines changed

8 files changed

+77
-63
lines changed

src/fastapi_cloud_cli/commands/deploy.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from fastapi_cloud_cli.commands.login import login
2222
from fastapi_cloud_cli.utils.api import APIClient, BuildLogError, TooManyRetriesError
2323
from fastapi_cloud_cli.utils.apps import AppConfig, get_app_config, write_app_config
24-
from fastapi_cloud_cli.utils.auth import is_logged_in
24+
from fastapi_cloud_cli.utils.auth import Identity
2525
from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
2626

2727
logger = logging.getLogger(__name__)
@@ -534,8 +534,10 @@ def deploy(
534534
logger.debug("Deploy command started")
535535
logger.debug("Deploy path: %s, skip_wait: %s", path, skip_wait)
536536

537+
identity = Identity()
538+
537539
with get_rich_toolkit() as toolkit:
538-
if not is_logged_in():
540+
if not identity.is_logged_in():
539541
logger.debug("User not logged in, prompting for login or waitlist")
540542

541543
toolkit.print_title("Welcome to FastAPI Cloud!", tag="FastAPI")

src/fastapi_cloud_cli/commands/env.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from fastapi_cloud_cli.utils.api import APIClient
99
from fastapi_cloud_cli.utils.apps import get_app_config
10-
from fastapi_cloud_cli.utils.auth import is_logged_in
10+
from fastapi_cloud_cli.utils.auth import Identity
1111
from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
1212
from fastapi_cloud_cli.utils.env import validate_environment_variable_name
1313

@@ -70,8 +70,10 @@ def list(
7070
List the environment variables for the app.
7171
"""
7272

73+
identity = Identity()
74+
7375
with get_rich_toolkit(minimal=True) as toolkit:
74-
if not is_logged_in():
76+
if not identity.is_logged_in():
7577
toolkit.print(
7678
"No credentials found. Use [blue]`fastapi login`[/] to login.",
7779
tag="auth",
@@ -123,9 +125,10 @@ def delete(
123125
Delete an environment variable from the app.
124126
"""
125127

128+
identity = Identity()
129+
126130
with get_rich_toolkit(minimal=True) as toolkit:
127-
# TODO: maybe this logic can be extracted to a function
128-
if not is_logged_in():
131+
if not identity.is_logged_in():
129132
toolkit.print(
130133
"No credentials found. Use [blue]`fastapi login`[/] to login.",
131134
tag="auth",
@@ -208,8 +211,10 @@ def set(
208211
Set an environment variable for the app.
209212
"""
210213

214+
identity = Identity()
215+
211216
with get_rich_toolkit(minimal=True) as toolkit:
212-
if not is_logged_in():
217+
if not identity.is_logged_in():
213218
toolkit.print(
214219
"No credentials found. Use [blue]`fastapi login`[/] to login.",
215220
tag="auth",

src/fastapi_cloud_cli/commands/login.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,7 @@
88

99
from fastapi_cloud_cli.config import Settings
1010
from fastapi_cloud_cli.utils.api import APIClient
11-
from fastapi_cloud_cli.utils.auth import (
12-
AuthConfig,
13-
get_auth_token,
14-
is_logged_in,
15-
is_token_expired,
16-
write_auth_config,
17-
)
11+
from fastapi_cloud_cli.utils.auth import AuthConfig, Identity, write_auth_config
1812
from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
1913

2014
logger = logging.getLogger(__name__)
@@ -82,13 +76,14 @@ def login() -> Any:
8276
"""
8377
Login to FastAPI Cloud. 🚀
8478
"""
85-
token = get_auth_token()
86-
if token is not None and is_token_expired(token):
79+
identity = Identity()
80+
81+
if identity.is_expired():
8782
with get_rich_toolkit(minimal=True) as toolkit:
8883
toolkit.print("Your session has expired. Logging in again...")
8984
toolkit.print_line()
9085

91-
if is_logged_in():
86+
if identity.is_logged_in():
9287
with get_rich_toolkit(minimal=True) as toolkit:
9388
toolkit.print("You are already logged in.")
9489
toolkit.print(

src/fastapi_cloud_cli/commands/whoami.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55
from rich_toolkit.progress import Progress
66

77
from fastapi_cloud_cli.utils.api import APIClient
8-
from fastapi_cloud_cli.utils.auth import is_logged_in
8+
from fastapi_cloud_cli.utils.auth import Identity
99
from fastapi_cloud_cli.utils.cli import handle_http_errors
1010

1111
logger = logging.getLogger(__name__)
1212

1313

1414
def whoami() -> Any:
15-
if not is_logged_in():
15+
identity = Identity()
16+
17+
if not identity.is_logged_in():
1618
print("No credentials found. Use [blue]`fastapi login`[/] to login.")
1719
return
1820

src/fastapi_cloud_cli/utils/api.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020

2121
from fastapi_cloud_cli import __version__
2222
from fastapi_cloud_cli.config import Settings
23-
from fastapi_cloud_cli.utils.auth import get_auth_token
23+
24+
from .auth import Identity
2425

2526
logger = logging.getLogger(__name__)
2627

@@ -132,14 +133,13 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Generator[T, None, None]:
132133
class APIClient(httpx.Client):
133134
def __init__(self) -> None:
134135
settings = Settings.get()
135-
136-
token = get_auth_token()
136+
identity = Identity()
137137

138138
super().__init__(
139139
base_url=settings.base_api_url,
140140
timeout=httpx.Timeout(20),
141141
headers={
142-
"Authorization": f"Bearer {token}",
142+
"Authorization": f"Bearer {identity.token}",
143143
"User-Agent": f"fastapi-cloud-cli/{__version__}",
144144
},
145145
)

src/fastapi_cloud_cli/utils/auth.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def read_auth_config() -> Optional[AuthConfig]:
4747
return AuthConfig.model_validate_json(auth_path.read_text(encoding="utf-8"))
4848

4949

50-
def get_auth_token() -> Optional[str]:
50+
def _get_auth_token() -> Optional[str]:
5151
logger.debug("Getting auth token")
5252
auth_data = read_auth_config()
5353

@@ -59,7 +59,7 @@ def get_auth_token() -> Optional[str]:
5959
return auth_data.access_token
6060

6161

62-
def is_token_expired(token: str) -> bool:
62+
def _is_token_expired(token: str) -> bool:
6363
try:
6464
parts = token.split(".")
6565

@@ -107,16 +107,24 @@ def is_token_expired(token: str) -> bool:
107107
return True
108108

109109

110-
def is_logged_in() -> bool:
111-
token = get_auth_token()
110+
class Identity:
111+
def __init__(self) -> None:
112+
self.token = _get_auth_token()
112113

113-
if token is None:
114-
logger.debug("Login status: False (no token)")
115-
return False
114+
def is_expired(self) -> bool:
115+
if not self.token:
116+
return True
117+
118+
return _is_token_expired(self.token)
119+
120+
def is_logged_in(self) -> bool:
121+
if self.token is None:
122+
logger.debug("Login status: False (no token)")
123+
return False
116124

117-
if is_token_expired(token):
118-
logger.debug("Login status: False (token expired)")
119-
return False
125+
if self.is_expired():
126+
logger.debug("Login status: False (token expired)")
127+
return False
120128

121-
logger.debug("Login status: True")
122-
return True
129+
logger.debug("Login status: True")
130+
return True

src/fastapi_cloud_cli/utils/sentry.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import sentry_sdk
22
from sentry_sdk.integrations.typer import TyperIntegration
33

4-
from .auth import is_logged_in
4+
from fastapi_cloud_cli.utils.auth import Identity
55

66
SENTRY_DSN = "https://230250605ea4b58a0b69c768e9ec1168@o4506985151856640.ingest.us.sentry.io/4508449198899200"
77

88

99
def init_sentry() -> None:
1010
"""Initialize Sentry error tracking only if user is logged in."""
11-
if not is_logged_in():
11+
identity = Identity()
12+
13+
if not identity.is_logged_in():
1214
return
1315

1416
sentry_sdk.init(

tests/test_auth.py

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
from fastapi_cloud_cli.utils.auth import (
88
AuthConfig,
9-
is_logged_in,
10-
is_token_expired,
9+
Identity,
10+
_is_token_expired,
1111
write_auth_config,
1212
)
1313

@@ -19,21 +19,21 @@ def test_is_token_expired_with_valid_token() -> None:
1919

2020
token = create_jwt_token({"exp": future_exp, "sub": "test_user"})
2121

22-
assert not is_token_expired(token)
22+
assert not _is_token_expired(token)
2323

2424

2525
def test_is_token_expired_with_expired_token() -> None:
2626
past_exp = int(time.time()) - 3600
2727
token = create_jwt_token({"exp": past_exp, "sub": "test_user"})
2828

29-
assert is_token_expired(token)
29+
assert _is_token_expired(token)
3030

3131

3232
def test_is_token_expired_with_no_exp_claim() -> None:
3333
token = create_jwt_token({"sub": "test_user"})
3434

3535
# Tokens without exp claim should be considered valid
36-
assert not is_token_expired(token)
36+
assert not _is_token_expired(token)
3737

3838

3939
@pytest.mark.parametrize(
@@ -47,12 +47,12 @@ def test_is_token_expired_with_no_exp_claim() -> None:
4747
],
4848
)
4949
def test_is_token_expired_with_malformed_token(token: str) -> None:
50-
assert is_token_expired(token)
50+
assert _is_token_expired(token)
5151

5252

5353
def test_is_token_expired_with_invalid_base64() -> None:
5454
token = "header.!!!invalid_signature!!!.signature"
55-
assert is_token_expired(token)
55+
assert _is_token_expired(token)
5656

5757

5858
def test_is_token_expired_with_invalid_json() -> None:
@@ -61,12 +61,26 @@ def test_is_token_expired_with_invalid_json() -> None:
6161
signature = base64.urlsafe_b64encode(b"signature").decode().rstrip("=")
6262
token = f"{header_encoded}.{payload_encoded}.{signature}"
6363

64-
assert is_token_expired(token)
64+
assert _is_token_expired(token)
65+
66+
67+
def test_is_token_expired_edge_case_exact_expiration() -> None:
68+
current_time = int(time.time())
69+
token = create_jwt_token({"exp": current_time, "sub": "test_user"})
70+
71+
assert _is_token_expired(token)
72+
73+
74+
def test_is_token_expired_edge_case_one_second_before() -> None:
75+
current_time = int(time.time())
76+
token = create_jwt_token({"exp": current_time + 1, "sub": "test_user"})
77+
78+
assert not _is_token_expired(token)
6579

6680

6781
def test_is_logged_in_with_no_token(temp_auth_config: Path) -> None:
6882
assert not temp_auth_config.exists()
69-
assert not is_logged_in()
83+
assert not Identity().is_logged_in()
7084

7185

7286
def test_is_logged_in_with_valid_token(temp_auth_config: Path) -> None:
@@ -75,7 +89,7 @@ def test_is_logged_in_with_valid_token(temp_auth_config: Path) -> None:
7589

7690
write_auth_config(AuthConfig(access_token=token))
7791

78-
assert is_logged_in()
92+
assert Identity().is_logged_in()
7993

8094

8195
def test_is_logged_in_with_expired_token(temp_auth_config: Path) -> None:
@@ -84,24 +98,10 @@ def test_is_logged_in_with_expired_token(temp_auth_config: Path) -> None:
8498

8599
write_auth_config(AuthConfig(access_token=token))
86100

87-
assert not is_logged_in()
101+
assert not Identity().is_logged_in()
88102

89103

90104
def test_is_logged_in_with_malformed_token(temp_auth_config: Path) -> None:
91105
write_auth_config(AuthConfig(access_token="not.a.valid.token"))
92106

93-
assert not is_logged_in()
94-
95-
96-
def test_is_token_expired_edge_case_exact_expiration() -> None:
97-
current_time = int(time.time())
98-
token = create_jwt_token({"exp": current_time, "sub": "test_user"})
99-
100-
assert is_token_expired(token)
101-
102-
103-
def test_is_token_expired_edge_case_one_second_before() -> None:
104-
current_time = int(time.time())
105-
token = create_jwt_token({"exp": current_time + 1, "sub": "test_user"})
106-
107-
assert not is_token_expired(token)
107+
assert not Identity().is_logged_in()

0 commit comments

Comments
 (0)