mirror of
https://github.com/Lyxminxx/jast-frontend.git
synced 2026-10-11 20:57:44 +02:00
Refresh access token and retry on 401
This commit is contained in:
1 parent
86490c40ea
commit
c171536bcb
1 file changed
+113
-66
+113
-66
@@ -9,43 +9,94 @@ class ApiClient:
|
|||||||
def base_url(self) -> str | None:
|
def base_url(self) -> str | None:
|
||||||
return self.storage.get_setting("server_url")
|
return self.storage.get_setting("server_url")
|
||||||
|
|
||||||
def get_headers(self) -> dict[str, str]:
|
def _headers(self, auth: bool = True) -> dict[str, str]:
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
token = self.storage.get_setting("auth_token")
|
if auth:
|
||||||
if token:
|
token = self.storage.get_setting("auth_token")
|
||||||
headers["Authorization"] = f"Bearer {token}"
|
if token:
|
||||||
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
def get_headers(self) -> dict[str, str]:
|
||||||
|
return self._headers(auth=True)
|
||||||
|
|
||||||
|
def _refresh_access_token(self) -> bool:
|
||||||
|
"""Swap the stored refresh token for a fresh access token.
|
||||||
|
|
||||||
|
Returns True only if a new access token was stored. Never raises: if it
|
||||||
|
did, the exception would surface from a caller's `except Exception` as a
|
||||||
|
message with no status code in it, which would break the views that
|
||||||
|
detect an expired session by looking for "401" in the error text.
|
||||||
|
"""
|
||||||
|
token = self.storage.get_setting("refresh_token")
|
||||||
|
if not token:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
res = self._request(
|
||||||
|
"POST", "/auth/refresh", auth=False, json={"refresh_token": token}
|
||||||
|
)
|
||||||
|
if res.status_code != 200:
|
||||||
|
return False
|
||||||
|
new_token = res.json().get("access_token")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
if not new_token:
|
||||||
|
return False
|
||||||
|
self.storage.save_setting("auth_token", new_token)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _request(
|
||||||
|
self, method: str, path: str, *, auth: bool = True, json=None
|
||||||
|
) -> httpx.Response:
|
||||||
|
"""Issue a request, transparently refreshing and retrying once on 401.
|
||||||
|
|
||||||
|
`auth=False` both omits the bearer and opts out of the retry, which is
|
||||||
|
what keeps /auth/refresh, /auth/login and /auth/register from recursing.
|
||||||
|
|
||||||
|
Transport errors propagate: each caller already turns them into
|
||||||
|
(False, str(err)), and that wording must not change.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}{path}"
|
||||||
|
with httpx.Client(timeout=self.timeout) as client:
|
||||||
|
res = client.request(method, url, json=json, headers=self._headers(auth))
|
||||||
|
if auth and res.status_code == 401:
|
||||||
|
if self._refresh_access_token():
|
||||||
|
# Re-reads auth_token, which save_setting updated synchronously.
|
||||||
|
res = client.request(
|
||||||
|
method, url, json=json, headers=self._headers(auth)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Give back the original 401 so callers' messages are unchanged.
|
||||||
|
self.logout()
|
||||||
|
return res
|
||||||
|
|
||||||
def get_me(self) -> tuple[bool, dict | str]:
|
def get_me(self) -> tuple[bool, dict | str]:
|
||||||
if not self.base_url: return False, "Server URL is not configured."
|
if not self.base_url: return False, "Server URL is not configured."
|
||||||
try:
|
try:
|
||||||
with httpx.Client(timeout=self.timeout) as client:
|
res = self._request("GET", "/auth/me")
|
||||||
res = client.get(f"{self.base_url}/auth/me", headers=self.get_headers())
|
if res.status_code == 200:
|
||||||
if res.status_code == 200:
|
return True, res.json()
|
||||||
return True, res.json()
|
return False, f"Failed to fetch profile ({res.status_code})"
|
||||||
return False, f"Failed to fetch profile ({res.status_code})"
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
return False, str(err)
|
return False, str(err)
|
||||||
|
|
||||||
def get_categories(self) -> tuple[bool, list]:
|
def get_categories(self) -> tuple[bool, list]:
|
||||||
if not self.base_url: return False, []
|
if not self.base_url: return False, []
|
||||||
try:
|
try:
|
||||||
with httpx.Client(timeout=self.timeout) as client:
|
res = self._request("GET", "/categories")
|
||||||
res = client.get(f"{self.base_url}/categories", headers=self.get_headers())
|
if res.status_code == 200:
|
||||||
if res.status_code == 200:
|
return True, res.json()
|
||||||
return True, res.json()
|
return False, []
|
||||||
return False, []
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return False, []
|
return False, []
|
||||||
|
|
||||||
def get_transactions(self) -> tuple[bool, list | str]:
|
def get_transactions(self) -> tuple[bool, list | str]:
|
||||||
if not self.base_url: return False, "Server URL is not configured."
|
if not self.base_url: return False, "Server URL is not configured."
|
||||||
try:
|
try:
|
||||||
with httpx.Client(timeout=self.timeout) as client:
|
res = self._request("GET", "/transactions")
|
||||||
res = client.get(f"{self.base_url}/transactions", headers=self.get_headers())
|
if res.status_code == 200:
|
||||||
if res.status_code == 200:
|
return True, res.json()
|
||||||
return True, res.json()
|
return False, f"Failed to fetch transactions ({res.status_code})"
|
||||||
return False, f"Failed to fetch transactions ({res.status_code})"
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
return False, str(err)
|
return False, str(err)
|
||||||
|
|
||||||
@@ -53,10 +104,9 @@ class ApiClient:
|
|||||||
if not self.base_url: return False, "Server URL is not configured."
|
if not self.base_url: return False, "Server URL is not configured."
|
||||||
payload = {"title": title, "amount": str(amount), "date": date_str, "category_id": category_id}
|
payload = {"title": title, "amount": str(amount), "date": date_str, "category_id": category_id}
|
||||||
try:
|
try:
|
||||||
with httpx.Client(timeout=self.timeout) as client:
|
res = self._request("POST", "/transactions", json=payload)
|
||||||
res = client.post(f"{self.base_url}/transactions", json=payload, headers=self.get_headers())
|
if res.status_code == 200: return True, "Transaction created"
|
||||||
if res.status_code == 200: return True, "Transaction created"
|
return False, f"Error creating transaction ({res.status_code})"
|
||||||
return False, f"Error creating transaction ({res.status_code})"
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
return False, str(err)
|
return False, str(err)
|
||||||
|
|
||||||
@@ -64,35 +114,35 @@ class ApiClient:
|
|||||||
if not self.base_url: return False, "Server URL is not configured."
|
if not self.base_url: return False, "Server URL is not configured."
|
||||||
payload = {"title": title, "amount": str(amount), "date": date_str, "category_id": category_id}
|
payload = {"title": title, "amount": str(amount), "date": date_str, "category_id": category_id}
|
||||||
try:
|
try:
|
||||||
with httpx.Client(timeout=self.timeout) as client:
|
res = self._request("PUT", f"/transactions/{transaction_id}", json=payload)
|
||||||
res = client.put(f"{self.base_url}/transactions/{transaction_id}", json=payload, headers=self.get_headers())
|
if res.status_code == 200: return True, "Transaction updated"
|
||||||
if res.status_code == 200: return True, "Transaction updated"
|
return False, f"Error updating transaction ({res.status_code})"
|
||||||
return False, f"Error updating transaction ({res.status_code})"
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
return False, str(err)
|
return False, str(err)
|
||||||
|
|
||||||
def delete_transaction(self, transaction_id: int) -> tuple[bool, str]:
|
def delete_transaction(self, transaction_id: int) -> tuple[bool, str]:
|
||||||
if not self.base_url: return False, "Server URL is not configured."
|
if not self.base_url: return False, "Server URL is not configured."
|
||||||
try:
|
try:
|
||||||
with httpx.Client(timeout=self.timeout) as client:
|
res = self._request("DELETE", f"/transactions/{transaction_id}")
|
||||||
res = client.delete(f"{self.base_url}/transactions/{transaction_id}", headers=self.get_headers())
|
if res.status_code == 200: return True, "Transaction deleted"
|
||||||
if res.status_code == 200: return True, "Transaction deleted"
|
return False, f"Error deleting transaction ({res.status_code})"
|
||||||
return False, f"Error deleting transaction ({res.status_code})"
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
return False, str(err)
|
return False, str(err)
|
||||||
|
|
||||||
def login(self, username: str, password: str) -> tuple[bool, str]:
|
def login(self, username: str, password: str) -> tuple[bool, str]:
|
||||||
if not self.base_url: return False, "Server URL is not configured."
|
if not self.base_url: return False, "Server URL is not configured."
|
||||||
try:
|
try:
|
||||||
with httpx.Client(timeout=self.timeout) as client:
|
response = self._request(
|
||||||
response = client.post(f"{self.base_url}/auth/login", json={"username": username, "password": password})
|
"POST", "/auth/login", auth=False,
|
||||||
if response.status_code == 200:
|
json={"username": username, "password": password},
|
||||||
data = response.json()
|
)
|
||||||
self.storage.save_setting("auth_token", data.get("access_token"))
|
if response.status_code == 200:
|
||||||
if data.get("refresh_token"):
|
data = response.json()
|
||||||
self.storage.save_setting("refresh_token", data.get("refresh_token"))
|
self.storage.save_setting("auth_token", data.get("access_token"))
|
||||||
return True, "Login successful"
|
if data.get("refresh_token"):
|
||||||
return False, "Invalid username or password"
|
self.storage.save_setting("refresh_token", data.get("refresh_token"))
|
||||||
|
return True, "Login successful"
|
||||||
|
return False, "Invalid username or password"
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
return False, str(err)
|
return False, str(err)
|
||||||
|
|
||||||
@@ -110,27 +160,25 @@ class ApiClient:
|
|||||||
if password: payload["password"] = password
|
if password: payload["password"] = password
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with httpx.Client(timeout=self.timeout) as client:
|
res = self._request("PUT", "/auth/me", json=payload)
|
||||||
res = client.put(f"{self.base_url}/auth/me", json=payload, headers=self.get_headers())
|
if res.status_code == 200:
|
||||||
if res.status_code == 200:
|
try: return True, res.json()
|
||||||
try: return True, res.json()
|
except Exception: return True, {}
|
||||||
except Exception: return True, {}
|
try:
|
||||||
try:
|
return False, res.json().get("detail", f"Update failed ({res.status_code})")
|
||||||
return False, res.json().get("detail", f"Update failed ({res.status_code})")
|
except Exception:
|
||||||
except Exception:
|
return False, f"Update failed ({res.status_code}): {res.text}"
|
||||||
return False, f"Update failed ({res.status_code}): {res.text}"
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
return False, str(err)
|
return False, str(err)
|
||||||
|
|
||||||
def delete_me(self) -> tuple[bool, str]:
|
def delete_me(self) -> tuple[bool, str]:
|
||||||
if not self.base_url: return False, "Server URL is not configured."
|
if not self.base_url: return False, "Server URL is not configured."
|
||||||
try:
|
try:
|
||||||
with httpx.Client(timeout=self.timeout) as client:
|
res = self._request("DELETE", "/auth/me")
|
||||||
res = client.delete(f"{self.base_url}/auth/me", headers=self.get_headers())
|
if res.status_code == 200:
|
||||||
if res.status_code == 200:
|
self.logout()
|
||||||
self.logout()
|
return True, "Account deleted."
|
||||||
return True, "Account deleted."
|
return False, "Failed to delete account."
|
||||||
return False, "Failed to delete account."
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
return False, str(err)
|
return False, str(err)
|
||||||
|
|
||||||
@@ -138,17 +186,16 @@ class ApiClient:
|
|||||||
if not self.base_url: return False, "Server URL is not configured."
|
if not self.base_url: return False, "Server URL is not configured."
|
||||||
payload = {"username": username, "password": password, "email": email or "", "first_name": first_name or "", "last_name": last_name or ""}
|
payload = {"username": username, "password": password, "email": email or "", "first_name": first_name or "", "last_name": last_name or ""}
|
||||||
try:
|
try:
|
||||||
with httpx.Client(timeout=self.timeout) as client:
|
res = self._request("POST", "/auth/register", auth=False, json=payload)
|
||||||
res = client.post(f"{self.base_url}/auth/register", json=payload)
|
if res.status_code == 200:
|
||||||
if res.status_code == 200:
|
data = res.json()
|
||||||
data = res.json()
|
self.storage.save_setting("auth_token", data.get("access_token"))
|
||||||
self.storage.save_setting("auth_token", data.get("access_token"))
|
if data.get("refresh_token"):
|
||||||
if data.get("refresh_token"):
|
self.storage.save_setting("refresh_token", data.get("refresh_token"))
|
||||||
self.storage.save_setting("refresh_token", data.get("refresh_token"))
|
return True, "Registration successful"
|
||||||
return True, "Registration successful"
|
|
||||||
|
|
||||||
try: err_detail = res.json().get("detail", "Registration failed")
|
try: err_detail = res.json().get("detail", "Registration failed")
|
||||||
except: err_detail = f"Registration failed ({res.status_code})"
|
except: err_detail = f"Registration failed ({res.status_code})"
|
||||||
return False, err_detail
|
return False, err_detail
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
return False, str(err)
|
return False, str(err)
|
||||||
Reference in new issue
Block a user