fix: route History upload through authenticated MotorCloud and scope sync state per user
This commit is contained in:
parent
ed269a0e67
commit
272a2b56ee
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue