From 86bdd490ac2abd2744c33ec9fc2a8963879fc435 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Sat, 23 May 2026 22:01:54 +0200 Subject: [PATCH] fix: Earn.fm collector (Supabase auth), exchange rates, and CSP - Rewrite Earn.fm collector to use Supabase email/password auth instead of the broken UUID token (which is only for the Docker bandwidth client) - Fix exchange rates: Frankfurter API returns 301 but httpx wasn't following redirects, causing empty fiat rates (only USD available) - Fix CSP: allow connect-src to cdn.jsdelivr.net for Chart.js source maps --- app/collectors/__init__.py | 2 +- app/collectors/earnfm.py | 55 ++++++++++++++++++++++++++++------- app/exchange_rates.py | 2 +- app/main.py | 4 +-- services/bandwidth/earnfm.yml | 10 +++---- 5 files changed, 53 insertions(+), 20 deletions(-) diff --git a/app/collectors/__init__.py b/app/collectors/__init__.py index 1a5d0c6..1f470fd 100644 --- a/app/collectors/__init__.py +++ b/app/collectors/__init__.py @@ -56,7 +56,7 @@ "repocket": ["email", "password"], "proxyrack": ["api_key"], "bitping": ["email", "password"], - "earnfm": ["token"], + "earnfm": ["email", "password"], "packetstream": ["auth_token"], "grass": ["access_token"], "bytelixir": ["session_cookie"], diff --git a/app/collectors/earnfm.py b/app/collectors/earnfm.py index 075b33d..88d9601 100644 --- a/app/collectors/earnfm.py +++ b/app/collectors/earnfm.py @@ -1,7 +1,7 @@ """Earn.fm earnings collector. -Authenticates via a UUID API key (EARNFM_TOKEN) obtained from the -Earn.fm dashboard at app.earn.fm > Account Settings. +Authenticates via Supabase (email/password) at sb.earn.fm, then uses +the access token to query the harvester balance API. """ from __future__ import annotations @@ -14,35 +14,67 @@ logger = logging.getLogger(__name__) +SUPABASE_URL = "https://sb.earn.fm" +SUPABASE_ANON_KEY = ( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "ewogICJyb2xlIjogImFub24iLAogICJpc3MiOiAic3VwYWJhc2UiLAogICJpYXQiOiAxNjkyNjU1MjAwLAogICJleHAiOiAxODUwNTA4MDAwCn0." + "jp-Uj5ro0jj7MHnlE8HHZRsZAFOI1d_T9n_9tnE09vM" +) API_BASE = "https://api.earn.fm/v2" class EarnFMCollector(BaseCollector): - """Collect earnings from Earn.fm's API using a token.""" + """Collect earnings from Earn.fm using Supabase email/password auth.""" platform = "earnfm" - def __init__(self, token: str) -> None: + def __init__(self, email: str, password: str) -> None: super().__init__() - self._token = token.strip() + self._email = email.strip() + self._password = password.strip() + self._access_token: str = "" + + async def _authenticate(self) -> str | None: + """Sign in via Supabase and return the access token.""" + client = self._get_client(timeout=30) + resp = await client.post( + f"{SUPABASE_URL}/auth/v1/token?grant_type=password", + headers={ + "apikey": SUPABASE_ANON_KEY, + "Content-Type": "application/json", + }, + json={"email": self._email, "password": self._password}, + ) + if resp.status_code == 400: + return None + resp.raise_for_status() + data = resp.json() + return data.get("access_token") async def collect(self) -> EarningsResult: """Fetch current Earn.fm balance.""" - if not self._token: + if not self._email or not self._password: return EarningsResult( platform=self.platform, balance=0.0, - error="No token configured — copy API key from app.earn.fm > Settings", + error="No credentials configured — enter Earn.fm email and password", ) try: + token = await self._authenticate() + if not token: + return EarningsResult( + platform=self.platform, + balance=0.0, + error="Invalid credentials — check Earn.fm email/password in Settings", + ) + client = self._get_client(timeout=30) - headers = {"X-API-Key": self._token} async def _fetch_balance() -> httpx.Response: return await client.get( f"{API_BASE}/harvester/view_balance", - headers=headers, + headers={"X-API-Key": token}, ) resp = await self._retry(_fetch_balance) @@ -51,13 +83,14 @@ async def _fetch_balance() -> httpx.Response: return EarningsResult( platform=self.platform, balance=0.0, - error="Token invalid or expired — refresh API key from app.earn.fm", + error="Auth token rejected — check Earn.fm email/password in Settings", ) resp.raise_for_status() data = resp.json() - balance = float((data.get("data") or {}).get("totalBalance", 0)) + balance_data = data.get("data") or {} + balance = float(balance_data.get("totalBalance", 0)) return EarningsResult( platform=self.platform, diff --git a/app/exchange_rates.py b/app/exchange_rates.py index fd22fd4..4b03edb 100644 --- a/app/exchange_rates.py +++ b/app/exchange_rates.py @@ -39,7 +39,7 @@ async def refresh() -> None: global _fiat_rates, _crypto_usd, _last_fetch try: - async with httpx.AsyncClient(timeout=15) as client: + async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client: # --- Crypto rates from CoinGecko (free, no key) --- if CRYPTO_IDS: ids = ",".join(CRYPTO_IDS.values()) diff --git a/app/main.py b/app/main.py index 3830cc1..d4150b7 100644 --- a/app/main.py +++ b/app/main.py @@ -309,7 +309,7 @@ async def dispatch(self, request, call_next): "style-src 'self' https://fonts.googleapis.com 'unsafe-inline'; " "font-src 'self' https://fonts.gstatic.com; " "img-src 'self' data:; " - "connect-src 'self'; " + "connect-src 'self' https://cdn.jsdelivr.net; " "frame-ancestors 'none'" ) return response @@ -1491,7 +1491,7 @@ async def api_collectors_meta(request: Request) -> list[dict[str, Any]]: "bitping": "Use your Bitping account email and password (same as nodes.bitping.com).", "bytelixir": "Log in at dash.bytelixir.com (tick Remember Me), press F12 → Application → expand Cookies in the left sidebar → click https://dash.bytelixir.com → find bytelixir_session → copy its Value.", "earnapp": "Log in at earnapp.com, press F12 → Application → Cookies, copy the oauth-refresh-token value.", - "earnfm": "Copy your UUID API key from app.earn.fm → Account Settings.", + "earnfm": "Use your Earn.fm account email and password (same as app.earn.fm login).", "grass": "Log in at app.getgrass.io, press F12 → Application → Local Storage, copy the accessToken value.", "honeygain": "Use your Honeygain account email and password (same as dashboard.honeygain.com).", "iproyal": "Use your IPRoyal Pawns account email and password (same as pawns.app).", diff --git a/services/bandwidth/earnfm.yml b/services/bandwidth/earnfm.yml index 84931a1..2fa221f 100644 --- a/services/bandwidth/earnfm.yml +++ b/services/bandwidth/earnfm.yml @@ -4,10 +4,10 @@ category: bandwidth status: active website: https://earn.fm description: > - Earn.fm pays you for sharing your internet bandwidth. Authenticates via - a UUID API key from the dashboard. Lightweight Docker image with - straightforward deployment. -short_description: Bandwidth sharing - API key auth, lightweight client + Earn.fm pays you for sharing your internet bandwidth. Earnings collector + uses email/password auth via Supabase. Lightweight Docker image with + straightforward deployment (Docker client uses separate UUID token). +short_description: Bandwidth sharing - email/password auth, lightweight client referral: signup_url: "https://earn.fm/ref/GEISYB91" @@ -57,4 +57,4 @@ platforms: [docker, linux] collector: type: api - notes: "API key auth. Dashboard at app.earn.fm shows earnings." + notes: "Supabase email/password auth. Docker client uses separate EARNFM_TOKEN (UUID) for bandwidth sharing only."