Initial commit
This commit is contained in:
189
lib/services/authentication_manager.dart
Normal file
189
lib/services/authentication_manager.dart
Normal file
@@ -0,0 +1,189 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import '../models/device.dart';
|
||||
import '../models/user_info.dart';
|
||||
import '../models/static_talkgroup.dart';
|
||||
import '../models/device_profile.dart';
|
||||
import 'brandmeister_client.dart';
|
||||
|
||||
class AuthenticationManager extends ChangeNotifier {
|
||||
static const String _tokenKey = 'apiToken';
|
||||
|
||||
final FlutterSecureStorage _storage = const FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(
|
||||
encryptedSharedPreferences: true,
|
||||
),
|
||||
iOptions: IOSOptions(
|
||||
accessibility: KeychainAccessibility.first_unlock,
|
||||
),
|
||||
);
|
||||
|
||||
bool _isAuthenticated = false;
|
||||
String _apiToken = '';
|
||||
UserInfo? _userInfo;
|
||||
BrandmeisterClient? _client;
|
||||
|
||||
bool get isAuthenticated => _isAuthenticated;
|
||||
String get apiToken => _apiToken;
|
||||
UserInfo? get userInfo => _userInfo;
|
||||
|
||||
AuthenticationManager() {
|
||||
_loadToken();
|
||||
}
|
||||
|
||||
Future<void> _loadToken() async {
|
||||
try {
|
||||
final token = await _storage.read(key: _tokenKey);
|
||||
if (token != null && token.isNotEmpty) {
|
||||
_apiToken = token;
|
||||
_client = BrandmeisterClient(apiToken: token);
|
||||
// Try to verify the token is still valid
|
||||
try {
|
||||
_userInfo = await _client!.whoami();
|
||||
_isAuthenticated = true;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
// Token is invalid, clear it
|
||||
await logout();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading token: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveToken(String token) async {
|
||||
try {
|
||||
await _storage.write(key: _tokenKey, value: token);
|
||||
_apiToken = token;
|
||||
_client = BrandmeisterClient(apiToken: token);
|
||||
} catch (e) {
|
||||
throw BrandmeisterError('Failed to save token: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<UserInfo> verifyAndSaveToken(String token) async {
|
||||
if (token.isEmpty) {
|
||||
throw BrandmeisterError('Token cannot be empty');
|
||||
}
|
||||
|
||||
final tempClient = BrandmeisterClient(apiToken: token);
|
||||
final user = await tempClient.whoami();
|
||||
|
||||
await _saveToken(token);
|
||||
_userInfo = user;
|
||||
_isAuthenticated = true;
|
||||
notifyListeners();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
try {
|
||||
await _storage.delete(key: _tokenKey);
|
||||
_apiToken = '';
|
||||
_userInfo = null;
|
||||
_isAuthenticated = false;
|
||||
_client = null;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Error during logout: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshUserInfo() async {
|
||||
if (_client == null) return;
|
||||
|
||||
try {
|
||||
_userInfo = await _client!.whoami();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Error refreshing user info: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Device Operations
|
||||
Future<List<Device>> getDevices() async {
|
||||
if (_client == null || _userInfo == null) {
|
||||
throw BrandmeisterError('Not authenticated');
|
||||
}
|
||||
|
||||
if (_userInfo!.username == null) {
|
||||
throw BrandmeisterError('Username not available');
|
||||
}
|
||||
|
||||
return await _client!.getDevicesByCallsign(_userInfo!.username!);
|
||||
}
|
||||
|
||||
Future<Device> getDevice(int dmrId) async {
|
||||
if (_client == null) {
|
||||
throw BrandmeisterError('Not authenticated');
|
||||
}
|
||||
|
||||
return await _client!.getDevice(dmrId);
|
||||
}
|
||||
|
||||
Future<DeviceProfile> getDeviceProfile(int dmrId) async {
|
||||
if (_client == null) {
|
||||
throw BrandmeisterError('Not authenticated');
|
||||
}
|
||||
|
||||
return await _client!.getDeviceProfile(dmrId);
|
||||
}
|
||||
|
||||
Future<List<StaticTalkgroup>> getTalkgroups(int dmrId) async {
|
||||
if (_client == null) {
|
||||
throw BrandmeisterError('Not authenticated');
|
||||
}
|
||||
|
||||
return await _client!.getDeviceTalkgroups(dmrId);
|
||||
}
|
||||
|
||||
Future<Map<String, String>> getAllTalkgroups() async {
|
||||
if (_client == null) {
|
||||
throw BrandmeisterError('Not authenticated');
|
||||
}
|
||||
|
||||
return await _client!.getTalkgroups();
|
||||
}
|
||||
|
||||
Future<void> linkTalkgroup({
|
||||
required int talkgroupId,
|
||||
required int dmrId,
|
||||
required int timeslot,
|
||||
}) async {
|
||||
if (_client == null) {
|
||||
throw BrandmeisterError('Not authenticated');
|
||||
}
|
||||
|
||||
await _client!.addTalkgroup(
|
||||
dmrId: dmrId,
|
||||
talkgroupId: talkgroupId,
|
||||
slot: timeslot,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> unlinkTalkgroup({
|
||||
required String talkgroupId,
|
||||
required int dmrId,
|
||||
required String timeslot,
|
||||
}) async {
|
||||
if (_client == null) {
|
||||
throw BrandmeisterError('Not authenticated');
|
||||
}
|
||||
|
||||
final tgId = int.tryParse(talkgroupId);
|
||||
final slot = int.tryParse(timeslot);
|
||||
|
||||
if (tgId == null || slot == null) {
|
||||
throw BrandmeisterError('Invalid talkgroup ID or timeslot');
|
||||
}
|
||||
|
||||
await _client!.removeTalkgroup(
|
||||
dmrId: dmrId,
|
||||
talkgroupId: tgId,
|
||||
slot: slot,
|
||||
);
|
||||
}
|
||||
}
|
||||
186
lib/services/brandmeister_client.dart
Normal file
186
lib/services/brandmeister_client.dart
Normal file
@@ -0,0 +1,186 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/device.dart';
|
||||
import '../models/user_info.dart';
|
||||
import '../models/static_talkgroup.dart';
|
||||
import '../models/device_profile.dart';
|
||||
|
||||
class BrandmeisterError implements Exception {
|
||||
final String message;
|
||||
final int? statusCode;
|
||||
|
||||
BrandmeisterError(this.message, {this.statusCode});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (statusCode != null) {
|
||||
return 'BrandmeisterError (HTTP $statusCode): $message';
|
||||
}
|
||||
return 'BrandmeisterError: $message';
|
||||
}
|
||||
}
|
||||
|
||||
class BrandmeisterClient {
|
||||
static const String baseUrl = 'https://api.brandmeister.network/v2';
|
||||
final String apiToken;
|
||||
|
||||
BrandmeisterClient({required this.apiToken});
|
||||
|
||||
Map<String, String> get _headers => {
|
||||
'Authorization': 'Bearer $apiToken',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
Future<T> _handleResponse<T>(
|
||||
http.Response response,
|
||||
T Function(dynamic) parser,
|
||||
) async {
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
try {
|
||||
final dynamic decoded = jsonDecode(response.body);
|
||||
return parser(decoded);
|
||||
} catch (e) {
|
||||
throw BrandmeisterError('Failed to decode response: $e');
|
||||
}
|
||||
} else {
|
||||
throw BrandmeisterError(
|
||||
'HTTP request failed: ${response.body}',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// User Endpoints
|
||||
Future<UserInfo> whoami() async {
|
||||
final response = await http.post(
|
||||
Uri.parse('$baseUrl/user/whoAmI'),
|
||||
headers: _headers,
|
||||
);
|
||||
return _handleResponse(response, (json) => UserInfo.fromJson(json));
|
||||
}
|
||||
|
||||
Future<UserInfo> getUser(int id) async {
|
||||
final response = await http.get(
|
||||
Uri.parse('$baseUrl/user/byID/$id'),
|
||||
headers: _headers,
|
||||
);
|
||||
return _handleResponse(response, (json) => UserInfo.fromJson(json));
|
||||
}
|
||||
|
||||
Future<List<UserInfo>> getUsersByCallsign(String callsign) async {
|
||||
final response = await http.get(
|
||||
Uri.parse('$baseUrl/user/byCall/$callsign'),
|
||||
headers: _headers,
|
||||
);
|
||||
return _handleResponse(
|
||||
response,
|
||||
(json) => (json as List).map((e) => UserInfo.fromJson(e)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
// Device Endpoints
|
||||
Future<Device> getDevice(int dmrId) async {
|
||||
final response = await http.get(
|
||||
Uri.parse('$baseUrl/device/$dmrId'),
|
||||
headers: _headers,
|
||||
);
|
||||
return _handleResponse(response, (json) => Device.fromJson(json));
|
||||
}
|
||||
|
||||
Future<DeviceProfile> getDeviceProfile(int dmrId) async {
|
||||
final response = await http.get(
|
||||
Uri.parse('$baseUrl/device/$dmrId/profile'),
|
||||
headers: _headers,
|
||||
);
|
||||
return _handleResponse(response, (json) => DeviceProfile.fromJson(json));
|
||||
}
|
||||
|
||||
Future<List<Device>> getDevicesByCallsign(String callsign) async {
|
||||
final response = await http.get(
|
||||
Uri.parse('$baseUrl/device/byCall?callsign=$callsign'),
|
||||
headers: _headers,
|
||||
);
|
||||
return _handleResponse(
|
||||
response,
|
||||
(json) => (json as List).map((e) => Device.fromJson(e)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Device>> getDevicesByMaster(int masterId) async {
|
||||
final response = await http.get(
|
||||
Uri.parse('$baseUrl/device/byMaster/$masterId'),
|
||||
headers: _headers,
|
||||
);
|
||||
return _handleResponse(
|
||||
response,
|
||||
(json) => (json as List).map((e) => Device.fromJson(e)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<StaticTalkgroup>> getDeviceTalkgroups(int dmrId) async {
|
||||
final response = await http.get(
|
||||
Uri.parse('$baseUrl/device/$dmrId/talkgroup'),
|
||||
headers: _headers,
|
||||
);
|
||||
return _handleResponse(
|
||||
response,
|
||||
(json) =>
|
||||
(json as List).map((e) => StaticTalkgroup.fromJson(e)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
// Talkgroup Management
|
||||
Future<void> addTalkgroup({
|
||||
required int dmrId,
|
||||
required int talkgroupId,
|
||||
required int slot,
|
||||
}) async {
|
||||
final response = await http.post(
|
||||
Uri.parse('$baseUrl/device/$dmrId/talkgroup'),
|
||||
headers: _headers,
|
||||
body: jsonEncode({
|
||||
'slot': slot,
|
||||
'group': talkgroupId,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throw BrandmeisterError(
|
||||
'Failed to add talkgroup: ${response.body}',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeTalkgroup({
|
||||
required int dmrId,
|
||||
required int talkgroupId,
|
||||
required int slot,
|
||||
}) async {
|
||||
final response = await http.delete(
|
||||
Uri.parse('$baseUrl/device/$dmrId/talkgroup/$slot/$talkgroupId'),
|
||||
headers: _headers,
|
||||
);
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throw BrandmeisterError(
|
||||
'Failed to remove talkgroup: ${response.body}',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Talkgroup Endpoints
|
||||
Future<Map<String, String>> getTalkgroups() async {
|
||||
final response = await http.get(
|
||||
Uri.parse('$baseUrl/talkgroup'),
|
||||
headers: _headers,
|
||||
);
|
||||
return _handleResponse(
|
||||
response,
|
||||
(json) => (json as Map<String, dynamic>).map(
|
||||
(key, value) => MapEntry(key, value.toString()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user