from __future__ import annotations from fastapi import APIRouter from fastapi.responses import JSONResponse from pydantic import BaseModel from app.config import settings from app.db.repositories import get_sync_state from app.db.session import get_session from app.isolarcloud import shared_token_store router = APIRouter(prefix="/api/isolarcloud", tags=["isolarcloud"]) @router.get("/status") def status() -> dict: if settings.isolarcloud_auth_mode == "oauth2" and shared_token_store.enabled(): with shared_token_store.locked() as fd: state = shared_token_store.read(fd) has_token = bool(state and state.get("access_token")) else: token_key = "isolarcloud_oauth_access_token" if settings.isolarcloud_auth_mode == "oauth2" else "isolarcloud_token" with get_session() as session: has_token = bool(get_sync_state(session, token_key)) return {"use_mock": settings.use_mock_isolarcloud, "auth_mode": settings.isolarcloud_auth_mode, "connected": has_token} @router.get("/oauth/authorize-url") def oauth_authorize_url() -> dict: from app.isolarcloud.oauth_client import ISolarCloudOAuthClient return {"url": ISolarCloudOAuthClient().authorize_url()} class OAuthCodeIn(BaseModel): code: str @router.post("/oauth/exchange") def oauth_exchange(body: OAuthCodeIn) -> JSONResponse: """Completes the OAuth2 consent flow: trades the `code` param (copied from the address bar after the user approved access) for a token pair, and returns the plants the user actually authorized.""" from app.isolarcloud.oauth_client import ISolarCloudAuthError, ISolarCloudOAuthClient client = ISolarCloudOAuthClient() try: auth_ps_list = client.exchange_code(body.code) except ISolarCloudAuthError as e: return JSONResponse({"ok": False, "error": str(e)}, status_code=400) except Exception as e: return JSONResponse({"ok": False, "error": f"Unexpected error: {e}"}, status_code=502) plants = [] try: plants = client.get_plant_list() except Exception: pass plants_by_id = {str(p.get("ps_id")): p.get("ps_name") for p in plants} return JSONResponse( { "ok": True, "authorized_plants": [{"ps_id": ps_id, "ps_name": plants_by_id.get(ps_id)} for ps_id in auth_ps_list], } ) @router.post("/connect") def connect() -> JSONResponse: """Logs in with the credentials configured in .env and, on success, returns the account's visible plants so the user can pick a `isolarcloud_plant_id` in Settings if they have more than one.""" from app.isolarcloud.client import ISolarCloudAuthError, ISolarCloudClient if settings.use_mock_isolarcloud: return JSONResponse({"ok": False, "error": "USE_MOCK_ISOLARCLOUD is still true -- set it to false in .env first."}, status_code=400) client = ISolarCloudClient() try: client.login() plants = client.get_plant_list() except ISolarCloudAuthError as e: return JSONResponse({"ok": False, "error": str(e)}, status_code=400) except Exception as e: # network/HTTP errors etc. return JSONResponse({"ok": False, "error": f"Unexpected error: {e}"}, status_code=502) return JSONResponse({"ok": True, "plants": [{"ps_id": p.get("ps_id"), "ps_name": p.get("ps_name")} for p in plants]})