Files
swift_companion/swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart
2026-09-30 08:24:20 +02:00

149 lines
4.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 : access + refresh token, un jeton réutilisé pour les requêtes API.
class IntraAuthService {
IntraAuthService._();
static final IntraAuthService instance = 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(
authorizationEndpoint: IntraConfig.authorizeUrl,
tokenEndpoint: IntraConfig.tokenUrl,
);
Future<void> 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,
scopes: ['public', 'projects'],
),
);
_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`.
Map<String, String> authHeaders() {
if (!isAuthenticated) {
throw StateError('Non connecté à l’Intra');
}
return {
'Authorization': 'Bearer $_accessToken',
'Accept': 'application/json',
};
}
Future<Map<String, dynamic>> fetchMe() async {
final response = await intraApiGet(
Uri.parse('${IntraConfig.apiBaseUrl}/me'),
auth: this,
);
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';
}
}