fix: route History upload through authenticated MotorCloud and scope sync state per user

This commit is contained in:
bponders12-cell 2026-09-01 20:40:42 -04:00
parent ed269a0e67
commit 272a2b56ee
1 changed files with 84 additions and 42 deletions

View File

@ -34,16 +34,16 @@ class GitHubUploadResult {
}) : timestamp = timestamp ?? DateTime.now();
}
/// Ride synchronization service.
/// Compatibility facade for the historical ride-detail sync service.
///
/// Historical versions of the ride-detail screen called this service directly,
/// which required a separate GitHub PAT even when the user was already signed
/// into MotorCloud with Google/Firebase. When no GitHub PAT is configured we now
/// use the authoritative Firebase + R2 provider, matching Settings > Back Up All.
/// GitHub remains available when a PAT is explicitly configured.
/// MotorSignal cloud backup is Firebase Authentication + Firestore + R2.
/// The old GitHub PAT exporter remains available only through the explicit
/// `uploadRideToGitHub` developer/export path and is never used by the normal
/// History -> Upload Now action.
class GitHubSyncService extends ChangeNotifier {
final AppDatabase _db;
final GitHubAuthService _auth;
final GitHubAuthService _githubAuth;
final AuthService _authService;
final SharedPreferences _prefs;
final http.Client _client;
@ -51,16 +51,40 @@ class GitHubSyncService extends ChangeNotifier {
String _statusMessage = '';
final Set<int> _uploadQueue = {};
GitHubSyncService(this._db, this._auth, this._prefs, [http.Client? client])
: _client = client ?? http.Client();
GitHubSyncService(
this._db,
this._githubAuth,
this._authService,
this._prefs, [http.Client? client],
) : _client = client ?? http.Client();
GitHubUploadState get state => _state;
String get statusMessage => _statusMessage;
Set<int> get queuedRideIds => Set.unmodifiable(_uploadQueue);
bool isRideUploaded(int rideId) => _prefs.getBool('github_uploaded_ride_$rideId') ?? false;
String? getRideCommitSha(int rideId) => _prefs.getString('github_sha_ride_$rideId');
String? getRideUploadTimestamp(int rideId) => _prefs.getString('github_uploaded_at_ride_$rideId');
String? _userScopedKey(String key, int rideId) {
final uid = _authService.currentUser?.uid;
if (uid == null || uid.isEmpty) return null;
return '${key}_${uid}_$rideId';
}
bool isRideUploaded(int rideId) {
final scoped = _userScopedKey('motorcloud_uploaded_ride', rideId);
if (scoped == null) return false;
return _prefs.getBool(scoped) ?? false;
}
String? getRideCommitSha(int rideId) {
final scoped = _userScopedKey('motorcloud_ride_remote_id', rideId);
if (scoped == null) return null;
return _prefs.getString(scoped);
}
String? getRideUploadTimestamp(int rideId) {
final scoped = _userScopedKey('motorcloud_uploaded_at_ride', rideId);
if (scoped == null) return null;
return _prefs.getString(scoped);
}
void queueRideForUpload(int rideId) {
if (isRideUploaded(rideId)) return;
@ -79,19 +103,43 @@ class GitHubSyncService extends ChangeNotifier {
}
}
/// Normal History -> Upload Now operation.
///
/// This deliberately ignores any configured GitHub PAT. A user's ride must
/// be written to that user's MotorCloud account, not to a public developer
/// repository. Authentication and ownership are enforced by Firebase/R2.
Future<GitHubUploadResult> uploadRide(int rideId, {bool isManual = false}) async {
// IMPORTANT: Google/Firebase authentication is the normal MotorCloud cloud
// authentication. A GitHub PAT is optional and only needed for explicit
// GitHub repository export.
final githubToken = _auth.getToken();
if (githubToken == null || githubToken.isEmpty) {
return _uploadToMotorCloud(rideId);
}
/// Explicit legacy/developer export. Not used by the normal ride UI.
Future<GitHubUploadResult> uploadRideToGitHub(int rideId) async {
final githubToken = _githubAuth.getToken();
if (githubToken == null || githubToken.isEmpty) {
return GitHubUploadResult(
success: false,
message: 'GitHub export requires an explicitly configured GitHub token.',
);
}
return _uploadToGitHub(rideId, githubToken);
}
Future<GitHubUploadResult> _uploadToMotorCloud(int rideId) async {
final user = _authService.currentUser;
if (user == null || user.isAnonymous) {
_state = GitHubUploadState.authRequired;
_statusMessage = 'Sign in to MotorCloud before uploading a ride.';
notifyListeners();
return GitHubUploadResult(success: false, message: _statusMessage);
}
if (isRideUploaded(rideId)) {
_state = GitHubUploadState.success;
_statusMessage = 'Ride #$rideId is already backed up to your MotorCloud account.';
notifyListeners();
return GitHubUploadResult(success: true, message: _statusMessage, commitSha: getRideCommitSha(rideId), targetPath: 'MotorCloud');
}
_state = GitHubUploadState.uploading;
_statusMessage = 'Backing up Ride #$rideId to MotorCloud...';
notifyListeners();
@ -99,7 +147,7 @@ class GitHubSyncService extends ChangeNotifier {
try {
final provider = FirebaseSyncProvider(
db: _db,
authService: FirebaseAuthService(),
authService: _authService,
);
final result = await provider.uploadRide(
SyncItem(
@ -110,14 +158,15 @@ class GitHubSyncService extends ChangeNotifier {
);
if (result.success) {
await _prefs.setBool('github_uploaded_ride_$rideId', true);
await _prefs.setString('github_uploaded_at_ride_$rideId', DateTime.now().toIso8601String());
if (result.remoteId != null) {
await _prefs.setString('github_sha_ride_$rideId', result.remoteId!);
}
final uploadedKey = _userScopedKey('motorcloud_uploaded_ride', rideId)!;
final timestampKey = _userScopedKey('motorcloud_uploaded_at_ride', rideId)!;
final remoteIdKey = _userScopedKey('motorcloud_ride_remote_id', rideId)!;
await _prefs.setBool(uploadedKey, true);
await _prefs.setString(timestampKey, DateTime.now().toIso8601String());
if (result.remoteId != null) await _prefs.setString(remoteIdKey, result.remoteId!);
_uploadQueue.remove(rideId);
_state = GitHubUploadState.success;
_statusMessage = 'Ride #$rideId backed up to MotorCloud';
_statusMessage = 'Ride #$rideId backed up to your MotorCloud account';
notifyListeners();
return GitHubUploadResult(
success: true,
@ -127,11 +176,7 @@ class GitHubSyncService extends ChangeNotifier {
);
}
if (result.requiresAuth) {
_state = GitHubUploadState.authRequired;
} else {
_state = GitHubUploadState.failed;
}
_state = result.requiresAuth ? GitHubUploadState.authRequired : GitHubUploadState.failed;
_statusMessage = result.errorMessage ?? 'MotorCloud backup failed';
notifyListeners();
return GitHubUploadResult(success: false, message: _statusMessage);
@ -154,8 +199,8 @@ class GitHubSyncService extends ChangeNotifier {
final dateStr = DateFormat('yyyy-MM-dd-HHmmss').format(ride.startTime.toLocal());
final folderName = 'field_test_session_${rideId}_$dateStr/field_test_session';
final repo = _auth.getRepository();
final branch = _auth.getBranch();
final repo = _githubAuth.getRepository();
final branch = _githubAuth.getBranch();
final fieldTestExport = FieldTestExportService(_db);
final exportService = ExportService(_db);
final sessionJson = await fieldTestExport.generateFieldTestSessionJson(rideId);
@ -262,17 +307,13 @@ class GitHubSyncService extends ChangeNotifier {
}
}
await _prefs.setBool('github_uploaded_ride_$rideId', true);
await _prefs.setString('github_uploaded_at_ride_$rideId', DateTime.now().toIso8601String());
if (lastCommitSha != null) await _prefs.setString('github_sha_ride_$rideId', lastCommitSha);
_uploadQueue.remove(rideId);
_state = GitHubUploadState.success;
_statusMessage = 'Ride #$rideId successfully uploaded to $repo ($branch)';
_statusMessage = 'Ride #$rideId exported to $repo ($branch)';
notifyListeners();
return GitHubUploadResult(success: true, message: _statusMessage, commitSha: lastCommitSha, targetPath: folderName);
} catch (e) {
_state = GitHubUploadState.failed;
_statusMessage = 'Upload failed for Ride #$rideId: $e';
_statusMessage = 'GitHub export failed for Ride #$rideId: $e';
notifyListeners();
return GitHubUploadResult(success: false, message: _statusMessage);
}
@ -281,7 +322,8 @@ class GitHubSyncService extends ChangeNotifier {
final githubSyncServiceProvider = ChangeNotifierProvider<GitHubSyncService>((ref) {
final db = ref.watch(databaseProvider);
final auth = ref.watch(githubAuthServiceProvider);
final githubAuth = ref.watch(githubAuthServiceProvider);
final authService = ref.watch(authServiceProvider);
final prefs = ref.watch(sharedPreferencesProvider);
return GitHubSyncService(db, auth, prefs);
return GitHubSyncService(db, githubAuth, authService, prefs);
});