cyberhybridhub/lib/services/auth_service_linux.dart
2026-05-20 10:22:58 -05:00

202 lines
5.6 KiB
Dart

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<AppUser?> _authController =
StreamController<AppUser?>.broadcast();
GoogleSignIn? _googleSignIn;
AppUser? _currentUser;
String? _idToken;
String? _refreshToken;
DateTime? _tokenExpiry;
Stream<AppUser?> get authStateChanges => _authController.stream;
AppUser? get currentUser => _currentUser;
Future<void> initialize() async {
if (!_hasOAuthConfig) {
_emitUser(null);
return;
}
_googleSignIn = GoogleSignIn(
params: GoogleSignInParams(
clientId: googleWebOAuthClientId!,
clientSecret: googleOAuthDesktopClientSecret!,
scopes: const <String>['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<void> 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<void> signOut() async {
await _googleSignIn?.signOut();
_clearTokens();
_emitUser(null);
}
Future<String?> 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<String, dynamic> 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<void> _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 <String, String>{
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'grant_type=refresh_token&refresh_token=$_refreshToken',
);
if (response.statusCode != 200) {
_clearTokens();
return;
}
final Map<String, dynamic> data =
jsonDecode(response.body) as Map<String, dynamic>;
_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<AppUser> _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 <String, String>{'Content-Type': 'application/json'},
body: jsonEncode(<String, dynamic>{
'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<String, dynamic> data =
jsonDecode(response.body) as Map<String, dynamic>;
_storeTokens(data);
return AppUser(
uid: data['localId'] as String,
email: data['email'] as String?,
displayName: data['displayName'] as String?,
photoUrl: data['photoUrl'] as String?,
);
}
}