""" OAuth API Routes ================ Endpoints for connecting external accounts (Google, Meta) via OAuth 2.0. """ import logging import secrets from flask import jsonify, request, redirect, session from flask_login import login_required, current_user from . import bp from database import SessionLocal, OAuthToken logger = logging.getLogger(__name__) @bp.route('/oauth/connect//', methods=['POST']) @login_required def oauth_connect(provider, service): """Initiate OAuth flow for connecting an external account. POST /api/oauth/connect/google/gbp POST /api/oauth/connect/meta/facebook """ from oauth_service import OAuthService # Validate provider/service valid_combinations = { ('google', 'gbp'), ('google', 'search_console'), ('meta', 'facebook'), ('meta', 'instagram'), } if (provider, service) not in valid_combinations: return jsonify({'success': False, 'error': f'Nieznany provider/service: {provider}/{service}'}), 400 # User must have a company if not current_user.company_id: return jsonify({'success': False, 'error': 'Musisz być przypisany do firmy'}), 403 # Generate CSRF state token state = f"{current_user.company_id}:{current_user.id}:{service}:{secrets.token_urlsafe(16)}" session['oauth_state'] = state oauth = OAuthService() auth_url = oauth.get_authorization_url(provider, service, state) if not auth_url: return jsonify({ 'success': False, 'error': f'OAuth nie skonfigurowany dla {provider}. Skontaktuj się z administratorem.' }), 503 return jsonify({'success': True, 'auth_url': auth_url}) @bp.route('/oauth/callback/', methods=['GET']) @login_required def oauth_callback(provider): """Handle OAuth callback from provider. GET /api/oauth/callback/google?code=...&state=... GET /api/oauth/callback/meta?code=...&state=... """ from oauth_service import OAuthService code = request.args.get('code') state = request.args.get('state') error = request.args.get('error') if error: logger.warning(f"OAuth error from {provider}: {error}") return redirect(f'/admin/company?oauth_error={error}') if not code or not state: return redirect('/admin/company?oauth_error=missing_params') # Validate state saved_state = session.pop('oauth_state', None) if not saved_state or saved_state != state: logger.warning(f"OAuth state mismatch for {provider}") return redirect('/admin/company?oauth_error=invalid_state') # Parse state: company_id:user_id:service:random try: parts = state.split(':') company_id = int(parts[0]) user_id = int(parts[1]) service = parts[2] except (ValueError, IndexError): return redirect('/admin/company?oauth_error=invalid_state_format') # Verify user owns this company if current_user.id != user_id or current_user.company_id != company_id: return redirect('/admin/company?oauth_error=unauthorized') # Exchange code for token oauth = OAuthService() token_data = oauth.exchange_code(provider, code) if not token_data: return redirect('/admin/company?oauth_error=token_exchange_failed') # Save token db = SessionLocal() try: success = oauth.save_token(db, company_id, user_id, provider, service, token_data) if success: logger.info(f"OAuth connected: {provider}/{service} for company {company_id} by user {user_id}") return redirect(f'/admin/company?oauth_success={provider}/{service}') else: return redirect('/admin/company?oauth_error=save_failed') finally: db.close() @bp.route('/oauth/status', methods=['GET']) @login_required def oauth_status(): """Get connected OAuth services for current user's company. GET /api/oauth/status """ from oauth_service import OAuthService if not current_user.company_id: return jsonify({'success': True, 'services': {}}) db = SessionLocal() try: oauth = OAuthService() services = oauth.get_connected_services(db, current_user.company_id) # Add available (but not connected) services all_services = { 'google/gbp': {'name': 'Google Business Profile', 'description': 'Pełne dane o wizytówce, opinie, insights'}, 'google/search_console': {'name': 'Google Search Console', 'description': 'Zapytania, CTR, pozycje w wyszukiwaniu'}, 'meta/facebook': {'name': 'Facebook', 'description': 'Reach, impressions, demographics, post insights'}, 'meta/instagram': {'name': 'Instagram', 'description': 'Stories, reels, engagement metrics'}, } result = {} for key, info in all_services.items(): connected = services.get(key, {}) result[key] = { **info, 'connected': bool(connected), 'account_name': connected.get('account_name'), 'expires_at': connected.get('expires_at'), 'is_expired': connected.get('is_expired', False), } return jsonify({'success': True, 'services': result}) finally: db.close() @bp.route('/oauth/disconnect//', methods=['POST']) @login_required def oauth_disconnect(provider, service): """Disconnect an OAuth service. POST /api/oauth/disconnect/google/gbp """ if not current_user.company_id: return jsonify({'success': False, 'error': 'Brak przypisanej firmy'}), 403 db = SessionLocal() try: token = db.query(OAuthToken).filter( OAuthToken.company_id == current_user.company_id, OAuthToken.provider == provider, OAuthToken.service == service, ).first() if token: token.is_active = False db.commit() logger.info(f"OAuth disconnected: {provider}/{service} for company {current_user.company_id}") return jsonify({'success': True, 'message': f'{provider}/{service} rozłączony'}) else: return jsonify({'success': False, 'error': 'Nie znaleziono połączenia'}), 404 finally: db.close()