import 'dart:convert'; import 'package:flutter_appauth/flutter_appauth.dart'; import 'package:http/http.dart' as http; import '../config/intra_config.dart'; /// OAuth2 Intra : un token réutilisé pour toutes les requêtes API. class IntraAuthService { IntraAuthService._(); static final IntraAuthService instance = IntraAuthService._(); final FlutterAppAuth _appAuth = const FlutterAppAuth(); String? _accessToken; String? get accessToken => _accessToken; bool get isAuthenticated => _accessToken != null && _accessToken!.isNotEmpty; static final AuthorizationServiceConfiguration _serviceConfiguration = AuthorizationServiceConfiguration( authorizationEndpoint: IntraConfig.authorizeUrl, tokenEndpoint: IntraConfig.tokenUrl, ); Future signIn() async { if (!IntraConfig.isConfigured) { throw StateError( 'CLIENT_ID, CLIENT_SECRET ou REDIRECT_URI manquant dans .env', ); } final result = await _appAuth.authorizeAndExchangeCode( AuthorizationTokenRequest( IntraConfig.clientId, IntraConfig.redirectUri, clientSecret: IntraConfig.clientSecret, serviceConfiguration: _serviceConfiguration, // `projects` requis pour projects_users ; skills via cursus_users (pas /experiences). scopes: ['public', 'projects'], ), ); _accessToken = result.accessToken; if (!isAuthenticated) { throw StateError('Connexion 42 annulée ou token absent'); } } void signOut() { _accessToken = null; } /// Header Authorization pour les appels `api.intra.42.fr`. Map authHeaders() { if (!isAuthenticated) { throw StateError('Non connecté à l’Intra'); } return { 'Authorization': 'Bearer $_accessToken', 'Accept': 'application/json', }; } Future> fetchMe() async { final response = await http.get( Uri.parse('${IntraConfig.apiBaseUrl}/me'), headers: authHeaders(), ); if (response.statusCode != 200) { throw Exception('GET /v2/me a échoué (${response.statusCode})'); } return jsonDecode(response.body) as Map; } }