108 lines
3.8 KiB
Dart
108 lines
3.8 KiB
Dart
import 'dart:async';
|
|
import 'dart:io' show Platform;
|
|
|
|
import 'package:google_sign_in/google_sign_in.dart';
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import 'drive_oauth_config.dart';
|
|
|
|
/// Full Drive access is required (not the narrower `drive.file` scope)
|
|
/// because users need to browse to and reuse folders that someone else
|
|
/// created and shared with them, not just folders/files this app itself
|
|
/// created. See the plan doc for the tradeoffs (this requires Google
|
|
/// Cloud Console "Testing" mode with explicit test users, to avoid needing
|
|
/// a full OAuth verification review).
|
|
const driveScopes = <String>['https://www.googleapis.com/auth/drive'];
|
|
|
|
/// Thrown when a Drive API call needs authorization that isn't currently
|
|
/// available without prompting the user, e.g. during a background sync.
|
|
class DriveNotAuthorizedException implements Exception {
|
|
@override
|
|
String toString() => 'Drive access is not currently authorized.';
|
|
}
|
|
|
|
/// Wraps `google_sign_in` for authenticating with Google and producing an
|
|
/// authenticated [http.Client] for the Drive API.
|
|
class DriveAuthService {
|
|
bool _initialized = false;
|
|
GoogleSignInAccount? _account;
|
|
|
|
bool get isSignedIn => _account != null;
|
|
|
|
String? get currentAccountEmail => _account?.email;
|
|
|
|
Future<void> _ensureInitialized() async {
|
|
if (_initialized) return;
|
|
await GoogleSignIn.instance.initialize(
|
|
clientId: Platform.isIOS ? DriveOAuthConfig.iosClientId : null,
|
|
serverClientId: Platform.isAndroid ? DriveOAuthConfig.androidServerClientId : null,
|
|
);
|
|
_initialized = true;
|
|
}
|
|
|
|
/// Attempts to restore a previous sign-in without any UI. Returns true if
|
|
/// the user is signed in and Drive access is already authorized.
|
|
Future<bool> attemptSilentSignIn() async {
|
|
await _ensureInitialized();
|
|
final account = await GoogleSignIn.instance.attemptLightweightAuthentication();
|
|
_account = account;
|
|
if (account == null) return false;
|
|
|
|
final authorization =
|
|
await account.authorizationClient.authorizationForScopes(driveScopes);
|
|
return authorization != null;
|
|
}
|
|
|
|
/// Interactive sign-in + Drive scope authorization. Must be called from a
|
|
/// user-initiated action (e.g. a button press).
|
|
Future<String> signIn() async {
|
|
await _ensureInitialized();
|
|
final account = await GoogleSignIn.instance.authenticate(scopeHint: driveScopes);
|
|
_account = account;
|
|
await account.authorizationClient.authorizeScopes(driveScopes);
|
|
return account.email;
|
|
}
|
|
|
|
Future<void> signOut() async {
|
|
await GoogleSignIn.instance.signOut();
|
|
_account = null;
|
|
}
|
|
|
|
/// Builds an [http.Client] that attaches a fresh Drive authorization
|
|
/// header to every request. Fetches headers per-request (rather than
|
|
/// once) so a client that lives across a long sync doesn't use a stale,
|
|
/// expired token. Never prompts for UI — suitable for background sync —
|
|
/// so throws [DriveNotAuthorizedException] if authorization isn't already
|
|
/// in place (the caller should treat that as "Drive is disconnected").
|
|
http.Client authenticatedHttpClient() {
|
|
final account = _account;
|
|
if (account == null) {
|
|
throw DriveNotAuthorizedException();
|
|
}
|
|
return _DriveHttpClient(account.authorizationClient);
|
|
}
|
|
}
|
|
|
|
class _DriveHttpClient extends http.BaseClient {
|
|
final GoogleSignInAuthorizationClient _authClient;
|
|
final http.Client _inner = http.Client();
|
|
|
|
_DriveHttpClient(this._authClient);
|
|
|
|
@override
|
|
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
|
final headers =
|
|
await _authClient.authorizationHeaders(driveScopes, promptIfNecessary: false);
|
|
if (headers == null) {
|
|
throw DriveNotAuthorizedException();
|
|
}
|
|
request.headers.addAll(headers);
|
|
return _inner.send(request);
|
|
}
|
|
|
|
@override
|
|
void close() {
|
|
_inner.close();
|
|
super.close();
|
|
}
|
|
}
|