MotorSignal-App/lib/services/github/github_auth_service.dart

141 lines
4.7 KiB
Dart

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<void> _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<void> 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<void> 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<void> setRepository(String repo) async {
await _prefs.setString(_keyRepo, repo.trim());
}
/// Target branch (default: main)
String getBranch() {
return _prefs.getString(_keyBranch) ?? defaultBranch;
}
Future<void> setBranch(String branch) async {
await _prefs.setString(_keyBranch, branch.trim());
}
/// Tests connectivity and permissions against GitHub API
Future<Map<String, dynamic>> 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<GitHubAuthService>((ref) {
final prefs = ref.watch(sharedPreferencesProvider);
return GitHubAuthService(prefs);
});