import 'dart:async'; import 'dart:convert'; import 'package:google_sign_in_all_platforms/google_sign_in_all_platforms.dart'; import 'package:http/http.dart' as http; import '../config/auth_config.dart'; import '../firebase_options.dart'; import '../models/app_user.dart'; class AuthServiceLinux { AuthServiceLinux._(); static final AuthServiceLinux instance = AuthServiceLinux._(); final StreamController _authController = StreamController.broadcast(); GoogleSignIn? _googleSignIn; AppUser? _currentUser; String? _idToken; String? _refreshToken; DateTime? _tokenExpiry; Stream get authStateChanges => _authController.stream; AppUser? get currentUser => _currentUser; Future initialize() async { if (!_hasOAuthConfig) { _emitUser(null); return; } _googleSignIn = GoogleSignIn( params: GoogleSignInParams( clientId: googleWebOAuthClientId!, clientSecret: googleOAuthDesktopClientSecret!, scopes: const ['openid', 'profile', 'email'], ), ); _googleSignIn!.authenticationState.listen((GoogleSignInCredentials? creds) { if (creds == null) { _emitUser(null); } }); final GoogleSignInCredentials? restored = await _googleSignIn!.silentSignIn(); if (restored?.idToken != null) { try { final AppUser user = await _signInToFirebaseWithIdp(restored!.idToken!); _emitUser(user); } catch (_) { // Ignore stale desktop sessions on startup. } } } bool get _hasOAuthConfig => googleWebOAuthClientId != null && googleOAuthDesktopClientSecret != null; Future signInWithGoogle() async { _validateOAuthConfig(); final GoogleSignInCredentials? credentials = await _googleSignIn!.signIn(); if (credentials == null) { throw StateError('Google Sign-In was cancelled.'); } final String? idToken = credentials.idToken; if (idToken == null) { throw StateError('Google Sign-In did not return an ID token.'); } final AppUser user = await _signInToFirebaseWithIdp(idToken); _emitUser(user); } Future signOut() async { await _googleSignIn?.signOut(); _clearTokens(); _emitUser(null); } Future getIdToken({bool forceRefresh = false}) async { if (_currentUser == null) { return null; } if (!forceRefresh && _idToken != null && _tokenExpiry != null && DateTime.now().isBefore(_tokenExpiry!)) { return _idToken; } if (_refreshToken != null) { await _refreshIdToken(); return _idToken; } return _idToken; } void _emitUser(AppUser? user) { _currentUser = user; if (user == null) { _clearTokens(); } _authController.add(user); } void _clearTokens() { _idToken = null; _refreshToken = null; _tokenExpiry = null; } void _storeTokens(Map data) { _idToken = data['idToken'] as String?; _refreshToken = data['refreshToken'] as String?; final int expiresIn = (data['expiresIn'] as String?) != null ? int.parse(data['expiresIn'] as String) : (data['expiresIn'] as num?)?.toInt() ?? 3600; _tokenExpiry = DateTime.now().add(Duration(seconds: expiresIn - 60)); } Future _refreshIdToken() async { if (_refreshToken == null) { return; } final Uri uri = Uri.parse( 'https://securetoken.googleapis.com/v1/token' '?key=${DefaultFirebaseOptions.web.apiKey}', ); final http.Response response = await http.post( uri, headers: const { 'Content-Type': 'application/x-www-form-urlencoded', }, body: 'grant_type=refresh_token&refresh_token=$_refreshToken', ); if (response.statusCode != 200) { _clearTokens(); return; } final Map data = jsonDecode(response.body) as Map; _idToken = data['id_token'] as String?; _refreshToken = data['refresh_token'] as String? ?? _refreshToken; final int expiresIn = (data['expires_in'] as num?)?.toInt() ?? 3600; _tokenExpiry = DateTime.now().add(Duration(seconds: expiresIn - 60)); } void _validateOAuthConfig() { if (!_hasOAuthConfig) { throw StateError( 'Linux desktop auth requires googleWebOAuthClientId and ' 'googleOAuthDesktopClientSecret in lib/config/auth_config.dart. ' 'Create a Web application OAuth client in Google Cloud Console ' '(same project as Firebase) and add ' 'http://127.0.0.1 as an authorized redirect URI.', ); } } Future _signInToFirebaseWithIdp(String idToken) async { final Uri uri = Uri.parse( 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp' '?key=${DefaultFirebaseOptions.web.apiKey}', ); final http.Response response = await http.post( uri, headers: const {'Content-Type': 'application/json'}, body: jsonEncode({ 'postBody': 'id_token=$idToken&providerId=google.com', 'requestUri': 'http://localhost', 'returnIdpCredential': true, 'returnSecureToken': true, }), ); if (response.statusCode != 200) { throw StateError( 'Firebase sign-in failed (${response.statusCode}): ${response.body}', ); } final Map data = jsonDecode(response.body) as Map; _storeTokens(data); return AppUser( uid: data['localId'] as String, email: data['email'] as String?, displayName: data['displayName'] as String?, photoUrl: data['photoUrl'] as String?, ); } }