import secrets import logging import httpx from fastapi import APIRouter, Request, Depends logger = logging.getLogger(__name__) from fastapi.responses import RedirectResponse from fastapi.templating import Jinja2Templates from sqlalchemy.orm import Session from web.auth import get_current_user from web.config import settings from web.database import get_db from web.models import User, EvotorConnection router = APIRouter(prefix="/evotor") templates = Jinja2Templates(directory="web/templates") EVOTOR_AUTHORIZE_URL = "https://oauth.evotor.ru/oauth/authorize" EVOTOR_TOKEN_URL = "https://oauth.evotor.ru/oauth/token" EVOTOR_STORES_URL = "https://api.evotor.ru/stores" def _redirect_uri() -> str: return f"{settings.BASE_URL}/evotor/callback" @router.get("") def evotor_page( request: Request, db: Session = Depends(get_db), user: User | None = Depends(get_current_user), ): if not user: return RedirectResponse("/login", 303) connection = db.query(EvotorConnection).filter(EvotorConnection.user_id == user.id).first() error = request.query_params.get("error") return templates.TemplateResponse("evotor.html", { "request": request, "user": user, "connection": connection, "error": error, }) @router.get("/connect") def evotor_connect(request: Request, user: User | None = Depends(get_current_user)): if not user: return RedirectResponse("/login", 303) state = secrets.token_urlsafe(32) request.session["evotor_oauth_state"] = state params = ( f"?client_id={settings.EVOTOR_CLIENT_ID}" f"&response_type=code" f"&redirect_uri={_redirect_uri()}" f"&scope={settings.EVOTOR_SCOPES.replace(' ', '%20')}" f"&state={state}" ) return RedirectResponse(EVOTOR_AUTHORIZE_URL + params, 302) @router.get("/callback") async def evotor_callback( request: Request, db: Session = Depends(get_db), user: User | None = Depends(get_current_user), ): if not user: return RedirectResponse("/login", 303) code = request.query_params.get("code") state = request.query_params.get("state") saved_state = request.session.pop("evotor_oauth_state", None) if not code or not state or state != saved_state: return RedirectResponse("/evotor?error=invalid_state", 303) # Exchange code for access token try: async with httpx.AsyncClient() as client: token_response = await client.post( EVOTOR_TOKEN_URL, data={ "grant_type": "authorization_code", "code": code, "redirect_uri": _redirect_uri(), "client_id": settings.EVOTOR_CLIENT_ID, "client_secret": settings.EVOTOR_CLIENT_SECRET, }, timeout=15, ) token_response.raise_for_status() token_data = token_response.json() except httpx.HTTPStatusError as e: logger.error("Evotor token exchange HTTP error %s: %s", e.response.status_code, e.response.text) return RedirectResponse("/evotor?error=token_exchange", 303) except Exception as e: logger.error("Evotor token exchange failed: %s", e, exc_info=True) return RedirectResponse("/evotor?error=token_exchange", 303) access_token = token_data.get("access_token") refresh_token = token_data.get("refresh_token") expires_in = token_data.get("expires_in") if not access_token: return RedirectResponse("/evotor?error=no_token", 303) # Fetch first store to save store info store_id = None store_name = None try: async with httpx.AsyncClient() as client: stores_response = await client.get( EVOTOR_STORES_URL, headers={"Authorization": f"Bearer {access_token}"}, timeout=15, ) if stores_response.status_code == 200: stores = stores_response.json() items = stores.get("items", stores) if isinstance(stores, dict) else stores if items: store_id = items[0].get("uuid") or items[0].get("id") store_name = items[0].get("name") except Exception: pass # Store info is optional; token is still saved # Save or update connection from datetime import datetime, timedelta now = datetime.utcnow() token_expires_at = now + timedelta(seconds=expires_in) if expires_in else None connection = db.query(EvotorConnection).filter(EvotorConnection.user_id == user.id).first() if connection: connection.access_token = access_token connection.refresh_token = refresh_token connection.token_expires_at = token_expires_at connection.store_id = store_id connection.store_name = store_name connection.is_online = True connection.last_checked_at = now else: connection = EvotorConnection( user_id=user.id, access_token=access_token, refresh_token=refresh_token, token_expires_at=token_expires_at, store_id=store_id, store_name=store_name, is_online=True, last_checked_at=now, ) db.add(connection) db.commit() return RedirectResponse("/connections", 303) @router.post("/disconnect") async def evotor_disconnect( request: Request, db: Session = Depends(get_db), user: User | None = Depends(get_current_user), ): if not user: return RedirectResponse("/login", 303) connection = db.query(EvotorConnection).filter(EvotorConnection.user_id == user.id).first() if connection: db.delete(connection) db.commit() return RedirectResponse("/connections", 303)