Stockbit Research research Playground @hansputera
auth.py — 3.8 KB Download
""" Authentication manager for Stockbit API. Handles main token and trading token lifecycle. """ import time import requests from typing import Optional from .exceptions import AuthenticationError, TokenExpiredError, InvalidPINError class AuthManager: EXODUS_BASE = "https://exodus.stockbit.com" CARINA_BASE = "https://carina.stockbit.com" def __init__(self, main_token: str, trading_pin: Optional[str] = None): self.main_token = main_token self.trading_pin = trading_pin self.trading_token: Optional[str] = None self.trading_refresh: Optional[str] = None self.trading_expires_at: float = 0.0 self.account_number: Optional[int] = None self._session = requests.Session() self._session.headers.update({ "Authorization": f"Bearer {main_token}", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", }) @property def _exodus_headers(self): return {"Authorization": f"Bearer {self.main_token}"} @property def _carina_headers(self): return {"Authorization": f"Bearer {self.trading_token}"} def get_sekuritas_token(self) -> str: resp = self._session.get( f"{self.EXODUS_BASE}/sekuritas/auth/token", headers=self._exodus_headers, ) resp.raise_for_status() body = resp.json() if "data" not in body: raise AuthenticationError(f"Failed to get sekuritas token: {body}") d = body["data"] if isinstance(d, dict) and "token" in d: return d["token"] return d def login_trading(self, sekuritas_token: Optional[str] = None, pin: Optional[str] = None) -> dict: sekuritas_token = sekuritas_token or self.get_sekuritas_token() pin = pin or self.trading_pin if not pin: raise InvalidPINError("Trading PIN is required") resp = self._session.post( f"{self.CARINA_BASE}/auth/v2/login", json={"login_token": sekuritas_token, "pin": pin}, ) resp.raise_for_status() payload = resp.json() if "data" not in payload: raise AuthenticationError(f"Trading login failed: {payload}") data = payload["data"] self.trading_token = data["access_token"] self.trading_refresh = data.get("refresh_token") self.account_number = data.get("account", {}).get("number") # JWT typically expires in 24h self.trading_expires_at = time.time() + 86400 return data def refresh_trading_token(self) -> str: if not self.trading_refresh: raise AuthenticationError("No refresh token available") resp = self._session.post( f"{self.CARINA_BASE}/auth/refresh", headers={"Authorization": f"Bearer {self.trading_token}"}, ) resp.raise_for_status() payload = resp.json() if "data" not in payload: raise AuthenticationError(f"Token refresh failed: {payload}") data = payload["data"] self.trading_token = data.get("access_token", self.trading_token) self.trading_refresh = data.get("refresh_token", self.trading_refresh) self.trading_expires_at = time.time() + 86400 return self.trading_token def ensure_trading_token(self) -> str: if not self.trading_token: if not self.trading_pin: raise AuthenticationError("Trading not logged in. Call login_trading() first.") self.login_trading() elif time.time() > self.trading_expires_at - 300: try: self.refresh_trading_token() except Exception: if self.trading_pin: self.login_trading() else: raise TokenExpiredError("Trading token expired and cannot refresh") return self.trading_token