Files
swift_companion/swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart

158 lines
4.5 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',
};
}
/// En-têtes pour télécharger la photo sur `cdn.intra.42.fr`.
///
/// L’URL vient du JSON user (`image.link` / `versions`). Le CDN refuse souvent
/// les requêtes sans Bearer (403). On réutilise le **même** access token que pour
/// l’API — pas un second OAuth. On n’envoie pas `Accept: application/json` ici.
Map<String, String> cdnImageHeaders() {
if (!isAuthenticated) return const {};
return {'Authorization': 'Bearer $_accessToken'};
}
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) — démo bonus, appelé depuis l’UI en `kDebugMode`.
void debugInvalidateAccessToken() {
if (!canRefresh) return;
_accessToken = 'invalid_access_token_for_refresh_test';
}
}