import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import '../../features/records/records_service.dart'; /// Secure credentials and repository configuration for GitHub ride sync class GitHubAuthService { static const String _keyToken = 'github_pat_token_enc'; static const String _keyRepo = 'github_target_repo'; static const String _keyBranch = 'github_target_branch'; static const String defaultRepo = 'bponders12-cell/ride_logs'; static const String defaultBranch = 'main'; final SharedPreferences _prefs; final http.Client _client; final FlutterSecureStorage _secureStorage; String? _token; GitHubAuthService(this._prefs, [http.Client? client, FlutterSecureStorage? secureStorage]) : _client = client ?? http.Client(), _secureStorage = secureStorage ?? const FlutterSecureStorage() { _migrateToken(); } Future _migrateToken() async { try { final secure = await _secureStorage.read(key: _keyToken); if (secure != null && secure.isNotEmpty) { _token = secure; return; } final legacy = _prefs.getString(_keyToken); if (legacy == null || legacy.isEmpty) return; final decoded = _decodeLegacy(legacy); await _secureStorage.write(key: _keyToken, value: decoded); await _prefs.remove(_keyToken); _token = decoded; } catch (_) {} } String _decodeLegacy(String raw) { try { return utf8.decode(base64.decode(raw)); } catch (_) { return raw; } } /// Reads stored Personal Access Token String? getToken() { if (_token != null && _token!.isNotEmpty) return _token; final legacy = _prefs.getString(_keyToken); if (legacy == null || legacy.isEmpty) return null; // Legacy values are read only to bridge an upgrade; constructor migration // moves them to secure storage and removes the old entry. return _decodeLegacy(legacy); } /// Stores Personal Access Token securely Future saveToken(String token) async { _token = token.trim(); try { await _secureStorage.write(key: _keyToken, value: _token); await _prefs.remove(_keyToken); } catch (_) { // Native secure storage is unavailable in plugin-less test environments; // keep the token only in memory rather than persist it insecurely. } } /// Clears stored token Future clearToken() async { await _prefs.remove(_keyToken); try { await _secureStorage.delete(key: _keyToken); } catch (_) {} _token = null; } /// Target GitHub repository (default: bponders12-cell/ride_logs) String getRepository() { return _prefs.getString(_keyRepo) ?? defaultRepo; } Future setRepository(String repo) async { await _prefs.setString(_keyRepo, repo.trim()); } /// Target branch (default: main) String getBranch() { return _prefs.getString(_keyBranch) ?? defaultBranch; } Future setBranch(String branch) async { await _prefs.setString(_keyBranch, branch.trim()); } /// Tests connectivity and permissions against GitHub API Future> testConnection() async { final token = getToken(); if (token == null || token.isEmpty) { return {'success': false, 'message': 'No GitHub token configured'}; } final repo = getRepository(); final url = Uri.parse('https://api.github.com/repos/$repo'); try { final response = await _client.get( url, headers: { 'Authorization': 'Bearer $token', 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'MotorSignal-Telemetry/1.0.2', }, ).timeout(const Duration(seconds: 10)); if (response.statusCode == 200) { final data = jsonDecode(response.body); return { 'success': true, 'message': 'Connected to ${data['full_name']} (${data['default_branch']})', 'canPush': data['permissions']?['push'] ?? true, }; } else if (response.statusCode == 401) { return {'success': false, 'message': 'Authentication failed (401: Invalid Token)'}; } else if (response.statusCode == 404) { return {'success': false, 'message': 'Repository not found or token lacks repo scope (404)'}; } else { return {'success': false, 'message': 'GitHub API error (${response.statusCode})'}; } } catch (e) { return {'success': false, 'message': 'Connection error: $e'}; } } } final githubAuthServiceProvider = Provider((ref) { final prefs = ref.watch(sharedPreferencesProvider); return GitHubAuthService(prefs); });