guidesoutenance

This commit is contained in:
2026-09-30 08:24:20 +02:00
parent 4de305bbf4
commit 07f88198c0
21 changed files with 1067 additions and 331 deletions

View File

@@ -1,11 +1,14 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:http/http.dart' as http;
import '../config/intra_config.dart';
import 'intra_http.dart';
import 'intra_oauth_tokens.dart';
/// OAuth2 Intra : un token réutilisé pour toutes les requêtes API.
/// OAuth2 Intra : access + refresh token, un jeton réutilisé pour les requêtes API.
class IntraAuthService {
IntraAuthService._();
static final IntraAuthService instance = IntraAuthService._();
@@ -13,9 +16,13 @@ class IntraAuthService {
final FlutterAppAuth _appAuth = const FlutterAppAuth();
String? _accessToken;
String? _refreshToken;
Future<bool>? _refreshInFlight;
String? get accessToken => _accessToken;
bool get isAuthenticated => _accessToken != null && _accessToken!.isNotEmpty;
bool get canRefresh =>
_refreshToken != null && _refreshToken!.isNotEmpty;
static final AuthorizationServiceConfiguration _serviceConfiguration =
AuthorizationServiceConfiguration(
@@ -36,20 +43,75 @@ class IntraAuthService {
IntraConfig.redirectUri,
clientSecret: IntraConfig.clientSecret,
serviceConfiguration: _serviceConfiguration,
// `projects` requis pour projects_users ; skills via cursus_users (pas /experiences).
scopes: ['public', 'projects'],
),
);
_accessToken = result.accessToken;
_applyOAuthResult(result.accessToken, result.refreshToken);
if (!isAuthenticated) {
throw StateError('Connexion 42 annulée ou token absent');
}
}
void _applyOAuthResult(String? access, String? refresh) {
_accessToken = access;
if (refresh != null && refresh.isNotEmpty) {
_refreshToken = refresh;
}
}
void signOut() {
_accessToken = null;
_refreshToken = null;
_refreshInFlight = null;
}
/// Rafraîchit l’access token (bonus). Retourne `false` si impossible.
Future<bool> refreshAccessToken() async {
if (!canRefresh) return false;
final inFlight = _refreshInFlight;
if (inFlight != null) return inFlight;
final future = _refreshAccessTokenOnce();
_refreshInFlight = future;
try {
return await future;
} finally {
if (identical(_refreshInFlight, future)) {
_refreshInFlight = null;
}
}
}
Future<bool> _refreshAccessTokenOnce() async {
if (!canRefresh) return false;
final response = await http.post(
Uri.parse(IntraConfig.tokenUrl),
headers: const {'Accept': 'application/json'},
body: <String, String>{
'grant_type': 'refresh_token',
'client_id': IntraConfig.clientId,
'client_secret': IntraConfig.clientSecret,
'refresh_token': _refreshToken!,
},
);
if (response.statusCode != 200) return false;
final decoded = jsonDecode(response.body);
if (decoded is! Map<String, dynamic>) return false;
final pair = parseOAuthTokenResponse(decoded);
if (pair == null) return false;
_accessToken = pair.accessToken;
if (pair.refreshToken != null) {
_refreshToken = pair.refreshToken;
}
return true;
}
/// Header Authorization pour les appels `api.intra.42.fr`.
@@ -64,15 +126,23 @@ class IntraAuthService {
}
Future<Map<String, dynamic>> fetchMe() async {
final response = await http.get(
final response = await intraApiGet(
Uri.parse('${IntraConfig.apiBaseUrl}/me'),
headers: authHeaders(),
auth: this,
);
if (response.statusCode != 200) {
throw Exception('GET /v2/me a échoué (${response.statusCode})');
}
return jsonDecode(response.body) as Map<String, dynamic>;
}
@visibleForTesting
void debugSetTokens({String? access, String? refresh}) {
_accessToken = access;
_refreshToken = refresh;
}
/// Simule un access token expiré (garde le refresh) — tests bonus en debug.
@visibleForTesting
void debugInvalidateAccessToken() {
if (!canRefresh) return;
_accessToken = 'invalid_access_token_for_refresh_test';
}
}