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(); }) : 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, /// MotorSignal cloud backup is Firebase Authentication + Firestore + R2.
/// which required a separate GitHub PAT even when the user was already signed /// The old GitHub PAT exporter remains available only through the explicit
/// into MotorCloud with Google/Firebase. When no GitHub PAT is configured we now /// `uploadRideToGitHub` developer/export path and is never used by the normal
/// use the authoritative Firebase + R2 provider, matching Settings > Back Up All. /// History -> Upload Now action.
/// GitHub remains available when a PAT is explicitly configured.
class GitHubSyncService extends ChangeNotifier { class GitHubSyncService extends ChangeNotifier {
final AppDatabase _db; final AppDatabase _db;
final GitHubAuthService _auth; final GitHubAuthService _githubAuth;
final AuthService _authService;
final SharedPreferences _prefs; final SharedPreferences _prefs;
final http.Client _client; final http.Client _client;
@ -51,16 +51,40 @@ class GitHubSyncService extends ChangeNotifier {
String _statusMessage = ''; String _statusMessage = '';
final Set<int> _uploadQueue = {}; final Set<int> _uploadQueue = {};
GitHubSyncService(this._db, this._auth, this._prefs, [http.Client? client]) GitHubSyncService(
: _client = client ?? http.Client(); this._db,
this._githubAuth,
this._authService,
this._prefs, [http.Client? client],
) : _client = client ?? http.Client();
GitHubUploadState get state => _state; GitHubUploadState get state => _state;
String get statusMessage => _statusMessage; String get statusMessage => _statusMessage;
Set<int> get queuedRideIds => Set.unmodifiable(_uploadQueue); Set<int> get queuedRideIds => Set.unmodifiable(_uploadQueue);
bool isRideUploaded(int rideId) => _prefs.getBool('github_uploaded_ride_$rideId') ?? false; String? _userScopedKey(String key, int rideId) {
String? getRideCommitSha(int rideId) => _prefs.getString('github_sha_ride_$rideId'); final uid = _authService.currentUser?.uid;
String? getRideUploadTimestamp(int rideId) => _prefs.getString('github_uploaded_at_ride_$rideId'); 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) { void queueRideForUpload(int rideId) {
if (isRideUploaded(rideId)) return; 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 { Future<GitHubUploadResult> uploadRide(int rideId, {bool isManual = false}) async {
// IMPORTANT: Google/Firebase authentication is the normal MotorCloud cloud return _uploadToMotorCloud(rideId);
// 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); return _uploadToGitHub(rideId, githubToken);
} }
Future<GitHubUploadResult> _uploadToMotorCloud(int rideId) async { 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; _state = GitHubUploadState.uploading;
_statusMessage = 'Backing up Ride #$rideId to MotorCloud...'; _statusMessage = 'Backing up Ride #$rideId to MotorCloud...';
notifyListeners(); notifyListeners();
@ -99,7 +147,7 @@ class GitHubSyncService extends ChangeNotifier {
try { try {
final provider = FirebaseSyncProvider( final provider = FirebaseSyncProvider(
db: _db, db: _db,
authService: FirebaseAuthService(), authService: _authService,
); );
final result = await provider.uploadRide( final result = await provider.uploadRide(
SyncItem( SyncItem(
@ -110,14 +158,15 @@ class GitHubSyncService extends ChangeNotifier {
); );
if (result.success) { if (result.success) {
await _prefs.setBool('github_uploaded_ride_$rideId', true); final uploadedKey = _userScopedKey('motorcloud_uploaded_ride', rideId)!;
await _prefs.setString('github_uploaded_at_ride_$rideId', DateTime.now().toIso8601String()); final timestampKey = _userScopedKey('motorcloud_uploaded_at_ride', rideId)!;
if (result.remoteId != null) { final remoteIdKey = _userScopedKey('motorcloud_ride_remote_id', rideId)!;
await _prefs.setString('github_sha_ride_$rideId', result.remoteId!); 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); _uploadQueue.remove(rideId);
_state = GitHubUploadState.success; _state = GitHubUploadState.success;
_statusMessage = 'Ride #$rideId backed up to MotorCloud'; _statusMessage = 'Ride #$rideId backed up to your MotorCloud account';
notifyListeners(); notifyListeners();
return GitHubUploadResult( return GitHubUploadResult(
success: true, success: true,
@ -127,11 +176,7 @@ class GitHubSyncService extends ChangeNotifier {
); );
} }
if (result.requiresAuth) { _state = result.requiresAuth ? GitHubUploadState.authRequired : GitHubUploadState.failed;
_state = GitHubUploadState.authRequired;
} else {
_state = GitHubUploadState.failed;
}
_statusMessage = result.errorMessage ?? 'MotorCloud backup failed'; _statusMessage = result.errorMessage ?? 'MotorCloud backup failed';
notifyListeners(); notifyListeners();
return GitHubUploadResult(success: false, message: _statusMessage); 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 dateStr = DateFormat('yyyy-MM-dd-HHmmss').format(ride.startTime.toLocal());
final folderName = 'field_test_session_${rideId}_$dateStr/field_test_session'; final folderName = 'field_test_session_${rideId}_$dateStr/field_test_session';
final repo = _auth.getRepository(); final repo = _githubAuth.getRepository();
final branch = _auth.getBranch(); final branch = _githubAuth.getBranch();
final fieldTestExport = FieldTestExportService(_db); final fieldTestExport = FieldTestExportService(_db);
final exportService = ExportService(_db); final exportService = ExportService(_db);
final sessionJson = await fieldTestExport.generateFieldTestSessionJson(rideId); 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; _state = GitHubUploadState.success;
_statusMessage = 'Ride #$rideId successfully uploaded to $repo ($branch)'; _statusMessage = 'Ride #$rideId exported to $repo ($branch)';
notifyListeners(); notifyListeners();
return GitHubUploadResult(success: true, message: _statusMessage, commitSha: lastCommitSha, targetPath: folderName); return GitHubUploadResult(success: true, message: _statusMessage, commitSha: lastCommitSha, targetPath: folderName);
} catch (e) { } catch (e) {
_state = GitHubUploadState.failed; _state = GitHubUploadState.failed;
_statusMessage = 'Upload failed for Ride #$rideId: $e'; _statusMessage = 'GitHub export failed for Ride #$rideId: $e';
notifyListeners(); notifyListeners();
return GitHubUploadResult(success: false, message: _statusMessage); return GitHubUploadResult(success: false, message: _statusMessage);
} }
@ -281,7 +322,8 @@ class GitHubSyncService extends ChangeNotifier {
final githubSyncServiceProvider = ChangeNotifierProvider<GitHubSyncService>((ref) { final githubSyncServiceProvider = ChangeNotifierProvider<GitHubSyncService>((ref) {
final db = ref.watch(databaseProvider); 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); final prefs = ref.watch(sharedPreferencesProvider);
return GitHubSyncService(db, auth, prefs); return GitHubSyncService(db, githubAuth, authService, prefs);
}); });