push oauth fix
Browse files- app/main.py +25 -1
- app/spotify_oauth.py +37 -1
app/main.py
CHANGED
|
@@ -19,9 +19,10 @@ from .spotify_oauth import (
|
|
| 19 |
build_authorize_url,
|
| 20 |
default_redirect_uri,
|
| 21 |
exchange_code_for_token,
|
|
|
|
| 22 |
generate_state,
|
| 23 |
validate_redirect_uri,
|
| 24 |
-
|
| 25 |
from .spotify_playlists_api import (
|
| 26 |
add_items_to_playlist,
|
| 27 |
create_playlist,
|
|
@@ -66,6 +67,12 @@ class RecommendationResponse(BaseModel):
|
|
| 66 |
playlist_external_url: str | None = None
|
| 67 |
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
def _extract_bearer_token(authorization: str | None) -> str:
|
| 70 |
if not authorization:
|
| 71 |
raise HTTPException(
|
|
@@ -299,6 +306,23 @@ def spotify_me(authorization: str | None = Header(default=None)) -> dict:
|
|
| 299 |
raise HTTPException(status_code=500, detail="Unexpected server error") from exc
|
| 300 |
|
| 301 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
@app.get("/spotify/artists/id/{artist_id}")
|
| 303 |
def spotify_get_artist(
|
| 304 |
artist_id: str,
|
|
|
|
| 19 |
build_authorize_url,
|
| 20 |
default_redirect_uri,
|
| 21 |
exchange_code_for_token,
|
| 22 |
+
exchange_code_for_token_pkce,
|
| 23 |
generate_state,
|
| 24 |
validate_redirect_uri,
|
| 25 |
+
)
|
| 26 |
from .spotify_playlists_api import (
|
| 27 |
add_items_to_playlist,
|
| 28 |
create_playlist,
|
|
|
|
| 67 |
playlist_external_url: str | None = None
|
| 68 |
|
| 69 |
|
| 70 |
+
class SpotifyMobileTokenRequest(BaseModel):
|
| 71 |
+
code: str
|
| 72 |
+
code_verifier: str
|
| 73 |
+
redirect_uri: str
|
| 74 |
+
|
| 75 |
+
|
| 76 |
def _extract_bearer_token(authorization: str | None) -> str:
|
| 77 |
if not authorization:
|
| 78 |
raise HTTPException(
|
|
|
|
| 306 |
raise HTTPException(status_code=500, detail="Unexpected server error") from exc
|
| 307 |
|
| 308 |
|
| 309 |
+
@app.post("/mobile/spotify/token")
|
| 310 |
+
def spotify_mobile_token(body: SpotifyMobileTokenRequest) -> dict:
|
| 311 |
+
try:
|
| 312 |
+
return exchange_code_for_token_pkce(
|
| 313 |
+
code=body.code,
|
| 314 |
+
redirect_uri=body.redirect_uri,
|
| 315 |
+
code_verifier=body.code_verifier,
|
| 316 |
+
)
|
| 317 |
+
except SpotifyAPIError as exc:
|
| 318 |
+
headers = {}
|
| 319 |
+
if exc.retry_after is not None:
|
| 320 |
+
headers["Retry-After"] = str(exc.retry_after)
|
| 321 |
+
raise HTTPException(status_code=exc.status_code, detail=exc.to_dict(), headers=headers) from exc
|
| 322 |
+
except Exception as exc:
|
| 323 |
+
raise HTTPException(status_code=500, detail="Unexpected server error") from exc
|
| 324 |
+
|
| 325 |
+
|
| 326 |
@app.get("/spotify/artists/id/{artist_id}")
|
| 327 |
def spotify_get_artist(
|
| 328 |
artist_id: str,
|
app/spotify_oauth.py
CHANGED
|
@@ -86,7 +86,6 @@ def exchange_code_for_token(
|
|
| 86 |
code: str,
|
| 87 |
redirect_uri: str,
|
| 88 |
) -> Dict[str, Any]:
|
| 89 |
-
"""Exchange authorization code for access token and refresh token."""
|
| 90 |
settings = get_settings()
|
| 91 |
if not settings.spotify_client_id or not settings.spotify_client_secret:
|
| 92 |
raise RuntimeError("Missing SPOTIFY_CLIENT_ID / SPOTIFY_CLIENT_SECRET")
|
|
@@ -117,6 +116,43 @@ def exchange_code_for_token(
|
|
| 117 |
return resp.json()
|
| 118 |
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
def refresh_access_token(*, refresh_token: str) -> Dict[str, Any]:
|
| 121 |
"""
|
| 122 |
Refresh an expired access token using a refresh token (Authorization Code flow).
|
|
|
|
| 86 |
code: str,
|
| 87 |
redirect_uri: str,
|
| 88 |
) -> Dict[str, Any]:
|
|
|
|
| 89 |
settings = get_settings()
|
| 90 |
if not settings.spotify_client_id or not settings.spotify_client_secret:
|
| 91 |
raise RuntimeError("Missing SPOTIFY_CLIENT_ID / SPOTIFY_CLIENT_SECRET")
|
|
|
|
| 116 |
return resp.json()
|
| 117 |
|
| 118 |
|
| 119 |
+
def exchange_code_for_token_pkce(
|
| 120 |
+
*,
|
| 121 |
+
code: str,
|
| 122 |
+
redirect_uri: str,
|
| 123 |
+
code_verifier: str,
|
| 124 |
+
) -> Dict[str, Any]:
|
| 125 |
+
settings = get_settings()
|
| 126 |
+
if not settings.spotify_client_id or not settings.spotify_client_secret:
|
| 127 |
+
raise RuntimeError("Missing SPOTIFY_CLIENT_ID / SPOTIFY_CLIENT_SECRET")
|
| 128 |
+
|
| 129 |
+
headers = {
|
| 130 |
+
"Authorization": _basic_auth_header(
|
| 131 |
+
settings.spotify_client_id, settings.spotify_client_secret
|
| 132 |
+
),
|
| 133 |
+
"Content-Type": "application/x-www-form-urlencoded",
|
| 134 |
+
}
|
| 135 |
+
data = {
|
| 136 |
+
"grant_type": "authorization_code",
|
| 137 |
+
"code": code,
|
| 138 |
+
"redirect_uri": redirect_uri,
|
| 139 |
+
"code_verifier": code_verifier,
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
resp = requests.post(SPOTIFY_TOKEN_URL, headers=headers, data=data, timeout=20)
|
| 143 |
+
if resp.status_code >= 400:
|
| 144 |
+
try:
|
| 145 |
+
raw = resp.json()
|
| 146 |
+
except Exception: # noqa: BLE001
|
| 147 |
+
raw = {"error": resp.text}
|
| 148 |
+
raise SpotifyAPIError(
|
| 149 |
+
status_code=resp.status_code,
|
| 150 |
+
message=str(raw.get("error_description") or raw.get("error") or "auth_error"),
|
| 151 |
+
raw=raw if isinstance(raw, dict) else None,
|
| 152 |
+
)
|
| 153 |
+
return resp.json()
|
| 154 |
+
|
| 155 |
+
|
| 156 |
def refresh_access_token(*, refresh_token: str) -> Dict[str, Any]:
|
| 157 |
"""
|
| 158 |
Refresh an expired access token using a refresh token (Authorization Code flow).
|