gmao/app_new/outlook/auth.py

184 lines
7.4 KiB
Python
Raw Normal View History

2026-08-14 18:02:25 +02:00
"""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/<int:account_id>', 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