"""Outlook Auth Routes - GMAO Collège""" from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify, send_file from flask_login import login_required, current_user from app_new.extensions import db from ..outlook.models import OutlookAccount, OutlookMail, OutlookFolder, OutlookAttachment, OutlookMailInterpretation, GmaoContext import os import subprocess import json import tempfile auth_bp = Blueprint('outlook_auth', __name__, template_folder='templates') # ─── API OAuth2 ─────────────────────────────────────────────────────────── @auth_bp.route('/api/start-auth', methods=['POST']) @login_required def api_start_auth(): """Démarre le processus d'authentification OAuth2.""" import requests from datetime import datetime, timezone, timedelta email = request.json.get('email') if not email: return jsonify({'error': 'Email requis'}), 400 # Constantes OAuth2 CLIENT_ID = "9199bf20-a13f-4107-85dc-02114787ef48" SCOPES = "https://outlook.office365.com/Mail.ReadWrite https://outlook.office365.com/Mail.Send offline_access" # Vérifier si le compte existe déjà — si oui, le supprimer pour re-authentifier existing = OutlookAccount.query.filter_by(email=email, user_id=current_user.id).first() if existing: # Supprimer les données associées puis le compte from app_new.outlook.models import OutlookMail, OutlookFolder OutlookMail.query.filter_by(account_id=existing.id).delete() OutlookFolder.query.filter_by(account_id=existing.id).delete() db.session.delete(existing) db.session.commit() try: # Découvrir le tenant ID domain = email.split('@')[1] discovery_url = f"https://login.microsoftonline.com/{domain}/.well-known/openid-configuration" resp = requests.get(discovery_url, timeout=10) tenant_id = resp.json().get('token_endpoint', '').split('/')[3] if resp.status_code == 200 else "befa6ca1-bada-43da-958c-cafc3c57ec70" # Demander le device code token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/devicecode" dev_resp = requests.post(token_url, data={'client_id': CLIENT_ID, 'scope': SCOPES}) if dev_resp.status_code != 200: return jsonify({'error': 'Erreur de configuration OAuth2'}), 500 device_data = dev_resp.json() # Créer le compte en attente account = OutlookAccount( user_id=current_user.id, email=email, tenant_id=tenant_id, refresh_token='', device_code=device_data.get('device_code'), user_code=device_data.get('user_code'), connection_status='pending', device_code_expires_at=datetime.now(timezone.utc) + timedelta(minutes=device_data.get('expires_in', 900)) ) db.session.add(account) db.session.commit() return jsonify({ 'verification_uri': device_data.get('verification_uri'), 'user_code': device_data.get('user_code'), 'expires_in': device_data.get('expires_in'), 'account_id': account.id }) except Exception as e: return jsonify({'error': f'Erreur: {str(e)}'}), 500 @auth_bp.route('/api/poll-auth/', methods=['POST']) @login_required def api_poll_auth(account_id): """Polling pour vérifier si l'authentification est terminée.""" import requests from datetime import datetime, timezone account = OutlookAccount.query.get(account_id) if not account or account.user_id != current_user.id: return jsonify({'error': 'Compte non trouvé'}), 404 if not account.device_code: return jsonify({'error': 'Device code non trouvé'}), 400 # Vérifier si le device code a expiré expires_at = account.device_code_expires_at if expires_at: if expires_at.tzinfo is None: expires_at = expires_at.replace(tzinfo=timezone.utc) if expires_at < datetime.now(timezone.utc): db.session.delete(account) db.session.commit() return jsonify({'error': 'Code expiré, veuillez recommencer', 'expired': True}), 400 CLIENT_ID = "9199bf20-a13f-4107-85dc-02114787ef48" try: # Polling Microsoft pour le token token_url = f"https://login.microsoftonline.com/{account.tenant_id}/oauth2/v2.0/token" resp = requests.post(token_url, data={ 'client_id': CLIENT_ID, 'grant_type': 'urn:ietf:params:oauth:grant-type:device_code', 'device_code': account.device_code }) data = resp.json() if resp.status_code == 200 and 'refresh_token' in data: # Succès - sauvegarder le refresh token from cryptography.fernet import Fernet import os key = os.environ.get('OUTLOOK_ENCRYPTION_KEY', Fernet.generate_key()) cipher = Fernet(key if isinstance(key, bytes) else key.encode()) account.refresh_token = cipher.encrypt(data['refresh_token'].encode()).decode() account.connection_status = 'active' account.last_connected_at = datetime.now(timezone.utc) account.device_code = None account.user_code = None account.device_code_expires_at = None account.is_active = True db.session.commit() return jsonify({ 'success': True, 'message': 'Authentification réussie', 'account_id': account.id }) if data.get('error') == 'authorization_pending': return jsonify({'pending': True, 'message': "En attente de l'autorisation"}) return jsonify({'error': data.get('error_description', data.get('error', 'Erreur inconnue'))}), 400 except Exception as e: return jsonify({'error': f'Erreur: {str(e)}'}), 500 # ─── API Synchronisation ─────────────────────────────────────────────────── def get_access_token(account): """Obtient un access_token à partir du refresh_token.""" import requests from cryptography.fernet import Fernet import os if not account.refresh_token: return None, "Aucun token d'authentification" key = os.environ.get('OUTLOOK_ENCRYPTION_KEY') if not key: return None, "Clé de chiffrement non configurée" cipher = Fernet(key if isinstance(key, bytes) else key.encode()) refresh_token = cipher.decrypt(account.refresh_token.encode()).decode() CLIENT_ID = "9199bf20-a13f-4107-85dc-02114787ef48" token_url = f"https://login.microsoftonline.com/{account.tenant_id}/oauth2/v2.0/token" resp = requests.post(token_url, data={ 'client_id': CLIENT_ID, 'grant_type': 'refresh_token', 'refresh_token': refresh_token, 'scope': 'https://outlook.office365.com/Mail.ReadWrite https://outlook.office365.com/Mail.Send offline_access' }) if resp.status_code != 200: return None, f"Erreur d'authentification: {resp.json().get('error_description', resp.text)}" return resp.json().get('access_token'), None